├── .gitignore ├── .idea ├── .name ├── compiler.xml ├── copyright │ └── profiles_settings.xml ├── encodings.xml ├── gradle.xml ├── misc.xml ├── modules.xml ├── runConfigurations.xml └── vcs.xml ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── nl │ │ └── jpelgrm │ │ └── retrofit2oauthrefresh │ │ └── ApplicationTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── nl │ │ │ └── jpelgrm │ │ │ └── retrofit2oauthrefresh │ │ │ ├── LoginActivity.java │ │ │ ├── MainActivity.java │ │ │ └── api │ │ │ ├── APIClient.java │ │ │ ├── ServiceGenerator.java │ │ │ └── objects │ │ │ └── AccessToken.java │ └── res │ │ ├── layout │ │ ├── activity_login.xml │ │ ├── activity_main.xml │ │ └── content_main.xml │ │ ├── menu │ │ └── menu_main.xml │ │ ├── mipmap-hdpi │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxxhdpi │ │ └── ic_launcher.png │ │ ├── values-v21 │ │ └── styles.xml │ │ ├── values-w820dp │ │ └── dimens.xml │ │ └── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── nl │ └── jpelgrm │ └── retrofit2oauthrefresh │ └── ExampleUnitTest.java ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | OAuth refresh -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 19 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 19 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 46 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Joris Pelgröm 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Retrofit 2 OAuth 2 + refresh token example 2 | 3 | A quick example on how to use Retrofit 2 to authenticate the user using OAuth 2, and use the refresh token to try to refresh the access token automatically when necessary. 4 | 5 | Based on [bkiers/retrofit-oauth](https://github.com/bkiers/retrofit-oauth), [Future Studio's blog post on Retrofit + OAuth](https://futurestud.io/blog/oauth-2-on-android-with-retrofit), many Stack Overflow questions (especially [this one](http://stackoverflow.com/a/31624433)) and a lot of experimentation. Please check/read these sources to get a better understanding of what is happening. While this code works out great for me, keep in mind that it isn't perfect. 6 | 7 | ## Requirements 8 | 9 | Add these dependencies to your build.gradle: 10 | 11 | ``` 12 | compile 'com.squareup.retrofit2:retrofit:2.0.0' 13 | compile 'com.squareup.retrofit2:converter-gson:2.0.0' 14 | ``` 15 | 16 | In this example, I'm using GSON as a converter, but technically you should be able to use anything you want. 17 | 18 | And of course the internet permission for devices pre-API 23, if you want to make API calls. 19 | 20 | 21 | 22 | ## Usage 23 | 24 | ### Obtaining the tokens 25 | 26 | - First, we have to make sure everything points to your application and/or the service. 27 | - Update the `API_LOGIN_URL`, `API_OAUTH_CLIENTID` and `API_OAUTH_CLIENTSECRET` in the LoginActivity to match the login URL, OAuth client ID and OAuth client secret for your application. 28 | - Update the redirect URL for your application to match the redirect URL specified in the manifest for the activity. To prevent other apps from launching, it's probably best to use something like `://oauth` (in the example: `nl.jpelgrm.retrofit2oauthrefresh://oauth`). 29 | - Also update the `API_OAUTH_REDIRECT` in the LoginActivity and ServiceGenerator to match the specified redirect URL. 30 | - Replace the `API_BASE_URL` in the ServiceGenerator with the API base URL for your service. 31 | - Update the OAuth token request and refresh endpoints in the APIClient to match the service endpoints. 32 | - Check the specified parameters in the APIClient. While this should match what most OAuth applications require, make sure that all required fields are present. 33 | - Check the possible response for a refresh token. Not all services return you a new refresh token (or one at all), so update the ServiceGenerator accordingly (lines 96-98). 34 | - Next, trigger a new intent to show the `API_LOGIN_URL` to the user (LoginActivity lines 35-47). This will allow the user to log in to the service or create a new account. 35 | - After the user is done, the service should redirect the user back to your app. In the `onResume`, we check the redirect URL and if everything is present, we request a new access token. 36 | - Finally, we save the access token and you probably should show the user a confirmation. 37 | 38 | ### Using the API 39 | 40 | - All information is now saved and can be used. Just create a new client using your access token: 41 | ``` 42 | APIClient client; 43 | final SharedPreferences prefs = this.getSharedPreferences(BuildConfig.APPLICATION_ID, Context.MODE_PRIVATE); 44 | 45 | AccessToken token = new AccessToken(); 46 | token.setAccessToken(prefs.getString("oauth.accesstoken", "")); 47 | token.setRefreshToken(prefs.getString("oauth.refreshtoken", "")); 48 | token.setTokenType(prefs.getString("oauth.tokentype", "")); 49 | token.setClientID(API_OAUTH_CLIENTID); 50 | token.setClientSecret(API_OAUTH_CLIENTSECRET); 51 | 52 | client = ServiceGenerator.createService(APIClient.class, token, this); 53 | ``` 54 | You could consider saving the client ID and client secret in a central place to make them reusable across activities and fragments. For example, bkiers's retrofit-oauth uses a `local.properties` file. 55 | - And make a call like you normally would. Check the [Retorofit 2 documentation](http://square.github.io/retrofit/) for more details. Example: 56 | ``` 57 | Call> call = client.syncWatched("movies"); 58 | call.enqueue(new Callback>() { 59 | @Override 60 | public void onResponse(Call> call, Response> response) { 61 | if(response.code() == 200) { 62 | List movies = response.body(); 63 | for(Watched movie : movies) { 64 | movieCollection.add(movie); 65 | } 66 | movieAdapter.notifyDataSetChanged(); 67 | } else { 68 | // TODO Handle problem with response 69 | } 70 | } 71 | 72 | @Override 73 | public void onFailure(Call> call, Throwable t) { 74 | // TODO Handle failure 75 | } 76 | }); 77 | ``` 78 | Keep in mind that, since the client automatically refreshes the access token when necessary, when you get a `response.code() == 401` this is *not* due to an expired access token, but probably due to the user revoking access. 79 | 80 | ## Add to an existing project 81 | 82 | If you want to add this to an existing project instead of cloning this one, make sure to add the following to your project: 83 | 84 | - Add the AccessToken object (api/objects) to your project. 85 | - Add the APICient or update your existing API client interface to include `getNewAccessToken` and `getRefreshAccessToken`. 86 | - Add the ServiceGenerator or update your existing class to match the one found in this project. 87 | - Update your manifest to include an intent filter for your OAuth activity. 88 | - Add code that triggers the browser with a login page. 89 | - Update the `onResume` in your OAuth activity to match the one found in this project. 90 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 23 5 | buildToolsVersion "23.0.2" 6 | 7 | defaultConfig { 8 | applicationId "nl.jpelgrm.retrofit2oauthrefresh" 9 | minSdkVersion 14 10 | targetSdkVersion 23 11 | versionCode 1 12 | versionName "1.0" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | } 21 | 22 | dependencies { 23 | compile fileTree(dir: 'libs', include: ['*.jar']) 24 | testCompile 'junit:junit:4.12' 25 | compile 'com.android.support:appcompat-v7:23.2.1' 26 | compile 'com.android.support:design:23.2.1' 27 | compile 'com.squareup.retrofit2:retrofit:2.0.0' 28 | compile 'com.squareup.retrofit2:converter-gson:2.0.0' 29 | } 30 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in C:\Users\Joris\AppData\Local\Android\sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /app/src/androidTest/java/nl/jpelgrm/retrofit2oauthrefresh/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package nl.jpelgrm.retrofit2oauthrefresh; 2 | 3 | import android.app.Application; 4 | import android.test.ApplicationTestCase; 5 | 6 | /** 7 | * Testing Fundamentals 8 | */ 9 | public class ApplicationTest extends ApplicationTestCase { 10 | public ApplicationTest() { 11 | super(Application.class); 12 | } 13 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 13 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /app/src/main/java/nl/jpelgrm/retrofit2oauthrefresh/LoginActivity.java: -------------------------------------------------------------------------------- 1 | package nl.jpelgrm.retrofit2oauthrefresh; 2 | 3 | import android.content.Context; 4 | import android.content.Intent; 5 | import android.content.SharedPreferences; 6 | import android.net.Uri; 7 | import android.support.v7.app.AppCompatActivity; 8 | import android.os.Bundle; 9 | import android.view.View; 10 | import android.widget.Button; 11 | 12 | import nl.jpelgrm.retrofit2oauthrefresh.api.APIClient; 13 | import nl.jpelgrm.retrofit2oauthrefresh.api.ServiceGenerator; 14 | import nl.jpelgrm.retrofit2oauthrefresh.api.objects.AccessToken; 15 | import retrofit2.Call; 16 | import retrofit2.Callback; 17 | import retrofit2.Response; 18 | 19 | public class LoginActivity extends AppCompatActivity { 20 | 21 | Button loginButton; 22 | 23 | // TODO Replace this with your own data 24 | public static final String API_LOGIN_URL = "https://example.com/oauthloginpage"; 25 | public static final String API_OAUTH_CLIENTID = "replace-me"; 26 | public static final String API_OAUTH_CLIENTSECRET = "replace-me"; 27 | public static final String API_OAUTH_REDIRECT = "nl.jpelgrm.retrofit2oauthrefresh://oauth"; 28 | 29 | @Override 30 | protected void onCreate(Bundle savedInstanceState) { 31 | super.onCreate(savedInstanceState); 32 | setContentView(R.layout.activity_login); 33 | 34 | loginButton = (Button) findViewById(R.id.loginButton); 35 | loginButton.setOnClickListener(new View.OnClickListener() { 36 | @Override 37 | public void onClick(View v) { 38 | Intent intent = new Intent( 39 | Intent.ACTION_VIEW, 40 | Uri.parse(API_LOGIN_URL)); 41 | // This flag is set to prevent the browser with the login form from showing in the history stack 42 | intent.setFlags(Intent.FLAG_ACTIVITY_NO_HISTORY); 43 | 44 | startActivity(intent); 45 | finish(); 46 | } 47 | }); 48 | } 49 | 50 | @Override 51 | protected void onResume() { 52 | super.onResume(); 53 | 54 | Uri uri = getIntent().getData(); 55 | if(uri != null && uri.toString().startsWith(API_OAUTH_REDIRECT)) { 56 | String code = uri.getQueryParameter("code"); 57 | if(code != null) { 58 | // TODO We can probably do something with this code! Show the user that we are logging them in 59 | 60 | final SharedPreferences prefs = this.getSharedPreferences( 61 | BuildConfig.APPLICATION_ID, Context.MODE_PRIVATE); 62 | 63 | APIClient client = ServiceGenerator.createService(APIClient.class); 64 | Call call = client.getNewAccessToken(code, API_OAUTH_CLIENTID, 65 | API_OAUTH_CLIENTSECRET, API_OAUTH_REDIRECT, 66 | "authorization_code"); 67 | call.enqueue(new Callback() { 68 | @Override 69 | public void onResponse(Call call, Response response) { 70 | int statusCode = response.code(); 71 | if(statusCode == 200) { 72 | AccessToken token = response.body(); 73 | prefs.edit().putBoolean("oauth.loggedin", true).apply(); 74 | prefs.edit().putString("oauth.accesstoken", token.getAccessToken()).apply(); 75 | prefs.edit().putString("oauth.refreshtoken", token.getRefreshToken()).apply(); 76 | prefs.edit().putString("oauth.tokentype", token.getTokenType()).apply(); 77 | 78 | // TODO Show the user they are logged in 79 | } else { 80 | // TODO Handle errors on a failed response 81 | } 82 | } 83 | 84 | @Override 85 | public void onFailure(Call call, Throwable t) { 86 | // TODO Handle failure 87 | } 88 | }); 89 | } else { 90 | // TODO Handle a missing code in the redirect URI 91 | } 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /app/src/main/java/nl/jpelgrm/retrofit2oauthrefresh/MainActivity.java: -------------------------------------------------------------------------------- 1 | package nl.jpelgrm.retrofit2oauthrefresh; 2 | 3 | import android.content.Context; 4 | import android.content.Intent; 5 | import android.content.SharedPreferences; 6 | import android.os.Bundle; 7 | import android.support.design.widget.FloatingActionButton; 8 | import android.support.design.widget.Snackbar; 9 | import android.support.v7.app.AppCompatActivity; 10 | import android.support.v7.widget.Toolbar; 11 | import android.view.View; 12 | import android.view.Menu; 13 | import android.view.MenuItem; 14 | 15 | import nl.jpelgrm.retrofit2oauthrefresh.api.APIClient; 16 | import nl.jpelgrm.retrofit2oauthrefresh.api.ServiceGenerator; 17 | import nl.jpelgrm.retrofit2oauthrefresh.api.objects.AccessToken; 18 | 19 | public class MainActivity extends AppCompatActivity { 20 | 21 | @Override 22 | protected void onCreate(Bundle savedInstanceState) { 23 | super.onCreate(savedInstanceState); 24 | setContentView(R.layout.activity_main); 25 | Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); 26 | setSupportActionBar(toolbar); 27 | 28 | FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab); 29 | fab.setOnClickListener(new View.OnClickListener() { 30 | @Override 31 | public void onClick(View view) { 32 | Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG) 33 | .setAction("Action", null).show(); 34 | } 35 | }); 36 | } 37 | 38 | @Override 39 | public boolean onCreateOptionsMenu(Menu menu) { 40 | // Inflate the menu; this adds items to the action bar if it is present. 41 | getMenuInflater().inflate(R.menu.menu_main, menu); 42 | return true; 43 | } 44 | 45 | @Override 46 | public boolean onOptionsItemSelected(MenuItem item) { 47 | // Handle action bar item clicks here. The action bar will 48 | // automatically handle clicks on the Home/Up button, so long 49 | // as you specify a parent activity in AndroidManifest.xml. 50 | int id = item.getItemId(); 51 | 52 | //noinspection SimplifiableIfStatement 53 | if (id == R.id.action_settings) { 54 | Intent i = new Intent(MainActivity.this, LoginActivity.class); 55 | startActivity(i); 56 | return true; 57 | } 58 | 59 | return super.onOptionsItemSelected(item); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /app/src/main/java/nl/jpelgrm/retrofit2oauthrefresh/api/APIClient.java: -------------------------------------------------------------------------------- 1 | package nl.jpelgrm.retrofit2oauthrefresh.api; 2 | 3 | import nl.jpelgrm.retrofit2oauthrefresh.api.objects.AccessToken; 4 | import retrofit2.Call; 5 | import retrofit2.http.Field; 6 | import retrofit2.http.FormUrlEncoded; 7 | import retrofit2.http.POST; 8 | 9 | public interface APIClient { 10 | 11 | @FormUrlEncoded 12 | @POST("/oauth/token") 13 | Call getNewAccessToken( 14 | @Field("code") String code, 15 | @Field("client_id") String clientId, 16 | @Field("client_secret") String clientSecret, 17 | @Field("redirect_uri") String redirectUri, 18 | @Field("grant_type") String grantType); 19 | 20 | @FormUrlEncoded 21 | @POST("/oauth/token") 22 | Call getRefreshAccessToken( 23 | @Field("refresh_token") String refreshToken, 24 | @Field("client_id") String clientId, 25 | @Field("client_secret") String clientSecret, 26 | @Field("redirect_uri") String redirectUri, 27 | @Field("grant_type") String grantType); 28 | 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/java/nl/jpelgrm/retrofit2oauthrefresh/api/ServiceGenerator.java: -------------------------------------------------------------------------------- 1 | package nl.jpelgrm.retrofit2oauthrefresh.api; 2 | 3 | import android.content.Context; 4 | import android.content.SharedPreferences; 5 | import android.os.Build; 6 | import android.widget.Toast; 7 | 8 | import com.google.gson.Gson; 9 | 10 | import java.io.IOException; 11 | 12 | import nl.jpelgrm.retrofit2oauthrefresh.BuildConfig; 13 | import nl.jpelgrm.retrofit2oauthrefresh.api.objects.AccessToken; 14 | import okhttp3.Authenticator; 15 | import okhttp3.Interceptor; 16 | import okhttp3.OkHttpClient; 17 | import okhttp3.Request; 18 | import okhttp3.Response; 19 | import okhttp3.Route; 20 | import retrofit2.Call; 21 | import retrofit2.Callback; 22 | import retrofit2.Retrofit; 23 | import retrofit2.converter.gson.GsonConverterFactory; 24 | 25 | public class ServiceGenerator { 26 | 27 | public static final String API_BASE_URL = "https://api.example.com"; 28 | public static final String API_OAUTH_REDIRECT = "nl.jpelgrm.retrofit2oauthrefresh://oauth"; 29 | 30 | private static OkHttpClient.Builder httpClient; 31 | 32 | private static Retrofit.Builder builder; 33 | 34 | private static Context mContext; 35 | private static AccessToken mToken; 36 | 37 | public static S createService(Class serviceClass) { 38 | httpClient = new OkHttpClient.Builder(); 39 | builder = new Retrofit.Builder() 40 | .baseUrl(API_BASE_URL) 41 | .addConverterFactory(GsonConverterFactory.create()); 42 | 43 | OkHttpClient client = httpClient.build(); 44 | Retrofit retrofit = builder.client(client).build(); 45 | return retrofit.create(serviceClass); 46 | } 47 | 48 | public static S createService(Class serviceClass, AccessToken accessToken, Context c) { 49 | httpClient = new OkHttpClient.Builder(); 50 | builder = new Retrofit.Builder() 51 | .baseUrl(API_BASE_URL) 52 | .addConverterFactory(GsonConverterFactory.create()); 53 | 54 | if(accessToken != null) { 55 | mContext = c; 56 | mToken = accessToken; 57 | final AccessToken token = accessToken; 58 | httpClient.addInterceptor(new Interceptor() { 59 | @Override 60 | public Response intercept(Chain chain) throws IOException { 61 | Request original = chain.request(); 62 | 63 | Request.Builder requestBuilder = original.newBuilder() 64 | .header("Accept", "application/json") 65 | .header("Content-type", "application/json") 66 | .header("Authorization", 67 | token.getTokenType() + " " + token.getAccessToken()) 68 | .method(original.method(), original.body()); 69 | 70 | Request request = requestBuilder.build(); 71 | return chain.proceed(request); 72 | } 73 | }); 74 | 75 | httpClient.authenticator(new Authenticator() { 76 | @Override 77 | public Request authenticate(Route route, Response response) throws IOException { 78 | if(responseCount(response) >= 2) { 79 | // If both the original call and the call with refreshed token failed, 80 | // it will probably keep failing, so don't try again. 81 | return null; 82 | } 83 | 84 | // We need a new client, since we don't want to make another call using our client with access token 85 | APIClient tokenClient = createService(APIClient.class); 86 | Call call = tokenClient.getRefreshAccessToken(mToken.getRefreshToken(), 87 | mToken.getClientID(), mToken.getClientSecret(), API_OAUTH_REDIRECT, 88 | "refresh_token"); 89 | try { 90 | retrofit2.Response tokenResponse = call.execute(); 91 | if(tokenResponse.code() == 200) { 92 | AccessToken newToken = tokenResponse.body(); 93 | mToken = newToken; 94 | SharedPreferences prefs = mContext.getSharedPreferences(BuildConfig.APPLICATION_ID, Context.MODE_PRIVATE); 95 | prefs.edit().putBoolean("oauth.loggedin", true).apply(); 96 | prefs.edit().putString("oauth.accesstoken", newToken.getAccessToken()).apply(); 97 | prefs.edit().putString("oauth.refreshtoken", newToken.getRefreshToken()).apply(); 98 | prefs.edit().putString("oauth.tokentype", newToken.getTokenType()).apply(); 99 | 100 | return response.request().newBuilder() 101 | .header("Authorization", newToken.getTokenType() + " " + newToken.getAccessToken()) 102 | .build(); 103 | } else { 104 | return null; 105 | } 106 | } catch(IOException e) { 107 | return null; 108 | } 109 | } 110 | }); 111 | } 112 | 113 | OkHttpClient client = httpClient.build(); 114 | Retrofit retrofit = builder.client(client).build(); 115 | return retrofit.create(serviceClass); 116 | } 117 | 118 | private static int responseCount(Response response) { 119 | int result = 1; 120 | while ((response = response.priorResponse()) != null) { 121 | result++; 122 | } 123 | return result; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /app/src/main/java/nl/jpelgrm/retrofit2oauthrefresh/api/objects/AccessToken.java: -------------------------------------------------------------------------------- 1 | package nl.jpelgrm.retrofit2oauthrefresh.api.objects; 2 | 3 | public class AccessToken { 4 | 5 | private String access_token; 6 | private String token_type; 7 | private Integer expires_in; 8 | private String refresh_token; 9 | private String scope; 10 | private String client_id; 11 | private String client_secret; 12 | 13 | public String getAccessToken() { 14 | return access_token; 15 | } 16 | 17 | public void setAccessToken(String access_token) { 18 | this.access_token = access_token; 19 | } 20 | 21 | public String getTokenType() { 22 | // OAuth requires uppercase Authorization HTTP header value for token type 23 | if(!Character.isUpperCase(token_type.charAt(0))) { 24 | token_type = Character.toString(token_type.charAt(0)).toUpperCase() + token_type.substring(1); 25 | } 26 | 27 | return token_type; 28 | } 29 | 30 | public void setTokenType(String token_type) { 31 | this.token_type = token_type; 32 | } 33 | 34 | public int getExpiry() { 35 | return expires_in; 36 | } 37 | 38 | public void setExpiry(int expires_in) { 39 | this.expires_in = expires_in; 40 | } 41 | 42 | public String getRefreshToken() { 43 | return refresh_token; 44 | } 45 | 46 | public void setRefreshToken(String refresh_token) { 47 | this.refresh_token = refresh_token; 48 | } 49 | 50 | public String getScope() { 51 | return scope; 52 | } 53 | 54 | public void setScope(String scope) { 55 | this.scope = scope; 56 | } 57 | 58 | public String getClientID() { 59 | return client_id; 60 | } 61 | 62 | public void setClientID(String client_id) { 63 | this.client_id = client_id; 64 | } 65 | 66 | public String getClientSecret() { 67 | return client_secret; 68 | } 69 | 70 | public void setClientSecret(String client_secret) { 71 | this.client_secret = client_secret; 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_login.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 14 | 15 | 21 | 22 | 23 | 24 |