Performing OAuth on Android with Custom Tabs

Whether we’re building third-party clients for existing API services, or working on our own product that communicates with our own API, it’s likely that we might be working with authentication that uses a form of OAuth. This standard of authentication approach is something that we’re bound to use at some point in our careers, but can often feel a bit tedious to implement in our applications. However, it doesn’t have to be – in this article we are going to take a look at how we can authorise our application using the OAuth2 flow using Chrome Custom Tabs to satisfy our authentication requirements.

But you may be thinking, why Chrome Custom Tabs?

  • This approach allows us to bypass the use of webviews for OAuth which can bring us a bunch of boilerplate into our applications
  • Custom Tabs are customisable, allowing us to alter the look and feel – matching the theme of our application
  • This customisation helps the user to feel like they haven’t left your app. For something that is security related, this helps to provide a sense of familiarity and security – rather than being taken to an external browser window

As you can see, Chrome Custom Tabs come with several advantages for both developers and users – which makes it a much more desirable tool when it comes to OAuth flows in Android applications.


To learn about how to implement the OAuth2 flow using the mentioned technologies we’re going to make use of the Product Hunt API. Regardless of the service you are using, the implementation flow will pretty much be the same, only with different URLs, tokens and other configuration values used. If you want to use the same API then you’ll need to setup an application, or have the details ready for your chosen API service. Regardless of what service you are using though, we are going to need a few things that are going to be provided / required by your chosen API service:

  • OAuth authorization endpoint – This is the URL which will be used to kick-off the authorization flow. This URL will be loaded as a webpage, allowing the user to grant you access to their account for the provided service
  • OAuth token endpoint – This is the URL which will be used to retrieve the access token which will be used by your application to make requests on behalf of the user.
  • Client ID – This is the client ID which represents your application. This would have been provided by the service you are building for after creating a new application from their developer portal.
  • Client secret – This is the client secret which represents your application. This would have been provided by the service you are building for after creating a new application from their developer portal.
  • Redirect URI – This is the redirect URI which the authorization screen will redirect to once that step in the authorisation flow has completed. This means that the user has completed that step in the authorization process and the response is ready for you to handle.
  • Response type – This will be provided as a URL parameter when hitting the OAuth authorization endpoint. This value states how you wish to receive the data response once the request here has completed. In the example for this article, and quite often anyway, this value will be sent as code.
  • Scope – This will be used to state the scope of permissions that you wish to be granted for the account that the user is giving you access to. This will vary per API that you are authenticating against, but often this will be things such as public, private, read, write etc.
  • Code – This is the code that will be used to retrieve the access token from the API. You will receive this code back from the OAuth authorization endpoint once the request returns a success response.

All of the above comes together to create several small steps which will be required for us to gain authorization from the user for their account, and then request an access token for us to use when making requests.

Now that we’ve covered the kind of data that we’ll be handling, you’ll need to make sure that you’ve found those properties from within the developer portal of the service that you are wishing to authenticate against – it’s best to have these in place before we get started in implementing the above steps.

However, before we do get started, I want to point out a couple of important parts of this process. For networking and triggering the requests we’ll be using retrofit / coroutines – I’m using these as they are not only my preferred way, but also because I feel the majority of the community will be using these technologies so felt it useful to provide the example in this format. However, the process of OAuth2 here will remain exactly the same with the Custom Tabs usage – you can switch out the networking for your desired method and things will still work exactly the same.


Before we get started we’re going to need to add the required dependency that will give us access to Custom Tabs:

implementation 'androidx.browser:browser:1.0.0'

Next, in our application we’re going to create a new Activity which will be used to house the authentication logic for our application. This activity will:

  • Launch the custom tab for the user to authenticate their account
  • Receive the authentication response once the user has completed the above authentication
  • Make a request to the API to retrieve the access token for the authenticated account
  • Pass the token back to the Activity that triggered this Token Activity flow

Keeping this in a separate Activity, outside of our Main or similar activiy, allows us to separate the responsibilities of this authentication flow. We’ll begin by creating this new activity and adding it to our manifest:

class AuthenticationActivity : AppCompatActivity() {

}
<application>
    <activity android:name=".AuthenticationActivity">
            
    </activity>
</application>

Now that we’ve added the activity to our project, lets begin by launching the authentication screen for our API service. For these, we’ll trigger a Custom Tab using the OAuth authorization endpoint that was previously mentioned above. For product Hunt this looks like http://api.producthunt.com/v2/oauth/authorize, along with the required query parameters for the authorization request.

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    val authorizationUrl = "http://api.producthunt.com/v2/oauth/" + authorize?...params..."

    val builder = CustomTabsIntent.Builder()
    val customTabsIntent = builder.build()
    customTabsIntent.launchUrl(
        this, Uri.parse(authorizationUrl))
}

At this point, the account authentication page is going to be shown within the chrome custom tab. Here the user will be able to grant (or deny…) our application access to their account. Once the user does grant our application access to their account, our application needs to be able to receive back an event to a) know this authorization has occurred and b) retrieve the response that is being provided by this authorization step – for this we’re going to require the use of a redirect URI.

When setting up your application from within the API developer portal you would have needed to provide a redirect Uri – as mentioned above, this is where the authorization step will redirect to once the flow has been completed. Within the Product Hunt API developer console we are shown a screen like this, which allows us to assign a redirect URI for our application. As mentioned, there should be something similar in the developer console for the API that you’re using.

Now, you may be thinking, but how is that going to hook back into my application? Within the activity declaration for the Token Activity in our manifest we’re going to add and intent-filter that will make use of this redirect-uri – this allows us to deep link into our application, with the Token Activity being the entry point.

<activity android:name=".AuthenticationActivity">
    <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:host="callback"
                android:scheme="auth"/>
    </intent-filter>
</activity>

You’ll notice that I defined the redirect-uri within the developer console as auth://callback. Here, this data has been mapped to place inside of the intent-filter – auth is the scheme and callback is the host.

At this point, when the redirect uri is used from with the custom tab, this activity of our application will be launched. However, we already had an instance of this activity running from its initial launch so we’re going to want to modify the launch mode to avoid there being more than one instance running at any given time. We’ll use singleTop here so that the same instance of the activity is used, which will avoid a new instance being created:

<activity android:name=".AuthenticationActivity"
          android:launchMode="singleTop">

    ....

</activity>

With singleTop being used for the launchMode of our activity, this means that any re-entry to the activity will be routed through onNewIntent(). This is because we are not creating a new instance, rather reusing what was already existing, which changes the entry point to the activity. When we enter the activity through onNewIntent() we need to make use of the received Intent reference to access the data that was passed to us from the account authorization step. In the case of the product hunt API, this authorization code comes back in the received uri with the key code. Using this key, we can access the query parameter of the uri from the intent data.

override fun onNewIntent(intent: Intent?) {
    super.onNewIntent(intent)
    val code = intent?.data?.getQueryParameter("code")
}

Now that we have the code from the account authorization step we need to exchange this for an access token using the product hunt API. We’re going to offload this work onto our view model.

override fun onNewIntent(intent: Intent?) {
    super.onNewIntent(intent)
    val code = intent?.data?.getQueryParameter("code")
    if (code != null) {
        viewModel.handleAuthorizationCode(code)
    } else {
        // handle error
    }
}

In our view model, we’re then going to need to implement the function that will allow our activity to retrieve the access token from the API endpoint, using the received code argument as a part of the query within the request. Here we’ll simply make a call to a repository function, which will use our retrofit networking service, in order to make the API request which will then eventually will emit this value to our activity.

fun handleAuthorizationCode(code: String) {
    viewModelScope.launch(Dispatchers.IO) {
        val result = dataRepository.retrieveAccessToken(code)
    }
}

When we call this authorize() function we’re going to need to perform an API request to exchange our code value for an access token. When an API request is made for this exchange the product hunt API returns us a response in the format of the defined TokenModel, this might vary per API however. In my repository I have defined a suspending function that will use my retrofit service to make the desired network request, passing the required data for the authorization flow.

suspend fun retrieveAccessToken(
    code: String, 
    clientId: String, 
    clientSecret: String
): TokenModel {
    return retrofitService.getAccessToken(
        clientSecret, 
        clientId,
        "auth://callback",
        "authorization_code",
        code
    )
}

class TokenModel(
    @SerializedName("access_token") val token: String,
    @SerializedName("token_type") val type: String
)

For the networking in my project I’m making use of retrofit, so here I’m going to create a suspending function within the retrofit interface that will return me a TokenModel instance. The URL endpoint used here is the OAuth token endpoint that we previously touched on early in this post.

@POST("/v2/oauth/token")
suspend fun getAccessToken(
    @Query("client_id") clientId: String,
    @Query("client_secret") clientSecret: String,
    @Query("redirect_uri") redirectUri: String,
    @Query("grant_type") grantType: String,
    @Query("code") code: String
): TokenModel

When this request completes, the result will be propagated back through the repository and to our view model that initially made the request. When we have this value we can then emit it via our live data reference so that our activity can handle the result.

fun handleAuthorizationCode(code: String) {
    viewModelScope.launch(Dispatchers.IO) {
        val result = dataRepository.retrieveAccessToken(code)
        withContext(Dispatchers.Main) {
            liveData.value = result.token
        }
    }
}

Now that we’re emitting data, we’re going to want to observe for this result with our Token Activity so that we can actually receive this live data event that we are emitting . Once we receive an event within this live data we can finish the Token Activity and pass back the retrieved token.

viewModel.tokenState.observe(this, Observer {
    setResult(RESULT_OK, Intent().apply { putExtra(KEY_TOKEN, it) })
    finish()
})

At this point our initial activity that triggered the authorization flow would have retrieved the result from OAuth process. If the token result is null then we can presume there has been an error somewhere in the flow (and we’ll need to let the user know), otherwise we can save the token and proceed to navigate the user into our application, making requests using the retrieved token as required.


In this article we’ve looked at how we implement the OAuth flow within our Android applications with the use of Chrome Custom Tabs. It’s important to note that in some cases users will not have access to Chrome on their Android device – for this you will need to add a check to ensure that the above solution can be carried out, falling back to an alternative route of OAuth if required.

If you’re using OAuth in your applications I’d love to hear if you’re already using the above solution, or if you have any more questions on how to implement the above then I’d love to hear from you. Feel free to reach out in a response here or on Twitter!

[twitter-follow screen_name=’hitherejoe’ show_count=’yes’]