diff --git a/Sources/FirebaseVerifier.swift b/Sources/FirebaseVerifier.swift index ccf46cf..129e133 100644 --- a/Sources/FirebaseVerifier.swift +++ b/Sources/FirebaseVerifier.swift @@ -37,23 +37,19 @@ public struct FirebaseVerifier { } func verify(token: String, allowExpired: Bool = false) throws -> VerifiedResult { let jwt = try JWT(token: token) - print("jwt: \(jwt)") - guard let kid = jwt.keyIdentifier else { - throw VerificationError(type: .notFound(key: "kid"), message: "Firebase ID token has no 'kid' claim.") - } - guard jwt.algorithmName == alghorithm else { - let message = "Firebase ID token has incorrect algorithm. Expected '\(alghorithm)' but got '\(String(describing: jwt.algorithmName))'. \(verifyIdTokenDocsMessage)" - throw VerificationError(type: .incorrectAlgorithm, message: message) - } - guard jwt.audience == projectId else { - let message = "Firebase ID token has incorrect 'aud' (audience) claim. Expected '\(projectId)' but got '\(String(describing: jwt.audience))'. \(projectIdMatchMessage) \(verifyIdTokenDocsMessage)" - throw VerificationError(type: .incorrectAudience, message: message) + assert(jwt.subject == jwt.userId) + if !allowExpired { + try jwt.verifyExpirationTime() } - guard jwt.issuer == "https://securetoken.google.com/\(projectId)" else { - let message = "Firebase ID token has incorrect 'iss' (issuer) claim. Expected https://securetoken.google.com/\(projectId) but got '\(String(describing: jwt.issuer))'. \(projectIdMatchMessage) + \(verifyIdTokenDocsMessage)" - throw VerificationError(type: .incorrectIssuer, message: message) + try jwt.verifyAlgorithm() + try jwt.verifyAudience(with: projectId) + try jwt.verifyIssuer(with: projectId) + + guard let keyIdentifier = jwt.keyIdentifier else { + throw VerificationError(type: .notFound(key: "kid"), message: "Firebase ID token has no 'kid' claim.") } + guard let subject = jwt.subject else { let message = "Firebase ID token has no 'sub' (subject) claim. \(verifyIdTokenDocsMessage)" throw VerificationError(type: .noSub, message: message) @@ -62,32 +58,28 @@ public struct FirebaseVerifier { let message = "Firebase ID token has 'sub' (subject) claim longer than 128 characters. \(verifyIdTokenDocsMessage)" throw VerificationError(type: .noSub, message: message) } - guard let publicKey = try fetchPublicKeys()?[kid] as? String else { - let message = "Firebase ID token has 'kid' claim which does not correspond to a known public key. Most likely the ID token is expired, so get a fresh token from your client app and try again. \(verifyIdTokenDocsMessage)" - throw VerificationError(type: .notFound(key: "public key"), message: message) - } - assert(jwt.subject == jwt.userId) - - let publicKeyLines = publicKey.split(separator: "\n") - let cert = String(publicKeyLines.prefix(through: publicKeyLines.count - 2).suffix(from: 1).joined()) - .makeBytes() - .base64URLDecoded + let cert = try fetchPublicCertificate(with: keyIdentifier) let signer = try RS256(x509Cert: cert) try jwt.verifySignature(using: signer) - if !allowExpired { - try jwt.verifyExpirationTime() - } - guard let authTime = jwt.expirationTime else { throw VerificationError(type: .notFound(key: "auth_time"), message: nil) } return VerifiedResult(userId: subject, authTime: authTime) } - private func fetchPublicKeys() throws -> NSDictionary? { + private func fetchPublicCertificate(with keyIdentifier: String) throws -> Bytes { // TODO: Cache-Control let response = try String(contentsOf: URL(string: "https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com")!) - return response.toJSON() as? NSDictionary + + guard let keys = response.toJSON() as? NSDictionary, let publicKey = keys[keyIdentifier] as? String else { + let message = "Firebase ID token has 'kid' claim which does not correspond to a known public key. Most likely the ID token is expired, so get a fresh token from your client app and try again. \(verifyIdTokenDocsMessage)" + throw VerificationError(type: .notFound(key: "public key"), message: message) + } + + let publicKeyLines = publicKey.split(separator: "\n") + return String(publicKeyLines.prefix(through: publicKeyLines.count - 2).suffix(from: 1).joined()) + .makeBytes() + .base64URLDecoded } } @@ -107,6 +99,21 @@ extension JWT { var subject: String? { return payloadStringValue(with: "sub") } var userId: String? { return payloadStringValue(with: "user_id") } + func verifyAlgorithm() throws { + if algorithmName == alghorithm { return } + let message = "Firebase ID token has incorrect algorithm. Expected '\(alghorithm)' but got '\(String(describing: algorithmName))'. \(verifyIdTokenDocsMessage)" + throw VerificationError(type: .incorrectAlgorithm, message: message) + } + func verifyAudience(with projectId: String) throws { + if audience == projectId { return } + let message = "Firebase ID token has incorrect 'aud' (audience) claim. Expected '\(projectId)' but got '\(String(describing: audience))'. \(projectIdMatchMessage) \(verifyIdTokenDocsMessage)" + throw VerificationError(type: .incorrectAudience, message: message) + } + func verifyIssuer(with projectId: String) throws { + if issuer == "https://securetoken.google.com/\(projectId)" { return } + let message = "Firebase ID token has incorrect 'iss' (issuer) claim. Expected https://securetoken.google.com/\(projectId) but got '\(String(describing: issuer))'. \(projectIdMatchMessage) + \(verifyIdTokenDocsMessage)" + throw VerificationError(type: .incorrectIssuer, message: message) + } func verifyExpirationTime() throws { guard let issuedAtTime = issuedAtTime else { throw VerificationError(type: .notFound(key: "iat"), message: nil) } guard let expirationTime = expirationTime else { throw VerificationError(type: .notFound(key: "exp"), message: nil) }