In this codelab, you'll learn how to use enable Single Sign-on (SSO) with Chrome Custom Tabs via the AppAuth library, and optionally push managed configuration to provide a user login hint.

What you'll learn

What you'll need

How will you use this tutorial?

Read it through only Read it and complete the exercises

How would rate your experience with building Android apps?

Novice Intermediate Proficient

You can either download all the sample code to your computer...

Download Zip

...or clone the GitHub repository from the command line.

$ git clone https://github.com/googlecodelabs/appauth-android-codelab

First, let's see what the finished sample app looks like. With the code downloaded, the following instructions describe how to open the completed sample app in Android Studio.

  1. Open Android Studio
  2. Open the appauth-android_codelab_sso_managed directory from the sample code folder (Select the Open an existing Android Studio project option on the welcome screen, or File > Open).
  3. Plug in your Android device and click the Run button. You should see the home screen of the sample app appear after a few seconds. Try signing-in to see how it works.

Frequently Asked Questions

Now that you've seen AppAuth in action, it's time to use AppAuth for authentication in your own app.

In Android Studio open the appauth-android_codelab_init directory from the sample code folder (File > Open).

The project won't build right away, as we have some boilerplate for AppAuth just to preserve the state and connect some buttons. You'll need to add the AppAuth gradle dependency in the next step before it can build.

Add the following line to the build.gradle to the in the app directory. This will make the AppAuth library available to your project.

build.gradle

compile 'net.openid:appauth:0.2.0'

The dependencies should now look like:

build.gradle

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    testCompile 'junit:junit:4.12'
    compile 'com.android.support:appcompat-v7:23.3.0'
    compile 'com.android.support:design:23.3.0'
    compile 'com.squareup.okhttp3:okhttp:3.2.0'
    compile 'com.squareup.picasso:picasso:2.5.2'
    compile 'net.openid:appauth:0.2.0'
}

The project already had a few dependencies that we'll be using later in the demo, like OkHttp and Picasso.

Now the dependency has been added, the project should build. Sync the project and run it.

  1. Click the Run button.

The sample app has a Sign-in button, and some UI widgets, but the button doesn't have any sign-in functionality yet, that's what we'll add in this codelab.

Create the ServiceConfiguration

Create the AuthorizationServiceConfiguration object in the AuthorizeListener::onClick method which declares the authorization and token endpoints of the OAuth server you wish to authorize with. In our example, we will use Google, but this will work with any compliant OAuth server.

MainActivity.java

AuthorizationServiceConfiguration serviceConfiguration = new AuthorizationServiceConfiguration(
  Uri.parse("https://accounts.google.com/o/oauth2/v2/auth") /* auth endpoint */,
  Uri.parse("https://www.googleapis.com/oauth2/v4/token") /* token endpoint */
);

If your server supports dynamic discovery, you can also fetch this configuration dynamically with AuthorizationServiceConfiguration.fetchFromIssuer. We'll stick with the static configuration for simplicity.

Build the AuthorizationRequest

Once you have an instance of AuthorizationServiceConfiguration, you can now build an instance of AuthorizationRequest which describes actual authorization request, including your OAuth client id, and the scopes you are requesting. Add the following code right below the previous block.

MainActivity.java

String clientId = "511828570984-fuprh0cm7665emlne3rnf9pk34kkn86s.apps.googleusercontent.com";
Uri redirectUri = Uri.parse("com.google.codelabs.appauth:/oauth2callback");
AuthorizationRequest.Builder builder = new AuthorizationRequest.Builder(
    serviceConfiguration,
    clientId,
    AuthorizationRequest.RESPONSE_TYPE_CODE,
    redirectUri
);
builder.setScopes("profile");
AuthorizationRequest request = builder.build();

Note that for the demo we are supplying a test OAuth client id. Be sure to register your own client ID when developing your own apps, and update the clientId and redirectUri values with your own (and the custom scheme registered in the AndroidManifest.xml). If using a different OAuth server, then you'll need to register a client for that server, following their documentation.

Create an instance of the AuthorizationService. Ideally, there is one instance of AuthorizationService per Activity. Still in the AuthorizeListener::onClick method, add the following code right below the code from the previous section:

MainActivity.java

AuthorizationService authorizationService = new AuthorizationService(view.getContext());

Create the PendingIntent to handle the authorization response, then perform the authorization request with performAuthorizationRequest. This will open the authorization request you configured previously in a Custom Tab (or the default browser if no browsers support Custom Tabs).

MainActivity.java

String action = "com.google.codelabs.appauth.HANDLE_AUTHORIZATION_RESPONSE";
Intent postAuthorizationIntent = new Intent(action);
PendingIntent pendingIntent = PendingIntent.getActivity(view.getContext(), request.hashCode(), postAuthorizationIntent, 0);
authorizationService.performAuthorizationRequest(request, pendingIntent);

The AuthorizationService is responsible for initiating the following OAuth code flow.

At this point, if you ran the code, it would perform the authorization request in the browser, using Custom Tabs if available, but the response would go nowhere.

That wouldn't be very useful! So let's add handling for the authorization response. Here we add hooks into the app in order to receive the authorization response from the browser. This involves registering for, and handling an Intent.

Registering the the intent

First register RedirectUriReceiverActivity with the following intent-filters in your AndroidManifest.xml, inside the <application> block. This registers the app to receive the OAuth2 authorization response intent from Chrome custom tabs or the system browser on your behalf.

AndroidManifest.xml

<activity android:name="net.openid.appauth.RedirectUriReceiverActivity">
  <intent-filter>
    <action android:name="android.intent.action.VIEW"/>
      <category android:name="android.intent.category.DEFAULT"/>
      <category android:name="android.intent.category.BROWSABLE"/>
      <data android:scheme="com.google.codelabs.appauth"/>
    </intent-filter>
</activity>

Then, add a new intent-filter to your <activity android:name=".MainActivity"> so AppAuth can pass the authorization response to your main activity.

AndroidManifest.xml

<intent-filter>
   <action android:name="com.google.codelabs.appauth.HANDLE_AUTHORIZATION_RESPONSE"/>
   <category android:name="android.intent.category.DEFAULT"/>
</intent-filter>

After these changes, your AndroidManifest.xml should have two activities defined as follows:

AndroidManifest.xml

<activity android:name=".MainActivity">
    <intent-filter>
        <action android:name="android.intent.action.MAIN"/>
        <category android:name="android.intent.category.LAUNCHER"/>
    </intent-filter>
    <intent-filter>
        <action android:name="com.google.codelabs.appauth.HANDLE_AUTHORIZATION_RESPONSE"/>
        <category android:name="android.intent.category.DEFAULT"/>
    </intent-filter>
</activity>

<activity android:name="net.openid.appauth.RedirectUriReceiverActivity">
  <intent-filter>
    <action android:name="android.intent.action.VIEW"/>
      <category android:name="android.intent.category.DEFAULT"/>
      <category android:name="android.intent.category.BROWSABLE"/>
      <data android:scheme="com.google.codelabs.appauth"/>
    </intent-filter>
</activity>

Now, back in MainActivity.java, add the following methods to your MainActivity class to handle the intents from RedirectUriReceiverActivity.

MainActivity.java

@Override
protected void onNewIntent(Intent intent) {
  checkIntent(intent);
}

private void checkIntent(@Nullable Intent intent) {
  if (intent != null) {
    String action = intent.getAction();
    switch (action) {
      case "com.google.codelabs.appauth.HANDLE_AUTHORIZATION_RESPONSE":
        if (!intent.hasExtra(USED_INTENT)) {
          handleAuthorizationResponse(intent);
          intent.putExtra(USED_INTENT, true);
        }
        break;
      default:
        // do nothing
    }
  }
}

@Override
protected void onStart() {
  super.onStart();
  checkIntent(getIntent());
}

Processing the Authorization Response

AppAuth provides the AuthorizationResponse to MainActivity, via the provided RedirectUriReceiverActivity. This can then be used to obtain a TokenResponse.

Obtain the AuthorizationResponse from the Intent passed to the MainActivity by adding the following code to the handleAuthorizationResponse method:

MainActivity.java

AuthorizationResponse response = AuthorizationResponse.fromIntent(intent);
AuthorizationException error = AuthorizationException.fromIntent(intent);
final AuthState authState = new AuthState(response, error);

The AuthState object created here is a convenient way to store details from the authorization session. You can update it with the results of new OAuth responses, and persist it to store the authorization session between app starts.

Next we will exchange that authorization code for the refresh and access tokens, and update the AuthState instance with that response. Add the following code right below the last block.

MainActivity.java

if (response != null) {
  Log.i(LOG_TAG, String.format("Handled Authorization Response %s ", authState.toJsonString()));
  AuthorizationService service = new AuthorizationService(this);
  service.performTokenRequest(response.createTokenExchangeRequest(), new AuthorizationService.TokenResponseCallback() {
    @Override
    public void onTokenRequestCompleted(@Nullable TokenResponse tokenResponse, @Nullable AuthorizationException exception) {
      if (exception != null) {
        Log.w(LOG_TAG, "Token Exchange failed", exception);
      } else {
        if (tokenResponse != null) {
          authState.update(tokenResponse, exception);
          persistAuthState(authState);
          Log.i(LOG_TAG, String.format("Token Response [ Access Token: %s, ID Token: %s ]", tokenResponse.accessToken, tokenResponse.idToken));
        }
      }
    }
  });
}


We provided the method persistAuthState in the starter project as some boilerplate to save and load the AuthState object. If you're adding AppAuth to your own app you may want to consider your own persistence design.

Now you have successfully configured, executed, and handled the response – let's run the sample and see what happens.

Search for "Token Response", you should see an access token, and an id token printed to the console. This means that you have successfully authorized the user.

The purpose of this this codelab to get an access token in order to make an authenticated API call – so let's actually use that accessToken, and make an API call in the next step.

While you can get the tokens directly from the token response, those tokens expire and must be refreshed occasionally. Using AuthState and making your REST API calls inside authState.performActionWithFreshTokens is recommended, as it will automatically ensure that the tokens are fresh (refreshing them when needed) before executing your code.

Getting Fresh Authorization Tokens

Here we use performActionWithFreshTokens to get a fresh access token. Add this code to the MakeApiCallListener::onClick method.

MainActivity.java

mAuthState.performActionWithFreshTokens(mAuthorizationService, new AuthState.AuthStateAction() {
  @Override public void execute(
      String accessToken,
      String idToken,
      AuthorizationException ex) {
    if (ex != null) {
      // negotiation for fresh tokens failed, check ex for more details
      return;
    }

    // use the access token to do something ...
    Log.i(LOG_TAG, String.format("TODO: make an API call with [Access Token: %s, ID Token: %s]", accessToken, idToken));
  }
});

Run your code. Now when you tap the "Make API Call" button, you should see a log message with the fresh access token and id token.

Making an API Call

Let's do something a little more interesting. We're going to replace the simple example above with a real API call that does the following:

  1. Starts an asynchronous task so as not to block the UI
  2. Fetches the user's profile from Google
  3. Displays the profile information and picture in our app.

MainActivity.java

mAuthState.performActionWithFreshTokens(mAuthorizationService, new AuthState.AuthStateAction() {
  @Override
  public void execute(@Nullable String accessToken, @Nullable String idToken, @Nullable AuthorizationException exception) {
    new AsyncTask<String, Void, JSONObject>() {
      @Override
      protected JSONObject doInBackground(String... tokens) {
        OkHttpClient client = new OkHttpClient();
        Request request = new Request.Builder()
            .url("https://www.googleapis.com/oauth2/v3/userinfo")
            .addHeader("Authorization", String.format("Bearer %s", tokens[0]))
            .build();

        try {
          Response response = client.newCall(request).execute();
          String jsonBody = response.body().string();
          Log.i(LOG_TAG, String.format("User Info Response %s", jsonBody));
          return new JSONObject(jsonBody);
        } catch (Exception exception) {
          Log.w(LOG_TAG, exception);
        }
        return null;
      }

      @Override
      protected void onPostExecute(JSONObject userInfo) {
        if (userInfo != null) {
          String fullName = userInfo.optString("name", null);
          String givenName = userInfo.optString("given_name", null);
          String familyName = userInfo.optString("family_name", null);
          String imageUrl = userInfo.optString("picture", null);
          if (!TextUtils.isEmpty(imageUrl)) {
            Picasso.with(mMainActivity)
                .load(imageUrl)
                .placeholder(R.drawable.ic_account_circle_black_48dp)
                .into(mMainActivity.mProfileView);
          }
          if (!TextUtils.isEmpty(fullName)) {
            mMainActivity.mFullName.setText(fullName);
          }
          if (!TextUtils.isEmpty(givenName)) {
            mMainActivity.mGivenName.setText(givenName);
          }
          if (!TextUtils.isEmpty(familyName)) {
            mMainActivity.mFamilyName.setText(familyName);
          }

          String message;
          if (userInfo.has("error")) {
            message = String.format("%s [%s]", mMainActivity.getString(R.string.request_failed), userInfo.optString("error_description", "No description"));
          } else {
            message = mMainActivity.getString(R.string.request_complete);
          }
          Snackbar.make(mMainActivity.mProfileView, message, Snackbar.LENGTH_SHORT)
              .show();
        }
      }
    }.execute(accessToken);
  }
});

That was a lot of code, but it's relatively straightforward. In the performActionWithFreshTokens method (which gives us a fresh access token to use), we kick off an async task that makes a REST call the Google Userinfo endpoint at https://www.googleapis.com/oauth2/v3/userinfo (in the doInBackground method), and then updates the UI with the result (in the onPostExecute method).

You can use this pattern to make your own asynchronous API calls!

Congratulations! You have now successfully authorized the user and performed an authenticated API call!

The API call we made to get the user's profile is just an example – Google has many RESTful APIs that require user authentication, from viewing the user's Calendar, to uploading YouTube videos on their behalf. All of these instructions work with any standard OAuth provider too, so you can use this to authenticate to any provider, for example you can authorize Spotify users, and make RESTful API calls to the Spotify API. If you publish multiple apps and manage your own identities, you can use AppAuth to achieve Single Sign-on between your own apps.

You will want to serialize the AuthState object to disk, to preserve the authorization state between app runs.

Other common uses for AppAuth include authenticating the user to your own backend by sending the idToken obtained from the Identity provider (which could be Google, or any other OpenID Connect provider), exchanging for your own session tokens and making authenticated API calls to your backend.

For devices under enterprise management, you can make the user experience even better using a few managed configuration tricks. For example, if you make an enterprise app (with an associated SaaS service that can authenticate to many different tenants or unique identity providers), you can make use of a login hint that provisions the domain (tenant) that authenticates the end user. In other words, your enterprise mobility manager (EMM) can provision the full email address of the user to your app so all they have to do is enter their password when prompted.

What enables EMM to provision this user email address and other managed configuration is an application restrictions API. Application restrictions allow us to define a schema of key-value pairs that can be configured by an Device Policy Controller (DPC) supplied by the EMM that has special admin privileges on the device. EMMs are able to read the schema you create by making API calls to Google Play or on device, ensuring that admins can configure your application without needing to do direct integration work with EMMs.

Update SDK Version

In the app/build.gradle file, increase the minimum SDK version to 21, which is required for managed configuration.

build.gradle

minSdkVersion 21

Creating the schema

We will start by creating the schema for our managed configuration. The first step is to add a reference in the Manifest which will point to our schema. Add the following to your manifest's <application> element

AndroidManifest.xml

<meta-data android:name="android.content.APP_RESTRICTIONS"
    android:resource="@xml/app_restrictions" />

Now we will add the schema xml file. Start by creating a new directory named xml in your apps res directory. Once you have done so, add a new file in the directory (right click > File > New), named app_restrictions.xml.

Populate the xml file with the following schema

app_restrictions.xml

<?xml version="1.0" encoding="utf-8"?>
<restrictions xmlns:android="http://schemas.android.com/apk/res/android" >

<restriction
    android:key="login_hint"
    android:title="@string/login_hint_title"
    android:restrictionType="string"
    android:description="@string/login_hint_description" />
</restrictions>

This schema includes one key value pair, of type string. When configuring in EMM consoles admins will see the title and description attributes. All values included above are mandatory.

In addition, add the following to strings.xml in the <resources> attribute

strings.xml

<string name="login_hint_title">Login Hint</string>
<string name="login_hint_description">This key will allow you to send a
   login hint to the identity provider such as email or username</string>

Restrictions can also be of type integer, bool, choice, multi-select, and hidden. For more details on the kinds of things you can include in a schema, see the RestrictionsManager documentation.

Retrieving Configurations once set

Now we will enable our application to read configurations set by a DPC. Add the following two methods to MainActivity. The first function retrieves app restrictions if they are set and responds accordingly. The second registers a BroadcastReceiver which listens for changes to app restrictions that are set.

MainActivity.java

private void getAppRestrictions(){
  RestrictionsManager restrictionsManager =
         (RestrictionsManager) this
                 .getSystemService(Context.RESTRICTIONS_SERVICE);

  Bundle appRestrictions = restrictionsManager.getApplicationRestrictions();

  if(!appRestrictions.isEmpty()){
    if(appRestrictions.getBoolean(UserManager.
           KEY_RESTRICTIONS_PENDING)!=true){
      mLoginHint = appRestrictions.getString(LOGIN_HINT);
    }
    else {
      Toast.makeText(this,R.string.restrictions_pending_block_user,
             Toast.LENGTH_LONG).show();
      finish();
    }
  }
}

private void registerRestrictionsReceiver(){
  IntentFilter restrictionsFilter =
         new IntentFilter(Intent.ACTION_APPLICATION_RESTRICTIONS_CHANGED);

  mRestrictionsReceiver = new BroadcastReceiver() {
    @Override
    public void onReceive(Context context, Intent intent) {
      getAppRestrictions();
    }
  };

  registerReceiver(mRestrictionsReceiver, restrictionsFilter);
}

You will also need to add LOGIN_HINT as a static member variable for MainActivity...

MainActivity.java

private final static String LOGIN_HINT = "login_hint";

and define restrictions_pending_block_user in strings.xml

strings.xml

<string name="restrictions_pending_block_user">This application is
        waiting for further configuration details and cannot continue
        to run. Please contact your IT admin for further details</string>

You will also need to add the following member variables to MainActivity, under ImageView mProfileView;

MainActivity.java

// login hint;
String mLoginHint;

// broadcast receiver for app restrictions changed broadcast
private BroadcastReceiver mRestrictionsReceiver;

Now we will use the new methods we just created. Add the following to the bottom of onCreate()

MainActivity.java

// Retrieve app restrictions and take appropriate action
getAppRestrictions();

Add the following method to the bottom of onStart()

MainActivity.java

// Register a receiver for app restrictions changed broadcast
registerRestrictionsReceiver();

Finally we will ensure that we also check for app restrictions in MainActivity's onResume(), and unregister the BroadcastReceiver in onStop(). Add the following to MainActivity

MainActivity.java

@Override
protected void onResume(){
  super.onResume();

  // Retrieve app restrictions and take appropriate action
  getAppRestrictions();

  // Register a receiver for app restrictions changed broadcast
  registerRestrictionsReceiver();
}

@Override
protected void onStop(){
  super.onStop();

  // Unregister receiver for app restrictions changed broadcast
  unregisterReceiver(mRestrictionsReceiver);
}

Now that we are able to get the login_hint configuration from an admin, let's use it in our authorization requests

Now that you have the managed configuration, you can include the login_hint in the Authorization Request. We will do so by including it as an additional parameter in our AuthorizationRequest. First, add a private final MainActivity to our AuthorizeListener so that we can access mLoginHint inside of the static class

MainActivity.java

public static class AuthorizeListener implements Button.OnClickListener {

  private final MainActivity mMainActivity;

  public AuthorizeListener(@NonNull MainActivity mainActivity) { 
    mMainActivity = mainActivity;
  }

Next we will update onCreate()to use AuthorizeListener's new constructor. Replace the code just after // wire click listeners with the following:

MainActivity.java

mAuthorize.setOnClickListener(new AuthorizeListener(this));

Create a new method for MainActivity that will allow us to retrieve mLoginHint

MainActivity.java

public String getLoginHint(){
 return mLoginHint;
}

We are now ready to take advantage of the login hint passed in by the admin. Add the following to the AuthorizeListener's onClick(), just above AuthorizationRequest request = builder.build():

MainActivity.java

if(mMainActivity.getLoginHint() != null){
  Map loginHintMap = new HashMap<String, String>();
  loginHintMap.put(LOGIN_HINT,mMainActivity.getLoginHint());
  builder.setAdditionalParameters(loginHintMap);

  Log.i(LOG_TAG, String.format("login_hint: %s", mMainActivity.getLoginHint()));

  Log.i(LOG_TAG, String.format("login_hint: %s", mMainActivity.getLoginHint()));
}

Now, when you launch a new OAuth Authorization Request to Google in the sample app with the login_hint set through managed configurationset through managed configuration, if multiple users are signed-in to Google, the hinted user will be automatically selected (and the account chooser step will be skipped), and if the hinted user is not signed-in already, they will be prompted to sign-in to that account.

You can setup a setup a test managed configuration to set login_hint managed configuration to set login_hint by taking the following steps

  1. Download the latest available version of TestDPC
  2. If TestDPC is not already installed on the device and set as device owner, install TestDPC on the device and set it as device owner using the following adb commands:
adb install path/to/TestDPC.apk
adb shell dpm set-device-owner com.afwsamples.testdpc/.DeviceAdminReceiver
  1. Plug in your Android device and click the Run button. You should see the Appauth App home screen appear after a few seconds.
  2. Leave the Appauth app and Launch TestDPC. Scroll down to "Manage app restrictions" and tap it.
  3. Choose AppAuth Codelab from the dropdown list, tap "load manifest restrictions". Tap the pencil next to "login_hint", enter a google account's email address's email address into the "Value" field of the dialog, and tap save. Tap the Save button on the bottom right hand corner of the screen.
  4. Launch the AppAuth Codelab app, and tap on "Make Authorization Request.
  5. For best results, sign in to two Google accounts in Chrome, and use the email address of one of them as the hint for the app restriction. Without the login_hint set you would be presented with an account chooser. With login_hint, the hinted account should be automatically selected.
  6. For best results, sign in to two Google accounts in Chrome, and use the email address of one of them as the hint for the app restriction. Without the login_hint set you would be presented with an account chooser. With login_hint, the hinted account should be automatically selected.

Users can now authenticate to your app using Chrome Custom Tabs, and use Managed Configuration to bootstrap the process for users with managed devices.

What we've covered

Next Steps

Learn More