---
name: dpearson2699/authentication
source: https://app.decimal.ai/s/dpearson2699-authentication@1/SKILL.md
source_sha256: 539986f36dc1
---

# Authentication

Implement authentication flows on iOS using the AuthenticationServices
framework, including Sign in with Apple, passkeys, OAuth/third-party web
auth, Password AutoFill, and biometric re-authentication.

## Contents

- [Sign in with Apple](#sign-in-with-apple)
- [Credential Handling](#credential-handling)
- [Credential State Checking](#credential-state-checking)
- [Token Validation](#token-validation)
- [Existing Account Setup Flows](#existing-account-setup-flows)
- [Passkeys](#passkeys)
- [ASWebAuthenticationSession (OAuth)](#aswebauthenticationsession-oauth)
- [Password AutoFill Credentials](#password-autofill-credentials)
- [Biometric Authentication](#biometric-authentication)
- [Security Boundaries](#security-boundaries)
- [SwiftUI SignInWithAppleButton](#swiftui-signinwithapplebutton)
- [Common Mistakes](#common-mistakes)
- [Review Checklist](#review-checklist)
- [References](#references)

## Sign in with Apple

Add the "Sign in with Apple" capability in Xcode before using these APIs.

### UIKit: ASAuthorizationController Setup

```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
    ) {
        switch (error as? ASAuthorizationError)?.code {
        case .canceled, .notInteractive:
            break
        case .failed:
            showError("Authorization failed")
        default:
            showError("Authorization failed: \(error.localizedDescription)")
        }
    }
}
```

## Credential Handling

| Credential data | Required handling |
|---|---|
| `user` | Persist this stable, per-team identifier for credential-state checks. |
| `email`, `fullName` | These optional values arrive only on first authorization; cache them immediately. |
| `identityToken`, `authorizationCode` | Send them to the server for validation or exchange; never trust them as client-side proof. |

Treat `realUserStatus` only as a fraud-prevention signal, not authentication
proof.

## 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() {
    let provider = ASAuthorizationAppleIDProvider()
    guard let userID = loadSavedUserID() else {
        showLoginScreen()
        return
    }

    provider.getCredentialState(forUserID: userID) { state, _ in
        DispatchQueue.main.async {
            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()
            }
        }
    }
}
```

### Credential Revocation Notification

```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.

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, and `exp` has not
passed. Exchange the short-lived authorization code on the server and store
the resulting app session token in Keychain.

## 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.

## Passkeys

Use passkeys only for a relying-party domain configured with a `webcredentials:`
Associated Domain and AASA entry. Registration and assertion each require a
fresh server challenge, an `ASAuthorizationPlatformPublicKeyCredentialProvider`,
an authorization controller with an active presentation anchor, and server-side
verification before issuing a session.

Load [references/passkeys.md](references/passkeys.md) for the canonical
registration, assertion, result handling, AutoFill-assisted, and physical
security-key flows.

## 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 {
    private weak var presentationAnchor: ASPresentationAnchor?

    init(presentationAnchor: ASPresentationAnchor) {
        self.presentationAnchor = presentationAnchor
    }

    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 {
        guard let presentationAnchor else {
            fatalError("ASWebAuthenticationSession needs the active window")
        }
        return presentationAnchor
    }
}
```

In SwiftUI, use `@Environment(\.webAuthenticationSession)` and call
`authenticate(using:callback:preferredBrowserSession:additionalHeaderFields:)`
with `.customScheme("myapp")` or `.https(host:path:)`; prefer `.ephemeral`
only when the provider flow should avoid shared browser cookies.

## Password AutoFill Credentials

Offer `ASAuthorizationPasswordProvider` alongside Sign in with Apple using the
single controller in [Existing Account Setup Flows](#existing-account-setup-flows).
Handle `ASPasswordCredential` in that controller's delegate.

Set `textContentType` on text fields for AutoFill to work:

```swift
usernameField.textContentType = .username
passwordField.textContentType = .password
```

## Biometric Authentication

Use `LAContext` from LocalAuthentication for local re-authentication before
showing account settings or starting sensitive actions. Do not treat a returned
`Bool` as proof to unlock a stored secret; protect secrets with Keychain access
control instead. See [references/keychain-biometric.md](references/keychain-biometric.md)
for the canonical `LAContext`, fallback, `SecAccessControl`, and
`.biometryCurrentSet` patterns.

**Required:** Add `NSFaceIDUsageDescription` to Info.plist. Missing this
key crashes on Face ID devices.

## Security Boundaries

This skill owns user-facing account authentication: Sign in with Apple,
passkeys, Password AutoFill, ASAuthorizationController, OAuth session
presentation, credential state, and local biometric re-authentication. Route
deep security work to `swift-security`: Keychain architecture/migration,
CryptoKit, Secure Enclave, certificate pinning/trust, keychain sharing, storage
hardening, and OWASP MASVS/MASTG. Keep only the storage minimum here: tokens and
secrets belong in Keychain; `LAContext.evaluatePolicy` alone must not release
protected secrets.

## SwiftUI SignInWithAppleButton

Use `SignInWithAppleButton` in SwiftUI views when the login surface is SwiftUI.
Request `.fullName` and `.email`, downcast a successful result to
`ASAuthorizationAppleIDCredential`, and pass it to the shared
[Token Validation](#token-validation) flow. Style with
`.signInWithAppleButtonStyle(...)`.

## Common Mistakes

- Assuming a saved local session means the Apple ID credential is still valid.
  Check credential state at launch and handle revocation notifications.
- Showing a full login screen before trying existing account setup flows.
  Treat `.notInteractive` as the normal "no local credential" path.
- Force-unwrapping `email` or `fullName`. Cache them on first authorization and
  handle `nil` later.
- Creating an `ASAuthorizationController` without a presentation context
  provider. Authorization UI needs the active presentation anchor.
- Storing identity tokens, authorization codes, access tokens, passwords, or
  passkey server state in `UserDefaults`, files, or Core Data. Store secrets in
  Keychain and keep relying-party passkey verification server-side.
- Adding passkey requests without `webcredentials:` Associated Domains for the
  relying-party domain, or trying to use app-native passkeys for unrelated
  websites.
- Expanding authentication work into CryptoKit, Secure Enclave, certificate
  pinning, or OWASP MASVS. Route those to `swift-security`.

## Review Checklist

- [ ] "Sign in with Apple" capability added in Xcode project
- [ ] `ASAuthorizationControllerPresentationContextProviding` implemented
- [ ] Credential state checked on every app launch (`getCredentialState(forUserID:completion:)`)
- [ ] `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
- [ ] Passkey relying party has `webcredentials:` Associated Domains configured
- [ ] Passkey registration/assertion challenges come from the server and are verified server-side
- [ ] Deep Keychain, CryptoKit, Secure Enclave, certificate pinning, and MASVS work routed to `swift-security`

## References

- Keychain & biometric patterns: [references/keychain-biometric.md](references/keychain-biometric.md)
- Passkey patterns: [references/passkeys.md](references/passkeys.md)
- [AuthenticationServices](https://sosumi.ai/documentation/authenticationservices)
- [ASAuthorizationAppleIDProvider](https://sosumi.ai/documentation/authenticationservices/asauthorizationappleidprovider)
- [ASAuthorizationAppleIDCredential](https://sosumi.ai/documentation/authenticationservices/asauthorizationappleidcredential)
- [ASAuthorizationController](https://sosumi.ai/documentation/authenticationservices/asauthorizationcontroller)
- [ASWebAuthenticationSession](https://sosumi.ai/documentation/authenticationservices/aswebauthenticationsession)
- [Supporting passkeys](https://sosumi.ai/documentation/authenticationservices/supporting-passkeys)
- [ASAuthorizationPlatformPublicKeyCredentialProvider](https://sosumi.ai/documentation/authenticationservices/asauthorizationplatformpublickeycredentialprovider)
- [ASAuthorizationPasswordProvider](https://sosumi.ai/documentation/authenticationservices/asauthorizationpasswordprovider)
- [SignInWithAppleButton](https://sosumi.ai/documentation/authenticationservices/signinwithapplebutton)
- [Implementing User Authentication with Sign in with Apple](https://sosumi.ai/documentation/authenticationservices/implementing-user-authentication-with-sign-in-with-apple)