Implementing HAAPI Fallback

Implementing HAAPI Fallback

Intro

When using the Hypermedia Authentication API (HAAPI), the mobile client proves its identity to the Identity Server before allowing authentication to begin. In production environments this is done by accessing public certificate details of the Apple or Google key with which the app is signed. This results in an access token being issued with a haapi scope, which is then used for every API request during the authentication process.

To access the signing certificate details, hardware support is required, which may not be present in certain mobile devices, especially for older Android versions. The Mobile Fallback Attestation article explains the design for using secure alternative methods to attest the client identity. This tutorial will show how to implement this pattern using the Curity Identity Server.

Identity Server Version

The instructions in this tutorial require version 7.0 or later of the Curity Identity Server

Run the Code Example

The Android HAAPI Code Example article enables developers to run a demo app that uses hypermedia based authentication, via a few simple steps:

  • Clone the GitHub repository
  • Copy in a license file for the Curity Identity Server
  • Run a start-idsvr.sh script to spin up a preconfigured Curity Identity Server running in Docker
  • Run the app on an emulator or device via Android Studio

To reproduce failed signing key based attestation, run the Admin UI and edit the Android attestation policy via the Facilities menu. Then select the following production options:

Enable Production Attestation

Emulators lack the hardware support needed for production attestation, so this will lead to the following error:

Attestation Error

Identity Server Fallback Configuration

This section will describe how to configure the Curity Identity Server to support fallback attestation when needed. Instances of the mobile app running on non-compliant devices will then prove their identity based on an alternative client credential, resulting in the creation of a dynamic client.

Add an Authentication Method

The quickest way to get an end-to-end solution working is to start with a simple fixed string secret. This is not the most secure option and could be considered a stage 1 solution. Edit the HAAPI client and add a client credential that will be used when Dynamic Client Registration needs to be performed. Alternatively import the following XML to overwrite the existing code example's settings:

<config xmlns="http://tail-f.com/ns/config/1.0">
    <profiles xmlns="https://curity.se/ns/conf/base">
    <profile>
    <id>token-service</id>
    <type xmlns:as="https://curity.se/ns/conf/profile/oauth">as:oauth-service</type>
      <settings>
        <authorization-server xmlns="https://curity.se/ns/conf/profile/oauth">
          <client-store>
            <config-backed>
              <client>
                <id>haapi-android-client</id>
                <client-name>Haapi Android Client</client-name>
                <secret>Password1</secret>
                <redirect-uris>app://haapi</redirect-uris>
                <proof-key>
                  <require-proof-key>false</require-proof-key>
                </proof-key>
                <refresh-token-ttl>3600</refresh-token-ttl>
                <audience>haapi-client</audience>
                <scope>openid</scope>
                <scope>profile</scope>
                <user-authentication>
                </user-authentication>
                <capabilities>
                  <code>
                  </code>
                  <haapi>
                  </haapi>
                </capabilities>
                <attestation>
                  <android>
                    <package-name>io.curity.haapidemo</package-name>
                    <signature-digest>Z2DKEZO2XWFWQnApoRCzhqhIxzODe7BUsArj4Up9oKQ=</signature-digest>
                    <android-policy>android-dev-policy</android-policy>
                  </android>
                </attestation>
              </client>
            </config-backed>
          </client-store>
        </authorization-server>
      </settings>
    </profile>
  </profiles>
</config>

Enable DCR

Next navigate to Profiles / Token Service / Dynamic Client Registration and enable Templatized option, which provides the best management options for mobile apps.

Add a Template Client

Then add a template client called haapi-template-client, with the Code Flow and HAAPI capabilities, or import the following XML. The OAuth settings such as scopes must match those of the main HAAPI client. Also ensure that the allow-without-attestation value is set to true.

<config xmlns="http://tail-f.com/ns/config/1.0">
  <profiles xmlns="https://curity.se/ns/conf/base">
    <profile>
    <id>token-service</id>
    <type xmlns:as="https://curity.se/ns/conf/profile/oauth">as:oauth-service</type>
      <settings>
        <authorization-server xmlns="https://curity.se/ns/conf/profile/oauth">
          <client-store>
            <config-backed>
              <client>
                <id>haapi-template-client</id>
                <client-name>haapi-template-client</client-name>
                <redirect-uris>app://haapi</redirect-uris>
                <proof-key>
                  <require-proof-key>false</require-proof-key>
                </proof-key>
                <refresh-token-ttl>3600</refresh-token-ttl>
                <audience>haapi-client</audience>
                <scope>openid</scope>
                <scope>profile</scope>
                <user-authentication>
                </user-authentication>
                <capabilities>
                  <code>
                  </code>
                  <haapi>
                    <allow-without-attestation>true</allow-without-attestation>
                  </haapi>
                </capabilities>
                <dynamic-client-registration-template>
                  <secret/>
                  <authenticate-client-by>haapi-android-client</authenticate-client-by>
                </dynamic-client-registration-template>
                <use-pairwise-subject-identifiers>
                  <sector-identifier>haapi-template-client</sector-identifier>
                </use-pairwise-subject-identifiers>
                <validate-port-on-loopback-interfaces>true</validate-port-on-loopback-interfaces>
              </client>
            </config-backed>
          </client-store>
        </authorization-server>
      </settings>
    </profile>
  </profiles>
</config>

Android Code Updates

The following sections explain the main changes to code in an Android app, to add support for DCR fallback when using the Curity Android SDK.

Android HAAPI SDK Version

Before following these steps, ensure that your Android app is using version 2.1.1 or later of the Curity Android HAAPI SDK

Add Configuration Values

Values must be added to the application configuration to represent the template client ID, the registration endpoint, and the type of DCR client credential you are using. In the code example, this is done simply by uncommenting the last three lines in the configuration object, after which fallback attestation will work, and you will be able to continue to sign in as the test user.

Configuration(
    name = name,
    clientId = "haapi-android-client",
    baseURLString = "https://10.0.2.2:8443",
    tokenEndpointURI = "https://10.0.2.2:8443/oauth/v2/oauth-token",
    authorizationEndpointURI = "https://10.0.2.2:8443/oauth/v2/oauth-authorize",
    userInfoEndpointURI = "https://10.0.2.2:8443/oauth/v2/oauth-userinfo",
    metaDataBaseURLString = "https://10.0.2.2:8443/oauth/v2/oauth-anonymous",
    redirectURI = "app://haapi",
    followRedirect = true,
    isSSLTrustVerificationEnabled = false,
    selectedScopes = listOf("openid", "profile"),

    // dcrTemplateClientId = "haapi-template-client",
    // dcrClientRegistrationEndpointUri = "https://10.0.2.2:8443/token-service/oauth-registration",
    // deviceSecret = "Password1"
)

Create Main SDK Objects

The main SDK objects used by apps are the HaapiManager and the OAuthTokenManager. With the latest SDK these should be created using a new factory class and some builder methods, to return an accessor object:

val dcrConfiguration = DcrConfiguration(
    templateClientId = configuration.dcrTemplateClientId!!,
    clientRegistrationEndpointUri = URI(configuration.dcrClientRegistrationEndpointUri),
    context = context
)

val dcrClientCredentials =
    ClientAuthenticationMethodConfiguration.Secret(configuration.deviceSecret!!)

val accessor = accessorFactory
    .setDcrConfiguration(dcrConfiguration)
    .setClientAuthenticationMethodConfiguration(dcrClientCredentials)
    .create()

val haapiManager = accessor.haapiManager
val oauthTokenManager = accessor.oauthTokenManager

The accessor class should be created during login, after which the objects will typically be used across multiple views. The code example therefore uses a global repository to store the accessor containing the SDK objects. After logging out, the code example shows how objects must be closed.

if (accessor != null) {
    accessor.haapiManager.close()
    accessor = null
}

SDK Behavior

The first time this code runs, the SDK will try to perform client attestation in the most secure way. If this is not possible then the SDK will fallback to registering a dynamic client, using the credential provided. This will result in a dynamic client ID and client secret that the SDK persists in mobile storage. This dynamic client ID and secret will then be used to get the HAAPI access token.

On all subsequent authentication attempts, the application will continue to create objects in the above manner, and does not need to be aware of the dynamic client ID being used. The SDK will detect that there is already a dynamic client ID and secret stored on this device, and will again use them to create the HAAPI access token. In both cases the HAAPI flow will then proceed in the normal way, using the access token to protect each hypermedia API request.

Strong Credentials

Once the process for implementing DCR fallback is understood, the next step is to provide strong client credentials based on public key infrastructure (PKI). It is recommended to use one of the options described in the following sections, though other solutions can also be designed, and further options are explained in the HAAPI SDK Docs.

The strong credential solutions explained below also support distinct client keys per device, which is recommended for the highest security. Popular ways of enabling this are to use a Mobile Device Management (MDM) system or a Software Attestation Framework.

Using Client Certificates

Attesting the client identity via a client certificate requires the use of transport level security (TLS). The process for sending client certificates from Android code is first to replace the deviceSecret configuration field with new variables such as these.

FieldDescription
deviceKeyStoreAn Android keystore containing a client certificate to be sent in the Mutual TLS connection to the Identity Server
deviceKeyStorePasswordThe password needed to access the client certificate's key
serverTrustStoreAn Android keystore referencing the root authority of the Identity Server's SSL certificate

Standard Android keystore technology would then be used, to load keystores and supply them to the SDK. The server trust store could be a BKS file deployed with the Android app, whereas the client certificate could be a separate keystore that has been populated by a third party system.

val dcrClientCredentials =
    ClientAuthenticationMethodConfiguration.Mtls(
        clientKeyStore = deviceKeyStore,
        clientKeyStorePassword = deviceKeyStorePassword,
        serverTrustStore = serverTrustStore
    )

The Curity Identity Server will first need to be updated to allow Mutual TLS on the token endpoint and the registration endpoint. The HAAPI client's authentication method must then be set to Mutual TLS, and configured to trust a root certificate authority from the issuing third party:

Mutual TLS Configuration

Using Client Assertions

Alternatively, message level security can be used, which is sometimes easier to manage. This is achieved by sending a JWT client assertion from each device. Firstly this requires similar changes to application configuration settings, to use values such as these:

FieldDescription
deviceKeyStoreAn Android keystore containing a client key with which to sign a client assertion
deviceKeyStorePasswordThe password needed to access the client key

An Android keystore would again be used, to load the client keystore and supply the key. When required, the SDK will do the work of producing the JWT client assertion and sending it to the Identity Server.

val dcrClientCredentials =
    ClientAuthenticationMethodConfiguration.SignedJwt.Asymmetric(
      clientKeyStore = deviceKeyStore,
      clientKeyStorePassword = deviceKeyStorePassword,
      alias = "rsa",
      algorithmIdentifier = ClientAuthenticationMethodConfiguration.SignedJwt.Asymmetric.AlgorithmIdentifier.RS256
    )

First ensure that the Curity Identity Server is configured to allow use of JWT assertions for client authentication, and that asymmetric algorithms include that specified in the Android code:

Assertion Algorithms

Next update the Identity Server to point to a JWKS URI that provides a JSON Web Key Set (JWKS) containing trusted public keys in a JWK format. This URL can be provided via a simple API that manages public keys, or could potentially be an endpoint provided by a third party system. This mechanism enables distinct keys to be used to sign client assertions, which all belong to the same keyset.

JWKS URI Configuration

Crypto on Development Computers

Running an end-to-end Android and Identity Server setup is a little tricky for developers. The mobile deployments repo therefore has some Android Crypto Resources. The README file explains how to use OpenSSL to issue keys and certificates for development. Public keys can then be configured in the Identity Server. Private keys can be loaded from keystores into the Android code example, then used as the base for Mutual TLS or Client Assertion credentials.

Multi-User Behavior

Although Android apps reference DCR related details when creating HAAPI objects, the SDK always tries to use the most secure attestation, by verifying the app's production certificate details. This ensures that the majority of users will continue to use the highest security.

Only when this fails does the SDK fallback to using the DCR details. The small percentage of users with non-compliant devices will then use HAAPI based logins via DCR fallback, but with the same user experience.

Client Assurance Level

Once authentication completes, a client assurance level (cal) authentication attribute is available, to represent the strength with which the client app's identity was proven. This is illustrated below using a Debug Authentication Action. If required, a token procedure can be used to include this claim in access tokens. An API could then use it to deny access to sensitive operations unless strong attestation was used:

Client Assurance Level

Conclusion

This tutorial showed how to update code in a HAAPI secured Android app, to support DCR fallback. This ensures that that non-compliant devices can be identified in a secure way before authentication begins. For the most secure DCR fallback attestation, it is recommended to use strong credentials, with distinct client keys per device.