Porta on Android - Configuration for Kotlin

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 Porta 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/login.porta.com",
  "clientID": "client-mobile",
  "redirectUri": "io.porta.client:/callback",
  "postLogoutRedirectUri": "io.porta.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>client-mobile</id>
    <client-name>client-mobile</client-name>
    <no-authentication>true</no-authentication>
    <redirect-uris>io.porta.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.porta.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.porta.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.

Pattern

Description

Builders

Builder classes are used to create OAuth request messages

Callbacks

Callback functions are used to receive OAuth response messages

Error Codes

Error codes can be used to determine particular failure causes

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.

The login process follows these important best practices from RFC8252:

Best Practice

Description

Login via System Browser

Logins use a Chrome Custom Tab, meaning that the app itself never has access to the user's password

PKCE

Proof 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:porta: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 Parameter

Example Value

client_id

mobile-client

redirect_uri

io.porta.client:/callback

response_type

code

state

eBJSonBtp9AasrJuQZWA

nonce

_TT6iiN8eFeF7U6afzNNgQ

scope

openid profile

code_challenge

wmIZzT7QMPBLICXlvm19orboBMQnHKXGbMyyhfN8gPU

codechallengemethod

S256

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 Porta Identity Server's token endpoint with these parameters, including the code_verifier PKCE parameter:

Form Parameter

Example Value

grant_type

authorization_code

client_id

mobile-client

code

Cg85vgCfElPckCgzPYDcZUrMekzA1iv5

code_verifier

exOaauEnB0cLdBwXUXypYxr4j2CrkPNfWOsdIlNrKAdgL1c-bx-Uizzsgb-0Eio58ohD85zKjWqWQc2lvjSQ

redirect_uri

io.porta.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:

Data

Contains

Metadata

The Identity Server endpoints that AppAuth uses when sending OAuth request messages from the app

Token Response

The 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 Porta 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 Porta Identity Server's token endpoint, including the following payload fields:

Form Parameter

Example Value

grant_type

refresh_token

client_id

client-mobile

refresh_token

62a11202-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 Parameter

Example Value

postlogoutredirect_uri

io.porta.client:/logoutcallback

state

Ii8fYlMdVbX8fiMSmlI6SQ

idtokenhint

eyJraWQiOiIyMTg5NTc5MTYiLCJ4NXQiOiJCdEN1Vzl ...

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 Parameter

Usage

prompt

Set prompt=login to force the user to re-authenticate immediately

max-age

Set 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 Porta 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.

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.

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