Skip to content

Instantly share code, notes, and snippets.

@kylebshr
Last active October 22, 2024 03:01
Show Gist options
  • Save kylebshr/0d295fe450c9c02289a26fcf0aca5b16 to your computer and use it in GitHub Desktop.
Save kylebshr/0d295fe450c9c02289a26fcf0aca5b16 to your computer and use it in GitHub Desktop.
Verifying a Sign in with Apple JWT in Vapor 3
/*
Once you've signed in with Apple in your iOS app, turn the `identityToken` into a string with something like
`String(data: identityToken, encoding: .utf8)`. Then use that string in the Authorization header:
`urlRequest.addValue("Bearer \(identityString)", forHTTPHeaderField: "Authorization")`
*/
import Vapor
import JWT
struct AppleJWT: JWTPayload {
let iss: IssuerClaim
let aud: AudienceClaim
let exp: ExpirationClaim
let iat: IssuedAtClaim
let sub: SubjectClaim
let c_hash: String
let email: String
let email_verified: String
let auth_time: Date
func verify(using signer: JWTSigner) throws {
try exp.verifyNotExpired()
}
}
struct User: Content {
var email: String
}
final class AuthenticationController {
func verifyAppleJWT(_ req: Request) throws -> Future<User> {
guard let bearer = req.http.headers.bearerAuthorization else {
throw Abort(.unauthorized)
}
return try req.client().get("https://appleid.apple.com/auth/keys").flatMap { response in
return try response.content.decode(JWKS.self).map { jwks in
let jwt = try JWT<AppleJWT>(from: bearer.token, verifiedUsing: JWTSigners(jwks: jwks))
return User(email: jwt.payload.email)
}
}
}
}
@kdeda
Copy link

kdeda commented Oct 22, 2024

fileprivate extension Request {
    struct UserCredentials {
        public var email: String
        public var bundleID: String
        public var expiration: Date
    }

    /**
     https://github.com/vapor/jwt-kit
     https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api
     https://github.com/vapor/jwt-kit/blob/02a0fa600eee1bdc892013d62fc795fc623a5cc3/Sources/JWTKit/Vendor/AppleIdentityToken.swift#L3
     https://swiftinit.org/docs/vapor.jwt-kit/jwtkit/appleidentitytoken
     */
    func validateJWT() async throws -> UserCredentials {
        guard let jwtToken = headers[URLRequest.appleJWTToken].first
        else {
            Log4swift[Self.self].error("bad request: '\(self)'")
            throw ServerError.badRequest
        }

        do {
            let clientResponse = try await client.get("https://appleid.apple.com/auth/keys")
            let jwks = try clientResponse.content.decode(JWKS.self)
            let keys = try await JWTKeyCollection().add(jwks: jwks)

            let payload = try await keys.verify(jwtToken, as: AppleIdentityToken.self)
            let rv = UserCredentials(
                email: payload.email ?? "",
                bundleID: payload.audience.value.first ?? "app.bundle.id",
                expiration: payload.expires.value
            )

            Log4swift[Self.self].info("payload.user: '\(rv)'")
            Log4swift[Self.self].info("payload: '\(payload)'")
            return rv
        } catch {
            Log4swift[Self.self].info("error: '\(error)'")
            throw error
        }
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment