Skip to content

Instantly share code, notes, and snippets.

@mr-archano
Forked from burgalon/AccountAuthenticator.java
Created January 16, 2016 11:40
Show Gist options
  • Select an option

  • Save mr-archano/c44ee1bf084cc2d742d9 to your computer and use it in GitHub Desktop.

Select an option

Save mr-archano/c44ee1bf084cc2d742d9 to your computer and use it in GitHub Desktop.

Revisions

  1. @burgalon burgalon revised this gist Jul 16, 2014. 1 changed file with 25 additions and 0 deletions.
    25 changes: 25 additions & 0 deletions build.gradle
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,25 @@
    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.+'
    }
  2. @burgalon burgalon revised this gist Jul 16, 2014. 1 changed file with 3 additions and 0 deletions.
    3 changes: 3 additions & 0 deletions BaseActivity.java
    Original file line number Diff line number Diff line change
    @@ -1,3 +1,6 @@
    // 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;
  3. @burgalon burgalon revised this gist Jul 16, 2014. 1 changed file with 106 additions and 0 deletions.
    106 changes: 106 additions & 0 deletions BaseActivity.java
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,106 @@
    @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);
    }
    }
  4. @burgalon burgalon revised this gist Jul 16, 2014. 1 changed file with 1 addition and 0 deletions.
    1 change: 1 addition & 0 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1 @@
    This snippet works with Doorkeeper and uses refresh token (with refresh token rotation).
  5. @burgalon burgalon created this gist Jul 16, 2014.
    110 changes: 110 additions & 0 deletions AccountAuthenticator.java
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,110 @@
    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;
    }
    }
    14 changes: 14 additions & 0 deletions AccountAuthenticatorService.java
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,14 @@
    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;
    }
    }
    28 changes: 28 additions & 0 deletions AndroidManifest.xml
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,28 @@
    <?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>
    79 changes: 79 additions & 0 deletions ApiAuthenticator.java
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,79 @@
    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;
    }
    }
    23 changes: 23 additions & 0 deletions ApiHeaders.java
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,23 @@
    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");
    }
    }
    44 changes: 44 additions & 0 deletions ApiModule.java
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,44 @@
    @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);
    }
    }
    23 changes: 23 additions & 0 deletions ApiService.java
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,23 @@
    // 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);
    }
    13 changes: 13 additions & 0 deletions AuthConstants.java
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,13 @@
    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;
    }
    61 changes: 61 additions & 0 deletions LoginFragment.java
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,61 @@
    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));
    }

    }
    8 changes: 8 additions & 0 deletions auhenticator.xml
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,8 @@
    // 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" />