This snippet works with Doorkeeper and uses refresh token (with refresh token rotation).
-
Star
(214)
You must be signed in to star a gist -
Fork
(59)
You must be signed in to fork a gist
-
-
Save burgalon/dd289d54098068701aee to your computer and use it in GitHub Desktop.
| public class AccountAuthenticator extends AbstractAccountAuthenticator { | |
| private final Context context; | |
| @Inject @ClientId String clientId; | |
| @Inject @ClientSecret String clientSecret; | |
| @Inject ApiService apiService; | |
| public AccountAuthenticator(Context context) { | |
| super(context); | |
| this.context = context; | |
| ((App) context.getApplicationContext()).inject(this); | |
| } | |
| /* | |
| * The user has requested to add a new account to the system. We return an intent that will launch our login screen | |
| * if the user has not logged in yet, otherwise our activity will just pass the user's credentials on to the account | |
| * manager. | |
| */ | |
| @Override | |
| public Bundle addAccount(AccountAuthenticatorResponse response, String accountType, | |
| String authTokenType, String[] requiredFeatures, Bundle options) { | |
| Timber.v("addAccount()"); | |
| final Intent intent = new Intent(context, AuthenticatorActivity.class); | |
| intent.putExtra(AccountManager.KEY_ACCOUNT_TYPE, accountType); | |
| intent.putExtra(LoginFragment.PARAM_AUTHTOKEN_TYPE, authTokenType); | |
| intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response); | |
| final Bundle bundle = new Bundle(); | |
| bundle.putParcelable(AccountManager.KEY_INTENT, intent); | |
| return bundle; | |
| } | |
| @Override | |
| public Bundle confirmCredentials(AccountAuthenticatorResponse response, Account account, Bundle options) { | |
| return null; | |
| } | |
| @Override | |
| public Bundle editProperties(AccountAuthenticatorResponse response, String accountType) { | |
| return null; | |
| } | |
| // See /Applications/android-sdk-macosx/samples/android-18/legacy/SampleSyncAdapter/src/com/example/android/samplesync/authenticator/Authenticator.java | |
| // Also take a look here https://github.com/github/android/blob/d6ba3f9fe2d88967f56e9939d8df7547127416df/app/src/main/java/com/github/mobile/accounts/AccountAuthenticator.java | |
| @Override | |
| public Bundle getAuthToken(AccountAuthenticatorResponse response, Account account, String authTokenType, | |
| Bundle options) throws NetworkErrorException { | |
| Timber.d("getAuthToken() account="+account.name+ " type="+account.type); | |
| final Bundle bundle = new Bundle(); | |
| // If the caller requested an authToken type we don't support, then | |
| // return an error | |
| if (!authTokenType.equals(AUTHTOKEN_TYPE)) { | |
| Timber.d("invalid authTokenType" + authTokenType); | |
| bundle.putString(AccountManager.KEY_ERROR_MESSAGE, "invalid authTokenType"); | |
| return bundle; | |
| } | |
| // Extract the username and password from the Account Manager, and ask | |
| // the server for an appropriate AuthToken | |
| final AccountManager accountManager = AccountManager.get(context); | |
| // Password is storing the refresh token | |
| final String password = accountManager.getPassword(account); | |
| if (password != null) { | |
| Timber.i("Trying to refresh access token"); | |
| try { | |
| AccessToken accessToken = apiService.refreshAccessToken(password, clientId, clientSecret); | |
| if (accessToken!=null && !TextUtils.isEmpty(accessToken.accessToken)) { | |
| bundle.putString(AccountManager.KEY_ACCOUNT_NAME, account.name); | |
| bundle.putString(AccountManager.KEY_ACCOUNT_TYPE, ACCOUNT_TYPE); | |
| bundle.putString(AccountManager.KEY_AUTHTOKEN, accessToken.accessToken); | |
| accountManager.setPassword(account, accessToken.refreshToken); | |
| return bundle; | |
| } | |
| } catch (Exception e) { | |
| Timber.e(e, "Failed refreshing token."); | |
| } | |
| } | |
| // Otherwise... start the login intent | |
| Timber.i("Starting login activity"); | |
| final Intent intent = new Intent(context, AuthenticatorActivity.class); | |
| intent.putExtra(LoginFragment.PARAM_USERNAME, account.name); | |
| intent.putExtra(LoginFragment.PARAM_AUTHTOKEN_TYPE, authTokenType); | |
| intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response); | |
| bundle.putParcelable(AccountManager.KEY_INTENT, intent); | |
| return bundle; | |
| } | |
| @Override | |
| public String getAuthTokenLabel(String authTokenType) { | |
| return authTokenType.equals(AUTHTOKEN_TYPE) ? authTokenType : null; | |
| } | |
| @Override | |
| public Bundle hasFeatures(AccountAuthenticatorResponse response, Account account, String[] features) | |
| throws NetworkErrorException { | |
| final Bundle result = new Bundle(); | |
| result.putBoolean(KEY_BOOLEAN_RESULT, false); | |
| return result; | |
| } | |
| @Override | |
| public Bundle updateCredentials(AccountAuthenticatorResponse response, Account account, String authTokenType, | |
| Bundle options) { | |
| return null; | |
| } | |
| } |
| public class AccountAuthenticatorService extends Service { | |
| private static AccountAuthenticator AUTHENTICATOR = null; | |
| @Override | |
| public IBinder onBind(Intent intent) { | |
| return intent.getAction().equals(ACTION_AUTHENTICATOR_INTENT) ? getAuthenticator().getIBinder() : null; | |
| } | |
| private AccountAuthenticator getAuthenticator() { | |
| if (AUTHENTICATOR == null) | |
| AUTHENTICATOR = new AccountAuthenticator(this); | |
| return AUTHENTICATOR; | |
| } | |
| } |
| <?xml version="1.0" encoding="utf-8"?> | |
| <manifest | |
| xmlns:android="http://schemas.android.com/apk/res/android" | |
| package="com.example.app"> | |
| <service | |
| android:name=".authenticator.AccountAuthenticatorService" | |
| android:exported="false" | |
| android:process=":auth"> | |
| <intent-filter> | |
| <action android:name="android.accounts.AccountAuthenticator"/> | |
| </intent-filter> | |
| <meta-data | |
| android:name="android.accounts.AccountAuthenticator" | |
| android:resource="@xml/authenticator"/> | |
| </service> | |
| <activity | |
| android:name=".authenticator.AuthenticatorActivity" | |
| android:screenOrientation="portrait" | |
| android:configChanges="orientation|keyboardHidden|screenSize" | |
| android:excludeFromRecents="true" | |
| android:hardwareAccelerated="true"/> | |
| </application> | |
| </manifest> |
| public class ApiAuthenticator implements OkAuthenticator { | |
| AccountManager accountManager; | |
| Application application; | |
| @Inject | |
| public ApiAuthenticator(Application application, AccountManager accountManager) { | |
| this.application = application; | |
| this.accountManager = accountManager; | |
| } | |
| @Override | |
| public Credential authenticate(Proxy proxy, URL url, List<Challenge> challenges) | |
| throws IOException { | |
| // Do not try to authenticate oauth related endpoints | |
| if (url.getPath().startsWith("/oauth")) return null; | |
| for (Challenge challenge : challenges) { | |
| if (challenge.getScheme().equals("Bearer")) { | |
| Account[] accounts = accountManager.getAccountsByType(AuthConstants.ACCOUNT_TYPE); | |
| if (accounts.length != 0) { | |
| String oldToken = accountManager.peekAuthToken(accounts[0], | |
| AuthConstants.AUTHTOKEN_TYPE); | |
| if (oldToken != null) { | |
| Timber.d("invalidating auth token"); | |
| accountManager.invalidateAuthToken(AuthConstants.ACCOUNT_TYPE, oldToken); | |
| } | |
| try { | |
| Timber.d("calling accountManager.blockingGetAuthToken"); | |
| String token = accountManager.blockingGetAuthToken(accounts[0], | |
| AuthConstants.AUTHTOKEN_TYPE, false); | |
| if(token==null) { | |
| accountManager.removeAccount(accounts[0], null, null); | |
| } | |
| // Do not retry certain URLs | |
| //if (url.getPath().startsWith("/donotretry")) { | |
| // return null; | |
| //} else if (token != null) { | |
| if (token != null) { | |
| return token(token); | |
| } | |
| } catch (OperationCanceledException e) { | |
| e.printStackTrace(); | |
| } catch (AuthenticatorException e) { | |
| e.printStackTrace(); | |
| } | |
| } | |
| } | |
| } | |
| return null; | |
| } | |
| private Credential token(String token) { | |
| try { | |
| // TODO: when there is support for different types of Credentials, stop using reflection | |
| Constructor<?> constructor = Credential.class.getDeclaredConstructor(String.class); | |
| Assert.assertTrue(Modifier .isPrivate(constructor.getModifiers())); | |
| constructor.setAccessible(true); | |
| return (Credential) constructor.newInstance("Bearer " + token); | |
| } catch (InstantiationException e) { | |
| e.printStackTrace(); | |
| } catch (IllegalAccessException e) { | |
| e.printStackTrace(); | |
| } catch (NoSuchMethodException e) { | |
| e.printStackTrace(); | |
| } catch (InvocationTargetException e) { | |
| e.printStackTrace(); | |
| } | |
| return null; | |
| } | |
| @Override | |
| public Credential authenticateProxy(Proxy proxy, URL | |
| url, List<Challenge> challenges) throws IOException { | |
| return null; | |
| } | |
| } |
| public final class ApiHeaders implements RequestInterceptor { | |
| private Application application; | |
| @Inject | |
| public ApiHeaders(Application application) { | |
| this.application = application; | |
| } | |
| @Override | |
| public void intercept(RequestFacade request) { | |
| AccountManager accountManager = AccountManager.get(application); | |
| Account[] accounts = accountManager.getAccountsByType(AuthConstants.ACCOUNT_TYPE); | |
| if (accounts.length != 0) { | |
| String token = | |
| accountManager.peekAuthToken(accounts[0], AuthConstants.AUTHTOKEN_TYPE); | |
| if (token != null) { | |
| request.addHeader("Authorization", "Bearer " + token); | |
| } | |
| } | |
| request.addHeader("Accept", "application/javascript, application/json"); | |
| } | |
| } |
| @Module( | |
| complete = false, | |
| library = true | |
| ) | |
| public final class ApiModule { | |
| public static final String PRODUCTION_API_URL = "http://api.pow-app.192.168.56.1.xip.io/"; | |
| private static final String CLIENT_ID = "CLIENT_ID"; | |
| private static final String CLIENT_SECRET = "CLIENT_SECRET"; | |
| @Provides @Singleton @ClientId String provideClientId() { | |
| return CLIENT_ID; | |
| } | |
| @Provides @Singleton @ClientSecret String provideClientSecret() { | |
| return CLIENT_SECRET; | |
| } | |
| @Provides @Singleton Endpoint provideEndpoint() { | |
| return Endpoints.newFixedEndpoint(PRODUCTION_API_URL); | |
| } | |
| @Provides @Singleton Client provideClient(OkHttpClient client) { | |
| return new OkClient(client); | |
| } | |
| @Provides @Singleton | |
| RestAdapter provideRestAdapter(Endpoint endpoint, Client client, ApiHeaders headers, Gson gson) { | |
| return new RestAdapter.Builder() | |
| .setClient(client) | |
| .setEndpoint(endpoint) | |
| .setConverter(new GsonConverter(gson)) | |
| .setRequestInterceptor(headers) | |
| .setErrorHandler(new RestErrorHandler()) | |
| .build(); | |
| } | |
| @Provides @Singleton ApiService provideApiService(RestAdapter restAdapter) { | |
| return restAdapter.create(ApiService.class); | |
| } | |
| @Provides @Singleton ApiDatabase provideApiDatabase(ApiService service) { | |
| return new ApiDatabase(service); | |
| } | |
| } |
| // Retrofit interface | |
| public interface ApiService { | |
| // Auth | |
| @FormUrlEncoded | |
| @POST("/oauth/token?grant_type=password") AccessToken getAccessToken( | |
| @Field("username") String email, | |
| @Field("password") String password, | |
| @Field("client_id") String clientId, | |
| @Field("client_secret") String clientSecret); | |
| @FormUrlEncoded | |
| @POST("/oauth/token?grant_type=refresh_token") AccessToken refreshAccessToken( | |
| @Field("refresh_token") String refreshToken, | |
| @Field("client_id") String clientId, | |
| @Field("client_secret") String clientSecret); | |
| @FormUrlEncoded | |
| @POST("/oauth/token?grant_type=password") Observable<AccessToken> getAccessTokenObservable( | |
| @Field("username") String email, | |
| @Field("password") String password, | |
| @Field("client_id") String clientId, | |
| @Field("client_secret") String clientSecret); | |
| } |
| // src/main/res/xml/authenticator.xml | |
| <?xml version="1.0" encoding="utf-8"?> | |
| <account-authenticator xmlns:android="http://schemas.android.com/apk/res/android" | |
| android:accountType="com.example" | |
| android:icon="@drawable/app_icon" | |
| android:label="@string/application_name" | |
| android:smallIcon="@drawable/app_icon" /> |
| public interface AuthConstants { | |
| // Account type id | |
| String ACCOUNT_TYPE = "com.example"; | |
| // Account name | |
| String ACCOUNT_NAME = "Example"; | |
| // Provider ID | |
| String PROVIDER_AUTHORITY = "com.example.sync"; | |
| // Auth token type | |
| String AUTHTOKEN_TYPE = ACCOUNT_TYPE; | |
| } |
| // An activity which handles listening to AccountManager changes and invoking AuthenticatorActivity if no account are available | |
| // Also hanldes Dagger injections, provides an Otto bus, and allows subscription to observables | |
| // while listening to activity lifecycle | |
| @SuppressLint("Registered") public class BaseActivity extends FragmentActivity | |
| implements OnAccountsUpdateListener { | |
| @Inject AppContainer appContainer; | |
| @Inject ScopedBus bus; | |
| @Inject AccountManager accountManager; | |
| private ViewGroup container; | |
| private ObjectGraph activityGraph; | |
| private SubscriptionManager<Activity> subscriptionManager; | |
| @Override protected void onCreate(Bundle savedInstanceState) { | |
| super.onCreate(savedInstanceState); | |
| buildActivityGraphAndInject(); | |
| // Inject any extras | |
| Dart.inject(this); | |
| } | |
| private void buildActivityGraphAndInject() { | |
| // Create the activity graph by .plus-ing our modules onto the application graph. | |
| App app = App.get(this); | |
| activityGraph = app.getApplicationGraph().plus(getModules().toArray()); | |
| // Inject ourselves so subclasses will have dependencies fulfilled when this method returns. | |
| activityGraph.inject(this); | |
| container = appContainer.get(this, app); | |
| } | |
| /** Inject the given object into the activity graph. */ | |
| public void inject(Object o) { | |
| activityGraph.inject(o); | |
| } | |
| /** | |
| * A list of modules to use for the individual activity graph. Subclasses can override this | |
| * method to provide additional modules provided they call and include the modules returned by | |
| * calling {@code super.getModules()}. | |
| */ | |
| protected List<Object> getModules() { | |
| return Arrays.<Object>asList(new ActivityModule(this)); | |
| } | |
| @Override protected void onResume() { | |
| super.onResume(); | |
| bus.resumed(); | |
| bus.register(this); | |
| // Watch to make sure the account still exists. | |
| if(requireLogin()) accountManager.addOnAccountsUpdatedListener(this, null, true); | |
| } | |
| @Override protected void onPause() { | |
| bus.unregister(this); | |
| bus.paused(); | |
| if(requireLogin()) accountManager.removeOnAccountsUpdatedListener(this); | |
| super.onPause(); | |
| } | |
| protected boolean requireLogin() { | |
| return true; | |
| } | |
| @Override protected void onDestroy() { | |
| // Eagerly clear the reference to the activity graph to allow it to be garbage collected as | |
| // soon as possible. | |
| activityGraph = null; | |
| if(subscriptionManager!=null) subscriptionManager.unsubscribeAll(); | |
| super.onDestroy(); | |
| } | |
| protected void inflateLayout(int layoutResID) { | |
| getLayoutInflater().inflate(layoutResID, container); | |
| // Inject Views | |
| ButterKnife.inject(this); | |
| } | |
| public static BaseActivity get(Fragment fragment) { | |
| return (BaseActivity) fragment.getActivity(); | |
| } | |
| @Override public void onAccountsUpdated(Account[] accounts) { | |
| for (Account account : accounts) { | |
| if (AuthConstants.ACCOUNT_TYPE.equals(account.type)) { | |
| return; | |
| } | |
| } | |
| // No accounts so start the authenticator activity | |
| Intent intent = new Intent(this, AuthenticatorActivity.class); | |
| startActivity(intent); | |
| finish(); | |
| } | |
| protected <O> Subscription subscribe(final Observable<O> source, final Observer<O> observer) { | |
| if (subscriptionManager == null) { | |
| subscriptionManager = new ActivitySubscriptionManager(this); | |
| } | |
| return subscriptionManager.subscribe(source, observer); | |
| } | |
| } |
| dependencies { | |
| .... | |
| compile 'com.android.support:appcompat-v7:19.+' | |
| compile 'com.squareup.dagger:dagger:1.2.1' | |
| provided 'com.squareup.dagger:dagger-compiler:1.2.1' | |
| compile 'com.squareup:otto:1.3.+' | |
| compile 'com.squareup.okhttp:okhttp:1.5.+' | |
| compile 'com.squareup.picasso:picasso:2.2.0' | |
| compile 'com.squareup.retrofit:retrofit:1.5.+' | |
| debugCompile 'com.squareup.retrofit:retrofit-mock:1.5.+' | |
| compile 'com.jakewharton:butterknife:5.1.+' | |
| compile 'com.jakewharton.timber:timber:2.2.2' | |
| debugCompile 'com.jakewharton.madge:madge:1.1.1' | |
| debugCompile 'com.jakewharton.scalpel:scalpel:1.1.1' | |
| compile 'com.netflix.rxjava:rxjava-core:0.19.+' | |
| compile 'com.netflix.rxjava:rxjava-android:0.19.+' | |
| compile 'com.f2prateek.dart:dart:1.1.+' | |
| } |
| class LoginFragment extends BaseFragment { | |
| @Inject AccountManager accountManager; | |
| @InjectView(R.id.et_email) EditText emailText; | |
| @InjectView(R.id.et_password) EditText passwordText; | |
| @InjectView(R.id.sign_in) Button signInButton; | |
| @Inject @ClientId String clientId; | |
| @Inject @ClientSecret String clientSecret; | |
| ... | |
| private void doLogin(final String email, String password) { | |
| Observable<AccessToken> accessTokenObservable = | |
| apiService.getAccessTokenObservable(email, password, | |
| clientId, | |
| clientSecret); | |
| subscribe(accessTokenObservable, new EndlessObserver<AccessToken>() { | |
| @Override public void onNext(AccessToken accessToken) { | |
| Account account = addOrFindAccount(email, accessToken.refreshToken); | |
| // accountManager.setUserData(account, AccountAuthenticator.USER_ID, accessToken.userId); | |
| accountManager.setAuthToken(account, AuthConstants.AUTHTOKEN_TYPE, accessToken.accessToken); | |
| finishAccountAdd(email, accessToken.accessToken, accessToken.refreshToken); | |
| } | |
| @Override public void onError(Throwable throwable) { | |
| Timber.e(throwable, "Could not sign in"); | |
| Toast.makeText(getActivity(), throwable.getMessage(), Toast.LENGTH_LONG).show(); | |
| } | |
| }); | |
| } | |
| private Account addOrFindAccount(String email, String password) { | |
| Account[] accounts = accountManager.getAccountsByType(AuthConstants.ACCOUNT_TYPE); | |
| Account account = accounts.length != 0 ? accounts[0] : | |
| new Account(email, AuthConstants.ACCOUNT_TYPE); | |
| if (accounts.length == 0) { | |
| accountManager.addAccountExplicitly(account, password, null); | |
| } else { | |
| accountManager.setPassword(accounts[0], password); | |
| } | |
| return account; | |
| } | |
| private void finishAccountAdd(String accountName, String authToken, String password) { | |
| final Intent intent = new Intent(); | |
| intent.putExtra(AccountManager.KEY_ACCOUNT_NAME, accountName); | |
| intent.putExtra(AccountManager.KEY_ACCOUNT_TYPE, AuthConstants.ACCOUNT_TYPE); | |
| if (authToken != null) | |
| intent.putExtra(AccountManager.KEY_AUTHTOKEN, authToken); | |
| intent.putExtra(AccountManager.KEY_PASSWORD, password); | |
| setAccountAuthenticatorResult(intent.getExtras()); | |
| getActivity().setResult(Activity.RESULT_OK, intent); | |
| getActivity().finish(); | |
| // Go back to the main activity | |
| startActivity(new Intent(activityContext, MainActivity.class)); | |
| } | |
| } |
Hi,
Please, could you share this example with me?
Thank you!
Hi,
Please share with me a complete example too ;-).
Tk.
Hi,
I also want your nice example. :-)
Is it possible to share?
Thanks.
+1
+1 !!!
This looks fantastic, though there are so many missing pieces that it's really hard to learn from.
It looks like this example is largely based on this project here: https://github.com/f2prateek/android-couchpotato; I can see that many of the missing classes and references are found in that project.
I know this is a year old, but any chance you could upload the Android project in its entirety? 😄
For anyone who's looking for a base implementation of Account Authenticator check out, https://github.com/Udinic/AccountAuthenticator/blob/master/src/com/udinic/accounts_authenticator_example/authentication/AuthenticatorActivity.java first before starting on this.
Can you please share the example with me too? I miss too many dependencies I'm not able to figure out how to setup correctly the project.
Thanks
Hey,
I would really like to learn from the complete example =)!
If you could share it with me, you would help me a lot!
With best regards!
Can you please share an updated example? That would be really cool!
Hi,
I also want your nice example.Is it possible to authenticate to my own oauth server other than google using this example?
Is it possible to share?
Thanks.
I am not too sure which constants value for LoginFragment.
I found complete example for OAuth2 using concept presented in this gist.
Please look at:
https://github.com/Udinic/AccountAuthenticator
and tutorial:
http://blog.udinic.com/2013/04/24/write-your-own-android-authenticator/
Please make the example with rxandroid Observable
At first, I did not understand this gist but after many posts on Android Authenticator, this gist is a great resource.
Thank you for sharing.
Thanks
How do you test the authenticator since it relies on the account manager?
Gracias!!
Hi Burgalon,
I would really appreciate it if you can share this example with me. I tried to add this to Android Studio but I am getting errors with libs.