Kotlin Android App using AppAuth

Kotlin Android App using AppAuth

This tutorial shows how to run a code example that implements mobile OpenID Connect in a Kotlin App according to RFC8252, using the open source AppAuth Android library. The demo app shows how to handle all of the OAuth lifecycle events and also how to use error details returned from the library.

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 an up-to-date version of Android Studio, then clone the GitHub repository and open the app subfolder:

Android Project

The code example is a Single Activity App developed in Kotlin, though the main OAuth integration is done in the AppAuthHandler class, which should be easy to adapt into other types of Android app. The example also uses the following Android coding techniques in order to implement AppAuth with clean code:

AspectDescription
NavigationAndroid navigation is used to swap out the main fragment based on the current values of data
View ModelsView models are used to exclude processing code from views and to manage data values
HTTP RequestsKotlin Coroutines are used to implement non blocking code when sending HTTP requests and receiving HTTP responses

Quick Start

The easiest way to run the code example is to point it to a deployed and preconfigured instance of the Curity Identity Server, running in Docker. This is done via a script included with the example that is explained in the Mobile Setup how-to:

./start-idsvr.sh

The result is to provide a working internet URL for the Curity Identity Server, such as https://baa467f55bc7.eu.ngrok.io, ready for the mobile app to connect to. The Admin UI is also available and can be accessed using the following details:

PropertyValue
URLhttps://localhost:6749/admin
Useradmin
PasswordPassword1

The deployed system also includes a user account of demouser / Password1 that can be used to sign in:

Android Password Login

Configuration

The code example uses a configuration file at ./android/app/src/res/raw/config.json and contains the following OAuth settings. If you are using the above quick start, it will automatically be updated with the Curity Identity Server base URL, or you can provide the base URL of your own system if you prefer:

{
  "issuer": "https://baa467f55bc7.eu.ngrok.io/oauth/v2/oauth-anonymous",
  "clientID": "mobile-client",
  "redirectUri": "io.curity.client:/callback",
  "postLogoutRedirectUri": "io.curity.client:/logoutcallback",
  "scope": "openid profile"
}

The code example requires an OAuth client that uses the Authorization Code Flow (PKCE) and its full XML is shown below:

<client>
    <id>mobile-client</id>
    <client-name>mobile-client</client-name>
    <no-authentication>true</no-authentication>
    <redirect-uris>io.curity.client:/callback</redirect-uris>
    <proof-key>
        <require-proof-key>true</require-proof-key>
    </proof-key>
    <refresh-token-ttl>3600</refresh-token-ttl>
    <scope>openid</scope>
    <scope>profile</scope>
    <user-authentication>
        <allowed-authenticators>Username-Password</allowed-authenticators>
        <allowed-post-logout-redirect-uris>io.curity.client:/logoutcallback</allowed-post-logout-redirect-uris>
    </user-authentication>
    <capabilities>
        <code>
        </code>
    </capabilities>
    <validate-port-on-loopback-interfaces>true</validate-port-on-loopback-interfaces>
</client>

AppAuth Integration

AppAuth libraries are included in the app’s build.gradle file as a library dependency, and the Custom URI Scheme is also registered here. This scheme is used by the code example for both login and logout redirects:

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

AppAuth coding 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

OAuth Lifecycle

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:

Android Password Login

The login process follows these important 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

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, config.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 the code_challenge PKCE parameters:

Query ParameterExample Value
client_idmobile-client
redirect_uriio.curity.client:/callback
response_typecode
state_eB_JSonBtp9AasrJuQZWA
nonce_TT6iiN8eFeF7U6afzNNgQ
scopeopenid profile
code_challengewmIZzT7QMPBLICXlvm19orboBMQnHKXGbMyyhfN8gPU
code_challenge_methodS256

When needed the library enables the app to customize OpenID Connect parameters. An example is to use the acr_values query parameter to specify a particular runtime 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 = mutableMapOf<String, String>()
val tokenRequest = response.createTokenExchangeRequest(extraParams)
authorizationService.performTokenRequest(tokenRequest) { tokenResponse, handleTokenExchangeCallback }

This sends an authorization code grant message, which is a POST to the Curity Identity Server's token endpoint with these parameters, including the code_verifier PKCE parameter:

Form ParameterExample Value
grant_typeauthorization_code
client_idmobile-client
codeCg85vgCfElPckCgzPYDcZUrMekzA1iv5
code_verifierex_OaauEnB0cLdBwXUXypYxr4j2CrkPNfWOsdI_lNrKAdgL1c-bx-Uizzsgb-0Eio58ohD85zKjWqWQc2lvjSQ
redirect_uriio.curity.client:/callback

When 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 signing in.

OAuth State

The demo app stores the following information in an ApplicationStateManager helper class, which uses the AppAuth library's AuthState class:

DataContains
MetadataThe Identity Server endpoints that AppAuth uses when sending OAuth request messages from the app
Token ResponseThe access token, refresh token and ID token that are returned to the app

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 = mutableMapOf<String, String>()
val tokenRequest = TokenRequest.Builder(metadata, config.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, including the following payload fields:

Form ParameterExample Value
grant_typerefresh_token
client_idmobile-client
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
post_logout_redirect_uriio.curity.client:/logoutcallback
stateIi8fYlMdVbX8fiMSmlI6SQ
id_token_hinteyJraWQiOiIyMTg5NTc5MTYiLCJ4NXQiOiJCdEN1Vzl ...

Logout Alternatives

It can sometimes be difficult to get the exact behavior desired when using end session requests. A better option is usually to just remove tokens from the app and return the app to the unauthenticated view. Subsequent sign in behavior can then be controlled via the following OpenID Connect fields. This can also be useful when testing, in order to sign in as multiple users on the same device:

OpenID Connect Request ParameterUsage
promptSet prompt=login to force the user to re-authenticate immediately
max-ageSet max-age=N to specify the maximum elapsed time in seconds before which the user must re-authenticate

Extending Authentication

Once AppAuth has been integrated it is then possible to extend authentication by simply changing the configuration of the mobile client in the Curity Identity Server, without needing any code changes in the mobile app. Webauthn is an option worth exploring, where users authenticate via familiar mobile credentials, but strong security is used.

Techniques

Storing Tokens

In order to prevent the need for a user login on every app restart, an app can potentially use the device's features for secure storage, and save tokens from the AuthState class to mobile secure storage, such as Encrypted Shared Preferences.

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 it on the device. By default the Chrome Custom Tab window 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

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 the Android 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:

Error Response

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

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:

AreaDescription
Dynamic Client RegistrationAuthenticated DCR should be used, so that each instance of the mobile app gets a unique client ID and client secret
HTTPS Redirect ResponsesAn HTTPS URL should be used to guarantee that a malicious app cannot 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 library, which manages 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.