/images/resources/tutorials/haapi/haapi-mobile-getting-started.png

Android Integration of the HAAPI Mobile UI SDK

On this page

The UI SDK for the Hypermedia Authentication API (HAAPI) enables you to quickly implement a security hardened OAuth or OpenID Connect flow for a mobile app. This tutorial demonstrates a fast implementation in a brand new mobile app. For an introduction to HAAPI concepts, see the overview article, or browse the HAAPI Whitepaper for details on the security design.

An end-to-end solution involves calls from the SDK to the HAAPI endpoints of the Curity Identity Server. The SDK first uses attestation to provide proof of the app's identity, then sends an OpenID Connect authentication request. The login flow is then dictated by responses from the API, and the SDK implements many frontend behaviors, which can include navigation, waiting for user input, invoking an external browser, polling, and responding to invalid input.

Provide an Application

Start by opening Android Studio, then create a new project using the Empty Views Activity option. Set Kotlin as the development language. Also set the minimum SDK version to API level 26 or higher, to ensure the hardware support needed for mobile attestation is present.

Create Project Android

Replace the content of the app/res/layout/activity_main.xml file with the following layout:

xml
123456789101112131415161718192021
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Unauthenticated View"
android:layout_marginTop="10dp"
style="@style/HeaderStyle"/>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/btn_login"
android:text="Start HAAPI Login"
android:layout_marginTop="10dp"
style="@style/ButtonStyle"/>
</LinearLayout>

Replace the app/res/values/colors.xml file with the following values:

xml
12345678
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
<color name="blue">#5865F1</color>
<color name="blue_dark">#005B9F</color>
<color name="purple">#D55BA0</color>
</resources>

Use the following simple application theme as the content of the app/res/values/themes/themes.xml files:

xml
1234567891011121314151617181920212223
<resources xmlns:tools="http://schemas.android.com/tools">
<style name="Theme.DemoApp" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<item name="colorPrimary">@color/black</item>
<item name="colorPrimaryVariant">@color/blue_dark</item>
<item name="colorSecondary">@color/black</item>
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
</style>
<style name="HeaderStyle" parent="android:Widget.TextView">
<item name="android:textColor">@color/black</item>
<item name="android:textSize">24sp</item>
<item name="android:textStyle">bold</item>
<item name="android:gravity">center</item>
</style>
<style name="ButtonStyle" parent="Widget.MaterialComponents.Button">
<item name="android:backgroundTint">@color/blue</item>
<item name="android:layout_margin">20dp</item>
<item name="android:minHeight">50dp</item>
<item name="android:textAllCaps">false</item>
</style>
</resources>

Run the Initial App

Next, run the minimal app, to show the initial screen with the basic theme provided above:

Empty App Android

Integrate the HAAPI SDK

Before you can implement the mobile security, you must register a client for the mobile app in the Curity Identity Server, and integrate SDK libraries. Do so by adding the UI SDK dependency to your project's gradle file, which will then be downloaded from Maven Central:

groovy
1234
dependencies {
...
implementation 'se.curity.identityserver:identityserver.haapi.android.ui.widget:4.0.0'
}

Replace the contents of MainActivity.kt with the following Kotlin code, while keeping the correct package name for your app. By default, a single line of code is used to launch the authentication flow, and a single line of code to receive tokens once it has completed. This code can be added wherever you would like within your mobile app:

kotlin
123456789101112131415161718192021222324252627282930313233343535
package io.curity.demoapp
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Button
import androidx.activity.result.ActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import se.curity.identityserver.haapi.android.driver.HaapiLogger
import se.curity.identityserver.haapi.android.ui.widget.HaapiFlowActivity
import se.curity.identityserver.haapi.android.ui.widget.models.OauthModel
class MainActivity : AppCompatActivity() {
private val startHaapiActivityForResult = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()) {
receiveTokenResponse(it)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
HaapiLogger.enabled = true
HaapiLogger.isDebugEnabled = true
val button = findViewById<Button>(R.id.btn_login)
button.setOnClickListener {
startHaapiActivityForResult.launch(HaapiFlowActivity.newIntent(this))
}
}
fun receiveTokenResponse(result: ActivityResult) {
val tokens = result.data?.getParcelableExtra(HaapiFlowActivity.className) as? OauthModel.Token
}
}

Next, implement the HaapiUIWidgetApplication interface in your app's Application class, to supply your OAuth settings and point to your instance of the Curity Identity Server. The objects created here are used throughout the lifetime of your app, as the user signs in and out:

kotlin
1234567891011121314151617181920212223242526272829303132
package io.curity.demoapp
import android.app.Application
import se.curity.identityserver.haapi.android.ui.widget.HaapiUIWidgetApplication
import se.curity.identityserver.haapi.android.ui.widget.PresentationMode
import se.curity.identityserver.haapi.android.ui.widget.WidgetConfiguration
import java.net.URI
import java.time.Duration
class DemoApplication: Application(), HaapiUIWidgetApplication {
private val baseUri = URI("https://login.example.com")
override val widgetConfiguration: WidgetConfiguration =
WidgetConfiguration.Builder(
clientId = "haapi-android-ui-client",
baseUri = baseUri,
tokenEndpointUri = baseUri.resolve("/oauth/v2/oauth-token"),
authorizationEndpointUri = baseUri.resolve("/oauth/v2/oauth-authorize"),
appRedirect = "app://haapi"
)
.setPresentationMode(PresentationMode.MODAL)
.setAutoPollingDuration(Duration.ofSeconds(10))
.setTokenBoundConfiguration(createTokenBoundConfiguration())
.setOauthAuthorizationParamsProvider {
WidgetConfiguration.OAuthAuthorizationParams(
scope = listOf("openid", "profile")
)
}
.build()
}

You can reference the HAAPI code example configuration as a guide. You should configure a TokenBoundConfiguration object if running version 8.7 or later of the Curity Identity Server. This object is required so that requests for OAuth tokens are protected by DPoP proofs, as described in the Security Lifecycle tutorial.

Next, ensure that you reference this class in your Android manifest file at app/manifests/AndroidManifest.xml:

xml
123456789
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application
android:name=".DemoApplication"
android:label="HAAPI Android Demo App"
...
</application>
</manifest>

If required, additional authentication request parameters, such as prompt and max_age can be included in the extraRequestParameters field of the OAuthAuthorizationParams object. To support different languages, the HAAPI SDK enables you to specify a custom Accept/Language header. The following example forces a login on every authentication request, and also uses Portuguese translations for login screens:

kotlin
12345678910111213141516171819202122232425262727
class DemoApplication: Application(), HaapiUIWidgetApplication {
private val baseUri = URI("https://login.example.com")
override val widgetConfiguration: WidgetConfiguration =
WidgetConfiguration.Builder(
clientId = "haapi-android-ui-client",
baseUri = baseUri,
tokenEndpointUri = baseUri.resolve("/oauth/v2/oauth-token"),
authorizationEndpointUri = baseUri.resolve("/oauth/v2/oauth-authorize"),
appRedirect = "app://haapi"
)
.setPresentationMode(PresentationMode.MODAL)
.setTokenBoundConfiguration(createTokenBoundConfiguration())
.setAutoPollingDuration(Duration.ofSeconds(10))
.setOauthAuthorizationParamsProvider {
WidgetConfiguration.OAuthAuthorizationParams(
scope = listOf("openid", "profile"),
extraRequestParameters = mapOf("prompt" to "login")
)
}
.setHttpHeadersProvider {
mapOf("Accept-Language" to "pt")
}
.build()
}

If you need to support devices without hardware attestation support, you can do so by configuring attestation fallback, though you will ignore those options when getting started.

Update the Manifest File

Next, ensure that the HAAPI flow activity from the SDK, and the custom application class, are also registered in your Android manifest file. Also apply the HAAPI default theming to this activity:

xml
123456789
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application android:name=".DemoApplication">
<activity android:name=".MainActivity" ... />
<activity
android:name="se.curity.identityserver.haapi.android.ui.widget.HaapiFlowActivity"
android:label="Haapi Flow"
android:theme="@style/Theme.Haapi.Ui.Widget.Activity.Modal"/>
</application>
</manifest>

Authenticate a User

When you click the app's Start HAAPI Login button, the SDK will read the configuration details from the application class, then run the HAAPI login flow. This triggers a code flow, so the user can authenticate in many ways. All authentication requests are sent in an API driven manner.

The following example uses a simple username and password authenticator, with the default look and feel. A modal window can be used, which enables you to implement authentication without impacting the main app's navigation.

Initial Login

Once authentication is complete, the app receives a token response. This consists of an access token, and a refresh token if enabled. When the openid scope is used, an ID token is also received. A real app would then store tokens in memory or secure storage, then use them to call APIs, in the standard way.

Handle Error Responses

Authentication can fail for various reasons, such as invalid input, invalid credentials or misconfiguration of the security settings. The UI SDK handles error responses for you, and provides default presentation. Ths error look and feel is shown in the username password screenshots.

In the event of technical errors where HAAPI cannot continue, an error display is presented in an alert dialog. The following unexpected error was simulated by configuring an invalid client ID. Note that an error_description is returned if expose detailed error messages is enabled against the token service of the Curity Identity Server. This is useful during development but it is more secure to disable it in production.

Android Login Error

If you prefer, you can instead receive the exception as the authentication result, and handle it yourself. This behavior is enabled by configuring setShouldAutoHandleFlowErrorFeedback(false) on the configuration builder, and then adding code to receive the error:

kotlin
123456789101112131415161718192021222324
private val startHaapiActivityForResult = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) {
val model = it.data?.getParcelableExtra(HaapiFlowActivity.className) as? OauthModel
if (model is OauthModel.Token) {
val authenticatedActivity = AuthenticatedActivity.newIntent(this, userInfoURI, model)
startActivity(authenticatedActivity)
}
if (model is OauthModel.Error) {
Log.d("DEBUG", "Problem encountered: ${model.error}, ${model.errorDescription}")
}
}
if (activityResult.resultCode == RESULT_CANCELED) {
Log.d("DEBUG", "User canceled the login attempt");
}
}
fun receiveErrorResponse(result: ActivityResult) {
val error = result.data?.getSerializableExtra(HaapiFlowActivity.className) as? Throwable
}

Use SDK Logging

During development, you can enable logger classes in the UI SDK to get further information on technical behavior. Do so by adding the following code to your application's startup code:

kotlin
12
HaapiLogger.enabled = true
HaapiLogger.isDebugEnabled = true

You can then view output in Android Studio's logcat window, to see information that includes both API JSON responses and the results of frontend processing:

text
123456
HAAPI_UI_FLOW_FRAGMENT - submitting authenticator item selection result SelectionItemModel(title=Username-Password, type=html-form, icon=hui_ic_authenticator_html_form, action={"continueActions":[],"errorActions":[],"fields":[],"type":"application\/x-www-form-urlencoded","method":"GET","href":"\/authn\/authentication\/Username-Password"}, shouldKeepFocus=false)
HAAPI_UI_FLOW - submit() is invoked for form:{"continueActions":[],"errorActions":[],"fields":[],"type":"application\/x-www-form-urlencoded","method":"GET","href":"\/authn\/authentication\/Username-Password"} and params:{}
HAAPI_UI_FLOW - IsLoading observer triggered true
HAAPI_SDK_FLOW - submitForm with {"continueActions":[],"errorActions":[],"fields":[],"type":"application\/x-www-form-urlencoded","method":"GET","href":"\/authn\/authentication\/Username-Password"} with parameters keys: []
HAAPI_SDK_HTTP - Response = {"links":[{"href":"/authn/authentication/Username-Password/forgot-password","rel":"forgot-password","title":"Forgot your password?"},{"href":"/authn/authentication/Username-Password/forgot-account-id","rel":"forgot-account-id","title":"Forgot your username?"},{"href":"/authn/registration/Username-Password","rel":"register-create","title":"Create account"}],"metadata":{"viewName":"authenticator/html-form/authenticate/get"},"type":"authentication-step","actions":[{"template":"form","kind":"login","title":"Login","model":{"href":"/authn/authentication/Username-Password","method":"POST","type":"application/x-www-form-urlencoded","actionTitle":"Login","fields":[{"name":"userName","type":"username","label":"Username"},{"name":"password","type":"password","label":"Password"}]}}]}
HAAPI_UI_FLOW - Receive a new HaapiResponse: {"metadata":{"viewName":"authenticator\/html-form\/authenticate\/get"},"type":"AUTHENTICATION_STEP","actions":[{"kind":"login","title":{"literal":"Login"},"model":{"actionTitle":{"literal":"Login"},"continueActions":[],"errorActions":[],"fields":[{"name":"userName","label":{"literal":"Username"}},{"name":"password","label":{"literal":"Password"}}],"type":"application\/x-www-form-urlencoded","method":"POST","href":"\/authn\/authentication\/Username-Password"}}],"links":[{"href":"\/authn\/authentication\/Username-Password\/forgot-password","rel":"forgot-password","title":{"literal":"Forgot your password?"}},{"href":"\/authn\/authentication\/Username-Password\/forgot-account-id","rel":"forgot-account-id","title":{"literal":"Forgot your username?"}},{"href":"\/authn\/registration\/Username-Password","rel":"register-create","title":{"literal":"Create account"}}],"messages":[]}

Code Example

When getting started for HAAPI on Android, take a look at the HAAPI Android code example, which provides a working end-to-end setup for developers. This app also demonstrates various customization techniques, for controlling the login user experience.

Conclusion

At this stage, the mobile app's security implementation is complete. The next tutorial focuses on the default look and feel for username and password flows.

Join our Newsletter

Get the latest on identity management, API Security and authentication straight to your inbox.

Start Free Trial

Try the Curity Identity Server for Free. Get up and running in 10 minutes.

Start Free Trial