/images/resources/tutorials/haapi/haapi-mobile-advanced-customizations.png

Advanced Login Customizations for HAAPI Mobile Apps

On this page

A mobile app that uses the HAAPI UI SDK can customize login screens in various ways. When getting started, use the basic customizations explained in the Android and iOS tutorials, to implement themes and change static text. This tutorial explains how to implement more advanced extensibility. These customizations usually fit into one of the following categories, to provide complete control over data, logic and presentation.

  • Add or remove particular UI elements within a login screen.
  • Replace an entire login screen.
  • Implement login screens for a custom authenticator.

Before using advanced customizations, understand the stages of an end-to-end HAAPI flow. Authenticators in the Curity Identity Server can provide a HAAPI representation that mobile apps receive as a JSON API response. The UI SDK then deserializes response data to UI models, and uses model data to dynamically create login screens that contain UI elements.

HAAPI Stages

HAAPI Representations

The mobile app receives Hypermedia Authentication API responses from the Curity Identity Server, where the response data expresses authentication steps. Authentication steps, in turn, represent an authenticator or an authentication action. Consequently, authenticators and authentication actions define the shape of the steps, the HAAPI representation.

When considering advanced customizations, it can be useful to learn how authenticators and authentication actions define the representation by studying the Server SDK. The following example representation is from the Username Password Authenticator, which you can use or adapt if the built-in HTML form authenticator's representation does not meet your needs.

java
123456789101112131415161718192021222324252627282930313233343536373839404142434445
public class AuthenticateGetRepresentationFunction implements RepresentationFunction
{
private static final Message MSG_TITLE = Message.ofKey("meta.title");
private static final Message MSG_ACTION = Message.ofKey("view.authenticate");
private static final Message MSG_HEADER = Message.ofKey("view.top-header");
private static final Message MSG_USERNAME = Message.ofKey("view.username");
private static final Message MSG_PASSWORD = Message.ofKey("view.password");
private static final Message MSG_FORGOT_PASSWORD = Message.ofKey("view.forgot-password");
private static final Message MSG_FORGOT_ACCOUNT_ID = Message.ofKey("view.forgot-account-id");
private static final Message MSG_REGISTER = Message.ofKey("view.no-account");
@Override
public Representation apply(RepresentationModel model, RepresentationFactory factory)
{
String authUrl = model.getString("_authUrl");
Optional<String> registerUrl = model.getOptionalString("_registerUrl");
Optional<String> username = model.getOptionalString("_username");
return factory.newAuthenticationStep(step -> {
step.addMessage(MSG_HEADER, HaapiContract.MessageClasses.HEADING);
step.addFormAction(HaapiContract.Actions.Kinds.LOGIN, URI.create(authUrl),
HttpMethod.POST,
MediaType.X_WWW_FORM_URLENCODED,
MSG_TITLE,
MSG_ACTION,
fields -> {
fields.addUsernameField("userName", MSG_USERNAME, username.orElse(""));
fields.addPasswordField("password", MSG_PASSWORD);
});
step.addLink(URI.create(authUrl + "/forgot-password"),
HaapiContract.Links.Relations.FORGOT_PASSWORD,
MSG_FORGOT_PASSWORD);
step.addLink(URI.create(authUrl + "/forgot-account-id"),
FORGOT_ACCOUNT_ID,
MSG_FORGOT_ACCOUNT_ID);
registerUrl.ifPresent(regUrl -> step.addLink(
URI.create(regUrl),
HaapiContract.Links.Relations.REGISTER_CREATE,
MSG_REGISTER));
});
}
}

The server uses a Data Model to serialize HAAPI representations to a JSON hypermedia wire format and returns it to the mobile app. The data in the server's JSON responses includes static text from message files that you can customize. The HAAPI UI SDK dynamically builds login screens with UI elements from the server response. The user interacts with login screens to perform actions like navigation and entering data. Eventually, the user submits forms containing data that the view posts to the authenticator's HTTP Routes.

Hypermedia Authentication API Messages

The UI SDK sends an HTTP GET request when it first triggers an authenticator, after which the UI SDK receives a hypermedia API response. When customizing behavior it is useful to view HAAPI messages and study the response data and its impact on the UI.

The following example response payload represents an HTML form (username and password) hypermedia API response returned to the mobile app. In this example, the UI SDK creates elements like labels, input fields, buttons and hyperlinks.

json
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748
{
"links": [
{
"href": "/authn/authentication/HtmlForm/forgot-password",
"rel": "forgot-password",
"title": "Forgot your password?"
},
{
"href": "/authn/authentication/HtmlForm/forgot-account-id",
"rel": "forgot-account-id",
"title": "Forgot your username?"
},
{
"href": "/authn/registration/HtmlForm",
"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/HtmlForm",
"method": "POST",
"type": "application/x-www-form-urlencoded",
"actionTitle": "Login",
"fields": [
{
"name": "userName",
"type": "username",
"label": "Username"
},
{
"name": "password",
"type": "password",
"label": "Password"
}
]
}
}
]
}

When the user performs actions like navigation or submitting forms, the UI SDK sends a request to the server. In particular, the frontend sends authentication proofs to the server for validation. The following example shows username and password properties posted back to the server as form URL-encoded data.

text
12345678910
POST /authn/authentication/HtmlForm HTTP/1.1
Host: login.example.com
User-Agent: haapidemo/1 CFNetwork/3826.500.111.2.2 Darwin/24.4.0
Content-Length: 36
Accept: application/vnd.auth+json
Content-Type: application/x-www-form-urlencoded
Authorization: DPoP eyJraWQiOiJkenMtZlEzZ...
Dpop: eyJhbGciOiJF...
userName=demouser&password=Password1

To gain a solid understanding of HAAPI messages, consider running a ngrok mobile setup so that you can easily view HAAPI messages sent between a mobile app and the Curity Identity Server.

ngrok Messages

From the server's viewpoint, only the input and output data matters. The mobile app can customize its data, logic and presentation however it likes, as long as it sends the user's input to the server with the expected parameters.

Customize Data with Model Transformations

The UI SDK deserializes HAAPI representations into a number of UI models. The following table summarizes the most commonly used UI model types. The Android and iOS reference documentation explains each UI model in further detail.

UI Model TypeBehavior
FormModelPresents a collection of interaction items to the user.
SelectorModelPresents a list of selection items to the user.
GenericModelPresents both interaction and selection items to the user.
PollingModelPerforms a timed, repeated action like polling the server to check for login completion outside of the app.
ProblemModelReturns problem details to the mobile app if there is a server-side or client-side error.

To work with UI models, start by creating a custom data mapper, which enables some types of model modifications before passing the UI model to views.

For Android, extend the HaapiUIWidgetApplication instance to override the dataMappersFactory method and return a data mapper object:

kotlin
1234567
@OptIn(ExperimentalHaapiApi::class)
override val dataMappersFactory: DataMappersFactory
get() = CustomDataMapper(
redirectTo = configuration.redirectURI,
autoPollingDuration = widgetConfiguration.autoPollingDuration,
useDefaultExternalBrowser = widgetConfiguration.useDefaultExternalBrowser
)

You can transform UI models based on their generic type, for example to apply the same customization to all views that use a FormModel. Alternatively, transform particular UI models using a property, such as a view name that identifies a particular login screen. The following example demonstrates the latter approach.

kotlin
123456789101112131415161718192021
@OptIn(ExperimentalHaapiApi::class)
class CustomDataMapper(
redirectTo: String,
autoPollingDuration: Duration,
useDefaultExternalBrowser: Boolean
) : HaapiDataMappersFactory(redirectTo, autoPollingDuration, useDefaultExternalBrowser) {
override val mapHaapiResponseToModel: (se.curity.identityserver.haapi.android.sdk.models.HaapiResponse) -> UIModel
get() = {
val defaultModel = super.mapHaapiResponseToModel(it)
val formModel = defaultModel as? FormModel
if (formModel?.viewName == "authenticator/html-form/authenticate/get") {
CustomHtmlFormLoginModel.create(formModel)
} else {
defaultModel
}
}
...
}

To transform UI models, first include the org.jetbrains.kotlin.plugin.parcelize plugin in your Android project. Then transform the UI model to provide a new UI model to the view. For example, the following code snippet removes the Forgot Username link from the password login screen. The code also removes the default login title and adds some extra data to the model.

kotlin
123456789101112131415161718192021222324252626
@Parcelize
data class CustomHtmlFormLoginModel(
val extraData: String,
override val interactionItems: List<InteractionItem>,
override val linkItems: List<LinkItemModel>,
override val messageItems: List<InfoMessageModel>,
override val templateArea: String?,
override val viewName: String?
) : FormModel {
companion object {
fun create(formModel: FormModel): CustomHtmlFormLoginModel {
return CustomHtmlFormLoginModel(
extraData = getExtraData(),
interactionItems = formModel.interactionItems.filter { !it.key.contains("Login") },
linkItems = formModel.linkItems.filter { !it.href.contains("forgot-account-id") },
messageItems = formModel.messageItems,
templateArea = formModel.templateArea,
formModel.viewName
)
}
}
}

Customize Logic with Subclassed Views

For each UI model type, the UI SDK provides a corresponding view type whose name depends on the mobile platform. The following table provides a summary of the main view types and the UI model types that they process.

UI Model TypeAndroid View TypeiOS View Type
FormModelFormFragmentFormViewController
SelectorModelSelectorFragmentSelectorViewController
GenericModelGenericFragmentGenericViewController
PollingModelPollingFragmentPollingViewController
ProblemModelProblemFragmentProblemViewController

In some cases, the default login screen may mostly meet your needs but require minor customizations to UI controls or frontend logic. To begin customizing views, create a custom resolver object that can selectively override view creation.

kotlin
12
@OptIn(ExperimentalHaapiApi::class)
override val customFragmentResolver: FragmentResolver = CustomFragmentResolver()

Within the custom resolver object, use UI model properties or the UI model type to override views. The following example overrides the view logic for the HTML form's login screen and the authentication selection screen:

kotlin
12345678910111213141516171819202122232425262728293031323334353636
@OptIn(ExperimentalHaapiApi::class)
class CustomFragmentResolver : FragmentResolver {
private val defaultResolver = HaapiFlowFragmentResolver.Builder().build()
override fun <T : UIModel> getFragment(uiModel: T): HaapiFlowFragment<T> {
if (uiModel is FormModel) {
return createFormFragment(uiModel) as HaapiFlowFragment<T>
}
if (uiModel is SelectorModel) {
return createSelectorFragment(uiModel) as HaapiFlowFragment<T>
}
return defaultResolver.getFragment(uiModel)
}
private fun createFormFragment(model: FormModel): HaapiFlowFragment<FormModel> {
if (model is CustomHtmlFormLoginModel) {
return CustomHtmlFormLoginFragment.newInstance(model)
}
return FormFragment.newInstance(model)
}
private fun createSelectorFragment(model: SelectorModel): HaapiFlowFragment<SelectorModel> {
if (model.viewName == "views/select-authenticator/index") {
return CustomAuthenticationSelectionFragment.newInstance(model)
}
return SelectorFragment.newInstance(model)
}
}

You can then implement overrides to inject custom logic. Most commonly, this involves adding UI elements to the view or writing additional client-side validation before allowing the submission of a login form. The following code shows how to get started with view overrides.

On Android, each view type renders a number of standard elements, summarized in the below table.

Element NameDescription
hui_fragment_$VIEWTYPE_info_viewThe main area for the view.
hui_fragment_$VIEWTYPE_recycler_viewAdditional items, such as items to select in a selector view.
hui_fragment_$VIEWTYPE_bottom_spaceVertical spacing.
hui_fragment_$VIEWTYPE_links_recycler_viewLinks that show at the end of the view.

To partially customize a view, create a new XML layout file that includes the standard elements and also any custom elements. The following example first renders a banner at the top of the view and then renders standard elements below it.

xml
1234567891011121314151617181920212223242526272828
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
<TextView
android:id="@+id/banner_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/primary"
android:gravity="center"
android:textStyle="bold"
android:textSize="18sp"
android:padding="20dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<se.curity.identityserver.haapi.android.ui.widget.components.InfoView
android:id="@+id/hui_fragment_form_info_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/banner_text"
app:layout_constraintEnd_toEndOf="parent" />
...
</androidx.constraintlayout.widget.ConstraintLayout>

Then, reference the ID of the custom layout element in the view's code. The following example looks up a custom element for header banner text and sets its runtime value. When required, the view's code can use custom model data.

kotlin
123456789101112131415161718192021222324252627
class CustomHtmlFormLoginFragment : FormFragment(layoutId = R.layout.haapi_html_login_form) {
companion object {
fun newInstance(uiModel: UIModel) : CustomHtmlFormLoginFragment {
return CustomHtmlFormLoginFragment().apply {
arguments = uiModelBundle(uiModel)
}
}
}
override fun preRender(uiModel: UIModel?, view: View, savedInstanceState: Bundle?) {
super.preRender(uiModel, view, savedInstanceState)
if (uiModel is CustomHtmlFormLoginModel) {
val bannerView = view.findViewById<TextView>(R.id.banner_text)
bannerView.text = getBannerTextFromExtraData(uiModel.extraData)
}
}
override fun preSubmit(
interactionAction: InteractionAction,
keysValues: Map<String, Any>,
block: (Boolean, Map<String, Any>) -> Unit
) {
super.preSubmit(interactionAction, keysValues, block)
}
}

Replace Whole Screens with Custom Views

In some cases, subclassed views may be insufficient to meet deeper requirements, like a precise layout or the use of custom UI elements. Therefore, the UI SDK allows you to completely replace entire screens.

The following snippet shows the main code to render a custom authentication selection screen that overrides the HaapiFlowFragment<SelectorModel> base class. The custom layout renders some header text and then a list of buttons and descriptive text for each authenticator. The custom view sorts the passkeys item first, to encourage users to select a modern and secure login method. Ultimately, the select base class method sends the correct data to the server.

kotlin
1234567891011121314151617181920212223242526272829303132
class CustomAuthenticationSelectionFragment: HaapiFlowFragment<SelectorModel>(R.layout.haapi_authentication_selector) {
companion object {
fun newInstance(uiModel: UIModel) : CustomAuthenticationSelectionFragment {
return CustomAuthenticationSelectionFragment().apply {
arguments = uiModelBundle(uiModel)
}
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val formModel = extractUIModelFromArguments<SelectorModel>()
val selectorItems = formModel.selectorItems.map { it as SelectorItemModel }
val sortedItems = selectorItems.sortedWith { first, second ->
when {
first.type == "passkeys" -> -1
second.type == "passkeys" -> 1
else -> first.title.compareTo(second.title)
}
}
val recyclerView = view.findViewById(R.id.selector_items_recycler_view) as? RecyclerView
val viewModels = sortedItems.map { CustomAuthenticationSelectionItemViewModel(it) }
recyclerView?.adapter = CustomAuthenticationSelectionArrayAdapter(viewModels, {
select(it)
})
}
}

Replacing an entire screen requires you to provide a custom XML layout file. This example uses the following XML, where an Android RecyclerView control presents the list of authenticators.

xml
123456789101112131415161718192021222324252627282930
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
<TextView
android:id="@+id/header_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/primary"
android:gravity="center"
android:textStyle="bold"
android:textSize="24dp"
android:text="Choose a Login Method"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/selector_items_recycler_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/header_text"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
android:overScrollMode="never" />
</androidx.constraintlayout.widget.ConstraintLayout>

Developer Resources

To implement advanced login customizations with HAAPI, follow this tutorial's approach to identify the extensibility points, gain a deeper understanding of HAAPI objects and gradually deliver your preferred login user experience. Read much more about extensibility techniques in the Android and iOS developer documentation.

Check out the Android HAAPI UI SDK and iOS HAAPI UI SDK code examples to see the extensibility use cases from this tutorial in action. The code examples also provide an ngrok setup to enable you to view and learn more about HAAPI messages. The deployments include working associated domains setups for development computers with trusted HTTPS internet URLs, to enable advanced logins, such as with native passkeys.

Summary

The Hypermedia Authentication API provides hardened OAuth security for mobile apps. The HAAPI UI SDK enables low code integrations with a native user experience. The SDK also provides extensibility so that you can control the login user experience. Each mobile team can make their own frontend customizations, without dependencies on an identity team that manages the Curity Identity Server.

With the HAAPI UI SDK and the Curity Identity Server's support for Authentication Plugins, you can even build bespoke authentication methods that use the native behaviors of a mobile device. For example, the device camera or addons like card readers could potentially capture proof of a user's identity in many possible ways. A server side plugin could then validate that proof to authenticate the user.

Newsletter

Join our Newsletter

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

Newsletter

Start Free Trial

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

Start Free Trial

Was this helpful?