Skip to content

Commit

Permalink
Merge branch 'main' into crypto-rsa
Browse files Browse the repository at this point in the history
  • Loading branch information
ptoffy committed Oct 2, 2024
2 parents 4ef9cd0 + 7a3bfc5 commit 0f46b40
Show file tree
Hide file tree
Showing 12 changed files with 807 additions and 598 deletions.
1 change: 1 addition & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ jobs:
uses: vapor/ci/.github/workflows/run-unit-tests.yml@main
with:
with_api_check: ${{ github.event_name == 'pull_request' }}
warnings_as_errors: true
secrets: inherit

ios-tests:
Expand Down
6 changes: 5 additions & 1 deletion Sources/JWTKit/Claims/LocaleClaim.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import struct Foundation.Locale
#if compiler(<6.0) && !canImport(Darwin)
@preconcurrency import Foundation
#else
import Foundation
#endif

public struct LocaleClaim: JWTClaim, Equatable, ExpressibleByStringLiteral {
/// See ``JWTClaim``.
Expand Down
28 changes: 16 additions & 12 deletions Sources/JWTKit/Utilities/CustomizedJSONCoders.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,38 +8,42 @@ public protocol JWTJSONEncoder: Sendable {
func encode<T: Encodable>(_ value: T) throws -> Data
}

extension JSONDecoder: JWTJSONDecoder {}

extension JSONEncoder: JWTJSONEncoder {}

public extension JSONDecoder.DateDecodingStrategy {
static var integerSecondsSince1970: Self {
#if compiler(<6.0) && !canImport(Darwin)
extension JSONDecoder: JWTJSONDecoder, @unchecked Sendable {}
extension JSONEncoder: JWTJSONEncoder, @unchecked Sendable {}
#else
extension JSONDecoder: JWTJSONDecoder {}
extension JSONEncoder: JWTJSONEncoder {}
#endif

extension JSONDecoder.DateDecodingStrategy {
public static var integerSecondsSince1970: Self {
.custom { decoder in
let container = try decoder.singleValueContainer()
return try Date(timeIntervalSince1970: Double(container.decode(Int.self)))
}
}
}

public extension JSONEncoder.DateEncodingStrategy {
static var integerSecondsSince1970: Self {
extension JSONEncoder.DateEncodingStrategy {
public static var integerSecondsSince1970: Self {
.custom { date, encoder in
var container = encoder.singleValueContainer()
try container.encode(Int(date.timeIntervalSince1970.rounded(.towardZero)))
}
}
}

public extension JWTJSONEncoder where Self == JSONEncoder {
static var defaultForJWT: any JWTJSONEncoder {
extension JWTJSONEncoder where Self == JSONEncoder {
public static var defaultForJWT: any JWTJSONEncoder {
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .secondsSince1970
return encoder
}
}

public extension JWTJSONDecoder where Self == JSONDecoder {
static var defaultForJWT: any JWTJSONDecoder {
extension JWTJSONDecoder where Self == JSONDecoder {
public static var defaultForJWT: any JWTJSONDecoder {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .secondsSince1970
return decoder
Expand Down
25 changes: 17 additions & 8 deletions Tests/JWTKitTests/ClaimTests.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import JWTKit
import XCTest

final class ClaimTests: XCTestCase {
final class ClaimTests: XCTestCase, @unchecked Sendable {
func testBoolClaim() throws {
let str = #"{"trueStr":"true","trueBool":true,"falseStr":"false","falseBool":false}"#
var data = Data(str.utf8)
Expand All @@ -23,13 +23,15 @@ final class ClaimTests: XCTestCase {
let brazillianPortugese = try LocalePayload.from(ptBR)
let nadizaDialectSlovenia = try LocalePayload.from(#"{"locale":"sl-nedis"}"#)
let germanSwissPost1996 = try LocalePayload.from(#"{"locale":"de-CH-1996"}"#)
let chineseTraditionalTwoPrivate = try LocalePayload.from(#"{"locale":"zh-Hant-CN-x-private1-private2"}"#)
let chineseTraditionalTwoPrivate = try LocalePayload.from(
#"{"locale":"zh-Hant-CN-x-private1-private2"}"#)

XCTAssertEqual(plainEnglish.locale.value.identifier, "en")
XCTAssertEqual(brazillianPortugese.locale.value.identifier, "pt-BR")
XCTAssertEqual(nadizaDialectSlovenia.locale.value.identifier, "sl-nedis")
XCTAssertEqual(germanSwissPost1996.locale.value.identifier, "de-CH-1996")
XCTAssertEqual(chineseTraditionalTwoPrivate.locale.value.identifier, "zh-Hant-CN-x-private1-private2")
XCTAssertEqual(
chineseTraditionalTwoPrivate.locale.value.identifier, "zh-Hant-CN-x-private1-private2")

let encoded = try JSONEncoder().encode(brazillianPortugese)
let string = String(bytes: encoded, encoding: .utf8)!
Expand All @@ -44,7 +46,9 @@ final class ClaimTests: XCTestCase {

XCTAssertEqual(decoded.audience.value, [id.uuidString])
XCTAssertNoThrow(try decoded.audience.verifyIntendedAudience(includes: id.uuidString))
XCTAssertThrowsError(try decoded.audience.verifyIntendedAudience(includes: UUID().uuidString)) {
XCTAssertThrowsError(
try decoded.audience.verifyIntendedAudience(includes: UUID().uuidString)
) {
guard let jwtError = try? XCTUnwrap($0 as? JWTError) else { return }
XCTAssertEqual(jwtError.errorType, .claimVerificationFailure)
XCTAssert(jwtError.failedClaim is AudienceClaim)
Expand All @@ -53,26 +57,31 @@ final class ClaimTests: XCTestCase {
}

func testMultipleAudienceClaim() throws {
let id1 = UUID(), id2 = UUID()
let id1 = UUID()
let id2 = UUID()
let str = "{\"audience\":[\"\(id1.uuidString)\", \"\(id2.uuidString)\"]}"
let data = Data(str.utf8)
let decoded = try! JSONDecoder().decode(AudiencePayload.self, from: data)

XCTAssertEqual(decoded.audience.value, [id1.uuidString, id2.uuidString])
XCTAssertNoThrow(try decoded.audience.verifyIntendedAudience(includes: id1.uuidString))
XCTAssertNoThrow(try decoded.audience.verifyIntendedAudience(includes: id2.uuidString))
XCTAssertThrowsError(try decoded.audience.verifyIntendedAudience(includes: UUID().uuidString)) {
XCTAssertThrowsError(
try decoded.audience.verifyIntendedAudience(includes: UUID().uuidString)
) {
guard let jwtError = try? XCTUnwrap($0 as? JWTError) else { return }
XCTAssertEqual(jwtError.errorType, .claimVerificationFailure)
XCTAssert(jwtError.failedClaim is AudienceClaim)
XCTAssertEqual((jwtError.failedClaim as? AudienceClaim)?.value, [id1.uuidString, id2.uuidString])
XCTAssertEqual(
(jwtError.failedClaim as? AudienceClaim)?.value, [id1.uuidString, id2.uuidString])
}
}

func testExpirationEncoding() async throws {
let exp = ExpirationClaim(value: Date(timeIntervalSince1970: 2_000_000_000))
let parser = DefaultJWTParser()
let keyCollection = await JWTKeyCollection().add(hmac: .init(from: "secret".bytes), digestAlgorithm: .sha256, parser: parser)
let keyCollection = await JWTKeyCollection().add(
hmac: .init(from: "secret".bytes), digestAlgorithm: .sha256, parser: parser)
let jwt = try await keyCollection.sign(ExpirationPayload(exp: exp))
let parsed = try parser.parse(jwt.bytes, as: ExpirationPayload.self)
let header = parsed.header
Expand Down
112 changes: 58 additions & 54 deletions Tests/JWTKitTests/ECDSATests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import Crypto
import JWTKit
import XCTest

final class ECDSATests: XCTestCase {
final class ECDSATests: XCTestCase, @unchecked Sendable {
func testECDSADocs() async throws {
XCTAssertNoThrow(try ES256PublicKey(pem: ecdsaPublicKey))
}
Expand Down Expand Up @@ -54,7 +54,8 @@ final class ECDSATests: XCTestCase {
let key = ES256PrivateKey()
await keyCollection.add(ecdsa: key)
let token = try await keyCollection.sign(payload)
try await XCTAssertEqualAsync(await keyCollection.verify(token, as: TestPayload.self), payload)
try await XCTAssertEqualAsync(
await keyCollection.verify(token, as: TestPayload.self), payload)
}

func testECDSAPublicPrivate() async throws {
Expand All @@ -68,7 +69,7 @@ final class ECDSATests: XCTestCase {
admin: false,
exp: .init(value: .init(timeIntervalSince1970: 2_000_000_000))
)
for _ in 0 ..< 1000 {
for _ in 0..<1000 {
let token = try await keys.sign(payload, kid: "private")
// test private signer decoding
try await XCTAssertEqualAsync(await keys.verify(token, as: TestPayload.self), payload)
Expand Down Expand Up @@ -97,18 +98,18 @@ final class ECDSATests: XCTestCase {

// verify using jwks without alg
let jwksString = """
{
"keys": [
{
"kty": "EC",
"use": "sig",
"kid": "vapor",
"x": "\(x)",
"y": "\(y)"
}
]
}
"""
{
"keys": [
{
"kty": "EC",
"use": "sig",
"kid": "vapor",
"x": "\(x)",
"y": "\(y)"
}
]
}
"""

try await keys.add(jwksJSON: jwksString)
let foo = try await keys.verify(jwt, as: Foo.self)
Expand Down Expand Up @@ -136,18 +137,18 @@ final class ECDSATests: XCTestCase {

// verify using jwks without alg
let jwksString = """
{
"keys": [
{
"kty": "EC",
"use": "sig",
"kid": "vapor",
"x": "\(x)",
"y": "\(y)"
}
]
}
"""
{
"keys": [
{
"kty": "EC",
"use": "sig",
"kid": "vapor",
"x": "\(x)",
"y": "\(y)"
}
]
}
"""

try await keys.add(jwksJSON: jwksString)
let foo = try await keys.verify(jwt, as: Foo.self)
Expand Down Expand Up @@ -175,18 +176,18 @@ final class ECDSATests: XCTestCase {

// verify using jwks without alg
let jwksString = """
{
"keys": [
{
"kty": "EC",
"use": "sig",
"kid": "vapor",
"x": "\(x)",
"y": "\(y)"
}
]
}
"""
{
"keys": [
{
"kty": "EC",
"use": "sig",
"kid": "vapor",
"x": "\(x)",
"y": "\(y)"
}
]
}
"""

try await keys.add(jwksJSON: jwksString)
let foo = try await keys.verify(jwt, as: Foo.self)
Expand Down Expand Up @@ -243,7 +244,8 @@ final class ECDSATests: XCTestCase {

let params = ec.parameters!
try await keys.add(ecdsa: ES256PublicKey(parameters: params), kid: "params")
try await XCTAssertTrueAsync(try await keys.getKey(for: "params").verify(signature, signs: message))
try await XCTAssertTrueAsync(
try await keys.getKey(for: "params").verify(signature, signs: message))
XCTAssertEqual(ec.curve, .p256)
}

Expand All @@ -257,7 +259,8 @@ final class ECDSATests: XCTestCase {

let params = ec.parameters!
try await keys.add(ecdsa: ES384PublicKey(parameters: params), kid: "params")
try await XCTAssertTrueAsync(try await keys.getKey(for: "params").verify(signature, signs: message))
try await XCTAssertTrueAsync(
try await keys.getKey(for: "params").verify(signature, signs: message))
XCTAssertEqual(ec.curve, .p384)
}

Expand All @@ -271,22 +274,23 @@ final class ECDSATests: XCTestCase {

let params = ec.parameters!
try await keys.add(ecdsa: ES512PublicKey(parameters: params), kid: "params")
try await XCTAssertTrueAsync(try await keys.getKey(for: "params").verify(signature, signs: message))
try await XCTAssertTrueAsync(
try await keys.getKey(for: "params").verify(signature, signs: message))
XCTAssertEqual(ec.curve, .p521)
}
}

let ecdsaPrivateKey = """
-----BEGIN PRIVATE KEY-----
MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQg2sD+kukkA8GZUpmm
jRa4fJ9Xa/JnIG4Hpi7tNO66+OGgCgYIKoZIzj0DAQehRANCAATZp0yt0btpR9kf
ntp4oUUzTV0+eTELXxJxFvhnqmgwGAm1iVW132XLrdRG/ntlbQ1yzUuJkHtYBNve
y+77Vzsd
-----END PRIVATE KEY-----
"""
-----BEGIN PRIVATE KEY-----
MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQg2sD+kukkA8GZUpmm
jRa4fJ9Xa/JnIG4Hpi7tNO66+OGgCgYIKoZIzj0DAQehRANCAATZp0yt0btpR9kf
ntp4oUUzTV0+eTELXxJxFvhnqmgwGAm1iVW132XLrdRG/ntlbQ1yzUuJkHtYBNve
y+77Vzsd
-----END PRIVATE KEY-----
"""
let ecdsaPublicKey = """
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2adMrdG7aUfZH57aeKFFM01dPnkx
C18ScRb4Z6poMBgJtYlVtd9ly63URv57ZW0Ncs1LiZB7WATb3svu+1c7HQ==
-----END PUBLIC KEY-----
"""
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2adMrdG7aUfZH57aeKFFM01dPnkx
C18ScRb4Z6poMBgJtYlVtd9ly63URv57ZW0Ncs1LiZB7WATb3svu+1c7HQ==
-----END PUBLIC KEY-----
"""
Loading

0 comments on commit 0f46b40

Please sign in to comment.