---
title: "Add native AuthKit sign-in to your iOS app with the WorkOS iOS SDK"
description: "A step-by-step guide to PKCE-based sign-in with the WorkOS iOS SDK's PublicClient."
url: https://workos.com/blog/ios-sdk-authkit-sign-in-tutorial
published: "Aug 10, 2026"
---

# Add native AuthKit sign-in to your iOS app with the WorkOS iOS SDK

WorkOS just shipped its first native mobile SDK: [workos-ios](https://github.com/workos/workos-ios). It gives Swift apps async-first access to AuthKit and the full WorkOS API, with generated typed resource clients, structured error handling, safe retries, and `AsyncSequence`-based auto-pagination, plus helpers for native auth flows, webhook verification, and Vault's local crypto.

The headline feature for mobile teams is `PublicClient`. Since a mobile app is a public client, it never needs an API key or client secret baked into the binary - it authenticates with just your application's client ID, using PKCE. This tutorial walks through wiring that up: from a blank SwiftUI project to a working "Sign in with AuthKit" button.

## What you'll build

A single-screen SwiftUI app with a sign-in button that opens AuthKit in a secure system browser session, then exchanges the result for an authenticated session - no server required for this part of the flow.

## Prerequisites

- A [WorkOS account](https://dashboard.workos.com/) and an application configured in the dashboard, with AuthKit enabled
- Xcode 26+, targeting iOS 16+ (the SDK also supports Mac Catalyst 16+, macOS 13+, tvOS 16+, watchOS 9+, and visionOS 1+)
- Swift 6.2+

## 1. Configure your app in the WorkOS dashboard

In the [Applications](https://dashboard.workos.com/environment/applications) section of the WorkOS Dashboard, open your application and go to the **Redirects** tab. Add the redirect URI your app will call back to after sign-in and mark it as the default. WorkOS rejects the flow if you send it to a redirect URI that isn't registered here first, so this has to happen before you write any code.

For a native app, this is typically a custom URL scheme or a universal link, for example:

```clike
myapp://auth/callback
```

Note that in production environments the redirect URI can't use `http:` or `localhost` (both are fine in sandbox for local testing). If you go with a custom scheme, you'll also need to register it in Xcode under your target's **Info → URL Types**, so iOS routes the callback into your app.

Copy the application's **client ID** - you'll need it in code. You will not need an API key for this flow; keep API keys out of the app entirely.

## 2. Add the SDK

In Xcode: **File → Add Package Dependencies**, then enter:

```clike
https://github.com/workos/workos-ios
```

Or add it directly to `Package.swift`:

```clike
dependencies: [
    .package(url: "https://github.com/workos/workos-ios", from: "0.1.0")
],
targets: [
    .target(
        name: "YourApp",
        dependencies: [
            .product(name: "WorkOS", package: "workos-ios")
        ]
    )
]
```

## 3. Kick off the PKCE flow

Create a `PublicClient` with your client ID, then ask it for an authorization URL. The SDK generates the PKCE code verifier and challenge for you:

```clike
import WorkOS

let workos = PublicClient(clientID: "client_...")

let authorization = try workos.getAuthorizationUrlWithPKCE(
    redirectUri: "myapp://auth/callback",
    provider: "authkit"
)
```

`authorization` gives you back the URL to present to the user, plus the `codeVerifier` and `state` you'll need after the redirect. Hold on to `codeVerifier` (e.g., in memory for the duration of the flow) - you'll need it to complete the exchange.

## 4. Present the sign-in screen

Use `ASWebAuthenticationSession` to open `authorization.url` in a secure, system-managed browser session, so cookies and passkeys from the user's default browser are available and the flow feels native:

```clike
import AuthenticationServices

final class AuthKitSignIn: NSObject, ASWebAuthenticationPresentationContextProviding {
    func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
        ASPresentationAnchor()
    }

    func signIn(
        authorization: PublicClient.PKCEAuthorization,
        callbackScheme: String
    ) async throws -> (code: String, state: String) {
        try await withCheckedThrowingContinuation { continuation in
            let session = ASWebAuthenticationSession(
                url: authorization.url,
                callbackURLScheme: callbackScheme
            ) { callbackURL, error in
                if let error {
                    continuation.resume(throwing: error)
                    return
                }
                guard
                    let callbackURL,
                    let components = URLComponents(url: callbackURL, resolvingAgainstBaseURL: false),
                    let code = components.queryItems?.first(where: { $0.name == "code" })?.value,
                    let state = components.queryItems?.first(where: { $0.name == "state" })?.value
                else {
                    continuation.resume(throwing: URLError(.badServerResponse))
                    return
                }
                continuation.resume(returning: (code, state))
            }
            session.presentationContextProvider = self
            session.start()
        }
    }
}
```

(Adjust the `PublicClient.PKCEAuthorization` type name to whatever the SDK's generated type is called if it differs - check autocomplete or the [DocC reference](https://workos.github.io/workos-ios/documentation/workos/) for the exact signature in the version you install.)

## 5. Validate state and exchange the code

Before exchanging anything, confirm the `state` your app receives matches the `state` from step 3 - this is what protects the flow from CSRF. Then hand the authorization code and the original PKCE verifier back to `PublicClient`:

```clike
guard state == authorization.state else {
    throw URLError(.userAuthenticationRequired)
}

let authentication = try await workos.authenticateWithCode(
    code: code,
    codeVerifier: authorization.codeVerifier
)
```

A successful exchange gives you back an authenticated session for the user - their profile plus the tokens your app needs going forward. Store any tokens in the Keychain rather than `UserDefaults` or in-memory-only state, and check the SDK's API reference for the exact shape of the response in the version you're using, since generated SDKs like this one iterate quickly.

## 6. Wire it into SwiftUI

Putting it together in a minimal view:

```clike
struct SignInView: View {
    @State private var isSignedIn = false
    @State private var errorMessage: String?

    private let workos = PublicClient(clientID: "client_...")
    private let signIn = AuthKitSignIn()

    var body: some View {
        VStack(spacing: 16) {
            if isSignedIn {
                Text("You're signed in 🎉")
            } else {
                Button("Sign in with AuthKit") {
                    Task { await handleSignIn() }
                }
            }
            if let errorMessage {
                Text(errorMessage).foregroundStyle(.red)
            }
        }
        .padding()
    }

    private func handleSignIn() async {
        do {
            let authorization = try workos.getAuthorizationUrlWithPKCE(
                redirectUri: "myapp://auth/callback",
                provider: "authkit"
            )
            let result = try await signIn.signIn(
                authorization: authorization,
                callbackScheme: "myapp"
            )
            guard result.state == authorization.state else {
                errorMessage = "State mismatch - aborting sign-in."
                return
            }
            _ = try await workos.authenticateWithCode(
                code: result.code,
                codeVerifier: authorization.codeVerifier
            )
            isSignedIn = true
        } catch {
            errorMessage = error.localizedDescription
        }
    }
}
```

## Where to go from here

That's a full native sign-in loop with zero secrets in the app bundle. From here, the same SDK covers a few things worth exploring next:

- [**Storing and refreshing session tokens**](https://workos.com/blog/ios-sdk-token-storage-refresh-guide) in the Keychain, and telling a terminal `invalid_grant` apart from a transient network blip
- **Calling the WorkOS API from a trusted server** with `WorkOSClient(apiKey:)`, using the generated typed resource methods and structured errors
- **Auto-pagination** over list endpoints via `AsyncSequence`, so you can `for await` through results instead of hand-rolling cursor loops
- **Vault's local crypto helpers**, if you need to encrypt sensitive data on-device
- **Webhook verification helpers**, for the server side of your integration

Full reference: [github.com/workos/workos-ios](https://github.com/workos/workos-ios) and the [AuthKit docs](https://workos.com/docs/authkit).
