PasskeyBase
S

Swift / iOS SDK

PasskeyBaseAuth

Native iOS SDK with platform passkey support via ASAuthorizationController, SwiftUI integration, and secure Keychain token storage. Supports iOS 16+.

← All SDKs


Installation

Add the SDK using Swift Package Manager. In Xcode, go to File > Add Package Dependenciesand enter the repository URL, or add it directly to your Package.swift.

Package.swift
// In Xcode: File > Add Package Dependencies...
// Enter the repository URL:
// https://github.com/passkeybase/swift-sdk

// Or add to your Package.swift:
dependencies: [
    .package(
        url: "https://github.com/passkeybase/swift-sdk",
        from: "1.0.0"
    )
]

// Then add the product to your target:
.target(
    name: "MyApp",
    dependencies: [
        .product(name: "PasskeyBaseAuth", package: "swift-sdk")
    ]
)

Requirements: iOS 16.0+, macOS 13.0+, Xcode 15+. The SDK usesAuthenticationServicesfor platform passkey operations, which requires theAssociated Domainscapability configured in your app target.


Quick Start with SwiftUI

Wrap your root view in a PasskeyBaseProviderto make authentication state available throughout your app via the environment.

MyApp.swift
import SwiftUI
import PasskeyBaseAuth

@main
struct MyApp: App {
    let passkeyBase = PasskeyBaseClient(
        clientId: "pk_live_abc123",
        domain: "myapp.auth.ing"
    )

    var body: some Scene {
        WindowGroup {
            PasskeyBaseProvider(client: passkeyBase) {
                ContentView()
            }
        }
    }
}

Then use @EnvironmentObjectto access the auth state in any view.

ContentView.swift
import SwiftUI
import PasskeyBaseAuth

struct ContentView: View {
    @EnvironmentObject var auth: AuthState

    var body: some View {
        Group {
            if auth.isAuthenticated {
                VStack(spacing: 16) {
                    Text("Welcome, \(auth.user?.displayName ?? "User")")
                        .font(.title2)
                    Text("ID: \(auth.user?.id ?? "")")
                        .font(.caption)
                        .foregroundStyle(.secondary)

                    Button("Log Out") {
                        Task { await auth.logout() }
                    }
                    .buttonStyle(.bordered)
                }
            } else {
                LoginView()
            }
        }
    }
}

Native Passkey Registration

Register a new passkey using the platform authenticator. This usesASAuthorizationControllerto present the system passkey creation dialog with Face ID or Touch ID.

PasskeyRegistrationManager.swift
import AuthenticationServices
import PasskeyBaseAuth

class PasskeyRegistrationManager: NSObject,
    ASAuthorizationControllerDelegate,
    ASAuthorizationControllerPresentationContextProviding
{
    private let client: PasskeyBaseClient
    private var continuation: CheckedContinuation<Void, Error>?

    init(client: PasskeyBaseClient) {
        self.client = client
    }

    func register(displayName: String) async throws {
        // 1. Get registration options from PasskeyBase
        let options = try await client.getRegistrationOptions(
            displayName: displayName
        )

        // 2. Create the platform key request
        let provider = ASAuthorizationPlatformPublicKeyCredentialProvider(
            relyingPartyIdentifier: options.rpId
        )
        let request = provider.createCredentialRegistrationRequest(
            challenge: options.challenge,
            name: displayName,
            userID: options.userId
        )

        // 3. Present the authorization controller
        try await withCheckedThrowingContinuation { continuation in
            self.continuation = continuation
            let controller = ASAuthorizationController(
                authorizationRequests: [request]
            )
            controller.delegate = self
            controller.presentationContextProvider = self
            controller.performRequests()
        }
    }

    func authorizationController(
        controller: ASAuthorizationController,
        didCompleteWithAuthorization authorization: ASAuthorization
    ) {
        guard let credential = authorization.credential
            as? ASAuthorizationPlatformPublicKeyCredentialRegistration
        else {
            continuation?.resume(throwing: PasskeyBaseError.unexpectedCredentialType)
            return
        }

        Task {
            do {
                // 4. Send the attestation to PasskeyBase
                try await client.completeRegistration(
                    credentialId: credential.credentialID,
                    attestation: credential.rawAttestationObject!,
                    clientDataJSON: credential.rawClientDataJSON
                )
                continuation?.resume()
            } catch {
                continuation?.resume(throwing: error)
            }
        }
    }

    func authorizationController(
        controller: ASAuthorizationController,
        didCompleteWithError error: Error
    ) {
        continuation?.resume(throwing: error)
    }

    func presentationAnchor(
        for controller: ASAuthorizationController
    ) -> ASPresentationAnchor {
        ASPresentationAnchor()
    }
}

Associated Domains: You must configure an associated domain (webcredentials:myapp.auth.ing) in your app's entitlements and host an apple-app-site-association file on your PasskeyBase domain. This is configured automatically in the dashboard.


Native Passkey Authentication

Authenticate a user with an existing passkey. The system will prompt for biometric verification (Face ID / Touch ID) and return the signed assertion.

PasskeyAuthManager.swift
import AuthenticationServices
import PasskeyBaseAuth

class PasskeyAuthManager: NSObject,
    ASAuthorizationControllerDelegate,
    ASAuthorizationControllerPresentationContextProviding
{
    private let client: PasskeyBaseClient
    private var continuation: CheckedContinuation<AuthResult, Error>?

    init(client: PasskeyBaseClient) {
        self.client = client
    }

    func authenticate() async throws -> AuthResult {
        // 1. Get authentication options from PasskeyBase
        let options = try await client.getAuthenticationOptions()

        // 2. Create the assertion request
        let provider = ASAuthorizationPlatformPublicKeyCredentialProvider(
            relyingPartyIdentifier: options.rpId
        )
        let request = provider.createCredentialAssertionRequest(
            challenge: options.challenge
        )

        // Allow credentials from the allowed list
        if let allowedCredentials = options.allowCredentials {
            request.allowedCredentials = allowedCredentials.map {
                ASAuthorizationPlatformPublicKeyCredentialDescriptor(
                    credentialID: $0
                )
            }
        }

        // 3. Present the authorization controller
        return try await withCheckedThrowingContinuation { continuation in
            self.continuation = continuation
            let controller = ASAuthorizationController(
                authorizationRequests: [request]
            )
            controller.delegate = self
            controller.presentationContextProvider = self
            controller.performRequests()
        }
    }

    func authorizationController(
        controller: ASAuthorizationController,
        didCompleteWithAuthorization authorization: ASAuthorization
    ) {
        guard let credential = authorization.credential
            as? ASAuthorizationPlatformPublicKeyCredentialAssertion
        else {
            continuation?.resume(throwing: PasskeyBaseError.unexpectedCredentialType)
            return
        }

        Task {
            do {
                // 4. Verify the assertion with PasskeyBase
                let result = try await client.completeAuthentication(
                    credentialId: credential.credentialID,
                    authenticatorData: credential.rawAuthenticatorData,
                    clientDataJSON: credential.rawClientDataJSON,
                    signature: credential.signature
                )
                continuation?.resume(returning: result)
            } catch {
                continuation?.resume(throwing: error)
            }
        }
    }

    func authorizationController(
        controller: ASAuthorizationController,
        didCompleteWithError error: Error
    ) {
        continuation?.resume(throwing: error)
    }

    func presentationAnchor(
        for controller: ASAuthorizationController
    ) -> ASPresentationAnchor {
        ASPresentationAnchor()
    }
}

OAuth 2.0 Login

For browsers-based login flows, use ASWebAuthenticationSessionto open the PasskeyBase hosted login page. This supports PKCE automatically.

OAuthLoginManager.swift
import AuthenticationServices
import PasskeyBaseAuth

class OAuthLoginManager: NSObject,
    ASWebAuthenticationPresentationContextProviding
{
    private let client: PasskeyBaseClient

    init(client: PasskeyBaseClient) {
        self.client = client
    }

    func login() async throws -> AuthResult {
        // Build the authorization URL with PKCE
        let (authURL, codeVerifier) = try client.buildAuthorizationURL(
            redirectURI: "myapp://callback",
            scopes: ["openid", "profile"]
        )

        // Present the web authentication session
        let callbackURL = try await withCheckedThrowingContinuation {
            (continuation: CheckedContinuation<URL, Error>) in

            let session = ASWebAuthenticationSession(
                url: authURL,
                callbackURLScheme: "myapp"
            ) { callbackURL, error in
                if let error = error {
                    continuation.resume(throwing: error)
                } else if let callbackURL = callbackURL {
                    continuation.resume(returning: callbackURL)
                } else {
                    continuation.resume(
                        throwing: PasskeyBaseError.missingCallback
                    )
                }
            }
            session.presentationContextProvider = self
            session.prefersEphemeralWebBrowserSession = false
            session.start()
        }

        // Exchange the authorization code for tokens
        return try await client.handleCallback(
            url: callbackURL,
            codeVerifier: codeVerifier
        )
    }

    func presentationAnchor(
        for session: ASWebAuthenticationSession
    ) -> ASPresentationAnchor {
        ASPresentationAnchor()
    }
}

Custom URL Scheme: Register your callback URL scheme (e.g., myapp) in your app's Info.plist underCFBundleURLTypes, and add the redirect URI (myapp://callback) to your OAuth client settings in the dashboard.


Token Storage (Keychain)

The SDK automatically stores tokens in the iOS Keychain. Tokens are encrypted at rest and protected by the device's Secure Enclave. You can also interact with the Keychain store directly.

Token storage
import PasskeyBaseAuth

// The SDK stores tokens in the Keychain automatically.
// You can also manage storage directly:

// Store tokens after authentication
try KeychainStore.shared.save(
    accessToken: result.accessToken,
    idToken: result.idToken,
    refreshToken: result.refreshToken,
    expiresAt: result.expiresAt
)

// Retrieve the current access token
if let accessToken = try KeychainStore.shared.getAccessToken() {
    // Use the token for API calls
    var request = URLRequest(url: apiURL)
    request.setValue(
        "Bearer \(accessToken)",
        forHTTPHeaderField: "Authorization"
    )
}

// Check if tokens are still valid
let isValid = KeychainStore.shared.hasValidTokens()

// Clear all stored tokens on logout
try KeychainStore.shared.clear()

// Configure the Keychain access group for app extensions
let store = KeychainStore(
    service: "com.myapp.auth",
    accessGroup: "group.com.myapp.shared"
)

Management API

The Management API lets you manage users, passkeys, and OAuth clients programmatically.

Security: The Management API key should only be used server-side or in trusted environments. Never ship the API key in your production iOS app bundle.

Management API
import PasskeyBaseAuth

let management = PasskeyBaseManagement(
    domain: "myapp.auth.ing",
    apiKey: "mgmt_key_abc123"  // Keep this server-side!
)

// List users
let users = try await management.listUsers(
    page: 1,
    perPage: 20
)
for user in users.data {
    print("\(user.displayName) - \(user.passkeyCount) passkeys")
}

// Get a specific user
let user = try await management.getUser(id: "usr_abc123")

// Delete a user and all their passkeys
try await management.deleteUser(id: "usr_abc123")

// List passkeys for a user
let passkeys = try await management.listPasskeys(
    userId: "usr_abc123"
)

// Delete a specific passkey
try await management.deletePasskey(
    userId: "usr_abc123",
    passkeyId: "pk_xyz789"
)

// List OAuth clients
let clients = try await management.listClients()

// Create a new OAuth client
let newClient = try await management.createClient(
    name: "My iOS App",
    redirectURIs: ["myapp://callback"]
)

Enrollment Tokens

Enroll passkeys for users who are already authenticated through your existing auth system. Your backend creates an enrollment token via theManagement API, and the iOS SDK uses it to register a passkey on the device.

Enrollment
import PasskeyBaseAuth

// 1. Request an enrollment token from your backend
let tokenResponse = try await URLSession.shared.data(
    for: enrollmentRequest
)
let enrollToken = try JSONDecoder().decode(
    EnrollmentTokenResponse.self,
    from: tokenResponse.0
).token

// 2. Enroll a passkey using the token
let enrollmentManager = PasskeyEnrollmentManager(client: passkeyBase)
try await enrollmentManager.enrollPasskey(
    token: enrollToken,
    displayName: "iPhone 15 Pro"
)

// The user's device passkey is now linked to their account.
// Future logins will recognize this passkey.

Error Handling

The SDK defines a PasskeyBaseErrorenum with typed cases for every error condition. Use Swift's pattern matching to handle each case.

Error handling
import PasskeyBaseAuth

do {
    let result = try await authManager.authenticate()
    handleSuccess(result)
} catch PasskeyBaseError.networkError(let underlying) {
    // Connection failed, timeout, etc.
    showAlert("Network error: \(underlying.localizedDescription)")
} catch PasskeyBaseError.serverError(let status, let message) {
    // Server returned an error (4xx, 5xx)
    showAlert("Server error \(status): \(message)")
} catch PasskeyBaseError.userCancelled {
    // User dismissed the passkey prompt
    // No action needed
} catch PasskeyBaseError.credentialNotFound {
    // No matching passkey on this device
    showAlert("No passkey found. Please register first.")
} catch PasskeyBaseError.tokenExpired {
    // Access token or enrollment token expired
    try await refreshTokens()
} catch PasskeyBaseError.invalidState {
    // OAuth state mismatch (possible CSRF)
    showAlert("Authentication failed. Please try again.")
} catch {
    showAlert("Unexpected error: \(error.localizedDescription)")
}
ErrorDescription
networkErrorConnection failed, DNS resolution error, or timeout.
serverErrorServer returned an HTTP error (4xx or 5xx).
userCancelledUser dismissed the passkey or authentication prompt.
credentialNotFoundNo matching passkey found on the device.
tokenExpiredAccess token or enrollment token has expired.
invalidStateOAuth state parameter mismatch (potential CSRF).
unexpectedCredentialTypeAuthorization controller returned an unexpected credential type.

Next steps