---
name: authentication
description: "iOS sign-in with AuthenticationServices and LocalAuthentication: Sign in with Apple, passkey (WebAuthn) sign-up and login via ASAuthorizationPlatformPublicKeyCredentialProvider, ASAuthorizationController credential state and revocation, ASWebAuthenticationSession OAuth, third-party login, Password AutoFill, server identity-token validation, LAContext biometric re-auth prompts and policy. Use when adding or reviewing account flows."
metadata:
  source: multi-agent-pipeline
---

# Authentication

This skill owns the account-facing side of identity on iOS: Sign in with
Apple, passkeys, Password AutoFill, `ASAuthorizationController`, presenting an
OAuth session with `ASWebAuthenticationSession`, credential state, and local
biometric re-authentication. Code samples assume iOS 26 with Swift 6.3.

Deeper material:

- [Passkeys](references/passkeys.md) - prerequisites, registration, sign-in, AutoFill, security keys, failure modes
- [Keychain and biometrics](references/keychain-biometric.md) - token storage, `LAContext`, access-controlled items

## Sign in with Apple

Before any code, add the Apple sign-in capability to the app target in Signing & Capabilities.

### Starting the request (UIKit)

```swift
import AuthenticationServices
import UIKit

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

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

extension WelcomeViewController: ASAuthorizationControllerPresentationContextProviding {
    func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
        guard let window = view.window else {
            preconditionFailure("Start Sign in with Apple only while this screen is on screen")
        }
        return window   // ASPresentationAnchor is UIWindow; UIWindow() is deprecated in iOS 26
    }
}
```

### Delegate callbacks

```swift
extension WelcomeViewController: ASAuthorizationControllerDelegate {
    func authorizationController(controller: ASAuthorizationController,
                                 didCompleteWithAuthorization authorization: ASAuthorization) {
        let credential = authorization.credential
        guard let apple = credential as? ASAuthorizationAppleIDCredential else { return }
        AccountVault.shared.rememberAppleUser(apple.user)          // stable per-team ID
        if let name = apple.fullName { ProfileDraft.shared.name = name }  // first time only
        if let mail = apple.email { ProfileDraft.shared.email = mail }    // first time only
        Task { try await SessionAPI.exchange(token: apple.identityToken, code: apple.authorizationCode) }
    }

    func authorizationController(controller: ASAuthorizationController,
                                 didCompleteWithError error: Error) {
        guard let authError = error as? ASAuthorizationError else { return }
        switch authError.code {
        case .canceled:        break                       // user closed the sheet
        case .notInteractive:  break                       // silent check found nothing
        case .failed, .invalidResponse, .notHandled:
            showSignInProblem(authError)
        default:
            showSignInProblem(authError)
        }
    }
}
```

`user` is a stable identifier scoped to your team; save it in the Keychain so
you can check credential state later. `identityToken` is a JWT for the server
and `authorizationCode` is a short-lived code the server exchanges; send both.

### What the credential contains

| Property | Type | Availability |
|---|---|---|
| `user` | `String` | Every time |
| `email` | `String?` | First authorization only, if requested |
| `fullName` | `PersonNameComponents?` | First authorization only, if requested |
| `identityToken` | `Data?` | Every time, a UTF-8 JWT |
| `authorizationCode` | `Data?` | Every time, short-lived |
| `realUserStatus` | `ASUserDetectionStatus` | Anti-fraud hint; do not count on it after the first attempt |

Capture name and email the moment they arrive and send them to your backend.
Apple never sends them again, and deleting and reinstalling the app does not
bring them back. Format a name with
`PersonNameComponentsFormatter().string(from:)` before storing it.

### Credential state

People can revoke the app at any time under Settings > Apple Account >
Sign-In & Security, so check on every launch:

```swift
@MainActor
func verifyAppleCredential(for userID: String) async {
    let provider = ASAuthorizationAppleIDProvider()
    let credentialState: ASAuthorizationAppleIDProvider.CredentialState
    do {
        credentialState = try await provider.credentialState(forUserID: userID)
    } catch {
        SessionStore.shared.presentLogin()
        return
    }
    switch credentialState {
    case .authorized:  SessionStore.shared.resume()
    case .revoked:     SessionStore.shared.signOutAndWipe()
    case .notFound:    SessionStore.shared.presentLogin()
    case .transferred: SessionStore.shared.migrateTransferredUser(userID)
    @unknown default:  SessionStore.shared.presentLogin()
    }
}
```

The function runs on the main actor and awaits the async form of
`getCredentialState(forUserID:completion:)`, so the session calls need no hop
back to the main queue.

`.transferred` means the app moved to a different developer team and the user
identifier must be migrated. Also subscribe to
`ASAuthorizationAppleIDProvider.credentialRevokedNotification` and sign the
user out as soon as it fires.

### Server validation

Never accept the identity token on the device alone. Hand both values to your
backend and keep only what it returns:

```swift
enum SessionAPI {
    struct Session: Decodable { let accessToken: String }
    private static let transport = URLSession(configuration: .ephemeral)

    static func exchange(token: Data?, code: Data?) async throws {
        guard let token, let code,
              let jwt = String(data: token, encoding: .utf8),
              let grant = String(data: code, encoding: .utf8) else { throw URLError(.badServerResponse) }

        guard let endpoint = URL(string: "https://api.tasknest.app/auth/apple") else { throw URLError(.badURL) }
        var call = URLRequest(url: endpoint)
        call.httpMethod = "POST"
        call.addValue("application/json", forHTTPHeaderField: "Content-Type")
        let body = try JSONEncoder().encode(["identity_token": jwt, "authorization_code": grant])
        let (bytes, reply) = try await transport.upload(for: call, from: body)
        let status = (reply as? HTTPURLResponse)?.statusCode ?? 0
        if status != 200 { throw URLError(.userAuthenticationRequired) }

        let session = try JSONDecoder().decode(Session.self, from: bytes)
        try TokenVault(service: "auth").store(Data(session.accessToken.utf8), account: "access")
    }
}
```

`TokenVault` is in the [Keychain reference](references/keychain-biometric.md). The
server verifies the JWT signature against Apple's published key set (the JWKS
document served from `appleid.apple.com` under `/auth/keys`) and checks that `iss` is
`https://appleid.apple.com`, `aud` is your bundle ID, and `exp` is in the future.

### Existing accounts first

Before showing a login screen, quietly ask the system whether the person
already has an Apple ID credential or a saved password for the app:

```swift
extension WelcomeViewController {
    func offerSavedCredentials() {
        let requests: [ASAuthorizationRequest] = [
            ASAuthorizationAppleIDProvider().createRequest(),
            ASAuthorizationPasswordProvider().createRequest(),
        ]
        let controller = ASAuthorizationController(authorizationRequests: requests)
        controller.delegate = self
        controller.presentationContextProvider = self
        controller.performRequests(options: .preferImmediatelyAvailableCredentials)
    }
}
```

Run it from `viewDidAppear` or during launch. When nothing is stored, the
delegate receives `.notInteractive`; treat that silently and show the usual
login.

### SwiftUI button

```swift
import AuthenticationServices
import SwiftUI

struct AppleSignInRow: View {
    let model: LoginModel

    var body: some View {
        SignInWithAppleButton(.continue) { request in
            request.requestedScopes = [.fullName, .email]
        } onCompletion: { outcome in
            switch outcome {
            case .success(let auth):
                if let apple = auth.credential as? ASAuthorizationAppleIDCredential {
                    model.completeAppleSignIn(apple)   // same server validation as UIKit
                }
            case .failure(let error):
                model.report(error)
            }
        }
        .signInWithAppleButtonStyle(.whiteOutline)
        .frame(height: 48)
    }
}
```

The button lives in AuthenticationServices but needs SwiftUI imported in the
same file.

## Passkeys

Passkeys replace passwords with WebAuthn credentials for both account creation and login. They need
the Associated Domains entitlement with a `webcredentials:` entry for the
relying-party domain; requests for a domain that is not associated fail.
Platform passkeys sync through iCloud Keychain.

- Create `ASAuthorizationPlatformPublicKeyCredentialProvider(relyingPartyIdentifier:)`.
- Register with `createCredentialRegistrationRequest(challenge:name:userID:)` using a
  challenge and user ID `Data` from the server.
- Sign in with `createCredentialAssertionRequest(challenge:)` and a fresh server challenge.
- Results arrive as `ASAuthorizationPlatformPublicKeyCredentialRegistration` or
  `ASAuthorizationPlatformPublicKeyCredentialAssertion`; the relying-party server verifies them.
- For inline suggestions, mark the username field `.username`, include an
  assertion request, and call `performAutoFillAssistedRequests()`.
- Reach for `ASAuthorizationSecurityKeyPublicKeyCredentialProvider` only when a
  physical security key is required.

Details and code: [passkeys reference](references/passkeys.md).

## OAuth with `ASWebAuthenticationSession`

Third-party logins (Google, GitHub and similar) go through
`ASWebAuthenticationSession`, never an embedded `WKWebView`.

```swift
final class GitHubLogin: NSObject, ASWebAuthenticationPresentationContextProviding {
    private var session: ASWebAuthenticationSession?

    func start(authorizeURL: URL) {
        let session = ASWebAuthenticationSession(url: authorizeURL,
                                                 callback: .customScheme("tasknest")) { url, error in
            guard error == nil, let url,
                  let items = URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems,
                  let code = items.first(where: { $0.name == "code" })?.value else { return }
            Task { try await OAuthBackend.redeem(code: code) }
        }
        session.presentationContextProvider = self
        session.prefersEphemeralWebBrowserSession = true   // no shared browser cookies
        session.start()
        self.session = session
    }

    func presentationAnchor(for webSession: ASWebAuthenticationSession) -> ASPresentationAnchor {
        let scenes = UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }
        if let key = scenes.compactMap(\.keyWindow).first { return key }
        guard let scene = scenes.first else { preconditionFailure("No window scene to present from") }
        return ASPresentationAnchor(windowScene: scene)
    }
}
```

In SwiftUI, read `@Environment(\.webAuthenticationSession)` and call
`authenticate(using:callback:preferredBrowserSession:additionalHeaderFields:)`
with `.customScheme(...)` or `.https(host:path:)`. Ask for the `.ephemeral`
browser session only when the provider's flow should not share cookies.

## Password AutoFill

`ASAuthorizationPasswordProvider` puts saved Keychain passwords in the same
controller as Sign in with Apple. The delegate then branches on the credential:

```swift
switch authorization.credential {
case let apple as ASAuthorizationAppleIDCredential:
    finishApple(apple)
case let saved as ASPasswordCredential:
    signIn(username: saved.user, password: saved.password)
default:
    break
}
```

AutoFill only lights up when the text fields declare `textContentType`
`.username` and `.password`.

## Biometric re-authentication

Use `LAContext` to confirm the device owner before opening account settings or
a sensitive action. The `Bool` it returns is a UI gate, not permission to read
a secret. Put Keychain access control on anything valuable (see
[Keychain and biometrics](references/keychain-biometric.md)).

```swift
import LocalAuthentication

func confirmOwner() async -> Bool {
    let context = LAContext()
    var problem: NSError?
    guard context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &problem) else {
        return false
    }
    let reason = "Confirm it's you to change your email"
    let passed = try? await context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: reason)
    return passed == true
}
```

Info.plist must include `NSFaceIDUsageDescription`, the same rule the
`swift-security` skill sets for biometric keychain items. The `LAContext` header
asks apps to supply it: its string is shown the first time the app uses Face ID,
and once the user denies Face ID for the app, evaluations fail with
`LAError.biometryNotAvailable`.

## Security boundaries

This skill covers: Apple ID sign-in, passkeys, AutoFill of saved passwords, the
`ASAuthorizationController` flow, presenting OAuth web sessions, checking
credential state, and biometric re-authentication on the device.

The minimum storage rule stays here too: tokens and secrets live in the
Keychain, and `LAContext.evaluatePolicy` on its own must never unlock a
protected secret.

Hand off to `swift-security`: Keychain architecture and migration,
biometric-gated keychain items and their access-control policy, CryptoKit, Secure Enclave, certificate pinning
and trust evaluation, sharing items between apps, hardening local storage, and
mapping against OWASP MASVS/MASTG. App and device attestation belongs to `device-integrity`.

## Common mistakes

- Assuming a cached session means Apple has not revoked the link. Check state at launch and react to the revocation notification.
- Jumping straight to the login form without first asking for saved credentials;
  `.notInteractive` is simply the "nothing saved" answer.
- Force-unwrapping `email` or `fullName`.
- Building an `ASAuthorizationController` with no presentation context provider.
- Writing JWTs from Apple, one-time codes, session tokens, passwords or
  passkey server records into `UserDefaults`, plain files or Core Data. Use the
  Keychain, and keep passkey verification on the server.
- Passkey requests without a `webcredentials:` Associated Domains entry, or
  using app passkeys for an unrelated website.
- Drifting into CryptoKit, Secure Enclave, pinning or MASVS work instead of
  routing it to `swift-security`.

## Review checklist

- [ ] Sign in with Apple capability enabled
- [ ] `ASAuthorizationControllerPresentationContextProviding` implemented
- [ ] `getCredentialState(forUserID:completion:)` runs on every launch
- [ ] `credentialRevokedNotification` observed and signs the user out
- [ ] `email` and `fullName` saved on first authorization
- [ ] `identityToken` validated by the server
- [ ] Tokens kept in the Keychain
- [ ] Existing-account flows run before the login UI
- [ ] `.canceled`, `.failed` and `.notInteractive` handled
- [ ] `NSFaceIDUsageDescription` present
- [ ] OAuth uses `ASWebAuthenticationSession`, not `WKWebView`
- [ ] `prefersEphemeralWebBrowserSession` set where cookie isolation matters
- [ ] `textContentType` set on username and password fields
- [ ] Passkey relying party listed under `webcredentials:` Associated Domains
- [ ] Passkey challenges issued and verified by the server
- [ ] Deep Keychain, CryptoKit, Secure Enclave, pinning and MASVS questions routed to `swift-security`

## References

- [AuthenticationServices](https://developer.apple.com/documentation/authenticationservices)
- [ASAuthorizationAppleIDProvider](https://developer.apple.com/documentation/authenticationservices/asauthorizationappleidprovider)
- [ASAuthorizationAppleIDCredential](https://developer.apple.com/documentation/authenticationservices/asauthorizationappleidcredential)
- [ASAuthorizationController](https://developer.apple.com/documentation/authenticationservices/asauthorizationcontroller)
- [ASWebAuthenticationSession](https://developer.apple.com/documentation/authenticationservices/aswebauthenticationsession)
- [Supporting passkeys](https://developer.apple.com/documentation/authenticationservices/supporting-passkeys)
- [ASAuthorizationPlatformPublicKeyCredentialProvider](https://developer.apple.com/documentation/authenticationservices/asauthorizationplatformpublickeycredentialprovider)
- [ASAuthorizationPasswordProvider](https://developer.apple.com/documentation/authenticationservices/asauthorizationpasswordprovider)
- [SignInWithAppleButton](https://developer.apple.com/documentation/authenticationservices/signinwithapplebutton)
- Sample code: the AuthenticationServices sample on implementing user authentication with Apple ID sign-in
