Skip to content

iOS SDK

This guide provides instructions on integrating Authany with an iOS app.

The Authany iOS SDK supports iOS 11.0 and higher. This tutorial builds the demo app with Apple's Observation framework (@Observable) and Swift Concurrency (async/await), which require iOS 17.0 or higher, Xcode 16 or later, and the Swift 6 language mode. If you need to support older iOS versions, you can apply the same structure using ObservableObject and the SDK's completion handlers instead.

Follow this guide to add Authany to your iOS app in 🕐 10 minutes.

Note

You can find the full code for the demo app for this tutorial in this Github repo

Setup Application in Authany

Sign up for an Authany Portal account at https://manage.authany.com/. Or you can use your self-deployed Authany.

From the Project listing, create a new Project or select an existing Project. After that, we will need to create an Authany client application in the project.

Step 1: Create an application in the Portal

Go to Applications on the left menu bar.

Click ⊕Add Application in the top toolbar.

Input the name of your application and select Native App as the application type. Click "Save".

Create an application

You will see a list of guides that can help you for setting up, then click "Next".

Step 2: Configure the application

Here you'll need to define a custom URI scheme that Authany will use to redirect users back to your app after authentication. For our example app, the custom URL scheme is com.example.authgeardemo, and the full Redirect URI built on top of it is com.example.authgeardemo://host/path. For further instructions on setting up a custom URI scheme in iOS, see the official documentation here.

Head back to Authany Portal, and add com.example.authgeardemo://host/path as Redirect URI.

Click "Save" button and note the Client ID. and Endpoint for your new client application as you'll use them later in your iOS application. You can also obtain the Client ID again from the Applications list later.

Fill in the Authorized Redirect URI

Add Authany to your iOS Application

In this step, we'll add user authentication to a simple iOS app using the Authany iOS SDK and the client application we created in the previous steps.

Pre-requisites

To follow the steps in this guide seamlessly, you should have the following:

  • Xcode 16 or later
  • A project targeting iOS 17.0 or later, using the Swift 6 language mode
  • Some knowledge of SwiftUI and Swift Concurrency (async/await)

Step 1: Create new iOS project

For the purpose of this guide, we'll create a new project in Xcode. Skip this step if you're adding Authany to your existing app.

To create a new project, open Xcode and navigate to File > New > Project. Create your new project with the following details:

  • Project Name: my_demo_app
  • choose SwiftUI as Interface Leave other fields unchanged and proceed to create the project.

Xcode new project

Step 2: Install Authany SDK

The Authany iOS SDK makes it easy to interact with Authany services from your iOS project.

To add Authany SDK to your project, in Xcode navigate to File > Add Package Dependencies and enter https://github.com/authgear/authgear-sdk-ios.git in the Package URL text field. Select the Up to Next Major Version dependency rule starting from 2.0.0.

Click Add Package to proceed.

Xcode package manager

On the next screen, select your application under Add to Target then click on Add Package.

xcode add package

Alternatively, if your project uses cocoapods, install the SDK using:

pod 'Authgear', :git => 'https://github.com/authgear/authgear-sdk-ios.git'

Step 3: Create the authentication model

We'll keep all authentication logic in one place — an @Observable model that owns a single Authgear instance and exposes a small, UI-friendly state. The view (next step) just renders that state and calls the model's methods.

Create a new Swift file named AuthenticationModel.swift and add the following:

//  AuthenticationModel.swift

import Authgear
import Observation

// Sendable snapshots produced *inside* the SDK's completion handlers so that
// no non-Sendable Authgear type crosses the `await` boundary.
private enum AuthOutcome: Sendable {
    case signedIn(userID: String)
    case cancelled
    case failed(message: String)
}

private enum VoidOutcome: Sendable {
    case ok
    case failed(message: String)
}

@MainActor
@Observable
final class AuthenticationModel {
    enum State: Equatable {
        case loading
        case signedOut
        case signedIn(userID: String)
    }

    private(set) var state: State = .loading

    // Non-nil drives an error alert in the view.
    var errorMessage: String?

    private let authgear: Authgear
    private var didConfigure = false

    init() {
        authgear = Authgear(
            clientId: "<CLIENT_ID>",
            endpoint: "<AUTHGEAR_ENDPOINT>"
        )
    }

    // Configure the SDK once and restore any existing session.
    func configure() async {
        guard !didConfigure else { return }
        didConfigure = true

        let outcome: VoidOutcome = await withCheckedContinuation { continuation in
            authgear.configure { result in
                switch result {
                case .success:
                    continuation.resume(returning: .ok)
                case let .failure(error):
                    continuation.resume(returning: .failed(message: error.localizedDescription))
                }
            }
        }

        switch outcome {
        case .ok:
            // Refresh the session if the user has an existing one.
            if authgear.sessionState == .authenticated {
                await refreshCurrentUser()
            } else {
                state = .signedOut
            }
        case let .failed(message):
            errorMessage = message
            state = .signedOut
        }
    }

    // Start the interactive login flow.
    func logIn() async {
        state = .loading

        let outcome: AuthOutcome = await withCheckedContinuation { continuation in
            authgear.authenticate(redirectURI: "com.example.authgeardemo://host/path") { result in
                switch result {
                case let .success(userInfo):
                    continuation.resume(returning: .signedIn(userID: userInfo.sub))
                case let .failure(error):
                    if let authgearError = error as? AuthgearError,
                       case .cancel = authgearError {
                        continuation.resume(returning: .cancelled)
                    } else {
                        continuation.resume(returning: .failed(message: error.localizedDescription))
                    }
                }
            }
        }

        apply(outcome)
    }

    // Log out and clear the local session.
    func logOut() async {
        state = .loading

        let outcome: VoidOutcome = await withCheckedContinuation { continuation in
            authgear.logout { result in
                switch result {
                case .success:
                    continuation.resume(returning: .ok)
                case let .failure(error):
                    continuation.resume(returning: .failed(message: error.localizedDescription))
                }
            }
        }

        switch outcome {
        case .ok:
            state = .signedOut
        case let .failed(message):
            errorMessage = message
            // The SDK's session state is authoritative after a failed logout.
            if authgear.sessionState == .authenticated {
                await refreshCurrentUser()
            } else {
                state = .signedOut
            }
        }
    }

    // Open the pre-built user settings page.
    func openUserSettings() {
        authgear.open(page: .settings)
    }

    // Refresh the signed-in user's info; downgrades to signed-out if the
    // refresh token is no longer valid.
    private func refreshCurrentUser() async {
        let outcome: AuthOutcome = await withCheckedContinuation { continuation in
            authgear.fetchUserInfo { result in
                switch result {
                case let .success(userInfo):
                    continuation.resume(returning: .signedIn(userID: userInfo.sub))
                case let .failure(error):
                    continuation.resume(returning: .failed(message: error.localizedDescription))
                }
            }
        }

        apply(outcome)
    }

    private func apply(_ outcome: AuthOutcome) {
        switch outcome {
        case let .signedIn(userID):
            state = .signedIn(userID: userID)
        case .cancelled:
            state = .signedOut
        case let .failed(message):
            errorMessage = message
            state = .signedOut
        }
    }
}

Replace "<CLIENT_ID>" and "<AUTHANY_ENDPOINT>" with the client ID and endpoint from the configuration page of the client project you created earlier.

Note

Why the AuthOutcome / VoidOutcome enums? The Authany SDK ships completion-handler APIs, and its result types (UserInfo, SessionState, AuthgearError) are not Sendable. To stay clean under the Swift 6 language mode, each call is wrapped with withCheckedContinuation, and the completion closure captures only the continuation and resumes with a small Sendable value extracted inside the closure. That way no non-Sendable SDK type crosses the await boundary, so the model compiles under strict concurrency checking without extra annotations.

In the demo app repo, the client ID, endpoint, and redirect URI are factored into a small Constants.swift enum instead of being hard-coded in the initializer — a good pattern for real projects.

Here's what each method does:

  • configure() — initializes the SDK once (guarded by didConfigure) and, if a previous session exists, refreshes it so returning users land straight on the signed-in screen.
  • logIn() — runs the interactive authentication flow. User cancellation is handled distinctly from real errors, so cancelling the login sheet doesn't raise an error alert.
  • logOut() — ends the current session. On failure it treats the SDK's sessionState as authoritative rather than assuming the user is signed out.
  • openUserSettings() — opens Authany's pre-built User Settings page, where users can view and modify their profile attributes and security settings.
  • refreshCurrentUser() — calls fetchUserInfo to update sessionState and retrieve the user's sub (a unique user ID). This also detects a session that has been revoked remotely: sessionState becomes .noSession and the model drops back to signed-out.

Step 4: Build the ContentView

Now replace the contents of ContentView.swift with a state-driven view that observes the model. It triggers configure() from .task when the view first appears, wraps the async actions in Task {}, and surfaces any errorMessage with an .alert:

//  ContentView.swift

import SwiftUI

struct ContentView: View {
    @State private var model = AuthenticationModel()

    var body: some View {
        VStack(spacing: 16) {
            switch model.state {
            case .loading:
                ProgressView()
            case .signedOut:
                signedOutView
            case let .signedIn(userID):
                signedInView(userID: userID)
            }
        }
        .padding()
        .task {
            await model.configure()
        }
        .alert(
            "Something went wrong",
            isPresented: Binding(
                get: { model.errorMessage != nil },
                set: { isPresented in
                    if !isPresented { model.errorMessage = nil }
                }
            ),
            presenting: model.errorMessage
        ) { _ in
            Button("OK", role: .cancel) {}
        } message: { message in
            Text(message)
        }
    }

    private var signedOutView: some View {
        VStack(spacing: 16) {
            Image(systemName: "globe")
                .imageScale(.large)
                .foregroundStyle(.tint)
            Text("My Demo App")
            Button("Login") {
                Task { await model.logIn() }
            }
        }
    }

    private func signedInView(userID: String) -> some View {
        VStack(spacing: 16) {
            Text("Welcome user \(userID)")
            Button("User Settings") {
                model.openUserSettings()
            }
            Button("Logout") {
                Task { await model.logOut() }
            }
        }
    }
}

#Preview {
    ContentView()
}

The view has no direct SDK calls — it only reads model.state and invokes the model's methods. When signed out it shows the Login button; while an operation is in flight it shows a ProgressView; once signed in it greets the user and offers User Settings and Logout.

Checkpoint

Run your app now. It should launch and settle on the Login screen. Tapping Login will attempt to open the authentication page — but it won't return to your app yet. We'll register the redirect URI scheme next.

Step 5: Register the URI Scheme for the Redirect URI

For Authany to redirect back into your app after authentication, register the custom URL scheme — the part of the redirect URI before ://. For our redirect URI com.example.authgeardemo://host/path, the scheme is com.example.authgeardemo.

Open your project's Info.plist or project settings UI in Xcode and add the following:

Navigate to Targets > {Your project} > Info and expand the URL Types section.

Add a new URL scheme with the following details:

Identifier: CFBundleURLTypes

URL Schemes: com.example.authgeardemo

Role: Editor

xcode project properties

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
    <dict>
        <!-- Other entries -->
        <key>CFBundleURLTypes</key>
        <array>
            <dict>
                <key>CFBundleTypeRole</key>
                <string>Editor</string>
                <key>CFBundleURLName</key>
                <string>CFBundleURLTypes</string>
                <key>CFBundleURLSchemes</key>
                <array>
                    <string>com.example.authgeardemo</string>
                </array>
            </dict>
        </array>
    </dict>
</plist>

Warning

Register the bare scheme com.example.authgeardemo in CFBundleURLSchemesnot the full redirect URI com.example.authgeardemo://host/path. CFBundleURLSchemes expects only the scheme (the text before ://). The full redirect URI is what you register in the Authany Portal and pass to authenticate(redirectURI:).

Step 6: Run and test

Run your app again and try logging in. Because the redirect scheme is now registered, Authany will redirect back to your app after authentication and the view will switch to the signed-in screen.

Try the full flow:

  • Login — completes authentication and shows "Welcome user <sub>".
  • User Settings — opens Authany's pre-built settings page.
  • Logout — ends the session and returns to the Login screen.

If configuration fails (for example, a wrong endpoint) or authentication errors out, the .alert you added in Step 4 surfaces the message instead of failing silently.

Understanding session state

You may want to know whether the user has logged in (for example, to show a Login button only when they haven't).

The sessionState reflects the user's logged-in state in the SDK's local state. Even if sessionState is .authenticated, the session may be invalid if it was revoked remotely. Hence, after initializing the SDK, call fetchUserInfo to update sessionState as soon as it is proper to do so — which is exactly what configure()refreshCurrentUser() does in the model above.

The value of sessionState can be .unknown, .noSession or .authenticated. Initially it is .unknown. After a call to authgear.configure, it becomes .authenticated if a previous session was found, or .noSession if no such session existed.

Using the Access Token in HTTP Requests

Call refreshAccessTokenIfNeeded every time before using the access token; it checks and makes a network call only if the access token has expired. Then include the access token in the Authorization header of your request. As with the other SDK calls, you can bridge the completion handler into async/await with a continuation:

func callProtectedAPI() async throws {
    try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
        authgear.refreshAccessTokenIfNeeded { result in
            continuation.resume(with: result)
        }
    }

    // The access token is ready to use. It can be empty if the user is not
    // logged in or the session is invalid.
    guard let accessToken = authgear.accessToken else {
        // The user is not logged in, or the token is expired.
        return
    }

    // Example only — use your own networking library.
    var urlRequest = URLRequest(url: URL(string: "YOUR_SERVER_URL")!)
    urlRequest.setValue("Bearer \(accessToken)", forHTTPHeaderField: "authorization")
    // ... continue making your request
}

Next steps

To protect your application server from unauthorized access. You will need to integrate your backend with Authany.

Backend/API Integration

iOS SDK Reference

For detailed documentation on the iOS SDK, visit iOS SDK Reference.