Smart Lock: A new way for seamless Sign-In

Smart lock Aka Smart lock for the password is a Google API  which allows the user to login into the application without entering the username and password !! Isn’t this just amazing? Yes, I agree it is, with this API the application developers can design their application in such a way that the username/passwords or tokens can be stored/saved to your google account and synced across devices. Do not worry,  username/password is well encrypted before being synced/saved to your google account.

In this blog post we would be covering the following points:

  • Dependencies required to use the Smart lock
  • How to save valid Credentials
  • How to request saved credentials and use them to sign-in the user.
  • Save and retrieve the multiple credentials
  • Delete invalid credentials

Prerequisite

Google Play Sevices latest version.

  • Adding Dependency
dependencies {
    compile fileTree(include: ['*.jar'], dir: 'libs')
    testCompile 'junit:junit:4.12'
    compile 'com.google.android.gms:play-services-auth:11.0.4'
    ...........................
}
  • Adding Code to save Valid Credentials :

Create an Activity requesting username/password from the user, let’s name this activity as LoginActivity.Add a GoogleApiClient instance variable in it and instantiate this variable in the onCreate() method of the activity.

private GoogleApiClient mGoogleApiClient;

LoginActivity should implement GoogleApiClient.ConnectionCallbacks and GoogleApiClient.OnConnectionFailedListener.

LoginActivity extends AppCompatActivity implements
        GoogleApiClient.ConnectionCallbacks,
        GoogleApiClient.OnConnectionFailedListener

Hence three methods would get Overridden in the LoginActivity.java

@Override
public void onConnected(Bundle bundle) {
    Log.d(TAG, "onConnected");
}

@Override
public void onConnectionSuspended(int cause) {
    Log.d(TAG, "onConnectionSuspended: " + cause);
}

@Override
public void onConnectionFailed(ConnectionResult connectionResult) {
    Log.d(TAG, "onConnectionFailed: " + connectionResult);
}

Now, when the user enters the details in the form field(username, password) and clicks on the login button, check whether the entered credentials are valid or not if yes:

Create Credential Object with the help of username and password.

Credential credential = new Credential.Builder(username)
        .setPassword(password)
        .build();

Save the Credential Object

private void saveCredential(final Credential credential) {
    Auth.CredentialsApi.save(mGoogleApiClient,
            credential).setResultCallback(new ResultCallback<Status>() {
        @Override
        public void onResult(Status status) 
                if (status.isSuccess()) {
                    Log.d(TAG, "Credential saved");
                    goToHomePage();
                } else {
                    Log.d(TAG, "Attempt to save credential failed " +
                            status.getStatusMessage() + " " +status.getStatusCode());
                    resolveResult(status, RC_SAVE);
            }
        }
    });

If the credential user is trying to save is new, the user must first give permission to save it. Let’s add a method to ask the user for the permission :

private void resolveResult(Status status, int requestCode) {
    // We don't want to fire multiple resolutions at once since that can result
    // in stacked dialogs after rotation or another similar event.
    if (mIsResolving) {
        Log.w(TAG, "resolveResult: already resolving.");
        return;
    }
    if (status.hasResolution()) {
        Log.d(TAG, "STATUS: RESOLVING");
        try {
            status.startResolutionForResult(this, requestCode);
            mIsResolving = true;
        } catch (IntentSender.SendIntentException e) {
            Log.e(TAG, "STATUS: Failed to send resolution.", e);
        }
    } else {
        goToHomePage();
      }
}

You might be wondering, what is this goToHomePage() method,startResolutionForResult() and what are its usages?

Well, in the method call resolveResult() we are asking permission to save the credentials, what if the user denies the permission? what would happen then? In order to handle this scenario, we have added this method so that the user gets redirected to the next screen.

startResolutionForResult() method, The result from this method would be sent back to the calling activity through onActivityResult method();

Now, What happens when the user gives permission or does not give permission? To handle that we add another method to our activity.

@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
   if (requestCode == RC_SAVE) {
        Log.d(TAG, "Result code: " + resultCode);
        if (resultCode == RESULT_OK) {
            Log.d(TAG, "Credential Save: OK");
        } else {
            Log.e(TAG, "Credential Save Failed");
        }
        goToHomePage();
    }
    mIsResolving = false;
}
  • How to request the saved credentials in the application

In the callback method of GoogleApiClient “onConnected” method, when the connection is established call the method requestCredentials().

@Override
public void onConnected(Bundle bundle) {
    Log.d(TAG, "onConnected");
    requestCredentials();
}

Let’s go the implementation of requestCredentialsMethod in the login activity.

private void requestCredentials() {
        mIsRequesting = true;
        CredentialRequest request = new CredentialRequest.Builder()
                .setPasswordLoginSupported(true)
                .setAccountTypes(IdentityProviders.GOOGLE)
                .build();

        Auth.CredentialsApi.request(mGoogleApiClient, request).setResultCallback(
                new ResultCallback<CredentialRequestResult>() {
                    @Override
                    public void onResult(CredentialRequestResult credentialRequestResult) {
                        mIsRequesting = false;
                        Status status = credentialRequestResult.getStatus();
                        if (credentialRequestResult.getStatus().isSuccess()) {
                            /* Successfully read the credential without any user interaction, this
                             means there was only a single credential and you want user to auto
                             sign-in */
                            Credential credential = credentialRequestResult.getCredential();
                            processRetrievedCredential(credential);
                        } else if (status.getStatusCode() == CommonStatusCodes.RESOLUTION_REQUIRED) {
                            /* This is most likely the case where the user has multiple saved
                             credentials and needs to pick one.*/
                              resolveResult(status, RC_READ);
                        } else if (status.getStatusCode() == CommonStatusCodes.SIGN_IN_REQUIRED) {
                            /* This is most likely the case where the user does not currently
                               have any saved credentials and thus needs to provide a username
                               and password to sign in.
                               write your next workflow step here like enabling your login button */
                        } else {
                                Log.w(TAG, "Unrecognized status code: " + status.getStatusCode());
                                //write your next workflow step here like enabling your login button
                        }
                    }
                }
        );
    }

Let’s check out the processRetrievedCredential(credential) method :

private void processRetrievedCredential(Credential credential) {
    String accountType = credential.getAccountType();
    if(accountType == null) {
        if (isValidCredential(credential)) {
            goToHomePage();
        } else {
            /* This is likely due to the credential being changed outside of
             Smart Lock,ie: away from Android or Chrome. The credential should be deleted
             and the user allowed to enter a valid credential. */
            Toast.makeText(this, "Retrieved credentials are invalid, so will be deleted.", Toast.LENGTH_LONG).show();
            deleteCredential(credential); // we would talk about this a little later.
            requestCredentials();            
        }
    }
    else if (accountType.equals(IdentityProviders.GOOGLE)) {
        // The user has previously signed in with Google Sign-In. Silently
        // sign in the user with the same ID.
        // See https://developers.google.com/identity/sign-in/android/
        GoogleSignInOptions gso =
                new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
                        .requestEmail()
                        .build();
        mGoogleApiClient = new GoogleApiClient.Builder(this)
                .enableAutoManage(this, this)
                .addApi(Auth.GOOGLE_SIGN_IN_API, gso)
                .setAccountName(credential.getId())
                .build();
        OptionalPendingResult<GoogleSignInResult> opr =
                Auth.GoogleSignInApi.silentSignIn(mGoogleApiClient);
        // ...
    }
  • Retrieving Multiple Saved Credentials

There might be a scenario where the user has saved multiple username and password combination in the system. To handle such a scenario we have called resolveResult(status, RC_READ) method in the requestCredentialsMethod(). 

Now, we need to update the onActivityResult() method to handle the reading of credentials, so our updated onActivityResult() method becomes:

@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    if (requestCode == RC_READ) {
        if (resultCode == RESULT_OK) {
            Credential credential = data.getParcelableExtra(Credential.EXTRA_KEY);
            processRetrievedCredential(credential);
        } else {
            Log.e(TAG, "Credential Read: NOT OK");
        }
    } else if (requestCode == RC_SAVE) {
        Log.d(TAG, "Result code: " + resultCode);
        if (resultCode == RESULT_OK) {
            Log.d(TAG, "Credential Save: OK");
        } else {
            Log.e(TAG, "Credential Save Failed");
        }
        goToHomePage();
    }
    mIsResolving = false;
}
  • Deleting a stored credential

If a user changes their credential on a platform which doesn’t support SmartLock, for example, you have saved your password the first time you log in to the application(APP A). Let’s assume you are using same account details for another app(APP B) and you change your account password through that application. Now the next time you would try to login with the saved credentials in the application (APP A) the server would return you invalid credentials. So at this point in time, you need to delete the saved credentials and then save the new one. It can be done with the following call :

private void deleteCredential(Credential credential) {
    Auth.CredentialsApi.delete(mGoogleApiClient,
            credential).setResultCallback(new ResultCallback<Status>() {
        @Override
        public void onResult(Status status) {
            if (status.isSuccess()) {
                Log.d(TAG, "Credential successfully deleted.");
            } else {
                /* This may be due to the credential not existing, possibly
                 already deleted via another device/app. */
                Log.d(TAG, "Credential not deleted successfully.");
            }
        }
    });
}
  • Preventing Automatic Sign-in

In case you have enabled auto login in the app, the user would re-login in the app automatically by using the stored credentials. In our sample code, the user is redirected to homePage on successful login. In order to avoid that, we have added a logout button on our homepage. When user taps on this button, we need to write following piece of code on logout button click listener in order to prevent automatic sign-in.Kindly note the HomePage Activity would also need to implement the ConnectionCallbacks and must have the GoogleApiClient object with it.

Auth.CredentialsApi.disableAutoSignIn(mGoogleApiClient);
Intent intent = new Intent(view.getContext(), LoginActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);
finish();

Smart Lock download the complete project from here.

Note: Feel free to contact me,for any suggestion/improvements/modifications.