authentication — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited authentication (Agent Skill) and scored it 45/100 (orange). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 2 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 2 flagged
A base64 string of 128+ characters appears in a documentation file. Encoded prompt injection hides the hostile instruction in base64 — invisible to keyword filters — and relies on the agent's ability to decode it at runtime. There is no normal authoring reason to embed a multi-hundred-byte base64 blob in skill docs.
*.sig, SIGNATURES) outside the documentation.Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
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.
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
) {
switch (error as? ASAuthorizationError)?.code {
case .canceled, .notInteractive:
break
case .failed:
showError("Authorization failed")
default:
showError("Authorization failed: \(error.localizedDescription)")
}
}
}| 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.
Check credential state on every app launch. The user may revoke access at any time via Settings > Apple Account > Sign-In & Security.
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()
}
}
}
}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.
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.
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 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 for the canonical registration, assertion, result handling, AutoFill-assisted, and physical security-key flows.
Use ASWebAuthenticationSession for OAuth and third-party authentication (Google, GitHub, etc.). Never use WKWebView for auth flows.
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.
Offer ASAuthorizationPasswordProvider alongside Sign in with Apple using the single controller in Existing Account Setup Flows. Handle ASPasswordCredential in that controller's delegate.
Set textContentType on text fields for AutoFill to work:
usernameField.textContentType = .username
passwordField.textContentType = .passwordUse 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 for the canonical LAContext, fallback, SecAccessControl, and .biometryCurrentSet patterns.
Required: Add NSFaceIDUsageDescription to Info.plist. Missing this key crashes on Face ID devices.
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.
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 flow. Style with .signInWithAppleButtonStyle(...).
Check credential state at launch and handle revocation notifications.
Treat .notInteractive as the normal "no local credential" path.
email or fullName. Cache them on first authorization andhandle nil later.
ASAuthorizationController without a presentation contextprovider. Authorization UI needs the active presentation anchor.
passkey server state in UserDefaults, files, or Core Data. Store secrets in Keychain and keep relying-party passkey verification server-side.
webcredentials: Associated Domains for therelying-party domain, or trying to use app-native passkeys for unrelated websites.
pinning, or OWASP MASVS. Route those to swift-security.
ASAuthorizationControllerPresentationContextProviding implementedgetCredentialState(forUserID:completion:))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 AutoFillwebcredentials: Associated Domains configuredswift-security~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.