Implementing Sign In with Apple in SwiftUI: A Practical Guide
Learn how to add Sign In with Apple to your iOS app with SwiftUI, AuthenticationServices, and a secure backend. The article walks through the UI, delegate handling, token extraction, and server‑side verification, plus common pitfalls and limits.
10 May 2026, 08:03 UTC

What you’ll get
By the end of this article you’ll have a ready‑to‑drop Sign In with Apple button in a SwiftUI view, a delegate that pulls the identity token, and a clear idea of how to verify that token on your backend before creating a user session. You’ll also know the common pitfalls that can break the flow or expose you to security risks.
Prerequisites
- iOS 13+ (Swift 5.5 or newer recommended)
- Xcode 13+ with the AuthenticationServices framework available
- An Apple Developer account and a registered bundle ID with the “Sign In with Apple” capability enabled
- Basic knowledge of SwiftUI and server‑side JWT handling (e.g., using Node.js, Python, or any language that can verify JWS)
Step 1: Enable the Capability
In Xcode, open your project’s target, go to the Signing & Capabilities tab, click the + Capability button, and add Sign In with Apple. This automatically adds the necessary entitlements and updates your bundle ID in the Apple Developer portal.
Step 2: Add the UI Button
The AuthenticationServices framework ships a ready‑made SignInWithAppleButton that complies with Apple’s Human Interface Guidelines. It handles sizing, color, and accessibility for you. Below is a minimal SwiftUI view that shows the button and stores the user’s Apple ID once the flow succeeds.
import SwiftUI
import AuthenticationServices
struct SignInView: View {
@State private var userId: String? = nil
@State private var errorMessage: String? = nil
var body: some View {
VStack(spacing: 20) {
if let userId = userId {
Text("Signed in as \(userId)")
} else {
SignInWithAppleButton(
onRequest: { request in
request.requestedScopes = [.fullName, .email]
},
onCompletion: { result in
handleAuthorization(result)
}
)
.frame(height: 44)
.signInWithAppleStyle(.black)
}
if let error = errorMessage {
Text(error).foregroundColor(.red)
}
}
.padding()
}
private func handleAuthorization(_ result: Result<ASAuthorization, Error>) {
switch result {
case .success(let auth):
if let credential = auth.credential as? ASAuthorizationAppleIDCredential {
guard let identityToken = credential.identityToken else {
errorMessage = "Missing identity token"
return
}
let tokenString = String(data: identityToken, encoding: .utf8) ?? ""
// Send tokenString to your backend for verification
verifyTokenOnServer(tokenString)
}
case .failure(let error):
errorMessage = error.localizedDescription
}
}
private func verifyTokenOnServer(_ token: String) {
guard let url = URL(string: "https:///verify") else { return }
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let body = ["token": token]
request.httpBody = try? JSONSerialization.data(withJSONObject: body, options: [])
URLSession.shared.dataTask(with: request) { data, response, error in
guard error == nil, let data = data else {
DispatchQueue.main.async { errorMessage = error?.localizedDescription }
return
}
if let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let uid = json["userId"] as? String {
DispatchQueue.main.async {
userId = uid
errorMessage = nil
}
} else {
DispatchQueue.main.async { errorMessage = "Invalid server response" }
}
}.resume()
}
}
Key points in the code:
- The button’s
onRequestclosure lets you request optional scopes (full name, email). Apple may not return these if the user has already signed in. - The
onCompletionclosure receives anASAuthorizationobject. If the credential is anASAuthorizationAppleIDCredential, you can pull theidentityToken(a JWT) andauthorizationCodeif needed. - Always convert the
identityTokento a UTF‑8 string before sending it to your server. - The
verifyTokenOnServerfunction demonstrates a simple POST request. In production, use HTTPS, proper error handling, and store the user ID securely (e.g., Keychain).
Step 3: Verify the Token Server‑Side
Apple signs the identityToken with a private key. Your backend must verify the signature, check the claims, and extract the unique sub claim (the user’s Apple ID). A typical Node.js example using the jsonwebtoken library looks like this:
const express = require('express')
const jwt = require('jsonwebtoken')
const fetch = require('node-fetch')
const app = express()
app.use(express.json())
// Cache Apple’s public keys for 24 hours
let appleKeys = null
let lastFetched = 0
async function getAppleKeys() {
const now = Date.now()
if (!appleKeys || now - lastFetched > 24 * 60 * 60 * 1000) {
const res = await fetch('https://appleid.apple.com/auth/keys')
const json = await res.json()
appleKeys = json.keys
lastFetched = now
}
return appleKeys
}
app.post('/verify', async (req, res) => {
const { token } = req.body
if (!token) return res.status(400).send({ error: 'Missing token' })
const keys = await getAppleKeys()
const decodedHeader = jwt.decode(token, { complete: true })
if (!decodedHeader) return res.status(400).send({ error: 'Invalid token' })
const key = keys.find(k => k.kid === decodedHeader.header.kid)
if (!key) return res.status(400).send({ error: 'Key not found' })
const publicKey = jwt.exportPublicKey(key)
try {
const payload = jwt.verify(token, publicKey, { algorithms: ['RS256'] })
// Validate claims
if (payload.iss !== 'https://appleid.apple.com' || payload.aud !== '') {
return res.status(400).send({ error: 'Invalid claims' })
}
// At this point the token is valid
// Create or fetch your user record using payload.sub (Apple user ID)
res.send({ userId: payload.sub })
} catch (e) {
res.status(400).send({ error: 'Token verification failed' })
}
})
app.listen(3000, () => console.log('Server running'))
Important security checks:
- Verify the
issclaim equalshttps://appleid.apple.com. - Verify the
audclaim matches your bundle ID. - Check the
expclaim to ensure the token hasn’t expired. - Only use the
subclaim as the persistent user identifier; never rely on the email or full name for authentication.
Common Mistakes & Limits
- Skipping server‑side verification: If you accept the identity token in the app without server validation, you expose yourself to replay attacks and token forgery.
- Using a custom button: Apple’s Human Interface Guidelines require the use of the provided button style. Customizing the button’s shape or color can lead to App Store rejection.
- Missing the capability: Without enabling Sign In with Apple in Xcode, the button will not render and the request will fail with a 400 error.
- Ignoring iOS version checks: Sign In with Apple is only available on iOS 13+ and macOS 10.15+. Provide a fallback OAuth flow for older devices.
- Hard‑coding the backend URL: Use a secure, environment‑specific endpoint. In production, store the URL in a secure config or use a reverse proxy.
- Not handling user consent: Apple may not provide the email or full name if the user has previously opted out. Your UI should handle missing optional data gracefully.
- Using the authorizationCode incorrectly: The
authorizationCodeis meant for exchanging a refresh token with Apple’s token endpoint. If you don’t need a refresh token, you can ignore it.
How to Test the Flow
- Run the app on a device or simulator with iOS 13+. Ensure the Sign In with Apple button appears.
- Tap the button, sign in with an Apple ID, and observe that the identity token is sent to the backend.
- Check the backend logs to confirm that the JWT is verified and the
subclaim is extracted. - After a successful login, the app should display the Apple user ID. If the ID changes after a second login, verify that you’re using the same Apple ID and that the
subclaim is consistent. - To test error handling, temporarily change the backend URL to an invalid address and verify that the app shows an error message.
Wrapping Up
Integrating Sign In with Apple in SwiftUI is straightforward when you follow the framework’s prescribed pattern: enable the capability, use the provided button, extract the identity token, and verify it on a secure server. By adhering to the UI guidelines and performing thorough server‑side validation, you provide a smooth, secure login experience that meets Apple’s requirements and protects your users.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.