Skip to content

Instantly share code, notes, and snippets.

@burgalon
Last active July 15, 2023 08:29
Star You must be signed in to star a gist
Save burgalon/dd289d54098068701aee to your computer and use it in GitHub Desktop.
Implementing OAuth2 with AccountManager, Retrofit and Dagger

This snippet works with Doorkeeper and uses refresh token (with refresh token rotation).

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));
}
}
@johninvictus
Copy link

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.

@ferrerojosh
Copy link

Thanks

@aoben10
Copy link

aoben10 commented Jan 22, 2020

How do you test the authenticator since it relies on the account manager?

@japharr
Copy link

japharr commented Feb 7, 2020

Gracias!!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment