This snippet works with Doorkeeper and uses refresh token (with refresh token rotation).
-
-
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)); | |
} | |
} |
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!!
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? 😄