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)); | |
} | |
} |
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!!
+1 !!!