<commit_before>import Foundation
import JWT

private let verifyIdTokenDocsMessage = "See https://firebase.google.com/docs/auth/admin/verify-id-tokens for details on how to retrieve an ID token."
private let projectIdMatchMessage = "Make sure the ID token comes from the same Firebase project as the service account used to authenticate this SDK."

public enum VerificationErrorType {
    case
    notFound(key: String),
    emptyProjectId,
    incorrectAlgorithm,
    incorrectAudience,
    incorrectIssuer,
    incorrectSubject,
    expirationTimeIsNotFuture,
    issuedAtTimeIsNotPast
}

public struct VerificationError: Error {
    let type: VerificationErrorType
    let message: String?
}

public struct VerifiedResult {
    let userId: String
    let authTime: Date
    // TODO: provider_id, firebase
}

public struct FirebaseVerifier {
    public let projectId: String
    init(projectId: String) throws {
        if projectId.isEmpty {
            throw VerificationError(type: .emptyProjectId, message: nil)
        }
        self.projectId = projectId
    }
    func verify(token: String, allowExpired: Bool = false) throws -> VerifiedResult {
        let jwt = try JWT(token: token)

        assert(jwt.subject == jwt.userId)
        if !allowExpired {
            try jwt.verifyExpirationTime()
        }
        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: .notFound(key: "sub"), message: message)
        }
        guard subject.characters.count <= 128 else {
            let message = "Firebase ID token has 'sub' (subject) claim longer than 128 characters. \(verifyIdTokenDocsMessage)"
            throw VerificationError(type: .incorrectSubject, message: message)
        }

        let cert = try fetchPublicCertificate(with: keyIdentifier)
        let signer = try RS256(x509Cert: cert)
        try jwt.verifySignature(using: signer)

        guard let authTime = jwt.expirationTime else { throw VerificationError(type: .notFound(key: "auth_time"), message: nil) }
        return VerifiedResult(userId: subject, authTime: authTime)
    }

    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")!)

        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
    }
}

extension JWT {
    private func payloadStringValue(with key: String) -> String? {<commit_msg>Refactor<commit_after>import Foundation
import JWT

private let verifyIdTokenDocsMessage = "See https://firebase.google.com/docs/auth/admin/verify-id-tokens for details on how to retrieve an ID token."
private let projectIdMatchMessage = "Make sure the ID token comes from the same Firebase project as the service account used to authenticate this SDK."

public enum VerificationErrorType {
    case
    notFound(key: String),
    emptyProjectId,
    incorrectAlgorithm,
    incorrectAudience,
    incorrectIssuer,
    incorrectSubject,
    expirationTimeIsNotFuture,
    issuedAtTimeIsNotPast
}

public struct VerificationError: Error {
    let type: VerificationErrorType
    let message: String?
}

public struct VerifiedResult {
    let userId: String
    let authTime: Date
    // TODO: provider_id, firebase
}

public struct FirebaseVerifier {
    public let projectId: String
    init(projectId: String) throws {
        if projectId.isEmpty {
            throw VerificationError(type: .emptyProjectId, message: nil)
        }
        self.projectId = projectId
    }
    func verify(token: String, allowExpired: Bool = false) throws -> VerifiedResult {
        let jwt = try JWT(token: token)

        assert(jwt.subject == jwt.userId)
        if !allowExpired {
            try jwt.verifyExpirationTime()
        }
        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: .notFound(key: "sub"), message: message)
        }
        guard subject.characters.count <= 128 else {
            let message = "Firebase ID token has 'sub' (subject) claim longer than 128 characters. \(verifyIdTokenDocsMessage)"
            throw VerificationError(type: .incorrectSubject, message: message)
        }

        let cert = try fetchPublicCertificate(with: keyIdentifier)
        let signer = try RS256(x509Cert: cert)
        try jwt.verifySignature(using: signer)

        guard let authTime = jwt.expirationTime else { throw VerificationError(type: .notFound(key: "auth_time"), message: nil) }
        return VerifiedResult(userId: subject, authTime: authTime)
    }

    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")!)

        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")
        assert(publicKeyLines.count >= 3)
        return String(publicKeyLines[1..<publicKeyLines.count - 1].joined())
            .makeBytes()
            .base64URLDecoded
    }
}

extension JWT {
    private func payloadStringValue(with key: String) -> String? {
{'fraction_non_alphanumeric': 0.08678556382530543, 'fraction_numerical': 0.005757618312034827, 'mean_word_length': 3.039705048213273, 'pattern_counts': {'":': 0, '<': 6, '<?xml version=': 0, '>': 10, 'https://': 4, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 2, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1184069', 'n_tokens_mistral': 1990, 'n_tokens_neox': 1942, 'n_words': 606}