Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Delightful First-Party Authentication Experiences

Delightful First-Party Authentication Experiences

En nuestro objetivo por desarrollar productos asombrosos, debemos reducir la fricción cognitiva del usuario, y uno de los escollos que se encuentra un usuario nada mas abrir nuestra app, es el proceso de autenticado. En esta charla enseñaremos diferentes soluciones que nos ofrece Google para facilitar la experiencia y la implementación de este proceso.

Saúl Díaz

April 14, 2016
Tweet

More Decks by Saúl Díaz

Other Decks in Programming

Transcript

  1. XML Declaration <-- in xml/account_authenticator --> <?xml version="1.0" encoding="utf-8"?> <account-authenticator

    xmlns:android="..." android:accountType="com.sefford.beauthentic" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:smallIcon="@mipmap/ic_launcher" />
  2. Bound Service public class AuthenticatorService extends Service { AuthenticAuthenticator mAuthenticator;

    @Override public void onCreate() { mAuthenticator = new AuthenticAuthenticator(this); } @Override public IBinder onBind(Intent intent) { return mAuthenticator.getIBinder(); } }
  3. Bound Service <-- in AndroidManifest.xml --> <service android:name=".services.AuthenticatorService"> <intent-filter> <action

    android:name="android.accounts.AccountAuthenticator" /> </intent-filter> <meta-data android:name="android.accounts.AccountAuthenticator" android:resource="@xml/account_authenticator" /> </service>
  4. Authenticator Retrieve Authtoken Response Includes KEY_INTENT? Get Token from KEY_AUHTOKEN

    Access Service Invalidate Authtoken Launch Auth Activity Attempt re- login Refresh Authtoken Authentication Failure Done Error Credentials still valid Could not recover “Valid” token
  5. Authenticator • Bundle addAccount(AccountAuthenticatorResponse response, String accountType, String authTokenType, String[]

    requiredFeatures, Bundle options) • Bundle confirmCredentials(AccountAuthenticatorResponse response, Account account, Bundle options) • Bundle getAuthToken(AccountAuthenticatorResponse response, Account account, String authTokenType, Bundle options)
  6. Implementing GetAuthToken() gracefully Has cached Authtoken? Has Valid credentials? Add

    Account Confirm Credentials Return Authtoken Nope Nope Yes Yes Success Failure To Login Activity Keep on
  7. AccountManager • AccountManagerFuture<Bundle> confirmCredentials(final Account account, final Bundle options, final

    Activity activity, final AccountManagerCallback<Bundle> callback, final Handler handler); • AccountManagerFuture<Bundle> getAuthToken(final Account account, final String authTokenType, final Bundle options, final boolean notifyAuthFailure, AccountManagerCallback<Bundle> callback, Handler handler);
  8. AccountManager (cont.) • Account[] getAccountsByType(String type) • String peekAuthToken(final Account

    account, final String authTokenType ); • void setPassword(final Account account, final String password); • void setAuthToken(Account account, final String authTokenType, final String authToken); • boolean addAccountExplicitly(Account account, String password, Bundle userdata) • void setUserData(final Account account, final String key, final String value); • String getUserData(final Account account, final String key);
  9. Permissions Permission Before Marshmallow From Marshmallow Used by Dangerous AUTHENTICATE_ACCOUNTS

    REQUIRED REMOVED confirmCredentials() setUserData() setPassword() NO GET_ACCOUNTS REQUIRED Only if you access non- declared account types getAccountsByType() YES MANAGE_ACCOUNTS REQUIRED REMOVED addAccountExplicitly() removeAccount() NO USE_CREDENTIALS REQUIRED REMOVED getAuhtToken() peekAuthToken() NO
  10. Show me the code final AccountManager am = AccountManager. get(this);

    final Bundle data = new Bundle(); data.putString(AuthenticAuthenticator. EXTRA_PASSWORD, “kmaru”); final Account account = new Account(“jtkirk”, AuthenticAuthenticator. ACCOUNT_TYPE); am.confirmCredentials(account, data, null, new AccountManagerCallback<Bundle>() { public void run(AccountManagerFuture<Bundle> future) { final Bundle result = future.getResult(); if (result.getBoolean(AccountManager. KEY_BOOLEAN_RESULT)) { am. addAccountExplicitly(account, “password”,Bundle.EMPTY); am.setAuthToken( account, AuthenticAuthenticator. AUTHTOKEN_TYPE, result.getString(AccountManager. KEY_AUTHTOKEN)); } // We edited some try-catch and error control }, null);
  11. Setting up Google Sign-in 1. Get your Google Play Services

    configuration json 2. Add the Google Play Services plugin and Auth dependency a. classpath 'com.google.gms:google-services:2.0.0-alpha5' b. apply plugin: 'com.google.gms.google-services' c. compile 'com.google.android.gms:play-services-auth:8.4.0' 3. ????? 4. Profit!
  12. What’s ????? this.gso = new GoogleSignInOptions.Builder(GoogleSignInOptions. DEFAULT_SIGN_IN) .requestEmail() .requestIdToken( "backend

    id token") // One time authentication code .requestServerAuthCode( "backend id token"); .build(); this.client = new GoogleApiClient.Builder(context) .addApi(Auth. GOOGLE_SIGN_IN_API, gso) .build(); Intent signInIntent = Auth. GoogleSignInApi.getSignInIntent( this.client); activity.startActivityForResult(signInIntent, GOOGLE_SIGN_IN);
  13. What’s ????? @Override protected void onActivityResult( int requestCode, int resultCode,

    Intent data) { super.onActivityResult(requestCode, resultCode, data); switch (requestCode) { // Result returned from launching the Intent from GoogleSignInApi.getSignInIntent(...); case GoogleApiAdapter. GOOGLE_SIGN_IN: GoogleSignInResult result = Auth. GoogleSignInApi.getSignInResultFromIntent(data); if (result.isSuccess()) { // Signed in successfully, show authenticated UI. GoogleSignInAccount acct = result.getSignInAccount(); createGoogleAccount(acct); } break; }
  14. Back to the Authenticator @Override public Bundle confirmCredentials(AccountAuthenticatorResponse response, Account

    account, Bundle options) throws NetworkErrorException { return selectLoginStrategy(Type. values()[options.getInt( EXTRA_TYPE)]) .confirmCredential(response, account, options); }
  15. Confirm Credential final Bundle bundle = new Bundle(); if (TextUtils.

    isEmpty(data.getString(AccountManager. KEY_AUTHTOKEN))) { final OptionalPendingResult<GoogleSignInResult> pendingResult = Auth.GoogleSignInApi.silentSignIn( client); // We can recover the “session” from via silent sign-in if (pendingResult != null) { // This is done in the background, but in the Authenticator we are always in it final GoogleSignInResult googleSignInResult = pendingResult.get(); if (googleSignInResult != null) { // We fetch the account, refresh the Authtoken and we return the result final GoogleSignInAccount signInAccount = googleSignInResult.getSignInAccount(); if (signInAccount != null) { bundle.putString(AccountManager. KEY_AUTHTOKEN, signInAccount.getIdToken()); bundle.putString(AccountManager. KEY_ACCOUNT_NAME, signInAccount.getDisplayName()); } } } } else { bundle.putString(AccountManager. KEY_AUTHTOKEN, data.getString(AccountManager. KEY_AUTHTOKEN)); }
  16. Smartlock for passwords • Google Sign-In like capabilities everywhere •

    Properly done it is bidirectional for other platforms like device-to-site and viceversa.
  17. Saving credentials // Case for user/password credentials Auth.CredentialsApi.save(client, new Credential.Builder(

    "jtkirk") // ID of the .setPassword( "kmaru") .build()) .setCallback(callback); // Case for Google Provider final GoogleSignInAccount acct = result.getSignInAccount(); Auth.CredentialsApi.save(client, new Credential.Builder( acct.getEmail()) .setAccountType(IdentityProviders. GOOGLE) .setName( acct.getDisplayName()) . setProfilePictureUri (acct.getPhotoUrl()) .build()) .setCallback(callback);
  18. The results Ask for existing Credentials Signed in! Confirm Credentials

    w/ Google Confirm Credentials w/ backend Input login data Google Sign-in Google Sign-in Password Sign-in No credentials Password Sign-in Save Credentials
  19. Firebase GCM Device A Device B 1A. Request GCM Token

    2A. Save into list of devices 3A. Retrieve List 4A. Notify GCM of Login 1B. Request GCM Token 2B. Save into list of devices 5B. Receive Login Credentials
  20. Talk is cheap, show me the code! @Override public void

    onMessageReceived(String from, final Bundle data) { if (!Sessions. isLogged(am)) { final Account account = new Account(data.getString(LoginGCMNotificationService. EXTRA_NAME), AuthenticAuthenticator. ACCOUNT_TYPE); final int loginType = Integer. valueOf(data.get(LoginGCMNotificationService. EXTRA_TYPE).toString()); final Bundle authData = new Bundle(); authData.putInt(AuthenticAuthenticator. EXTRA_TYPE, loginType); authData.putString(AuthenticAuthenticator. EXTRA_PASSWORD, data.getString(LoginGCMNotificationService. EXTRA_PASSWORD)); // Superflous check, we could trust our backend anyway! final Bundle result = am.getAuthToken(account, authData, null, null, null).getResult(); if (result.getBoolean(AccountManager. KEY_BOOLEAN_RESULT)) { Sessions. addAccount(am, account, data.getString(LoginGCMNotificationService. EXTRA_PASSWORD), Bundle. EMPTY); am.setUserData(account, AuthenticAuthenticator. EXTRA_TYPE, Integer. toString(loginType)); Intent intent = new Intent(getApplicationContext(), LoggedActivity. class); intent.setFlags(Intent. FLAG_ACTIVITY_NEW_TASK); startActivity(intent); } }
  21. XML Declaration <-- in xml/message_sync.xml --> <sync-adapter xmlns:android="http://schemas.android.com/apk/res/android" android:accountType="com.sefford.beauthentic" android:allowParallelSyncs="false"

    android:contentAuthority="com.sefford.beauthentic.sync.messages" android:isAlwaysSyncable="true" android:supportsUploading="true" android:userVisible="true" />
  22. Bound Service public class SyncService extends Service { // Storage

    for an instance of the sync adapter private static AuthenticSyncAdapter syncAdapter = null; // Object to use as a thread-safe lock private static final Object sSyncAdapterLock = new Object(); @Override public void onCreate() { synchronized (sSyncAdapterLock) { if (syncAdapter == null) { syncAdapter = new AuthenticSyncAdapter(getApplicationContext(), true); } } } @Override public IBinder onBind(Intent intent) { return syncAdapter.getSyncAdapterBinder(); } }
  23. Bound Service <-- in AndroidManifest.xml --> <service android:name=".services.SyncService" android:exported="true" android:process=":sync">

    <intent-filter> <action android:name="android.content.SyncAdapter" /> </intent-filter> <meta-data android:name="android.content.SyncAdapter" android:resource="@xml/message_sync" /> </service>
  24. Content Provider public class MessageProvider extends ContentProvider { public static

    final String AUTHORITY = "com.sefford.beauthentic.sync.messages"; @Override public boolean onCreate() { return false; } @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { return null;} @Override public String getType(Uri uri) { return null;} @Override public Uri insert(Uri uri, ContentValues values) { return null; } @Override public int delete(Uri uri, String selection, String[] selectionArgs) { return 0; } @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { return 0; } }
  25. SyncAdapter implementation public class AuthenticSyncAdapter extends AbstractThreadedSyncAdapter { @Override public

    void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient client, final SyncResult syncResult) { final Account primaryAccount = Sessions.getPrimaryPhoneAccount(AccountManager.get(getContext())); if (primaryAccount != null) { retrieveMessageFromFirebase(primaryAccount, new ValueEventListenerAdapter() { @Override public void onDataChange(DataSnapshot snapshot) { syncResult.stats.numUpdates++; final Intent intent = new Intent(LoggedActivity.ACTION_REFRESH); intent.putExtra(LoggedActivity.EXTRA_MESSAGE, TextUtils.isEmpty(snapshot.getValue().toString()) ? "" : snapshot.getValue().toString()); getContext().sendBroadcast(intent); } @Override public void onCancelled(FirebaseError firebaseError) { syncResult.stats.numIoExceptions++; } }); } } }
  26. SyncAdapter API // Enables syncing ContentResolver. setIsSyncable(newAccount, authority, 1); //

    Enables autosyncing ContentResolver. setSyncAutomatically(newAccount, authority, true); // Adds a periodic sync in seconds ContentResolver. addPeriodicSync(newAccount, authority, Bundle. EMPTY, 60); // Performs an inmediate sync ContentResolver.requestSync(newAccount, authority, Bundle. EMPTY);