A comprehensive Swift SDK for integrating OAuth42 authentication into iOS, macOS, tvOS, and watchOS applications.
- âś… OAuth2 Authorization Code Flow with PKCE (S256)
- âś… Automatic Token Refresh - transparent and seamless
- âś… Secure Keychain Storage - encrypted token persistence
- âś… OpenID Connect Support - full OIDC discovery and UserInfo
- âś… Modern Swift - async/await, Codable, and type-safe APIs
- âś… Cross-Platform - iOS 14+, macOS 11+, tvOS 14+, watchOS 7+
- âś… Zero Dependencies - pure Swift implementation
Add OAuth42Swift to your project using Xcode:
- File → Add Package Dependencies
- Enter the repository URL:
https://github.com/oauth42/oauth42-swift.git - Select version rule (e.g., "Up to Next Major: 1.0.0")
- Click Add Package
Or add it to your Package.swift file:
dependencies: [
.package(url: "https://github.com/oauth42/oauth42-swift.git", from: "1.0.0")
]import OAuth42Swiftlet client = OAuth42Client(
clientId: "your-client-id",
redirectURI: "myapp://oauth-callback",
issuer: "https://oauth42.example.com",
tokenStore: KeychainTokenStore(service: "com.example.myapp")
)// Build authorization URL
let authURL = try await client.buildAuthorizationURL()
// Open in ASWebAuthenticationSession (iOS/macOS)
// ... handle user authentication in browser ...
// Exchange authorization code for tokens
let tokens = try await client.exchangeCodeForTokens(
code: authorizationCode,
state: stateFromRedirect
)
// Fetch user information (automatically refreshes token if needed)
let userInfo = try await client.fetchUserInfo()
print("Logged in as: \(userInfo.email)")let client = OAuth42Client(
clientId: "your-client-id",
redirectURI: "myapp://oauth-callback",
issuer: "https://oauth42.example.com"
)let tokenStore = KeychainTokenStore(
service: "com.example.myapp",
accessGroup: nil // Optional: for sharing between apps
)
let client = OAuth42Client(
clientId: "your-client-id",
clientSecret: "your-secret", // Optional for confidential clients
redirectURI: "myapp://oauth-callback",
issuer: "https://oauth42.example.com",
scopes: ["openid", "profile", "email"], // Default scopes
tokenStore: tokenStore
)Note: OAuth42Swift supports two authentication flows:
- Browser-Based Flow (recommended for third-party apps)
- Password Flow (for first-party apps like OAuth42 Authenticator)
Choose the flow that matches your use case.
do {
let authURL = try await client.buildAuthorizationURL()
// Open authURL in browser or ASWebAuthenticationSession
} catch {
print("Failed to build auth URL: \(error)")
}Use ASWebAuthenticationSession for secure browser-based authentication:
import AuthenticationServices
func authenticate() {
Task {
do {
let authURL = try await client.buildAuthorizationURL()
let session = ASWebAuthenticationSession(
url: authURL,
callbackURLScheme: "myapp"
) { callbackURL, error in
Task {
await self.handleCallback(callbackURL: callbackURL, error: error)
}
}
session.presentationContextProvider = self
session.start()
} catch {
print("Authentication error: \(error)")
}
}
}
func handleCallback(callbackURL: URL?, error: Error?) async {
guard let callbackURL = callbackURL else {
print("Authentication cancelled or failed: \(String(describing: error))")
return
}
// Parse query parameters
guard let components = URLComponents(url: callbackURL, resolvingAgainstBaseURL: false),
let queryItems = components.queryItems else {
return
}
let code = queryItems.first { $0.name == "code" }?.value
let state = queryItems.first { $0.name == "state" }?.value
guard let code = code, let state = state else {
print("Missing code or state in callback")
return
}
do {
// Exchange code for tokens
let tokens = try await client.exchangeCodeForTokens(code: code, state: state)
print("Authentication successful! Access token: \(tokens.accessToken)")
// Fetch user info
let userInfo = try await client.fetchUserInfo()
print("Logged in as: \(userInfo.email)")
} catch {
print("Token exchange failed: \(error)")
}
}do {
let loginResponse = try await client.authenticateWithPassword(
email: "user@example.com",
password: "user_password"
)
print("Logged in as: \(loginResponse.user.email)")
print("Access token: \(loginResponse.accessToken)")
} catch OAuth42Error.mfaRequired(let message) {
// MFA is enabled, need to prompt for code
print("MFA required: \(message)")
} catch OAuth42Error.invalidCredentials(let message) {
// Invalid username/password
print("Login failed: \(message)")
} catch {
print("Error: \(error)")
}// First attempt without MFA code
do {
let loginResponse = try await client.authenticateWithPassword(
email: "user@example.com",
password: "user_password"
)
// Success - MFA not enabled
} catch OAuth42Error.mfaRequired {
// MFA is enabled, prompt user for code and retry
let mfaCode = promptUserForMFACode() // Your UI code
do {
let loginResponse = try await client.authenticateWithPassword(
email: "user@example.com",
password: "user_password",
mfaCode: mfaCode
)
print("Logged in with MFA: \(loginResponse.user.email)")
} catch {
print("MFA verification failed: \(error)")
}
}func loginWithCredentials(email: String, password: String) async {
do {
// Try login without MFA first
let response = try await client.authenticateWithPassword(
email: email,
password: password
)
// Success!
handleSuccessfulLogin(response)
} catch OAuth42Error.mfaRequired {
// MFA is enabled - show MFA input UI
showMFAPrompt { mfaCode in
Task {
await loginWithMFA(email: email, password: password, mfaCode: mfaCode)
}
}
} catch OAuth42Error.invalidCredentials(let message) {
showError("Invalid email or password: \(message)")
} catch {
showError("Login failed: \(error.localizedDescription)")
}
}
func loginWithMFA(email: String, password: String, mfaCode: String) async {
do {
let response = try await client.authenticateWithPassword(
email: email,
password: password,
mfaCode: mfaCode
)
handleSuccessfulLogin(response)
} catch OAuth42Error.invalidCredentials {
showError("Invalid MFA code")
} catch {
showError("Login failed: \(error.localizedDescription)")
}
}
func handleSuccessfulLogin(_ response: LoginResponse) {
// Tokens are automatically stored in Keychain (if configured)
// Navigate to authenticated screen
print("Logged in as: \(response.user.email)")
}// After authentication, check if user has MFA enabled
do {
let mfaStatus = try await client.getMFAStatus()
if mfaStatus.enabled {
print("MFA is enabled")
print("Backup codes remaining: \(mfaStatus.backupCodesRemaining ?? 0)")
if let lastUsed = mfaStatus.lastUsedAt {
print("Last used: \(lastUsed)")
}
} else {
print("MFA is not enabled")
}
} catch {
print("Failed to get MFA status: \(error)")
}The SDK automatically refreshes tokens when they expire:
// This automatically refreshes the token if expired
let userInfo = try await client.fetchUserInfo()// Manually refresh tokens
let newTokens = try await client.refreshTokens()
print("New access token: \(newTokens.accessToken)")// Get a valid token (automatically refreshes if expired)
let accessToken = try await client.getValidAccessToken()if let tokens = try client.getStoredTokens() {
if tokens.isExpired() {
print("Token is expired, will auto-refresh on next API call")
} else {
print("Token is valid for \(tokens.expiresAt.timeIntervalSinceNow) seconds")
}
}do {
try client.clearTokens()
print("Logged out successfully")
} catch {
print("Failed to clear tokens: \(error)")
}// Using stored tokens (auto-refresh)
let userInfo = try await client.fetchUserInfo()
print("User: \(userInfo.email)")
// Using explicit access token
let userInfo = try await client.fetchUserInfo(accessToken: "explicit-token")Use makeAuthenticatedRequest() for custom API endpoints:
// GET request
let url = URL(string: "https://api.oauth42.com/v1/profile")!
let (data, response) = try await client.makeAuthenticatedRequest(url: url)
if response.statusCode == 200 {
let profile = try JSONDecoder().decode(Profile.self, from: data)
print("Profile loaded: \(profile)")
}// POST request
let url = URL(string: "https://api.oauth42.com/v1/settings")!
let payload = ["theme": "dark", "notifications": true]
let body = try JSONEncoder().encode(payload)
let (data, response) = try await client.makeAuthenticatedRequest(
url: url,
method: "POST",
body: body
)import SwiftUI
import OAuth42Swift
@MainActor
class AuthManager: ObservableObject {
@Published var isAuthenticated = false
@Published var userInfo: UserInfo?
@Published var error: Error?
private let client: OAuth42Client
init() {
self.client = OAuth42Client(
clientId: "your-client-id",
redirectURI: "myapp://oauth-callback",
issuer: "https://oauth42.example.com",
tokenStore: KeychainTokenStore(service: "com.example.myapp")
)
// Check for existing session
checkStoredSession()
}
func checkStoredSession() {
Task {
do {
// Try to get stored tokens
if let tokens = try client.getStoredTokens() {
// Fetch user info (will auto-refresh if needed)
self.userInfo = try await client.fetchUserInfo()
self.isAuthenticated = true
}
} catch {
print("No valid session: \(error)")
}
}
}
func login() async {
do {
let authURL = try await client.buildAuthorizationURL()
// Open authURL in ASWebAuthenticationSession
// ... handle callback ...
} catch {
self.error = error
}
}
func logout() {
do {
try client.clearTokens()
self.isAuthenticated = false
self.userInfo = nil
} catch {
self.error = error
}
}
}
// Usage in SwiftUI
struct ContentView: View {
@StateObject private var auth = AuthManager()
var body: some View {
Group {
if auth.isAuthenticated {
ProfileView(userInfo: auth.userInfo)
} else {
LoginView(auth: auth)
}
}
}
}The SDK provides comprehensive error types:
do {
let userInfo = try await client.fetchUserInfo()
} catch OAuth42Error.authorizationFailed(let message) {
print("Authorization failed: \(message)")
} catch OAuth42Error.networkError(let error) {
print("Network error: \(error)")
} catch OAuth42Error.invalidState {
print("Possible CSRF attack detected")
} catch OAuth42Error.missingRefreshToken {
print("No refresh token available, re-authentication required")
} catch {
print("Unknown error: \(error)")
}Available error types:
invalidConfiguration(_:)- Invalid SDK configurationinvalidURL(_:)- Malformed URLnetworkError(_:)- Network/HTTP errorinvalidResponse(_:)- Invalid server responseauthorizationFailed(_:)- Authorization errortokenExchangeFailed(_:)- Token exchange errorrefreshFailed(_:)- Token refresh errormissingRefreshToken- No refresh token availablekeychainError(_:)- Keychain storage errorpkceGenerationFailed- PKCE generation errorinvalidState- State mismatch (CSRF protection)userCancelled- User cancelled authenticationmfaRequired(_:)- MFA code is required (password flow)invalidCredentials(_:)- Invalid username/password or MFA codeloginFailed(_:)- Password authentication failed
For custom networking configuration (proxies, SSL pinning, etc.):
let configuration = URLSessionConfiguration.default
configuration.timeoutIntervalForRequest = 30
let customSession = URLSession(configuration: configuration)
let client = OAuth42Client(
clientId: "your-client-id",
redirectURI: "myapp://oauth-callback",
issuer: "https://oauth42.example.com",
urlSession: customSession
)Share tokens between your app and extensions:
let tokenStore = KeychainTokenStore(
service: "com.example.myapp",
accessGroup: "group.com.example.myapp" // App Group ID
)let client = OAuth42Client(
clientId: "your-client-id",
redirectURI: "myapp://oauth-callback",
issuer: "https://oauth42.example.com",
scopes: ["openid", "profile", "email", "custom:read", "custom:write"]
)The SDK automatically fetches and caches OIDC configuration:
let config = try await client.fetchConfiguration()
print("Authorization endpoint: \(config.authorizationEndpoint)")
print("Token endpoint: \(config.tokenEndpoint)")
print("Supported scopes: \(config.scopesSupported ?? [])")- Use
ASWebAuthenticationSessionfor authentication - Configure URL scheme in Info.plist:
<key>CFBundleURLTypes</key> <array> <dict> <key>CFBundleURLSchemes</key> <array> <string>myapp</string> </array> </dict> </array>
- Use
ASWebAuthenticationSessionfor authentication - May require entitlements for Keychain access
- Limited authentication options (device code flow recommended)
- Consider companion device authentication
- Token storage via Keychain
- Authentication typically handled on paired iPhone
- iOS: 14.0+
- macOS: 11.0+
- tvOS: 14.0+
- watchOS: 7.0+
- Swift: 5.7+
- Xcode: 14.0+
- Always use PKCE - The SDK enforces PKCE (S256) for all authorization flows
- Secure storage - Use
KeychainTokenStorefor production apps - State validation - The SDK automatically generates and validates state parameters
- HTTPS only - Never use OAuth42 over unencrypted HTTP in production
- Redirect URI validation - Ensure your redirect URI is registered with OAuth42
- Token expiration - The SDK handles token refresh with a 60-second safety buffer
The SDK includes comprehensive test coverage:
# Run all tests
swift test
# Run specific test
swift test --filter OAuth42ClientTestsTest categories:
- Unit tests for all core functionality
- PKCE generation and validation
- Keychain storage operations
- Token expiration logic
- Integration tests (require running OAuth42 backend)
See the Examples directory for complete sample applications:
- iOS-Example: SwiftUI app with full authentication flow
- macOS-Example: AppKit-based macOS application
Integration tests require a running OAuth42 backend:
cd ~/localdev/oauth42
make up-local-dev-ssl
make run-local-oauth42Ensure your app has proper Keychain entitlements and signing configuration.
For self-signed certificates in development, implement a custom URLSessionDelegate:
class InsecureDelegate: NSObject, URLSessionDelegate {
func urlSession(
_ session: URLSession,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void
) {
if challenge.protectionSpace.host == "localhost" {
if let serverTrust = challenge.protectionSpace.serverTrust {
completionHandler(.useCredential, URLCredential(trust: serverTrust))
return
}
}
completionHandler(.performDefaultHandling, nil)
}
}
let session = URLSession(configuration: .default, delegate: InsecureDelegate(), delegateQueue: nil)
let client = OAuth42Client(..., urlSession: session)Contributions are welcome! Please feel free to submit a Pull Request.
This SDK is available under the MIT license. See the LICENSE file for more info.
- Documentation: https://docs.oauth42.com
- Issues: https://github.com/oauth42/oauth42-swift/issues
- Main Project: https://github.com/oauth42/oauth42
- OAuth42 - Main OAuth42 authentication server
- OAuth42 Rust SDK - Rust client library
- OAuth42 Python SDK - Python client library
- OAuth42 TypeScript SDK - TypeScript/JavaScript client library