OAuth 2.0 authentication library for iOS and macOS with PKCE support and customizable SwiftUI sign-in UI.
- iOS 18+ / macOS 15+
- Swift 6.2+
- Xcode 16+
Add to your Package.swift:
dependencies: [
.package(url: "https://github.com/nicktrienenern/RxAuthSwift.git", from: "1.0.0"),
]Then add the targets you need:
.target(
name: "YourApp",
dependencies: [
"RxAuthSwift", // Core OAuth logic
"RxAuthSwiftUI", // Sign-in UI components (optional)
]
)import RxAuthSwift
let config = RxAuthConfiguration(
issuer: "https://auth.example.com",
clientID: "your-client-id",
redirectURI: "yourapp://callback",
scopes: ["openid", "profile", "email"]
)Endpoint paths default to /api/oauth/authorize, /api/oauth/token, /api/oauth/userinfo but can be overridden:
let config = RxAuthConfiguration(
issuer: "https://auth.example.com",
clientID: "your-client-id",
redirectURI: "yourapp://callback",
authorizePath: "/oauth/authorize",
tokenPath: "/oauth/token",
userInfoPath: "/oauth/me"
)For native macOS sign-in, RxSignInView renders username/password fields instead of launching a browser. Password sign-in posts an OAuth password-grant request directly to nativePasswordTokenPath or tokenPath when no native override is provided:
let config = RxAuthConfiguration(
issuer: "https://auth.example.com",
clientID: "your-client-id",
redirectURI: "yourapp://callback",
nativePasswordTokenPath: "/api/oauth/token",
nativeSignupPath: "/api/oauth/signup"
)The signup endpoint receives JSON with client_id, username, password, optional name, and scope, and should return the same token JSON as the OAuth token endpoint.
Passkey sign-in and signup are enabled when the matching endpoint pairs are configured:
let config = RxAuthConfiguration(
issuer: "https://auth.example.com",
clientID: "your-client-id",
redirectURI: "yourapp://callback",
passkeyChallengePath: "/api/passkeys/authentication/options",
passkeyVerificationPath: "/api/passkeys/authentication/verify",
passkeyRegistrationChallengePath: "/api/passkeys/registration/options",
passkeyRegistrationVerificationPath: "/api/passkeys/registration/verify",
passkeyRelyingPartyIdentifier: "auth.example.com"
)The authentication challenge endpoint should return a base64url WebAuthn challenge, optional requestID, optional relying party identifier, and optional allowed credential IDs. The registration challenge endpoint should return a base64url challenge and user ID, plus optional requestID, username, and relying party identifier. Verification endpoints receive the platform passkey assertion or registration payload and return the same token JSON as the OAuth token endpoint.
let authManager = OAuthManager(configuration: config)You can inject custom token storage:
let authManager = OAuthManager(
configuration: config,
tokenStorage: MyCustomTokenStorage()
)@main
struct MyApp: App {
@State private var authManager = OAuthManager(configuration: config)
var body: some Scene {
WindowGroup {
ContentView()
.task {
await authManager.checkExistingAuth()
}
}
}
}Simple (with appearance customization):
import RxAuthSwiftUI
RxSignInView(
manager: authManager,
appearance: RxSignInAppearance(
icon: .image(Image("MyLogo")),
title: "Welcome",
subtitle: "Sign in to continue",
signInButtonTitle: "Get Started",
accentColor: .purple,
secondaryColor: .pink
)
)With callbacks:
RxSignInView(
manager: authManager,
onAuthSuccess: {
// Navigate to home, fetch user data, etc.
},
onAuthFailed: { error in
// Log error, show custom alert, etc.
}
)Advanced (fully custom header with ViewBuilder):
RxSignInView(
manager: authManager,
onAuthSuccess: { print("Signed in!") },
onAuthFailed: { error in print("Failed: \(error)") }
) {
VStack {
Image("Logo")
.resizable()
.frame(width: 100, height: 100)
Text("My App")
.font(.largeTitle.bold())
}
}switch authManager.authState {
case .unknown:
ProgressView()
case .unauthenticated:
RxSignInView(manager: authManager)
case .authenticated:
HomeView(user: authManager.currentUser)
}await authManager.logout()NotificationCenter.default.addObserver(
forName: .rxAuthSessionExpired,
object: nil,
queue: .main
) { _ in
// Handle session expiry
}The SignInIcon enum supports:
.systemImage("lock.shield.fill") // SF Symbol
.image(Image("MyLogo")) // SwiftUI Image
.assetImage("AppIcon", Bundle.main) // Asset catalog
.none // No iconImplement TokenStorageProtocol for custom storage backends:
public protocol TokenStorageProtocol: Sendable {
func saveAccessToken(_ token: String) throws
func getAccessToken() -> String?
func deleteAccessToken() throws
func saveRefreshToken(_ token: String) throws
func getRefreshToken() -> String?
func deleteRefreshToken() throws
func saveExpiresAt(_ date: Date) throws
func getExpiresAt() -> Date?
func isTokenExpired() -> Bool
func clearAll() throws
}UI components automatically use .glassEffect on iOS 26+ / macOS 26+ and fall back to .ultraThinMaterial / .borderedProminent on older platforms.
# macOS
bash scripts/build-macos.sh
# iOS
bash scripts/build-ios.shMIT