Kotlin Android App using AppAuth

Kotlin Android App using AppAuth

Code Examples / mobile-integration

This tutorial shows how to run a code example that implements mobile OpenID Connect in a Kotlin App using the open source AppAuth Android library. We will handle all of the OAuth lifecycle events and also show how to handle error conditions reliably.

Overview

The code example is a simple Android App with two views, the first of which is an Unauthenticated View to handle processing related to signing the user in:

Unauthenticated View

Once signed in the app switches to an Authenticated View, to simulate screens in real apps that work with access tokens and call APIs. This view presents details about tokens and also allows token refresh and logout operations to be tested.

Authenticated View

Get the Code

First ensure that you are running Android Studio 4.2.1 or later, then clone the GitHub repository and open its folder:

Android Project

The code example is a Single Activity App and uses the following modern Android coding techniques in order to implement AppAuth with simple code and good reliability.

AspectDescription
NavigationThe code example is a Single Activity App with simple navigation to swap out the main fragment
UI UpdatesView models are used in a basic way to make reading and writing UI elements simpler
HTTP RequestsKotlin Coroutines can simplify making HTTP requests and processing HTTP callbacks

Note however that the main OAuth integration is in the AppAuthHandler class and it should be able to adapt this code into any other style of Android app.

Setup

Connectivity from Mobile Devices

If you are running the Curity Identity Server on your development computer over port 8443, it is recommended to use ngrok to create a working connection from an emulator or device. This can be done via the following simple script:

ngrok http 8443 -log=stdout &
sleep 5
BASE_URL=$(curl -s http://localhost:4040/api/tunnels | jq -r '.tunnels[] | select(.proto == "https") | .public_url')
echo $BASE_URL

A generated URL such as https://ef5540f40573.eu.ngrok.io can then be configured as the Base URL in the Admin UI. Also set the protocol under Deployments to HTTP, as discussed in the Exposing Curity Using ngrok article.

Mobile ngrok

The OAuth settings for the demo app are configured in the ApplicationConfig source file and this will need updating with the ngrok URL:

class ApplicationConfig {
    val issuer: Uri = Uri.parse("https://ef5540f40573.eu.ngrok.io/oauth/v2/oauth-anonymous")
    val redirectUri: Uri = Uri.parse("io.curity.client:/callback")
    val postLogoutRedirectUri: Uri = Uri.parse("io.curity.client:/logoutcallback")
    val scope = "openid profile"
}

Enable Dynamic Client Registration

In the Curity Identity Server, ensure that Dynamic Client Registration is configured with default options. For this initial mobile sample, select the No Authentication option for non-templatized clients:

Dynamic Client Setup

AppAuth Integration

Redirect Scheme Registration

To integrate AppAuth libraries you need to edit the app’s build.gradle file to first include the library dependency, and then to register the Custom URI Scheme used to receive the response after OpenID Connect redirects:

android {
    defaultConfig {
        manifestPlaceholders = [
                'appAuthRedirectScheme': 'io.curity.client'
        ]
    }

AppAuth Patterns

AppAuth usage is based around a few key patterns that will be seen in the following sections and which are explained in further detail in the Android AppAuth Documentation.

PatternDescription
BuildersBuilder classes are used to create OAuth request messages
CallbacksCallback functions are used to receive OAuth response messages
Error CodesError codes can be used to determine particular failure causes

Dynamic Client Registration

Using DCR is recommended as a security best practice, in order to associate a unique instance of the app with each user. It potentially enables you to detect invalid usage patterns, such as multiple users signing in from the same device.

When the app is first run it downloads metadata from the Curity Identity Server’s metadata endpoint and then verifies that registration is enabled. The AppAuth builder classes then create the dynamic client registration request:

val extraParams = mutableMapOf<String, String>()
            extraParams.put("scope", ApplicationConfig.scope)
            extraParams.put("requires_consent", "false")
            extraParams.put("post_logout_redirect_uris", ApplicationConfig.postLogoutRedirectUri.toString())

val nonTemplatizedRequest =
    RegistrationRequest.Builder(
        metadata,
        listOf(config.redirectUri)
    )
        .setGrantTypeValues(listOf(GrantTypeValues.AUTHORIZATION_CODE))
        .setAdditionalParameters(extraParams)
        .build()
authorizationService.performRegistrationRequest(nonTemplatizedRequest, handleRegistrationResponseCallback)

This code results in a request message being sent with the following parameters. Additional request parameters can be sent to further refine behavior, as described in the Using Dynamic Client Registration article.

{
    "redirect_uris":["io.curity.client:/callback"],
    "post_logout_redirect_uris":["io.curity.client:/logoutcallback"],
    "application_type":"native",
    "grant_types":["authorization_code"],
    "scope":"openid profile"
}

The Identity Server then returns a payload that includes the Client ID and Client Secret the application instance should use for subsequent logins:

{
	"default_acr_values": ["urn:se:curity:authentication:html-form:Username-Password"],
	"application_type": "native",
	"registration_client_uri": "https://ef5540f40573.eu.ngrok.io/token-service/oauth-registration/87f1d9de-de2f-4e96-b27b-882cce0a3352",
	"registration_access_token_expires_in": 31536000,
	"registration_access_token": "e99ab41e-52a1-4fa3-b21b-fb95a398c33a",
	"client_id": "87f1d9de-de2f-4e96-b27b-882cce0a3352",
	"token_endpoint_auth_method": "client_secret_basic",
	"scope": "openid profile",
	"client_id_issued_at": 1624011134,
	"client_secret": "tfs9nad3dweFAa1CqpUj5p6NC4-092hb5Gg4SVRhOkc",
	"id_token_signed_response_alg": "RS256",
	"grant_types": ["authorization_code", "refresh_token"],
	"subject_type": "public",
	"redirect_uris": ["io.curity.client:/callback"],
    "post_logout_redirect_uris":["io.curity.client:/logoutcallback"],
	"client_secret_expires_at": 0,
	"token_endpoint_auth_methods": ["client_secret_basic", "client_secret_post"],
	"response_types": ["code", "id_token"],
	"refresh_token_ttl": 3600
}

The client is then created in a database record rather than being managed in the Admin UI. With the Curity Identity Server it is also possible to use Templatized DCR if you prefer an option that reduces duplication:

Database Clients

Login Redirects

When the login button is clicked, a standard OpenID Connect authorization redirect is triggered, which then presents a login screen from the Identity Server. You can then create an account or sign in as an existing user:

Android Password Login

The login process follows a number of best practices from RFC8252:

Best PracticeDescription
Login via System BrowserLogins use a Chrome Custom Tab, meaning that the app itself never has access to the user’s password
PKCEProof Key for Code Exchange prevents malicious apps being able to intercept redirect responses
User Specific Client SecretEach instance of the mobile app uses its own client secret from the dynamic client registration response

Authorization redirects are triggered by building an Android intent that will start a Chrome Custom Tab and return the response to a specified activity using StartActivityForResult. The code example receives the response in the app’s single activity without recreating it:

val extraParams = mutableMapOf<String, String>()
extraParams.put("acr_values", "urn:se:curity:authentication:html-form:Username-Password")

val request = AuthorizationRequest.Builder(metadata, registrationResponse.clientId,
        ResponseTypeValues.CODE,
        config.redirectUri)
        .setScopes(config.scope)
        .setAdditionalParameters(extraParams)
        .build()

val intent = authorizationService.getAuthorizationRequestIntent(request)

The message generated will have query parameters similar to those in the following table, and will include PKCE parameters:

Query ParameterExample Value
client_id87f1d9de-de2f-4e96-b27b-882cce0a3352
redirect_uriio.curity.client:/callback
response_typecode
state_eB_JSonBtp9AasrJuQZWA
nonce_TT6iiN8eFeF7U6afzNNgQ
scopeopenid profile
code_challengewmIZzT7QMPBLICXlvm19orboBMQnHKXGbMyyhfN8gPU
code_challenge_methodS256

When needed it is possible to include additional runtime parameters, such as the acr_values query parameter to specify a particular authentication method.

Login Completion

After the user has successfully authenticated, an authorization code is returned in the response message, which is then redeemed for tokens. In the demo app this response is returned to the unauthenticated fragment, which then runs the following code to complete authorization:

val extraParams = mapOf("client_secret" to registrationResponse.clientSecret)
val tokenRequest = response.createTokenExchangeRequest(extraParams)
authorizationService.performTokenRequest(tokenRequest) { tokenResponse, handleTokenExchangeCallback }

This results in a POST to the Curity Identity Server’s token endpoint, which includes PKCE parameters and the application instance’s unique client secret:

Form ParameterExample Value
grant_typeauthorization_code
codeCg85vgCfElPckCgzPYDcZUrMekzA1iv5
client_id87f1d9de-de2f-4e96-b27b-882cce0a3352
client_secrettfs9nad3dweFAa1CqpUj5p6NC4-092hb5Gg4SVRhOkc
code_verifierex_OaauEnB0cLdBwXUXypYxr4j2CrkPNfWOsdI_lNrKAdgL1c-bx-Uizzsgb-0Eio58ohD85zKjWqWQc2lvjSQ
redirect_uriio.curity.client:/callback

If login completes successfully, Android navigation is used to move the user to the authenticated view. The user can potentially cancel the Chrome Custom Tab, and the demo app handles this condition by remaining in the unauthenticated view so that the user can retry authentication.

OAuth State

The demo app stores all of the following data in an instance of AppAuth’s AuthState class:

DataContains
MetadataAll of the endpoint locations that AppAuth uses when sending OAuth request messages
Registration DataThe response from the DCR request, containing the app instance’s client ID and secret
Token DataAn access token, refresh token and id token are all returned to the app

When the app exits it saves its registration data to Android storage that is private to the app, so that the same Client ID and Client Secret are used when the app is restarted. For simplicity the code example uses simple Shared Preferences, but it is recommended to use Encrypted Shared Preferences for a real app. Token data can also potentially be saved to secure storage, to avoid logins on every app restart.

Using and Refreshing Access Tokens

Once the code is redeemed for tokens, most apps will then send access tokens to APIs as a message credential, in order for the user to be able to work with data.

With default settings in the Curity Identity Server the access token will expire every 15 minutes. You can use the refresh token to silently renew an access token with the following code:

val extraParams = mapOf("client_secret" to registrationResponse.clientSecret)
val tokenRequest = TokenRequest.Builder(metadata, registrationResponse.clientId)
    .setGrantType(GrantTypeValues.REFRESH_TOKEN)
    .setRefreshToken(refreshToken)
    .setAdditionalParameters(extraParams)
    .build()
authorizationService.performTokenRequest(tokenRequest, handleRefreshTokenResponse)

This results in a POST to the Curity Identity Server’s token endpoint, which again includes the app instance’s unique client secret:

Form ParameterExample Value
grant_typerefresh_token
client_id87f1d9de-de2f-4e96-b27b-882cce0a3352
client_secrettfs9nad3dweFAa1CqpUj5p6NC4-092hb5Gg4SVRhOkc
refresh_token62a11202-4302-42e1-983e-b26362093b67

Eventually the refresh token will also expire, meaning the user’s authenticated session needs to be renewed. This condition is detected by the code example, which checks for an invalid_grant error code in the token refresh error response:

if (ex.type == AuthorizationException.TYPE_OAUTH_TOKEN_ERROR &&
    ex.code.equals(AuthorizationException.TokenRequestErrors.INVALID_GRANT.code) {
}

End Session Requests

The user can also select the Sign Out button to end their authenticated session early. This results in an OpenID Connect end session redirect on the Chrome Custom Tab, triggered by the following code:

val request = EndSessionRequest.Builder(metadata)
    .setIdTokenHint(idToken)
    .setPostLogoutRedirectUri(config.postLogoutRedirectUri)
    .build()

authorizationService.performEndSessionRequest(request, pendingIntent)

The following query parameters are sent, which signs the user out at the Identity Server, removes the SSO cookie from the system browser, then returns to the app at the post logout redirect location:

Query ParameterExample Value
client_id87f1d9de-de2f-4e96-b27b-882cce0a3352
post_logout_redirect_uriio.curity.client:/logoutcallback
stateIi8fYlMdVbX8fiMSmlI6SQ
id_token_hinteyJraWQiOiIyMTg5NTc5MTYiLCJ4NXQiOiJCdEN1Vzl …

Techniques

Logging

The example app writes some debug logs containing AppAuth response details. Of course a real app should not log secure fields in this manner, and the example only does so for educational purposes:

Example Logging

Error Handling

AppAuth libraries provide good support for returning the standard OAuth error and error_description fields, and error objects also contain type and code numbers that correlate to their Error Definitions File.

The code example ensures that all four of these fields are captured, so that they can be displayed or logged in the event of unexpected failures, then looked up to find the underlying cause:

Error Response

Password Autofill

To avoid asking users to frequently type passwords on small mobile keyboards, you may want to use password autofill features, when the user has enabled them on the device.

By default the Chrome Custom Tab is abruptly dismissed after the user submits credentials, so the Save Password prompt cannot be selected. One way to resolve this is to activate user consent for the client, so that the browser remains active:

Save Password

There are other options in the Curity Identity Server that can improve usability for mobile users, and Webauthn is an option worth exploring, where users authenticate via familiar mobile credentials, but strong security is used.

Financial Grade

The initial Android code example would need extending in a couple of areas in order to fully meet Curity’s Mobile Best Practices, and these will be covered in separate code examples:

AreaDescription
Dynamic Client RegistrationAuthenticated DCR should be used, so that an access token is sent with regstration requests, and there are a few different ways to implement this
Receiving Redirect ResponsesAn HTTPS URL should be used to prevent a malicious app being able to impersonate your app, as recommended in the FAPI Read Profile for Native Apps

Hypermedia Authentication API

See the Android HAAPI Mobile Sample for an alternative financial grade solution, which implements OpenID Connect with standard messages but also provides these features:

  • Client attestation strongly verifies the app’s identity before allowing authentication attempts
  • For most forms of login the system browser is not used, so browser risks are eliminated
  • The app can render its own forms during the authentication workflow, for control over the user experience

Conclusion

OpenID Connect can be implemented fairly easily in an Android app by integrating the AppAuth libraries, which manage OAuth requests and responses in the standard way. Once integration is complete, the app can potentially use many other forms of authentication and multiple factors, with zero code changes.

Keep up with our latest articles and how-tos RSS feeds.