Swift / iOS SDK
PasskeyBaseAuth
Native iOS SDK with platform passkey support via ASAuthorizationController, SwiftUI integration, and secure Keychain token storage. Supports iOS 16+.
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.
// 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.
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.
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.
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.
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.
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.
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.
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.
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.
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)")
}| Error | Description |
|---|---|
networkError | Connection failed, DNS resolution error, or timeout. |
serverError | Server returned an HTTP error (4xx or 5xx). |
userCancelled | User dismissed the passkey or authentication prompt. |
credentialNotFound | No matching passkey found on the device. |
tokenExpired | Access token or enrollment token has expired. |
invalidState | OAuth state parameter mismatch (potential CSRF). |
unexpectedCredentialType | Authorization controller returned an unexpected credential type. |
Next steps
- OAuth Flow -- understand the authentication flow in detail
- Enrollment -- add passkeys to existing auth systems
- API Reference -- full Management API documentation
- Kotlin / Android SDK -- building for both platforms?