Skip to content

Instantly share code, notes, and snippets.

@StarWar
Forked from burgalon/AccountAuthenticator.java
Last active August 29, 2015 14:25
Show Gist options
  • Save StarWar/d986f07ecdb0200d34eb to your computer and use it in GitHub Desktop.
Save StarWar/d986f07ecdb0200d34eb to your computer and use it in GitHub Desktop.

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));
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment