authentication by dpearson2699/swift-ios-skills
npx skills add https://github.com/dpearson2699/swift-ios-skills --skill authentication使用 AuthenticationServices 框架在 iOS 上实现身份验证流程,包括“通过 Apple 登录”、OAuth/第三方网页认证、密码自动填充和生物识别认证。
在使用这些 API 之前,请在 Xcode 中添加“通过 Apple 登录”功能。
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!
}
}
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
extension LoginViewController: ASAuthorizationControllerDelegate {
func authorizationController(
controller: ASAuthorizationController,
didCompleteWithAuthorization authorization: ASAuthorization
) {
guard let credential = authorization.credential
as? ASAuthorizationAppleIDCredential else { return }
let userID = credential.user // 稳定的、唯一的、按团队划分的标识符
let email = credential.email // 首次授权后为 nil
let fullName = credential.fullName // 首次授权后为 nil
let identityToken = credential.identityToken // 用于服务器验证的 JWT
let authCode = credential.authorizationCode // 用于服务器交换的短期代码
// 将 userID 保存到 Keychain 以进行凭证状态检查
// Keychain 模式请参阅 references/keychain-biometric.md
saveUserID(userID)
// 将 identityToken 和 authCode 发送到您的服务器
authenticateWithServer(identityToken: identityToken, authCode: authCode)
}
func authorizationController(
controller: ASAuthorizationController,
didCompleteWithError error: any Error
) {
let authError = error as? ASAuthorizationError
switch authError?.code {
case .canceled:
break // 用户取消
case .failed:
showError("授权失败")
case .invalidResponse:
showError("无效响应")
case .notHandled:
showError("未处理")
case .notInteractive:
break // 非交互式请求失败 —— 静默检查时预期如此
default:
showError("未知错误")
}
}
}
ASAuthorizationAppleIDCredential 的属性及其行为:
| 属性 | 类型 | 首次授权 | 后续授权 |
|---|---|---|---|
user | String | 始终提供 | 始终提供 |
email | String? | 如果请求则提供 | nil |
fullName | PersonNameComponents? | 如果请求则提供 | nil |
identityToken | Data? | JWT (Base64) | JWT (Base64) |
authorizationCode | Data? | 短期代码 | 短期代码 |
realUserStatus | ASUserDetectionStatus | .likelyReal / .unknown | .unknown |
关键点: email 和 fullName 仅在首次授权时提供。请在初始注册流程中立即缓存它们。如果用户稍后删除并重新添加应用,这些值将不会被返回。
func handleCredential(_ credential: ASAuthorizationAppleIDCredential) {
// 始终持久化用户标识符
let userID = credential.user
// 立即缓存姓名和邮箱 —— 仅在首次授权时可用
if let fullName = credential.fullName {
let name = PersonNameComponentsFormatter().string(from: fullName)
UserProfile.saveName(name) // 持久化到您的后端
}
if let email = credential.email {
UserProfile.saveEmail(email) // 持久化到您的后端
}
}
在每次应用启动时检查凭证状态。用户可能随时通过“设置”>“Apple 账户”>“登录与安全”撤销访问权限。
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:
// 用户已撤销 —— 登出并清除本地数据
signOut()
showLoginScreen()
case .notFound:
showLoginScreen()
case .transferred:
// 应用转移到新团队 —— 迁移用户标识符
migrateUser()
@unknown default:
showLoginScreen()
}
} catch {
// 网络错误 —— 允许离线访问或重试
proceedToMainApp()
}
}
NotificationCenter.default.addObserver(
forName: ASAuthorizationAppleIDProvider.credentialRevokedNotification,
object: nil,
queue: .main
) { _ in
// 立即登出
AuthManager.shared.signOut()
}
identityToken 是一个 JWT。将其发送到您的服务器进行验证 —— 切勿仅信任客户端。
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)
// 将会话令牌存储在 Keychain 中 —— 请参阅 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 未过期。
在启动时,显示登录屏幕之前,静默检查是否存在现有的“通过 Apple 登录”和密码凭证:
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 和第三方身份验证(Google、GitHub 等)。切勿使用 WKWebView 进行身份验证流程。
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 // 无共享 cookie
session.start()
}
func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
ASPresentationAnchor()
}
}
struct OAuthLoginView: View {
@Environment(\.webAuthenticationSession) private var webAuthSession
var body: some View {
Button("使用提供商登录") {
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")
)
// 从 callbackURL 中提取授权码
}
}
}
}
回调类型:.customScheme("myapp") 用于 URL 方案重定向;.https(host:path:) 用于通用链接重定向(推荐)。
使用 ASAuthorizationPasswordProvider 在“通过 Apple 登录”旁边提供已保存的钥匙串凭证:
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()
}
// 在委托中:
func authorizationController(
controller: ASAuthorizationController,
didCompleteWithAuthorization authorization: ASAuthorization
) {
switch authorization.credential {
case let appleIDCredential as ASAuthorizationAppleIDCredential:
handleAppleIDLogin(appleIDCredential)
case let passwordCredential as ASPasswordCredential:
// 用户从钥匙串中选择了一个已保存的密码
signInWithPassword(
username: passwordCredential.user,
password: passwordCredential.password
)
default:
break
}
}
在文本字段上设置 textContentType 以使自动填充生效:
usernameField.textContentType = .username
passwordField.textContentType = .password
使用 LocalAuthentication 中的 LAContext 将面容 ID / 触控 ID 作为登录或重新认证机制。关于使用生物识别访问控制(SecAccessControl、.biometryCurrentSet)保护钥匙串项,请参阅 ios-security 技能。
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: "登录到您的账户"
)
}
必需: 在 Info.plist 中添加 NSFaceIDUsageDescription。缺少此键会在面容 ID 设备上导致崩溃。
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)
}
}
// 错误做法:假设用户仍然授权
func appDidLaunch() {
if UserDefaults.standard.bool(forKey: "isLoggedIn") {
showMainApp() // 用户可能已撤销访问权限!
}
}
// 正确做法:每次启动都检查凭证状态
func appDidLaunch() async {
await checkCredentialState() // 请参阅上面的“凭证状态检查”
}
// 错误做法:总是在启动时显示完整的登录界面
// 正确做法:首先调用 performExistingAccountSetupFlows();
// 仅在收到 .notInteractive 错误时才显示登录界面
// 错误做法:强制解包 email 或 fullName
let email = credential.email! // 在后续登录时崩溃
// 正确做法:优雅地处理 nil —— 仅在首次授权时可用
if let email = credential.email {
saveEmail(email) // 立即持久化
}
// 错误做法:跳过呈现上下文提供者
controller.delegate = self
controller.performRequests() // 可能无法正确显示界面
// 正确做法:始终设置呈现上下文提供者
controller.delegate = self
controller.presentationContextProvider = self // 正确显示界面所必需
controller.performRequests()
// 错误做法:将令牌存储在 UserDefaults 中
UserDefaults.standard.set(tokenString, forKey: "identityToken")
// 正确做法:存储在 Keychain 中
// Keychain 模式请参阅 references/keychain-biometric.md
try KeychainHelper.save(tokenData, forKey: "identityToken")
ASAuthorizationControllerPresentationContextProvidingcredentialState(forUserID:))credentialRevokedNotification 观察者;已处理登出email 和 fullName(不假设后续可用)identityToken 发送到服务器进行验证,而非仅信任客户端performExistingAccountSetupFlows.canceled、.failed、.notInteractiveNSFaceIDUsageDescription 用于生物识别认证ASWebAuthenticationSession 进行 OAuth(而非 WKWebView)prefersEphemeralWebBrowserSessiontextContentType 以启用自动填充references/keychain-biometric.md每周安装量
335
代码仓库
GitHub 星标数
269
首次出现
2026年3月8日
安全审计
安装于
codex331
opencode328
github-copilot328
amp328
cline328
kimi-cli328
Implement authentication flows on iOS using the AuthenticationServices framework, including Sign in with Apple, OAuth/third-party web auth, Password AutoFill, and biometric authentication.
Add the "Sign in with Apple" capability in Xcode before using these APIs.
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!
}
}
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")
}
}
}
ASAuthorizationAppleIDCredential properties and their behavior:
| Property | Type | First Auth | Subsequent Auth |
|---|---|---|---|
user | String | Always | Always |
email | String? | Provided if requested | nil |
fullName | PersonNameComponents? |
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.
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
}
}
Check credential state on every app launch. The user may revoke access at any time via Settings > Apple Account > Sign-In & Security.
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()
}
}
NotificationCenter.default.addObserver(
forName: ASAuthorizationAppleIDProvider.credentialRevokedNotification,
object: nil,
queue: .main
) { _ in
// Sign out immediately
AuthManager.shared.signOut()
}
The identityToken is a JWT. Send it to your server for validation -- never trust it client-side alone.
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.
On launch, silently check for existing Sign in with Apple and password credentials before showing a login screen:
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.
Use ASWebAuthenticationSession for OAuth and third-party authentication (Google, GitHub, etc.). Never use WKWebView for auth flows.
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()
}
}
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).
Use ASAuthorizationPasswordProvider to offer saved keychain credentials alongside Sign in with Apple:
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:
usernameField.textContentType = .username
passwordField.textContentType = .password
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.
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.
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)
}
}
// 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
}
// DON'T: Always show a full login screen on launch
// DO: Call performExistingAccountSetupFlows() first;
// show login UI only if .notInteractive error received
// 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
}
// 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()
// 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")
ASAuthorizationControllerPresentationContextProviding implementedcredentialState(forUserID:))credentialRevokedNotification observer registered; sign-out handledemail and fullName cached on first authorization (not assumed available later)identityToken sent to server for validation, not trusted client-side onlyperformExistingAccountSetupFlows called before showing login UI.canceled, .failed, references/keychain-biometric.mdWeekly Installs
335
Repository
GitHub Stars
269
First Seen
Mar 8, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
codex331
opencode328
github-copilot328
amp328
cline328
kimi-cli328
xdrop 文件传输脚本:Bun 环境下安全上传下载工具,支持加密分享
24,700 周安装
| Provided if requested |
nil |
identityToken | Data? | JWT (Base64) | JWT (Base64) |
authorizationCode | Data? | Short-lived code | Short-lived code |
realUserStatus | ASUserDetectionStatus | .likelyReal / .unknown | .unknown |
.notInteractiveNSFaceIDUsageDescription in Info.plist for biometric authASWebAuthenticationSession used for OAuth (not WKWebView)prefersEphemeralWebBrowserSession set for OAuth when appropriatetextContentType set on username/password fields for AutoFill