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