In this article
August 10, 2026
August 10, 2026

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

A step-by-step guide to PKCE-based sign-in with the WorkOS iOS SDK's PublicClient.

Explore with AI
Open in ChatGPT
Open in Claude
Open in Perplexity

WorkOS just shipped its first native mobile SDK: 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 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 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:

  
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:

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

Or add it directly to Package.swift:

  
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:

  
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:

  
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 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:

  
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:

  
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:

  • 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 and the AuthKit docs.