authentication

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Authentication

认证

Implement authentication flows on iOS using the AuthenticationServices framework, including Sign in with Apple, OAuth/third-party web auth, Password AutoFill, and biometric authentication.
使用AuthenticationServices框架在iOS上实现认证流程,包括使用Apple登录、OAuth/第三方网页认证、密码自动填充和生物识别认证。

Contents

目录

Sign in with Apple

使用Apple登录

Add the "Sign in with Apple" capability in Xcode before using these APIs.
在使用这些API之前,需要在Xcode中添加“使用Apple登录”功能权限。

UIKit: ASAuthorizationController Setup

UIKit: ASAuthorizationController 配置

swift
import AuthenticationServices

final class LoginViewController: UIViewController {
    func startSignInWithApple() {
        let provider = ASAuthorizationAppleIDProvider()
        let request = provider.createRequest()
        request.requestedScopes = [.fullName, .email]

        let controller = ASAuthorizationController(authorizationRequests: [request])
        controller.delegate = self
        controller.presentationContextProvider = self
        controller.performRequests()
    }
}

extension LoginViewController: ASAuthorizationControllerPresentationContextProviding {
    func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
        view.window!
    }
}
swift
import AuthenticationServices

final class LoginViewController: UIViewController {
    func startSignInWithApple() {
        let provider = ASAuthorizationAppleIDProvider()
        let request = provider.createRequest()
        request.requestedScopes = [.fullName, .email]

        let controller = ASAuthorizationController(authorizationRequests: [request])
        controller.delegate = self
        controller.presentationContextProvider = self
        controller.performRequests()
    }
}

extension LoginViewController: ASAuthorizationControllerPresentationContextProviding {
    func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
        view.window!
    }
}

Delegate: Handling Success and Failure

代理:处理成功与失败

swift
extension LoginViewController: ASAuthorizationControllerDelegate {
    func authorizationController(
        controller: ASAuthorizationController,
        didCompleteWithAuthorization authorization: ASAuthorization
    ) {
        guard let credential = authorization.credential
            as? ASAuthorizationAppleIDCredential else { return }

        let userID = credential.user  // Stable, unique, per-team identifier
        let email = credential.email  // nil after first authorization
        let fullName = credential.fullName  // nil after first authorization
        let identityToken = credential.identityToken  // JWT for server validation
        let authCode = credential.authorizationCode  // Short-lived code for server exchange

        // Save userID to Keychain for credential state checks
        // See references/keychain-biometric.md for Keychain patterns
        saveUserID(userID)

        // Send identityToken and authCode to your server
        authenticateWithServer(identityToken: identityToken, authCode: authCode)
    }

    func authorizationController(
        controller: ASAuthorizationController,
        didCompleteWithError error: any Error
    ) {
        let authError = error as? ASAuthorizationError
        switch authError?.code {
        case .canceled:
            break  // User dismissed
        case .failed:
            showError("Authorization failed")
        case .invalidResponse:
            showError("Invalid response")
        case .notHandled:
            showError("Not handled")
        case .notInteractive:
            break  // Non-interactive request failed -- expected for silent checks
        default:
            showError("Unknown error")
        }
    }
}
swift
extension LoginViewController: ASAuthorizationControllerDelegate {
    func authorizationController(
        controller: ASAuthorizationController,
        didCompleteWithAuthorization authorization: ASAuthorization
    ) {
        guard let credential = authorization.credential
            as? ASAuthorizationAppleIDCredential else { return }

        let userID = credential.user  // Stable, unique, per-team identifier
        let email = credential.email  // nil after first authorization
        let fullName = credential.fullName  // nil after first authorization
        let identityToken = credential.identityToken  // JWT for server validation
        let authCode = credential.authorizationCode  // Short-lived code for server exchange

        // Save userID to Keychain for credential state checks
        // See references/keychain-biometric.md for Keychain patterns
        saveUserID(userID)

        // Send identityToken and authCode to your server
        authenticateWithServer(identityToken: identityToken, authCode: authCode)
    }

    func authorizationController(
        controller: ASAuthorizationController,
        didCompleteWithError error: any Error
    ) {
        let authError = error as? ASAuthorizationError
        switch authError?.code {
        case .canceled:
            break  // User dismissed
        case .failed:
            showError("Authorization failed")
        case .invalidResponse:
            showError("Invalid response")
        case .notHandled:
            showError("Not handled")
        case .notInteractive:
            break  // Non-interactive request failed -- expected for silent checks
        default:
            showError("Unknown error")
        }
    }
}

Credential Handling

凭证处理

ASAuthorizationAppleIDCredential
properties and their behavior:
PropertyTypeFirst AuthSubsequent Auth
user
String
AlwaysAlways
email
String?
Provided if requested
nil
fullName
PersonNameComponents?
Provided if requested
nil
identityToken
Data?
JWT (Base64)JWT (Base64)
authorizationCode
Data?
Short-lived codeShort-lived code
realUserStatus
ASUserDetectionStatus
.likelyReal
/
.unknown
.unknown
Critical:
email
and
fullName
are provided ONLY on the first authorization. Cache them immediately during the initial sign-up flow. If the user later deletes and re-adds the app, these values will not be returned.
swift
func handleCredential(_ credential: ASAuthorizationAppleIDCredential) {
    // Always persist the user identifier
    let userID = credential.user

    // Cache name and email IMMEDIATELY -- only available on first auth
    if let fullName = credential.fullName {
        let name = PersonNameComponentsFormatter().string(from: fullName)
        UserProfile.saveName(name)  // Persist to your backend
    }
    if let email = credential.email {
        UserProfile.saveEmail(email)  // Persist to your backend
    }
}
ASAuthorizationAppleIDCredential
的属性及其行为:
属性类型首次认证后续认证
user
String
始终返回始终返回
email
String?
若请求则提供
nil
fullName
PersonNameComponents?
若请求则提供
nil
identityToken
Data?
JWT(Base64编码)JWT(Base64编码)
authorizationCode
Data?
短期有效代码短期有效代码
realUserStatus
ASUserDetectionStatus
.likelyReal
/
.unknown
.unknown
关键注意事项
email
fullName
仅在首次认证时提供。请在初始注册流程中立即缓存这些值。如果用户后续删除并重新安装应用,将不会返回这些值。
swift
func handleCredential(_ credential: ASAuthorizationAppleIDCredential) {
    // Always persist the user identifier
    let userID = credential.user

    // Cache name and email IMMEDIATELY -- only available on first auth
    if let fullName = credential.fullName {
        let name = PersonNameComponentsFormatter().string(from: fullName)
        UserProfile.saveName(name)  // Persist to your backend
    }
    if let email = credential.email {
        UserProfile.saveEmail(email)  // Persist to your backend
    }
}

Credential State Checking

凭证状态检查

Check credential state on every app launch. The user may revoke access at any time via Settings > Apple Account > Sign-In & Security.
swift
func checkCredentialState() async {
    let provider = ASAuthorizationAppleIDProvider()
    guard let userID = loadSavedUserID() else {
        showLoginScreen()
        return
    }

    do {
        let state = try await provider.credentialState(forUserID: userID)
        switch state {
        case .authorized:
            proceedToMainApp()
        case .revoked:
            // User revoked -- sign out and clear local data
            signOut()
            showLoginScreen()
        case .notFound:
            showLoginScreen()
        case .transferred:
            // App transferred to new team -- migrate user identifier
            migrateUser()
        @unknown default:
            showLoginScreen()
        }
    } catch {
        // Network error -- allow offline access or retry
        proceedToMainApp()
    }
}
每次应用启动时检查凭证状态。用户可能随时通过“设置 > Apple账户 > 登录与安全”撤销访问权限。
swift
func checkCredentialState() async {
    let provider = ASAuthorizationAppleIDProvider()
    guard let userID = loadSavedUserID() else {
        showLoginScreen()
        return
    }

    do {
        let state = try await provider.credentialState(forUserID: userID)
        switch state {
        case .authorized:
            proceedToMainApp()
        case .revoked:
            // User revoked -- sign out and clear local data
            signOut()
            showLoginScreen()
        case .notFound:
            showLoginScreen()
        case .transferred:
            // App transferred to new team -- migrate user identifier
            migrateUser()
        @unknown default:
            showLoginScreen()
        }
    } catch {
        // Network error -- allow offline access or retry
        proceedToMainApp()
    }
}

Credential Revocation Notification

凭证撤销通知

swift
NotificationCenter.default.addObserver(
    forName: ASAuthorizationAppleIDProvider.credentialRevokedNotification,
    object: nil,
    queue: .main
) { _ in
    // Sign out immediately
    AuthManager.shared.signOut()
}
swift
NotificationCenter.default.addObserver(
    forName: ASAuthorizationAppleIDProvider.credentialRevokedNotification,
    object: nil,
    queue: .main
) { _ in
    // Sign out immediately
    AuthManager.shared.signOut()
}

Token Validation

令牌验证

The
identityToken
is a JWT. Send it to your server for validation -- never trust it client-side alone.
swift
func sendTokenToServer(credential: ASAuthorizationAppleIDCredential) async throws {
    guard let tokenData = credential.identityToken,
          let token = String(data: tokenData, encoding: .utf8),
          let authCodeData = credential.authorizationCode,
          let authCode = String(data: authCodeData, encoding: .utf8) else {
        throw AuthError.missingToken
    }

    var request = URLRequest(url: URL(string: "https://api.example.com/auth/apple")!)
    request.httpMethod = "POST"
    request.setValue("application/json", forHTTPHeaderField: "Content-Type")
    request.httpBody = try JSONEncoder().encode(
        ["identityToken": token, "authorizationCode": authCode]
    )

    let (data, response) = try await URLSession.shared.data(for: request)
    guard (response as? HTTPURLResponse)?.statusCode == 200 else {
        throw AuthError.serverValidationFailed
    }
    let session = try JSONDecoder().decode(SessionResponse.self, from: data)
    // Store session token in Keychain -- see references/keychain-biometric.md
    try KeychainHelper.save(session.accessToken, forKey: "accessToken")
}
Server-side, validate the JWT against Apple's public keys at
https://appleid.apple.com/auth/keys
(JWKS). Verify:
iss
is
https://appleid.apple.com
,
aud
matches your bundle ID,
exp
not passed.
identityToken
是一个JWT。请将其发送到服务器进行验证——永远不要仅在客户端信任它。
swift
func sendTokenToServer(credential: ASAuthorizationAppleIDCredential) async throws {
    guard let tokenData = credential.identityToken,
          let token = String(data: tokenData, encoding: .utf8),
          let authCodeData = credential.authorizationCode,
          let authCode = String(data: authCodeData, encoding: .utf8) else {
        throw AuthError.missingToken
    }

    var request = URLRequest(url: URL(string: "https://api.example.com/auth/apple")!)
    request.httpMethod = "POST"
    request.setValue("application/json", forHTTPHeaderField: "Content-Type")
    request.httpBody = try JSONEncoder().encode(
        ["identityToken": token, "authorizationCode": authCode]
    )

    let (data, response) = try await URLSession.shared.data(for: request)
    guard (response as? HTTPURLResponse)?.statusCode == 200 else {
        throw AuthError.serverValidationFailed
    }
    let session = try JSONDecoder().decode(SessionResponse.self, from: data)
    // Store session token in Keychain -- see references/keychain-biometric.md
    try KeychainHelper.save(session.accessToken, forKey: "accessToken")
}
在服务器端,请根据Apple在
https://appleid.apple.com/auth/keys
(JWKS)提供的公钥验证JWT。需验证:
iss
https://appleid.apple.com
aud
与您的Bundle ID匹配,
exp
未过期。

Existing Account Setup Flows

现有账户设置流程

On launch, silently check for existing Sign in with Apple and password credentials before showing a login screen:
swift
func performExistingAccountSetupFlows() {
    let appleIDRequest = ASAuthorizationAppleIDProvider().createRequest()
    let passwordRequest = ASAuthorizationPasswordProvider().createRequest()

    let controller = ASAuthorizationController(
        authorizationRequests: [appleIDRequest, passwordRequest]
    )
    controller.delegate = self
    controller.presentationContextProvider = self
    controller.performRequests(
        options: .preferImmediatelyAvailableCredentials
    )
}
Call this in
viewDidAppear
or on app launch. If no existing credentials are found, the delegate receives a
.notInteractive
error -- handle it silently and show your normal login UI.
启动应用时,在显示登录界面之前,先静默检查是否存在已有的Apple登录和密码凭证:
swift
func performExistingAccountSetupFlows() {
    let appleIDRequest = ASAuthorizationAppleIDProvider().createRequest()
    let passwordRequest = ASAuthorizationPasswordProvider().createRequest()

    let controller = ASAuthorizationController(
        authorizationRequests: [appleIDRequest, passwordRequest]
    )
    controller.delegate = self
    controller.presentationContextProvider = self
    controller.performRequests(
        options: .preferImmediatelyAvailableCredentials
    )
}
viewDidAppear
或应用启动时调用此方法。如果未找到现有凭证,代理会收到
.notInteractive
错误——请静默处理并显示常规登录界面。

ASWebAuthenticationSession (OAuth)

ASWebAuthenticationSession(OAuth)

Use
ASWebAuthenticationSession
for OAuth and third-party authentication (Google, GitHub, etc.). Never use
WKWebView
for auth flows.
swift
import AuthenticationServices

final class OAuthController: NSObject, ASWebAuthenticationPresentationContextProviding {
    func startOAuthFlow() {
        let authURL = URL(string:
            "https://provider.com/oauth/authorize?client_id=YOUR_ID&redirect_uri=myapp://callback&response_type=code"
        )!
        let session = ASWebAuthenticationSession(
            url: authURL, callback: .customScheme("myapp")
        ) { callbackURL, error in
            guard let callbackURL, error == nil,
                  let code = URLComponents(url: callbackURL, resolvingAgainstBaseURL: false)?
                      .queryItems?.first(where: { $0.name == "code" })?.value else { return }
            Task { await self.exchangeCodeForTokens(code) }
        }
        session.presentationContextProvider = self
        session.prefersEphemeralWebBrowserSession = true  // No shared cookies
        session.start()
    }

    func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
        ASPresentationAnchor()
    }
}
使用
ASWebAuthenticationSession
处理OAuth和第三方认证(如Google、GitHub等)。永远不要使用
WKWebView
处理认证流程。
swift
import AuthenticationServices

final class OAuthController: NSObject, ASWebAuthenticationPresentationContextProviding {
    func startOAuthFlow() {
        let authURL = URL(string:
            "https://provider.com/oauth/authorize?client_id=YOUR_ID&redirect_uri=myapp://callback&response_type=code"
        )!
        let session = ASWebAuthenticationSession(
            url: authURL, callback: .customScheme("myapp")
        ) { callbackURL, error in
            guard let callbackURL, error == nil,
                  let code = URLComponents(url: callbackURL, resolvingAgainstBaseURL: false)?
                      .queryItems?.first(where: { $0.name == "code" })?.value else { return }
            Task { await self.exchangeCodeForTokens(code) }
        }
        session.presentationContextProvider = self
        session.prefersEphemeralWebBrowserSession = true  // No shared cookies
        session.start()
    }

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

SwiftUI WebAuthenticationSession

SwiftUI WebAuthenticationSession

swift
struct OAuthLoginView: View {
    @Environment(\.webAuthenticationSession) private var webAuthSession

    var body: some View {
        Button("Sign in with Provider") {
            Task {
                let url = URL(string: "https://provider.com/oauth/authorize?client_id=YOUR_ID")!
                let callbackURL = try await webAuthSession.authenticate(
                    using: url, callback: .customScheme("myapp")
                )
                // Extract authorization code from callbackURL
            }
        }
    }
}
Callback types:
.customScheme("myapp")
for URL scheme redirects;
.https(host:path:)
for universal link redirects (preferred).
swift
struct OAuthLoginView: View {
    @Environment(\.webAuthenticationSession) private var webAuthSession

    var body: some View {
        Button("Sign in with Provider") {
            Task {
                let url = URL(string: "https://provider.com/oauth/authorize?client_id=YOUR_ID")!
                let callbackURL = try await webAuthSession.authenticate(
                    using: url, callback: .customScheme("myapp")
                )
                // Extract authorization code from callbackURL
            }
        }
    }
}
回调类型:
.customScheme("myapp")
用于URL Scheme重定向;
.https(host:path:)
用于通用链接重定向(推荐)。

Password AutoFill Credentials

密码自动填充凭证

Use
ASAuthorizationPasswordProvider
to offer saved keychain credentials alongside Sign in with Apple:
swift
func performSignIn() {
    let appleIDRequest = ASAuthorizationAppleIDProvider().createRequest()
    appleIDRequest.requestedScopes = [.fullName, .email]

    let passwordRequest = ASAuthorizationPasswordProvider().createRequest()

    let controller = ASAuthorizationController(
        authorizationRequests: [appleIDRequest, passwordRequest]
    )
    controller.delegate = self
    controller.presentationContextProvider = self
    controller.performRequests()
}

// In delegate:
func authorizationController(
    controller: ASAuthorizationController,
    didCompleteWithAuthorization authorization: ASAuthorization
) {
    switch authorization.credential {
    case let appleIDCredential as ASAuthorizationAppleIDCredential:
        handleAppleIDLogin(appleIDCredential)
    case let passwordCredential as ASPasswordCredential:
        // User selected a saved password from keychain
        signInWithPassword(
            username: passwordCredential.user,
            password: passwordCredential.password
        )
    default:
        break
    }
}
Set
textContentType
on text fields for AutoFill to work:
swift
usernameField.textContentType = .username
passwordField.textContentType = .password
使用
ASAuthorizationPasswordProvider
在Apple登录选项旁提供已保存的钥匙串凭证:
swift
func performSignIn() {
    let appleIDRequest = ASAuthorizationAppleIDProvider().createRequest()
    appleIDRequest.requestedScopes = [.fullName, .email]

    let passwordRequest = ASAuthorizationPasswordProvider().createRequest()

    let controller = ASAuthorizationController(
        authorizationRequests: [appleIDRequest, passwordRequest]
    )
    controller.delegate = self
    controller.presentationContextProvider = self
    controller.performRequests()
}

// In delegate:
func authorizationController(
    controller: ASAuthorizationController,
    didCompleteWithAuthorization authorization: ASAuthorization
) {
    switch authorization.credential {
    case let appleIDCredential as ASAuthorizationAppleIDCredential:
        handleAppleIDLogin(appleIDCredential)
    case let passwordCredential as ASPasswordCredential:
        // User selected a saved password from keychain
        signInWithPassword(
            username: passwordCredential.user,
            password: passwordCredential.password
        )
    default:
        break
    }
}
为文本框设置
textContentType
以启用自动填充:
swift
usernameField.textContentType = .username
passwordField.textContentType = .password
##生物识别认证
使用LocalAuthentication框架中的
LAContext
实现Face ID / Touch ID作为登录或重新认证机制。如需使用生物识别访问控制(
SecAccessControl
,
.biometryCurrentSet
)保护钥匙串项目,请参考
ios-security
技能。
swift
import LocalAuthentication

func authenticateWithBiometrics() async throws -> Bool {
    let context = LAContext()
    var error: NSError?

    guard context.canEvaluatePolicy(
        .deviceOwnerAuthenticationWithBiometrics, error: &error
    ) else {
        throw AuthError.biometricsUnavailable
    }

    return try await context.evaluatePolicy(
        .deviceOwnerAuthenticationWithBiometrics,
        localizedReason: "Sign in to your account"
    )
}
必填项:在Info.plist中添加
NSFaceIDUsageDescription
。缺少此键会导致Face ID设备上应用崩溃。

Biometric Authentication

SwiftUI SignInWithAppleButton

Use
LAContext
from LocalAuthentication for Face ID / Touch ID as a sign-in or re-authentication mechanism. For protecting Keychain items with biometric access control (
SecAccessControl
,
.biometryCurrentSet
), see the
ios-security
skill.
swift
import LocalAuthentication

func authenticateWithBiometrics() async throws -> Bool {
    let context = LAContext()
    var error: NSError?

    guard context.canEvaluatePolicy(
        .deviceOwnerAuthenticationWithBiometrics, error: &error
    ) else {
        throw AuthError.biometricsUnavailable
    }

    return try await context.evaluatePolicy(
        .deviceOwnerAuthenticationWithBiometrics,
        localizedReason: "Sign in to your account"
    )
}
Required: Add
NSFaceIDUsageDescription
to Info.plist. Missing this key crashes on Face ID devices.
swift
import AuthenticationServices

struct AppleSignInView: View {
    @Environment(\.colorScheme) var colorScheme

    var body: some View {
        SignInWithAppleButton(.signIn) { request in
            request.requestedScopes = [.fullName, .email]
        } onCompletion: { result in
            switch result {
            case .success(let authorization):
                guard let credential = authorization.credential
                    as? ASAuthorizationAppleIDCredential else { return }
                handleCredential(credential)
            case .failure(let error):
                handleError(error)
            }
        }
        .signInWithAppleButtonStyle(
            colorScheme == .dark ? .white : .black
        )
        .frame(height: 50)
    }
}

SwiftUI SignInWithAppleButton

常见错误

1. 未在应用启动时检查凭证状态

swift
import AuthenticationServices

struct AppleSignInView: View {
    @Environment(\.colorScheme) var colorScheme

    var body: some View {
        SignInWithAppleButton(.signIn) { request in
            request.requestedScopes = [.fullName, .email]
        } onCompletion: { result in
            switch result {
            case .success(let authorization):
                guard let credential = authorization.credential
                    as? ASAuthorizationAppleIDCredential else { return }
                handleCredential(credential)
            case .failure(let error):
                handleError(error)
            }
        }
        .signInWithAppleButtonStyle(
            colorScheme == .dark ? .white : .black
        )
        .frame(height: 50)
    }
}
swift
// DON'T: Assume the user is still authorized
func appDidLaunch() {
    if UserDefaults.standard.bool(forKey: "isLoggedIn") {
        showMainApp()  // User may have revoked access!
    }
}

// DO: Check credential state every launch
func appDidLaunch() async {
    await checkCredentialState()  // See "Credential State Checking" above
}

Common Mistakes

2. 未执行现有账户设置流程

1. Not checking credential state on app launch

swift
// DON'T: Assume the user is still authorized
func appDidLaunch() {
    if UserDefaults.standard.bool(forKey: "isLoggedIn") {
        showMainApp()  // User may have revoked access!
    }
}

// DO: Check credential state every launch
func appDidLaunch() async {
    await checkCredentialState()  // See "Credential State Checking" above
}
swift
// DON'T: Always show a full login screen on launch
// DO: Call performExistingAccountSetupFlows() first;
//     show login UI only if .notInteractive error received

2. Not performing existing account setup flows

3. 假设邮箱/姓名始终会被提供

swift
// DON'T: Always show a full login screen on launch
// DO: Call performExistingAccountSetupFlows() first;
//     show login UI only if .notInteractive error received
swift
// DON'T: Force-unwrap email or fullName
let email = credential.email!  // Crashes on subsequent logins

// DO: Handle nil gracefully -- only available on first authorization
if let email = credential.email {
    saveEmail(email)  // Persist immediately
}

3. Assuming email/name are always provided

4. 未实现ASAuthorizationControllerPresentationContextProviding

swift
// DON'T: Force-unwrap email or fullName
let email = credential.email!  // Crashes on subsequent logins

// DO: Handle nil gracefully -- only available on first authorization
if let email = credential.email {
    saveEmail(email)  // Persist immediately
}
swift
// DON'T: Skip the presentation context provider
controller.delegate = self
controller.performRequests()  // May not display UI correctly

// DO: Always set the presentation context provider
controller.delegate = self
controller.presentationContextProvider = self  // Required for proper UI
controller.performRequests()

4. Not implementing ASAuthorizationControllerPresentationContextProviding

5. 将identityToken存储在UserDefaults中

swift
// DON'T: Skip the presentation context provider
controller.delegate = self
controller.performRequests()  // May not display UI correctly

// DO: Always set the presentation context provider
controller.delegate = self
controller.presentationContextProvider = self  // Required for proper UI
controller.performRequests()
swift
// DON'T: Store tokens in UserDefaults
UserDefaults.standard.set(tokenString, forKey: "identityToken")

// DO: Store in Keychain
// See references/keychain-biometric.md for Keychain patterns
try KeychainHelper.save(tokenData, forKey: "identityToken")

5. Storing identityToken in UserDefaults

审核清单

swift
// DON'T: Store tokens in UserDefaults
UserDefaults.standard.set(tokenString, forKey: "identityToken")

// DO: Store in Keychain
// See references/keychain-biometric.md for Keychain patterns
try KeychainHelper.save(tokenData, forKey: "identityToken")
  • 已在Xcode项目中添加“使用Apple登录”功能权限
  • 已实现
    ASAuthorizationControllerPresentationContextProviding
  • 每次应用启动时检查凭证状态(
    credentialState(forUserID:)
  • 已注册
    credentialRevokedNotification
    观察者,并处理登出逻辑
  • 已在首次认证时缓存
    email
    fullName
    (不假设后续会提供)
  • 已将
    identityToken
    发送到服务器验证,并非仅在客户端信任它
  • 令牌存储在钥匙串中,而非UserDefaults或文件
  • 在显示登录界面前调用了
    performExistingAccountSetupFlows
  • 已处理错误场景:
    .canceled
    .failed
    .notInteractive
  • 生物识别认证的Info.plist中已添加
    NSFaceIDUsageDescription
  • 使用
    ASWebAuthenticationSession
    处理OAuth(而非
    WKWebView
  • 已为OAuth设置
    prefersEphemeralWebBrowserSession
    (如适用)
  • 已为用户名/密码文本框设置
    textContentType
    以启用自动填充

Review Checklist

参考资料

  • "Sign in with Apple" capability added in Xcode project
  • ASAuthorizationControllerPresentationContextProviding
    implemented
  • Credential state checked on every app launch (
    credentialState(forUserID:)
    )
  • credentialRevokedNotification
    observer registered; sign-out handled
  • email
    and
    fullName
    cached on first authorization (not assumed available later)
  • identityToken
    sent to server for validation, not trusted client-side only
  • Tokens stored in Keychain, not UserDefaults or files
  • performExistingAccountSetupFlows
    called before showing login UI
  • Error cases handled:
    .canceled
    ,
    .failed
    ,
    .notInteractive
  • NSFaceIDUsageDescription
    in Info.plist for biometric auth
  • ASWebAuthenticationSession
    used for OAuth (not
    WKWebView
    )
  • prefersEphemeralWebBrowserSession
    set for OAuth when appropriate
  • textContentType
    set on username/password fields for AutoFill

References