this repo has no description
at ai_oauth 97 lines 3.8 kB view raw
1import CryptoKit 2import Foundation 3import Testing 4@testable import CoreATProtocol 5 6private struct DeterministicRandomGenerator: RandomDataGenerating { 7 func data(count: Int) throws -> Data { 8 Data(repeating: 0x42, count: count) 9 } 10} 11 12@Test("Base64URL encodes without padding and decodes back") 13func base64URLRoundTrip() throws { 14 let data = Data([0xde, 0xad, 0xbe, 0xef]) 15 let encoded = Base64URL.encode(data) 16 #expect(encoded.contains("=") == false) 17 let decoded = try Base64URL.decode(encoded) 18 #expect(decoded == data) 19} 20 21@Test("PKCE generator creates verifier within bounds and matching challenge") 22func pkceGeneratorProducesExpectedValues() throws { 23 let generator = PKCEGenerator(randomGenerator: DeterministicRandomGenerator()) 24 let values = try generator.makeValues() 25 #expect(values.verifier.count >= 43) 26 #expect(values.verifier.count <= 128) 27 28 let expectedDigest = SHA256.hash(data: Data(values.verifier.utf8)) 29 let expectedChallenge = Base64URL.encode(Data(expectedDigest)) 30 #expect(values.challenge == expectedChallenge) 31} 32 33@Test("DPoP generator signs payload with expected claims") 34func dpopGeneratorProducesValidProof() async throws { 35 let keyPair = DPoPKeyPair() 36 let generator = await DPoPGenerator(clock: { Date(timeIntervalSince1970: 1_700_000_000) }) 37 try await generator.updateKey(using: keyPair.export()) 38 let url = URL(string: "https://example.com/resource")! 39 let proof = try await generator.generateProof( 40 method: "GET", 41 url: url, 42 nonce: "nonce-value", 43 accessToken: "access-token" 44 ) 45 46 let components = proof.split(separator: ".") 47 #expect(components.count == 3) 48 49 let headerData = try Base64URL.decode(String(components[0])) 50 let payloadData = try Base64URL.decode(String(components[1])) 51 let signatureData = try Base64URL.decode(String(components[2])) 52 53 let header = try JSONSerialization.jsonObject(with: headerData) as? [String: Any] 54 let payload = try JSONSerialization.jsonObject(with: payloadData) as? [String: Any] 55 56 #expect(header?["typ"] as? String == "dpop+jwt") 57 #expect(header?["alg"] as? String == "ES256") 58 let jwk = header?["jwk"] as? [String: String] 59 #expect(jwk?["kty"] == "EC") 60 #expect(jwk?["crv"] == "P-256") 61 62 #expect(payload?["htm"] as? String == "GET") 63 #expect(payload?["htu"] as? String == "https://example.com/resource") 64 #expect(payload?["nonce"] as? String == "nonce-value") 65 #expect(payload?["ath"] as? String == Base64URL.encode(Data(SHA256.hash(data: Data("access-token".utf8))))) 66 67 if let iat = payload?["iat"] as? Int { 68 #expect(iat == 1_700_000_000) 69 } else { 70 Issue.record("DPoP payload missing iat") 71 } 72 73 let signingInput = Data((components[0] + "." + components[1]).utf8) 74 let signature = try P256.Signing.ECDSASignature(derRepresentation: signatureData) 75 #expect(keyPair.privateKey.publicKey.isValidSignature(signature, for: signingInput)) 76} 77 78@Test("OAuth session refresh heuristics") 79func oauthSessionRefreshLogic() { 80 let issuedAt = Date() 81 let session = OAuthSession( 82 did: "did:plc:example", 83 pdsURL: URL(string: "https://pds.example.com")!, 84 authorizationServer: URL(string: "https://auth.example.com")!, 85 tokenEndpoint: URL(string: "https://auth.example.com/token")!, 86 accessToken: "token", 87 refreshToken: "refresh", 88 tokenType: "DPoP", 89 scope: "atproto", 90 expiresIn: 3600, 91 issuedAt: issuedAt 92 ) 93 94 #expect(session.isExpired(relativeTo: issuedAt.addingTimeInterval(3500)) == false) 95 #expect(session.needsRefresh(relativeTo: issuedAt.addingTimeInterval(3300), threshold: 400)) 96 #expect(session.isExpired(relativeTo: issuedAt.addingTimeInterval(3600))) 97}