From apple-kit-skills
Implements iOS authentication patterns: Sign in with Apple, credential state checking, OAuth via ASWebAuthenticationSession, Password AutoFill, biometrics with LAContext. For Apple ID login, token validation, secure flows.
npx claudepluginhub dpearson2699/swift-ios-skills --plugin all-ios-skillsThis skill uses the workspace's default tool permissions.
Implement authentication flows on iOS using the AuthenticationServices
Generates design tokens/docs from CSS/Tailwind/styled-components codebases, audits visual consistency across 10 dimensions, detects AI slop in UI.
Records polished WebM UI demo videos of web apps using Playwright with cursor overlay, natural pacing, and three-phase scripting. Activates for demo, walkthrough, screen recording, or tutorial requests.
Delivers idiomatic Kotlin patterns for null safety, immutability, sealed classes, coroutines, Flows, extensions, DSL builders, and Gradle DSL. Use when writing, reviewing, refactoring, or designing Kotlin code.
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? | Provided if requested | nil |
identityToken | Data? | JWT (Base64) | JWT (Base64) |
authorizationCode | Data? | Short-lived code | Short-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.
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 swift-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, .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