AppAuth for iOS

authentication libraryiOSmacOStvOSOAuth 2.0OpenID ConnectSwiftmobile authenticationPKCE

Authentication Library

AppAuth for iOS

Overview

AppAuth for iOS is the official OpenID Foundation iOS, macOS, and tvOS OAuth 2.0 and OpenID Connect client implementation. As of 2025, it supports iOS 12+, macOS 10.14+, and is actively developed as version 2.0.0. It fully complies with RFC 8252 "OAuth 2.0 for Native Apps" latest best practices, providing secure authentication flows using SFAuthenticationSession and SFSafariViewController. With automatic PKCE (Proof Key for Code Exchange) implementation, support for both Custom URI Schemes and Universal Links, Device Authorization Grant for tvOS, and other Apple platform-optimized comprehensive authentication solutions. Swift Package Manager support enables easy integration into modern Swift development environments.

Details

AppAuth for iOS 2.0 series provides direct mapping of RFC 6749 (OAuth 2.0), RFC 7636 (PKCE), and OpenID Connect specifications, enabling enterprise-grade authentication capabilities integration into iOS applications. The architecture consists of the platform-independent AppAuthCore module and platform-specific implementations. Authentication flows use SFSafariViewController (iOS 9+) or SFAuthenticationSession (iOS 12+), explicitly excluding UIWebView and WKWebView support to ensure security. Core classes including OIDServiceConfiguration, OIDAuthorizationRequest, OIDTokenRequest, and OIDAuthState properly manage all standard OAuth 2.0 flows, supporting automatic token refresh and persistence.

Key Features

  • RFC Compliant Implementation: Complete compliance with OAuth 2.0, OpenID Connect, and PKCE specifications
  • Apple Optimized: SFSafariViewController, SFAuthenticationSession, and Universal Links support
  • tvOS Support: Device Authorization Grant (RFC 8628) for tvOS authentication
  • Automatic PKCE: Built-in PKCE implementation to automatically prevent authorization code attacks
  • Token Management: Comprehensive support for automatic refresh, persistence, and state management
  • Swift Integration: Swift Package Manager support and Swift app-optimized design

Advantages and Disadvantages

Advantages

  • OpenID Foundation official library ensuring industry-standard reliability and continued support
  • RFC 8252 compliance meets Apple Store review requirements and security standards
  • SFSafariViewController usage balances improved user experience with enhanced security
  • Automatic PKCE implementation achieves high protection levels without additional security configuration
  • Flexible choice between Universal Links and Custom URI Schemes
  • Comprehensive documentation and rich sample code improve development efficiency

Disadvantages

  • Minimum requirements of iOS 12+, macOS 10.14+ prevent usage on older devices
  • Only supports OAuth 2.0/OpenID Connect, not other authentication methods (custom auth, etc.)
  • Cannot be used with embedded WebView applications due to constraints
  • Customizing complex authentication flows requires deep OAuth knowledge
  • Enterprise environments require additional implementation for advanced configurations
  • Platform-specific implementation limits portability to other operating systems

Reference Pages

Usage Examples

Installation with CocoaPods and Swift Package Manager

# Podfile - Using CocoaPods
platform :ios, '12.0'

target 'YourApp' do
  use_frameworks!
  
  pod 'AppAuth', '~> 2.0'
end
// Package.swift - Using Swift Package Manager
// Xcode -> File -> Add Package Dependencies...
// Repository URL: https://github.com/openid/AppAuth-iOS

import PackageDescription

let package = Package(
    name: "YourApp",
    platforms: [
        .iOS(.v12)
    ],
    dependencies: [
        .package(
            url: "https://github.com/openid/AppAuth-iOS",
            from: "2.0.0"
        )
    ],
    targets: [
        .target(
            name: "YourApp",
            dependencies: [
                .product(name: "AppAuth", package: "AppAuth-iOS")
            ]
        )
    ]
)

URL Scheme Configuration and Redirect Handling

<!-- Info.plist - Custom URL Scheme configuration -->
<key>CFBundleURLTypes</key>
<array>
    <dict>
        <key>CFBundleURLName</key>
        <string>com.example.myapp.oauth</string>
        <key>CFBundleURLSchemes</key>
        <array>
            <string>com.example.myapp</string>
        </array>
    </dict>
</array>

<!-- Universal Links configuration (recommended) -->
<key>com.apple.developer.associated-domains</key>
<array>
    <string>applinks:myapp.example.com</string>
</array>
// AppDelegate.swift - Redirect handling for iOS 13 and earlier
import UIKit
import AppAuth

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    
    var window: UIWindow?
    var currentAuthorizationFlow: OIDExternalUserAgentSession?
    
    func application(
        _ app: UIApplication,
        open url: URL,
        options: [UIApplication.OpenURLOptionsKey: Any] = [:]
    ) -> Bool {
        // AppAuth OAuth redirect handling
        if let authorizationFlow = self.currentAuthorizationFlow,
           authorizationFlow.resumeExternalUserAgentFlow(with: url) {
            self.currentAuthorizationFlow = nil
            return true
        }
        
        return false
    }
}
// SceneDelegate.swift - Redirect handling for iOS 13+
import UIKit
import AppAuth

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    
    var window: UIWindow?
    var currentAuthorizationFlow: OIDExternalUserAgentSession?
    
    func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
        guard let url = URLContexts.first?.url else { return }
        
        // AppAuth OAuth redirect handling
        if let authorizationFlow = self.currentAuthorizationFlow,
           authorizationFlow.resumeExternalUserAgentFlow(with: url) {
            self.currentAuthorizationFlow = nil
        }
    }
}

Basic OAuth 2.0 Authentication Flow Implementation

// OAuthManager.swift - OAuth authentication manager
import Foundation
import AppAuth

class OAuthManager: ObservableObject {
    @Published var authState: OIDAuthState?
    @Published var isAuthenticated: Bool = false
    @Published var errorMessage: String?
    
    // OAuth configuration
    private let clientID = "your-client-id"
    private let redirectURI = URL(string: "com.example.myapp://oauth2redirect")!
    private let issuer = URL(string: "https://accounts.example.com")!
    
    // Auth state persistence key
    private let authStateKey = "AuthState"
    
    init() {
        loadAuthState()
    }
    
    func startAuthentication(from viewController: UIViewController) {
        // 1. Dynamic service configuration discovery
        OIDAuthorizationService.discoverConfiguration(forIssuer: issuer) { [weak self] configuration, error in
            guard let self = self else { return }
            
            if let error = error {
                DispatchQueue.main.async {
                    self.errorMessage = "Discovery failed: \(error.localizedDescription)"
                }
                return
            }
            
            guard let config = configuration else {
                DispatchQueue.main.async {
                    self.errorMessage = "Configuration is nil"
                }
                return
            }
            
            // 2. Create authorization request
            let request = OIDAuthorizationRequest(
                configuration: config,
                clientId: self.clientID,
                scopes: [OIDScopeOpenID, OIDScopeProfile, OIDScopeEmail],
                redirectURL: self.redirectURI,
                responseType: OIDResponseTypeCode,
                additionalParameters: nil
            )
            
            // 3. Start authorization flow
            self.performAuthorizationRequest(request, from: viewController)
        }
    }
    
    private func performAuthorizationRequest(
        _ request: OIDAuthorizationRequest,
        from viewController: UIViewController
    ) {
        // Get AppDelegate or SceneDelegate reference
        guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
            self.errorMessage = "Could not get app delegate"
            return
        }
        
        // Execute authorization flow
        appDelegate.currentAuthorizationFlow = OIDAuthState.authState(
            byPresenting: request,
            presenting: viewController
        ) { [weak self] authState, error in
            DispatchQueue.main.async {
                guard let self = self else { return }
                
                if let authState = authState {
                    print("Authorization success! Access token: \(authState.lastTokenResponse?.accessToken ?? "nil")")
                    self.setAuthState(authState)
                } else {
                    print("Authorization error: \(error?.localizedDescription ?? "Unknown error")")
                    self.errorMessage = error?.localizedDescription ?? "Unknown error"
                    self.setAuthState(nil)
                }
            }
        }
    }
    
    func refreshTokens() {
        guard let authState = authState else {
            self.errorMessage = "No auth state available"
            return
        }
        
        authState.performAction(freshTokens: { [weak self] accessToken, idToken, error in
            DispatchQueue.main.async {
                if let error = error {
                    self?.errorMessage = "Token refresh failed: \(error.localizedDescription)"
                } else {
                    print("Tokens refreshed successfully")
                    print("Access token: \(accessToken ?? "nil")")
                }
            }
        })
    }
    
    func makeAuthenticatedRequest(to url: URL, completion: @escaping (Data?, URLResponse?, Error?) -> Void) {
        guard let authState = authState else {
            completion(nil, nil, NSError(domain: "OAuthManager", code: -1, userInfo: [NSLocalizedDescriptionKey: "Not authenticated"]))
            return
        }
        
        authState.performAction(freshTokens: { accessToken, idToken, error in
            if let error = error {
                completion(nil, nil, error)
                return
            }
            
            guard let accessToken = accessToken else {
                completion(nil, nil, NSError(domain: "OAuthManager", code: -1, userInfo: [NSLocalizedDescriptionKey: "No access token available"]))
                return
            }
            
            var request = URLRequest(url: url)
            request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
            
            URLSession.shared.dataTask(with: request, completionHandler: completion).resume()
        })
    }
    
    func logout() {
        // Clear authentication state
        setAuthState(nil)
        
        // Additional: Call End Session Endpoint (if supported)
        // Note: Implementation varies by provider
    }
    
    private func setAuthState(_ authState: OIDAuthState?) {
        if self.authState != authState {
            self.authState = authState
            self.isAuthenticated = authState?.isAuthorized ?? false
            saveAuthState()
        }
    }
    
    private func saveAuthState() {
        if let authState = authState {
            let data = NSKeyedArchiver.archivedData(withRootObject: authState)
            UserDefaults.standard.set(data, forKey: authStateKey)
        } else {
            UserDefaults.standard.removeObject(forKey: authStateKey)
        }
    }
    
    private func loadAuthState() {
        guard let data = UserDefaults.standard.data(forKey: authStateKey),
              let authState = try? NSKeyedUnarchiver.unarchivedObject(ofClass: OIDAuthState.self, from: data) else {
            return
        }
        
        setAuthState(authState)
    }
}

SwiftUI Integration and Profile Display

// ContentView.swift - SwiftUI authentication UI
import SwiftUI
import AppAuth

struct ContentView: View {
    @StateObject private var oauthManager = OAuthManager()
    @State private var showingLogin = false
    @State private var userProfile: UserProfile?
    
    var body: some View {
        NavigationView {
            VStack(spacing: 20) {
                if oauthManager.isAuthenticated {
                    // Authenticated state
                    authenticatedView
                } else {
                    // Unauthenticated state
                    unauthenticatedView
                }
            }
            .padding()
            .navigationTitle("AppAuth Demo")
            .alert("Error", isPresented: .constant(oauthManager.errorMessage != nil)) {
                Button("OK") {
                    oauthManager.errorMessage = nil
                }
            } message: {
                Text(oauthManager.errorMessage ?? "")
            }
        }
    }
    
    private var authenticatedView: some View {
        VStack(spacing: 20) {
            Text("Welcome!")
                .font(.title)
                .foregroundColor(.green)
            
            if let profile = userProfile {
                UserProfileView(profile: profile)
            } else {
                ProgressView("Loading profile...")
                    .onAppear {
                        loadUserProfile()
                    }
            }
            
            HStack(spacing: 15) {
                Button("Refresh Tokens") {
                    oauthManager.refreshTokens()
                }
                .buttonStyle(.bordered)
                
                Button("Logout") {
                    oauthManager.logout()
                    userProfile = nil
                }
                .buttonStyle(.borderedProminent)
                .tint(.red)
            }
        }
    }
    
    private var unauthenticatedView: some View {
        VStack(spacing: 20) {
            Image(systemName: "lock.shield")
                .font(.system(size: 60))
                .foregroundColor(.blue)
            
            Text("OAuth 2.0 Authentication")
                .font(.title)
                .multilineTextAlignment(.center)
            
            Text("Sign in to access your account using OAuth 2.0 with AppAuth")
                .font(.body)
                .foregroundColor(.secondary)
                .multilineTextAlignment(.center)
            
            Button("Sign In") {
                if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
                   let window = windowScene.windows.first,
                   let rootViewController = window.rootViewController {
                    oauthManager.startAuthentication(from: rootViewController)
                }
            }
            .buttonStyle(.borderedProminent)
            .controlSize(.large)
        }
    }
    
    private func loadUserProfile() {
        guard let userInfoURL = URL(string: "https://api.example.com/userinfo") else { return }
        
        oauthManager.makeAuthenticatedRequest(to: userInfoURL) { data, response, error in
            DispatchQueue.main.async {
                if let error = error {
                    oauthManager.errorMessage = "Failed to load profile: \(error.localizedDescription)"
                    return
                }
                
                guard let data = data else {
                    oauthManager.errorMessage = "No data received"
                    return
                }
                
                do {
                    let profile = try JSONDecoder().decode(UserProfile.self, from: data)
                    self.userProfile = profile
                } catch {
                    oauthManager.errorMessage = "Failed to parse profile: \(error.localizedDescription)"
                }
            }
        }
    }
}

struct UserProfileView: View {
    let profile: UserProfile
    
    var body: some View {
        VStack(alignment: .leading, spacing: 12) {
            Text("User Profile")
                .font(.headline)
            
            HStack {
                AsyncImage(url: URL(string: profile.picture ?? "")) { image in
                    image
                        .resizable()
                        .aspectRatio(contentMode: .fill)
                } placeholder: {
                    Circle()
                        .fill(Color.gray.opacity(0.3))
                        .overlay(
                            Image(systemName: "person.fill")
                                .foregroundColor(.gray)
                        )
                }
                .frame(width: 60, height: 60)
                .clipShape(Circle())
                
                VStack(alignment: .leading, spacing: 4) {
                    Text(profile.name ?? "Unknown")
                        .font(.title3)
                        .fontWeight(.semibold)
                    
                    Text(profile.email ?? "No email")
                        .font(.caption)
                        .foregroundColor(.secondary)
                }
                
                Spacer()
            }
            
            Divider()
            
            VStack(alignment: .leading, spacing: 8) {
                ProfileRowView(label: "User ID", value: profile.sub)
                ProfileRowView(label: "Name", value: profile.name)
                ProfileRowView(label: "Email", value: profile.email)
                ProfileRowView(label: "Email Verified", value: profile.emailVerified?.description)
                ProfileRowView(label: "Locale", value: profile.locale)
            }
        }
        .padding()
        .background(Color(.systemGray6))
        .cornerRadius(12)
    }
}

struct ProfileRowView: View {
    let label: String
    let value: String?
    
    var body: some View {
        HStack {
            Text(label + ":")
                .fontWeight(.medium)
                .frame(width: 100, alignment: .leading)
            
            Text(value ?? "N/A")
                .foregroundColor(.secondary)
            
            Spacer()
        }
        .font(.caption)
    }
}

// UserProfile model
struct UserProfile: Codable {
    let sub: String
    let name: String?
    let email: String?
    let emailVerified: Bool?
    let picture: String?
    let locale: String?
    
    enum CodingKeys: String, CodingKey {
        case sub, name, email, picture, locale
        case emailVerified = "email_verified"
    }
}

tvOS Device Authorization Grant Implementation

// tvOSAuthManager.swift - tvOS Device Authorization Grant
import Foundation
import AppAuth

@available(tvOS 12.0, *)
class tvOSAuthManager: ObservableObject {
    @Published var authState: OIDAuthState?
    @Published var isAuthenticated: Bool = false
    @Published var userCode: String?
    @Published var verificationURI: String?
    @Published var errorMessage: String?
    
    private let clientID = "your-tv-client-id"
    private let issuer = URL(string: "https://accounts.example.com")!
    
    func startDeviceAuthorization() {
        // 1. Dynamic service configuration discovery
        OIDAuthorizationService.discoverConfiguration(forIssuer: issuer) { [weak self] configuration, error in
            guard let self = self else { return }
            
            if let error = error {
                DispatchQueue.main.async {
                    self.errorMessage = "Discovery failed: \(error.localizedDescription)"
                }
                return
            }
            
            guard let config = configuration else {
                DispatchQueue.main.async {
                    self.errorMessage = "Configuration is nil"
                }
                return
            }
            
            // 2. Create Device Authorization Request
            let request = OIDDeviceAuthorizationRequest(
                configuration: config,
                clientId: self.clientID,
                scopes: [OIDScopeOpenID, OIDScopeProfile, OIDScopeEmail],
                additionalParameters: nil
            )
            
            // 3. Start Device Authorization
            self.performDeviceAuthorizationRequest(request)
        }
    }
    
    private func performDeviceAuthorizationRequest(_ request: OIDDeviceAuthorizationRequest) {
        OIDAuthorizationService.perform(request) { [weak self] response, error in
            DispatchQueue.main.async {
                guard let self = self else { return }
                
                if let error = error {
                    self.errorMessage = "Device authorization failed: \(error.localizedDescription)"
                    return
                }
                
                guard let response = response else {
                    self.errorMessage = "No response received"
                    return
                }
                
                // Display user code and verification URL
                self.userCode = response.userCode
                self.verificationURI = response.verificationURI?.absoluteString
                
                // Start token polling
                self.startTokenPolling(with: response)
            }
        }
    }
    
    private func startTokenPolling(with deviceAuthResponse: OIDDeviceAuthorizationResponse) {
        let tokenRequest = OIDTokenRequest(
            configuration: deviceAuthResponse.request.configuration,
            grantType: OIDGrantTypeDeviceCode,
            authorizationCode: nil,
            redirectURL: nil,
            clientID: clientID,
            clientSecret: nil,
            scope: nil,
            refreshToken: nil,
            codeVerifier: nil,
            additionalParameters: [
                "device_code": deviceAuthResponse.deviceCode ?? ""
            ]
        )
        
        // Polling interval (typically 5 seconds)
        let pollingInterval = TimeInterval(deviceAuthResponse.interval?.intValue ?? 5)
        let expirationDate = Date().addingTimeInterval(TimeInterval(deviceAuthResponse.expiresIn?.intValue ?? 1800))
        
        pollForToken(request: tokenRequest, interval: pollingInterval, expirationDate: expirationDate)
    }
    
    private func pollForToken(request: OIDTokenRequest, interval: TimeInterval, expirationDate: Date) {
        // Check for expiration
        if Date() > expirationDate {
            DispatchQueue.main.async {
                self.errorMessage = "Device authorization expired"
                self.cleanup()
            }
            return
        }
        
        OIDAuthorizationService.perform(request) { [weak self] response, error in
            guard let self = self else { return }
            
            if let tokenResponse = response {
                // Success: Set authentication state
                DispatchQueue.main.async {
                    let authState = OIDAuthState(authorizationResponse: nil, tokenResponse: tokenResponse)
                    self.setAuthState(authState)
                    self.cleanup()
                }
            } else if let error = error as NSError? {
                // Error handling
                if error.domain == OIDOAuthTokenErrorDomain {
                    switch error.code {
                    case OIDTokenErrorCode.authorizationPending.rawValue:
                        // Still awaiting authorization: Poll again
                        DispatchQueue.main.asyncAfter(deadline: .now() + interval) {
                            self.pollForToken(request: request, interval: interval, expirationDate: expirationDate)
                        }
                    case OIDTokenErrorCode.slowDown.rawValue:
                        // Increase polling interval and retry
                        DispatchQueue.main.asyncAfter(deadline: .now() + interval * 2) {
                            self.pollForToken(request: request, interval: interval * 2, expirationDate: expirationDate)
                        }
                    default:
                        // Other errors
                        DispatchQueue.main.async {
                            self.errorMessage = "Token request failed: \(error.localizedDescription)"
                            self.cleanup()
                        }
                    }
                } else {
                    DispatchQueue.main.async {
                        self.errorMessage = "Token request failed: \(error.localizedDescription)"
                        self.cleanup()
                    }
                }
            }
        }
    }
    
    private func setAuthState(_ authState: OIDAuthState?) {
        self.authState = authState
        self.isAuthenticated = authState?.isAuthorized ?? false
    }
    
    private func cleanup() {
        userCode = nil
        verificationURI = nil
    }
    
    func logout() {
        setAuthState(nil)
    }
}

// tvOS authentication UI
struct tvOSAuthView: View {
    @StateObject private var authManager = tvOSAuthManager()
    
    var body: some View {
        VStack(spacing: 30) {
            if authManager.isAuthenticated {
                // Authenticated
                Text("Authentication Successful!")
                    .font(.title)
                    .foregroundColor(.green)
                
                Button("Logout") {
                    authManager.logout()
                }
            } else if let userCode = authManager.userCode, let verificationURI = authManager.verificationURI {
                // Display authentication code
                VStack(spacing: 20) {
                    Text("Complete Authentication")
                        .font(.title)
                    
                    Text("Go to \(verificationURI) on your phone or computer")
                        .font(.headline)
                    
                    Text("Enter this code:")
                        .font(.title2)
                    
                    Text(userCode)
                        .font(.largeTitle)
                        .fontWeight(.bold)
                        .padding()
                        .background(Color.blue)
                        .foregroundColor(.white)
                        .cornerRadius(8)
                    
                    Text("Waiting for authentication...")
                        .font(.caption)
                        .foregroundColor(.secondary)
                }
            } else {
                // Start authentication
                VStack(spacing: 20) {
                    Text("Sign In")
                        .font(.title)
                    
                    Button("Start Authentication") {
                        authManager.startDeviceAuthorization()
                    }
                    .buttonStyle(.borderedProminent)
                }
            }
            
            if let errorMessage = authManager.errorMessage {
                Text("Error: \(errorMessage)")
                    .foregroundColor(.red)
            }
        }
        .padding()
    }
}

Custom Browser and Advanced Configuration

// CustomBrowserManager.swift - Custom browser implementation
import Foundation
import AppAuth
import SafariServices

class CustomBrowserManager {
    
    static func createChromeUserAgent() -> OIDExternalUserAgent? {
        // Chrome custom browser (if Chrome is installed)
        return OIDExternalUserAgentIOSCustomBrowser.customBrowserChrome()
    }
    
    static func createFirefoxUserAgent() -> OIDExternalUserAgent? {
        // Firefox custom browser (if Firefox is installed)
        return OIDExternalUserAgentIOSCustomBrowser.customBrowserFirefox()
    }
    
    static func createSafariUserAgent() -> OIDExternalUserAgent {
        // Default Safari
        return OIDExternalUserAgentIOS(presenting: UIApplication.shared.windows.first?.rootViewController)!
    }
}

// EnterpriseOAuthManager.swift - Enterprise configuration
class EnterpriseOAuthManager: OAuthManager {
    
    override func startAuthentication(from viewController: UIViewController) {
        // Create service configuration with custom settings
        let configuration = createEnterpriseConfiguration()
        
        let request = OIDAuthorizationRequest(
            configuration: configuration,
            clientId: clientID,
            scopes: [OIDScopeOpenID, OIDScopeProfile, OIDScopeEmail, "custom:enterprise"],
            redirectURL: redirectURI,
            responseType: OIDResponseTypeCode,
            additionalParameters: [
                "prompt": "consent",
                "access_type": "offline",
                "include_granted_scopes": "true"
            ]
        )
        
        // Use custom browser
        let customUserAgent = CustomBrowserManager.createChromeUserAgent() ?? 
                             CustomBrowserManager.createSafariUserAgent()
        
        // Get AppDelegate reference
        guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
            self.errorMessage = "Could not get app delegate"
            return
        }
        
        // Execute authorization flow with custom user agent
        appDelegate.currentAuthorizationFlow = OIDAuthState.authState(
            byPresenting: request,
            externalUserAgent: customUserAgent
        ) { [weak self] authState, error in
            DispatchQueue.main.async {
                guard let self = self else { return }
                
                if let authState = authState {
                    print("Enterprise authorization success!")
                    self.setAuthState(authState)
                } else {
                    print("Enterprise authorization error: \(error?.localizedDescription ?? "Unknown error")")
                    self.errorMessage = error?.localizedDescription ?? "Unknown error"
                    self.setAuthState(nil)
                }
            }
        }
    }
    
    private func createEnterpriseConfiguration() -> OIDServiceConfiguration {
        // Enterprise endpoint configuration
        let authorizationEndpoint = URL(string: "https://enterprise.example.com/oauth2/authorize")!
        let tokenEndpoint = URL(string: "https://enterprise.example.com/oauth2/token")!
        let registrationEndpoint = URL(string: "https://enterprise.example.com/oauth2/register")!
        let endSessionEndpoint = URL(string: "https://enterprise.example.com/oauth2/logout")!
        
        return OIDServiceConfiguration(
            authorizationEndpoint: authorizationEndpoint,
            tokenEndpoint: tokenEndpoint,
            registrationEndpoint: registrationEndpoint,
            endSessionEndpoint: endSessionEndpoint
        )
    }
}