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

iOS 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 Xcode, then create a new iOS app. Set Swift as the development language and use SwiftUI as the user interface technology.

Create Project iOS

Ensure that the project's deployment target is 14.0 or later, then replace the contents of the ContentView source file with the following basic elements:

swift
123456789101112131415161718192021222324252627282930313233343536373839
import SwiftUI
struct ContentView: View {
@State private var isLoggingIn: Bool = false
var body: some View {
VStack {
Text("HAAPI iOS Demo App")
.font(.system(size: 20))
.fontWeight(.bold)
.frame(width: UIScreen.main.bounds.size.width * 1.0, height: 50, alignment: .leading)
.padding(.leading, 20)
.background(Color.black)
.foregroundColor(Color.white)
Text("Unauthenticated View")
.font(.system(size: 20))
.fontWeight(.bold)
.padding(.top, 10)
.padding(.bottom, 20)
Button {
isLoggingIn = true
} label: {
Text("Start HAAPI Login")
}
.frame(width: UIScreen.main.bounds.size.width * 0.8, height: 40)
.background(Color.blue)
.foregroundColor(Color.white)
.cornerRadius(5)
Spacer()
}
.padding(10)
}
}

Run the Initial App

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

Empty App iOS

Integrate the HAAPI SDK

You can obtain the SDK from the Curity GitHub repository, and add the SDK to your project either by using the Swift Package Manager or through CocoaPods.

You can add the following to your Package.swift file:

swift
1
.package(url: "https://github.com/curityio/ios-idsvr-haapi-ui-kit-dist")

If you're not using the Package.swift file, and you prefer to add the dependencies directly in Xcode, follow these steps:

  • In Xcode choose FileAdd Packages... from the menu.
  • Enter https://github.com/curityio/ios-idsvr-haapi-uikit-dist to the search field.
  • Choose the desired version policy (you can leave the default settings).
  • Click Add Package to add the dependency.

To use the SDK classes, start by replacing the ContentView file with the following code, to implement the HaapiFlowResult protocol. This code can be added wherever you would like within your mobile app:

swift
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768
import SwiftUI
import IdsvrHaapiUIKit
struct ContentView: View, HaapiFlowResult {
let haapiApplication: HaapiUIKitApplication
@State private var tokens: OAuthTokenModel? = nil
@State private var isLoggingIn: Bool = false
var body: some View {
VStack {
Text("HAAPI iOS Demo App")
.font(.system(size: 20))
.fontWeight(.bold)
.frame(width: UIScreen.main.bounds.size.width * 1.0, height: 50, alignment: .leading)
.padding(.leading, 20)
.background(Color.black)
.foregroundColor(Color.white)
Text("Unauthenticated View")
.font(.system(size: 20))
.fontWeight(.bold)
.padding(.top, 10)
.padding(.bottom, 20)
Button {
isLoggingIn = true
} label: {
Text("Start HAAPI Login")
}
.frame(width: UIScreen.main.bounds.size.width * 0.8, height: 40)
.background(Color.blue)
.foregroundColor(Color.white)
.cornerRadius(5)
Spacer()
}
.padding(10)
.sheet(isPresented: $isLoggingIn) {
HaapiFlow.start(
self,
haapiUIKitApplication: haapiApplication,
haapiDeepLinkManageable: HaapiDeepLinkManager.shared)
}
.alert("Login completed and tokens received", isPresented: Binding<Bool>.constant(oAuthTokenModel != nil)) {
} message: {
Text(oAuthTokenModel?.accessToken ?? "")
}
}
func didReceiveOAuthModel(_ tokens: OAuthModel) {
guard let model = tokens as? OAuthTokenModel else {
return
}
self.tokens = model
self.isLoggingIn = false
}
func didReceiveError(_ error: Error) {
self.tokens = nil
self.isLoggingIn = false
}
}

Update the application class to reference an app delegate class, to pass the application instance to the content view. Optionally, you can also add support for any authentication methods that use deep linking:

swift
123456789101112131415161718192021
import SwiftUI
import IdsvrHaapiUIKit
@main
struct DemoApp: App {
@UIApplicationDelegateAdaptor(DemoAppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
ContentView(haapiApplication: appDelegate.haapiUIKitApplication)
.onOpenURL(perform: handleUrl(url:))
}
}
func handleUrl(url: URL) {
if HaapiDeepLinkManager.shared.canHandleUrl(url) {
HaapiDeepLinkManager.shared.handleUrl(url)
}
}
}

Next, provide an AppDelegate class that configures the HAAPI SDK with your OAuth settings, to point the app 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:

swift
12345678910111213141516171819202122232425262728293031323334353637
import SwiftUI
import IdsvrHaapiUIKit
class DemoAppDelegate: NSObject, UIApplicationDelegate {
private enum Constants {
static let clientId = "haapi-ios-dev-client"
static let redirectUri = "haapi:start"
static let scopes = ["openid", "profile"]
static let baseURL = URL(string: "https://login.example.com")!
static let tokenEndpointURL = URL(string: "/oauth/v2/oauth-token", relativeTo: baseURL)!
static let authorizationEndpointURL = URL(string: "/oauth/v2/oauth-authorize", relativeTo: baseURL)!
}
var haapiUIKitConfiguration = HaapiUIKitConfigurationBuilder(clientId: Constants.clientId,
baseUrl: Constants.baseURL,
tokenEndpointUrl: Constants.tokenEndpointURL,
authorizationEndpointUrl: Constants.authorizationEndpointURL,
appRedirect: Constants.redirectUri)
.setOauthAuthorizationParamsProvider { OAuthAuthorizationParameters(scopes: Constants.scopes) }
.setPresentationMode(mode: PresentationMode.modal)
.setShouldAutoHandleFlowErrorFeedback(value: true)
.build()
var haapiUIKitApplication: HaapiUIKitApplication!
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
HaapiLogger.followUpTags = DriverFollowUpTag.allCases + SdkFollowUpTag.allCases + UIKitFollowUpTag.allCases
HaapiLogger.enabled = true
haapiUIKitApplication = HaapiUIKitApplicationBuilder(haapiUIKitConfiguration: haapiUIKitConfiguration)
.build()
return true
}
}

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:

swift
12345678910111213
var haapiUIKitConfiguration = HaapiUIKitConfigurationBuilder(clientId: Constants.clientId,
baseUrl: Constants.baseURL,
tokenEndpointUrl: Constants.tokenEndpointURL,
authorizationEndpointUrl: Constants.authorizationEndpointURL,
appRedirect: Constants.redirectUri)
.setOauthAuthorizationParamsProvider { OAuthAuthorizationParameters(
scopes: Constants.scopes,
extraRequestParameters: ["prompt": "login"]
) }
.setHttpHeadersProvider(provider: { ["Accept-Language": "pt"] })
.setPresentationMode(mode: PresentationMode.modal)
.setShouldAutoHandleFlowErrorFeedback(value: true)
.build()

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.

iOS successful login

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.

iOS failed login

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

swift
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475
import SwiftUI
import IdsvrHaapiUIKit
struct ContentView: View, HaapiFlowResult {
let haapiApplication: HaapiUIKitApplication
@State private var tokens: OAuthTokenModel? = nil
@State private var error: Error? = nil
@State private var isLoggingIn: Bool = false
var body: some View {
VStack {
Text("HAAPI iOS Demo App")
.font(.system(size: 20))
.fontWeight(.bold)
.frame(width: UIScreen.main.bounds.size.width * 1.0, height: 50, alignment: .leading)
.padding(.leading, 20)
.background(Color.black)
.foregroundColor(Color.white)
Text("Unauthenticated View")
.font(.system(size: 20))
.fontWeight(.bold)
.padding(.top, 10)
.padding(.bottom, 20)
Button {
isLoggingIn = true
} label: {
Text("Start HAAPI Login")
}
.frame(width: UIScreen.main.bounds.size.width * 0.8, height: 40)
.background(Color.blue)
.foregroundColor(Color.white)
.cornerRadius(5)
Spacer()
}
.padding(10)
.sheet(isPresented: $isLoggingIn) {
HaapiFlow.start(
self,
haapiUIKitApplication: haapiApplication,
haapiDeepLinkManageable: HaapiDeepLinkManager.shared)
}
.alert("Login completed and tokens received", isPresented: Binding<Bool>.constant(oAuthTokenModel != nil)) {
} message: {
Text(oAuthTokenModel?.accessToken ?? "")
}
.alert("Problem encountered", isPresented: Binding<Bool>.constant(error != nil)) {
} message: {
Text(error?.localizedDescription ?? "")
}
}
func didReceiveOAuthTokenModel(_ tokens: OAuthModel) {
self.isLoggingIn = false
guard let model = tokens as? OAuthTokenModel else {
return
}
self.tokens = model
self.error = nil
}
func didReceiveError(_ error: Error) {
self.isLoggingIn = false
self.error = error
self.tokens = nil
}
}

Use SDK Logging

During development, you can enable logger classes in the UI SDK to get further information on technical behavior. Do so in the app delegate class, by adding code similar to the following before creating the HaapiUIKitApplication class. If required, use the tag options to specify which logging areas are enabled.

swift
12
HaapiLogger.followUpTags = DriverFollowUpTag.allCases + SdkFollowUpTag.allCases + UIKitFollowUpTag.allCases
HaapiLogger.isInfoEnabled = true

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

bash
1234
2023-07-06 09:35:57.252770+0200 demoapp[36935:644803] [HAAPI_SDK_HTTP] [DEBUG] HaapiManager.334 : Received representation data: {"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","value":"demouser"},{"name":"password","type":"password","label":"Password"}]}}]}
2023-07-06 09:35:57.254300+0200 demoapp[36935:642276] [HAAPI_UI_FLOW] [DEBUG] HaapiFlowViewController.199 : isLoading did change to false
2023-07-06 09:35:57.259376+0200 demoapp[36935:644803] [HAAPI_UI_DATA] [DEBUG] DataMapper.108 : HaapiRepresentation cannot be mapped to UIOperation InteractiveFormStep(actions: [IdsvrHaapiUIKit.FormAction], type: IdsvrHaapiUIKit.RepresentationType.authenticationStep, metadata: Optional(IdsvrHaapiUIKit.Metadata(templateArea: nil, viewName: Optional("authenticator/html-form/authenticate/get"))), messages: [], links: [IdsvrHaapiUIKit.Link(href: "/authn/authentication/Username-Password/forgot-password", rel: "forgot-password", title: Optional(Forgot your password?), type: nil), IdsvrHaapiUIKit.Link(href: "/authn/authentication/Username-Password/forgot-account-id", rel: "forgot-account-id", title: Optional(Forgot your username?), type: nil), IdsvrHaapiUIKit.Link(href: "/authn/registration/Username-Password", rel: "register-create", title: Optional(Create account), type: nil)], properties: nil)
2023-07-06 09:35:57.266021+0200 demoapp[36935:642276] [HAAPI_UI_FLOW] [DEBUG] HaapiFlowViewController.208 : uiModel did change to Optional(IdsvrHaapiUIKit.FormModelImpl(messageItems: [], interactionItems: [IdsvrHaapiUIKit.InteractionItemSection(key: "login", label: "Login"), IdsvrHaapiUIKit.InteractionItemText, IdsvrHaapiUIKit.InteractionItemPassword, IdsvrHaapiUIKit.InteractionItemButton], linkItems: [IdsvrHaapiUIKit.LinkItemModelImpl(text: Optional("Forgot your password?"), href: "/authn/authentication/Username-Password/forgot-password", type: nil, action: IdsvrHaapiUIKit.Link(href: "/authn/authentication/Username-Password/forgot-password", rel: "forgot-password", title: Optional(Forgot your password?), type: nil)), IdsvrHaapiUIKit.LinkItemModelImpl(text: Optional("Forgot your username?"), href: "/authn/authentication/Username-Password/forgot-account-id", type: nil, action: IdsvrHaapiUIKit.Link(href: "/authn/authentication/Username-Password/forgot-account-id", rel: "forgot-account-id", title: Optional(Forgot your username?), type: nil)), IdsvrHaapiUIKit.LinkItemModelImpl(text: Optional("Create account"), href: "/authn/registration/Username-Password", type: nil, action: IdsvrHaapiUIKit.Link(href: "/authn/registration/Username-Password", rel: "register-create", title: Optional(Create account), type: nil))], templateArea: nil, viewName: Optional("authenticator/html-form/authenticate/get")))

Code Example

If you run into any problems with the technical setup, take a look at the HAAPI iOS 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