
Setting Up Sign in with Apple with Server Side Swift and SwiftUI
When developing a fitness app, we needed an easy and efficient way for users to authenticate. The standard user name and password interface for the Apple Watch would be cumbersome and challenging. Luckily, Sign in with Apple provides an easy alternative that is both secure and privacy-focused. This guide will show you how to:
- Set up Sign in with Apple on your server
- Implement Sign in with Apple in a SwiftUI app
- Handle authentication tokens securely
Understanding JWT Authentication
Before diving into the implementation, it's important to understand JSON Web Tokens (JWT). JWTs are a secure way to transmit information between parties as a JSON object. They are commonly used in authentication systems because they are digitally signed, which ensures the data hasn't been tampered with.
A JWT consists of three parts:
- A header containing metadata
- A payload with the actual data (claims)
- A signature to verify authenticity
For a detailed guide on working with JWTs in Swift, check out the comprehensive JWTKit tutorial on Swift on Server.
Server Implementation
Before implementing Sign in with Apple, you need to configure your App ID in Apple Developer Portal:
- Enable Sign in with Apple capability
- Note your Services ID and Bundle ID
Vapor
Vapor has great documentation on how to verify Apple's JWT tokens directly:
// setting up our JWT signer
app.jwt.signers.use(JWTSigner.hs512(key: jwtSecret))
// On request, verify the JWT token
let tokenValue = try await req.jwt.apple
.verify(body.token, applicationIdentifier: nil)
.subject
.value
Hummingbird
Handle both Apple's JWKs and any set of HMAC keys:
internal extension JWTKeyCollection {
private static let appleIDJWKSurl = "https://appleid.apple.com/auth/keys"
internal init(
configuration: SecurityConfiguration,
httpClient: HTTPClient = .shared
) async throws {
try await self.init(
jwksURL: Self.appleIDJWKSurl,
// your own HMAC key
hmacKey: .init(from: configuration.secretKey),
httpClient: httpClient
)
}
private init(
jwksURL: String,
hmacKey: HMACKey,
httpClient: HTTPClient = .shared
) async throws {
self.init()
// add the Apple JWKS to the JWTKeyCollection
let request = HTTPClientRequest(url: jwksURL)
let jwksResponse: HTTPClientResponse = try await httpClient.execute(
request
)
let jwksData = try await jwksResponse.body.collect(upTo: 1_000_000)
let jwks = try JSONDecoder().decode(JWKS.self, from: jwksData)
try self.add(jwks: jwks)
// add your own HMAC key to the JWTKeyCollection
self.add(hmac: hmacKey, digestAlgorithm: .sha512)
}
}
If you are using the OpenAPI generator from Apple, you can verify the JWT from the request body using the JWTKeyCollection
:
internal func createUser(
_ input: Operations.createUser.Input
) async throws -> Operations.createUser.Output {
guard case let .json(userBody) = input.body else {
return .undocumented(statusCode: 400, .init())
}
// verify the JWT token with our `JWTKeyCollection`
let jwt = try await keyCollection.verify(
userBody.appleIdentityToken,
as: AppleIdentityToken.self
)
// Bundle IDs for your Audience
let audiences = [
// iPhone app
"com.brightdigit.Bitness",
// Apple Watch app
"com.brightdigit.Bitness.watchkitapp",
// Web Site
"com.brightdigit.Bitness.AuthenticationServices",
]
var verifiedAudience: String?
var errors: [any Error] = []
// verify the audience
for audience in audiences {
do {
try jwt.audience.verifyIntendedAudience(includes: audience)
verifiedAudience = audience
break
} catch {
errors.append(error)
}
}
// if the audience is not verified, return 401 Unauthorized
guard let verifiedAudience else {
return Operations.createUser.Output.undocumented(
statusCode: 401,
.init()
)
}
}
If you are interested in learning more about JWT, JWKS, and more, definitely check out the article at Swift on Server or the documentation for JWTKit.
SwiftUI Implementation
Our main authentication view conditionally renders the Sign in with Apple button:
struct AuthenticationView: View {
@StateObject private var object: AuthenticationObject
private var service: AuthenticationService
@State private var loginResponse: LoginResponse?
var body: some View {
VStack {
SignInWithAppleButton(.signUp,
// update the ASAuthorizationOpenIDRequest with the correct scopes
onRequest: object.appleSignInWithRequest,
// handle the failure case or send the credentials to the server
onCompletion: { result in
object.appleSignInCompletedWith(result)
}
)
.frame(height: 40, alignment: .center)
}
}
internal func signInCompleted(_ result: Result<ASAuthorization, any Error>) {
let credential: (any ASAuthorizationAppleIDCredential)?
credential =
switch result {
case .failure: nil
case let .success(auth): auth.credential as? ASAuthorizationAppleIDCredential
}
guard let credential else {
return
}
// pass the credetial to the server createUser call
}
}
Security Best Practices
When implementing Sign in with Apple, follow these security best practices:
- Always verify tokens on the server side
- Use proper JWT validation including audience and issuer checks
- Store tokens securely using Keychain
- Implement proper error handling and user feedback
- Follow Apple's guidelines for button styling and placement
Next Steps
When developing gBeat, we ran into issues using Sign In With Apple, specifically when running in the Apple Watch Simulator. However, I did find a workaround, you can read about here.