├── .github ├── CODEOWNERS ├── dependabot.yml ├── runs-on.yml └── workflows │ ├── api-docs.yml │ ├── benchmark.yml │ └── test.yml ├── .gitignore ├── .spi.yml ├── .swift-format ├── Benchmarks ├── .gitignore ├── .swiftformat ├── Package.swift ├── Signing │ └── Signing.swift ├── Thresholds │ ├── Signing.ES256.p90.json │ ├── Signing.EdDSA.p90.json │ ├── Signing.RSA.p90.json │ ├── TokenLifecycle.ES256_Generated.p90.json │ ├── TokenLifecycle.ES256_PEM.p90.json │ ├── TokenLifecycle.EdDSA_Coordinates.p90.json │ ├── TokenLifecycle.EdDSA_Generated.p90.json │ ├── TokenLifecycle.RSA_PEM.p90.json │ ├── Verifying.ES256.p90.json │ ├── Verifying.EdDSA.p90.json │ └── Verifying.RS256.p90.json ├── TokenLifecycle │ └── TokenLifecycle.swift └── Verifying │ └── Verifying.swift ├── LICENSE ├── NOTICES.txt ├── Package.swift ├── README.md ├── Snippets ├── JWKExamples.swift └── JWTKitExamples.swift ├── Sources └── JWTKit │ ├── Claims │ ├── AudienceClaim.swift │ ├── BoolClaim.swift │ ├── ExpirationClaim.swift │ ├── GoogleHostedDomainClaim.swift │ ├── IDClaim.swift │ ├── IssuedAtClaim.swift │ ├── IssuerClaim.swift │ ├── JWTClaim.swift │ ├── JWTMultiValueClaim.swift │ ├── JWTUnixEpochClaim.swift │ ├── LocaleClaim.swift │ ├── NotBeforeClaim.swift │ ├── SubjectClaim.swift │ └── TenantIDClaim.swift │ ├── Docs.docc │ ├── images │ │ └── vapor-jwtkit-logo.svg │ ├── index.md │ └── theme-settings.json │ ├── ECDSA │ ├── ECDSA.swift │ ├── ECDSACurve.swift │ ├── ECDSACurveType.swift │ ├── ECDSAError.swift │ ├── ECDSAKeyTypes.swift │ ├── ECDSASigner.swift │ ├── ECDSASigningAlgorithm.swift │ ├── JWTKeyCollection+ECDSA.swift │ ├── P256+CurveType.swift │ ├── P384+CurveType.swift │ └── P521+CurveType.swift │ ├── EdDSA │ ├── EdDSA.swift │ ├── EdDSACurve.swift │ ├── EdDSAError.swift │ ├── EdDSASigner.swift │ └── JWTKeyCollection+EdDSA.swift │ ├── HMAC │ ├── HMAC.swift │ ├── HMACError.swift │ ├── HMACSigner.swift │ └── JWTKeyCollection+HMAC.swift │ ├── Insecure │ └── Insecure.swift │ ├── JWK │ ├── JWK.swift │ ├── JWKIdentifier.swift │ ├── JWKS.swift │ └── JWKSigner.swift │ ├── JWTAlgorithm.swift │ ├── JWTError.swift │ ├── JWTHeader+CommonFields.swift │ ├── JWTHeader.swift │ ├── JWTHeaderField.swift │ ├── JWTKeyCollection.swift │ ├── JWTParser.swift │ ├── JWTPayload.swift │ ├── JWTSerializer.swift │ ├── JWTSigner.swift │ ├── None │ ├── JWTKeyCollection+UnsecuredNone.swift │ └── UnsecuredNoneSigner.swift │ ├── RSA │ ├── JWTKeyCollection+RSA.swift │ ├── RSA.swift │ ├── RSAError.swift │ └── RSASigner.swift │ ├── Utilities │ ├── Base64URL.swift │ ├── CryptoSigner.swift │ ├── CustomizedJSONCoders.swift │ └── Utilities.swift │ ├── Vendor │ ├── AppleIdentityToken.swift │ ├── FirebaseAuthIdentityToken.swift │ ├── GoogleIdentityToken.swift │ └── MicrosoftIdentityToken.swift │ └── X5C │ ├── ValidationTimePayload.swift │ └── X5CVerifier.swift ├── Tests └── JWTKitTests │ ├── ClaimTests.swift │ ├── ECDSATests.swift │ ├── EdDSATests.swift │ ├── Helpers │ └── String+bytes.swift │ ├── JWTKitTests.swift │ ├── PSSTests.swift │ ├── RSATests.swift │ ├── Types │ ├── AudiencePayload.swift │ ├── BadBoolPayload.swift │ ├── BoolPayload.swift │ ├── ExpirationPayload.swift │ ├── LocalePayload.swift │ └── TestPayload.swift │ ├── VendorTokenTests.swift │ └── X5CTests.swift └── scripts ├── generate-certificates.sh └── generateTokens.swift /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @ptoffy 2 | /.github/CONTRIBUTING.md @ptoffy @0xTim @gwynne 3 | /.github/workflows/*.yml @ptoffy @0xTim @gwynne 4 | /.github/workflows/test.yml @ptoffy @gwynne 5 | /.spi.yml @ptoffy @0xTim @gwynne 6 | /.gitignore @ptoffy @0xTim @gwynne 7 | /LICENSE @ptoffy @0xTim @gwynne 8 | /README.md @ptoffy @0xTim @gwynne 9 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | enable-beta-ecosystems: true 3 | updates: 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "daily" 8 | groups: 9 | dependencies: 10 | patterns: 11 | - "*" 12 | - package-ecosystem: "swift" 13 | directory: "/" 14 | schedule: 15 | interval: "daily" 16 | groups: 17 | dependencies: 18 | patterns: 19 | - "*" 20 | -------------------------------------------------------------------------------- /.github/runs-on.yml: -------------------------------------------------------------------------------- 1 | # Use the runs-on.yml file in the vapor/ci repo. 2 | _extends: ci 3 | -------------------------------------------------------------------------------- /.github/workflows/api-docs.yml: -------------------------------------------------------------------------------- 1 | name: deploy-api-docs 2 | on: 3 | push: 4 | branches: 5 | - main 6 | 7 | jobs: 8 | build-and-deploy: 9 | uses: vapor/api-docs/.github/workflows/build-and-deploy-docs-workflow.yml@main 10 | secrets: inherit 11 | with: 12 | package_name: jwt-kit 13 | modules: JWTKit 14 | pathsToInvalidate: /jwtkit/* 15 | -------------------------------------------------------------------------------- /.github/workflows/benchmark.yml: -------------------------------------------------------------------------------- 1 | name: benchmark 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | sha: 6 | type: string 7 | required: true 8 | description: "The commit SHA to run the benchmarks against." 9 | push: 10 | branches: [main] 11 | 12 | jobs: 13 | benchmark: 14 | uses: vapor/ci/.github/workflows/run-benchmark.yml@main 15 | with: 16 | sha: ${{ inputs.sha }} 17 | secrets: inherit 18 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | concurrency: 3 | group: ${{ github.workflow }}-${{ github.ref }} 4 | cancel-in-progress: true 5 | on: 6 | pull_request: { types: [opened, reopened, synchronize, ready_for_review] } 7 | push: { branches: [main] } 8 | 9 | jobs: 10 | linux-integration: 11 | if: ${{ !(github.event.pull_request.draft || false) }} 12 | runs-on: ubuntu-latest 13 | container: swift:noble 14 | steps: 15 | - name: Check out JWTKit 16 | uses: actions/checkout@v4 17 | with: 18 | path: jwt-kit 19 | - name: Check out JWT provider 20 | uses: actions/checkout@v4 21 | with: 22 | repository: vapor/jwt 23 | path: jwt 24 | - name: Use local JWTKit 25 | run: swift package --package-path jwt edit jwt-kit --path ./jwt-kit 26 | - name: Run tests with Thread Sanitizer 27 | run: swift test --package-path jwt --sanitize=thread 28 | 29 | unit-tests: 30 | uses: vapor/ci/.github/workflows/run-unit-tests.yml@main 31 | with: 32 | with_api_check: ${{ github.event_name == 'pull_request' }} 33 | warnings_as_errors: true 34 | with_linting: true 35 | with_windows: true 36 | with_musl: true 37 | with_android: true 38 | ios_scheme_name: jwt-kit 39 | secrets: inherit 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Packages 2 | .build 3 | .index-build 4 | .DS_Store 5 | *.xcodeproj 6 | Package.pins 7 | Package.resolved 8 | DerivedData 9 | .swiftpm 10 | Tests/LinuxMain.swift 11 | .benchmarkBaselines/ 12 | Benchmarks/.benchmarkBaselines/ 13 | x5c_test_certs 14 | -------------------------------------------------------------------------------- /.spi.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | metadata: 3 | authors: "Maintained by the Vapor Core Team with hundreds of contributions from the Vapor Community." 4 | external_links: 5 | documentation: "https://api.vapor.codes/jwtkit/documentation/jwtkit/" -------------------------------------------------------------------------------- /.swift-format: -------------------------------------------------------------------------------- 1 | { 2 | "fileScopedDeclarationPrivacy": { 3 | "accessLevel": "private" 4 | }, 5 | "indentation": { 6 | "spaces": 4 7 | }, 8 | "indentConditionalCompilationBlocks": false, 9 | "indentSwitchCaseLabels": false, 10 | "lineBreakAroundMultilineExpressionChainComponents": false, 11 | "lineBreakBeforeControlFlowKeywords": false, 12 | "lineBreakBeforeEachArgument": false, 13 | "lineBreakBeforeEachGenericRequirement": false, 14 | "lineLength": 140, 15 | "maximumBlankLines": 1, 16 | "multiElementCollectionTrailingCommas": true, 17 | "noAssignmentInExpressions": { 18 | "allowedFunctions": [ 19 | "XCTAssertNoThrow" 20 | ] 21 | }, 22 | "prioritizeKeepingFunctionOutputTogether": false, 23 | "respectsExistingLineBreaks": true, 24 | "rules": { 25 | "AllPublicDeclarationsHaveDocumentation": false, 26 | "AlwaysUseLiteralForEmptyCollectionInit": false, 27 | "AlwaysUseLowerCamelCase": true, 28 | "AmbiguousTrailingClosureOverload": true, 29 | "BeginDocumentationCommentWithOneLineSummary": false, 30 | "DoNotUseSemicolons": true, 31 | "DontRepeatTypeInStaticProperties": true, 32 | "FileScopedDeclarationPrivacy": true, 33 | "FullyIndirectEnum": true, 34 | "GroupNumericLiterals": true, 35 | "IdentifiersMustBeASCII": true, 36 | "NeverForceUnwrap": false, 37 | "NeverUseForceTry": false, 38 | "NeverUseImplicitlyUnwrappedOptionals": false, 39 | "NoAccessLevelOnExtensionDeclaration": true, 40 | "NoAssignmentInExpressions": true, 41 | "NoBlockComments": true, 42 | "NoCasesWithOnlyFallthrough": true, 43 | "NoEmptyTrailingClosureParentheses": true, 44 | "NoLabelsInCasePatterns": true, 45 | "NoLeadingUnderscores": false, 46 | "NoParensAroundConditions": true, 47 | "NoPlaygroundLiterals": true, 48 | "NoVoidReturnOnFunctionSignature": true, 49 | "OmitExplicitReturns": false, 50 | "OneCasePerLine": true, 51 | "OneVariableDeclarationPerLine": true, 52 | "OnlyOneTrailingClosureArgument": true, 53 | "OrderedImports": true, 54 | "ReplaceForEachWithForLoop": true, 55 | "ReturnVoidInsteadOfEmptyTuple": true, 56 | "TypeNamesShouldBeCapitalized": true, 57 | "UseEarlyExits": false, 58 | "UseExplicitNilCheckInConditions": true, 59 | "UseLetInEveryBoundCaseVariable": true, 60 | "UseShorthandTypeNames": true, 61 | "UseSingleLinePropertyGetter": true, 62 | "UseSynthesizedInitializer": true, 63 | "UseTripleSlashForDocumentationComments": true, 64 | "UseWhereClausesInForLoops": false, 65 | "ValidateDocumentationComments": false 66 | }, 67 | "spacesAroundRangeFormationOperators": false, 68 | "tabWidth": 4, 69 | "version": 1 70 | } 71 | -------------------------------------------------------------------------------- /Benchmarks/.gitignore: -------------------------------------------------------------------------------- 1 | ../.gitignore -------------------------------------------------------------------------------- /Benchmarks/.swiftformat: -------------------------------------------------------------------------------- 1 | ../.swiftformat -------------------------------------------------------------------------------- /Benchmarks/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.9 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "benchmarks", 7 | platforms: [ 8 | .macOS(.v13) 9 | ], 10 | dependencies: [ 11 | .package(path: "../"), 12 | .package(url: "https://github.com/ordo-one/package-benchmark.git", from: "1.22.0"), 13 | ], 14 | targets: [ 15 | .executableTarget( 16 | name: "Signing", 17 | dependencies: [ 18 | .product(name: "Benchmark", package: "package-benchmark"), 19 | .product(name: "JWTKit", package: "jwt-kit"), 20 | ], 21 | path: "Signing", 22 | plugins: [ 23 | .plugin(name: "BenchmarkPlugin", package: "package-benchmark") 24 | ] 25 | ), 26 | .executableTarget( 27 | name: "Verifying", 28 | dependencies: [ 29 | .product(name: "Benchmark", package: "package-benchmark"), 30 | .product(name: "JWTKit", package: "jwt-kit"), 31 | ], 32 | path: "Verifying", 33 | plugins: [ 34 | .plugin(name: "BenchmarkPlugin", package: "package-benchmark") 35 | ] 36 | ), 37 | .executableTarget( 38 | name: "TokenLifecycle", 39 | dependencies: [ 40 | .product(name: "Benchmark", package: "package-benchmark"), 41 | .product(name: "JWTKit", package: "jwt-kit"), 42 | ], 43 | path: "TokenLifecycle", 44 | plugins: [ 45 | .plugin(name: "BenchmarkPlugin", package: "package-benchmark") 46 | ] 47 | ), 48 | ] 49 | ) 50 | -------------------------------------------------------------------------------- /Benchmarks/Signing/Signing.swift: -------------------------------------------------------------------------------- 1 | import Benchmark 2 | import Foundation 3 | import JWTKit 4 | 5 | let benchmarks = { 6 | Benchmark.defaultConfiguration = .init( 7 | metrics: [.peakMemoryResident, .mallocCountTotal], 8 | thresholds: [ 9 | .peakMemoryResident: .init( 10 | /// Tolerate up to 4% of difference compared to the threshold. 11 | relative: [.p90: 4], 12 | /// Tolerate up to one million bytes of difference compared to the threshold. 13 | absolute: [.p90: 1_100_000] 14 | ), 15 | .mallocCountTotal: .init( 16 | /// Tolerate up to 1% of difference compared to the threshold. 17 | relative: [.p90: 1], 18 | /// Tolerate up to 2 malloc calls of difference compared to the threshold. 19 | absolute: [.p90: 2] 20 | ), 21 | ] 22 | ) 23 | 24 | Benchmark("ES256") { benchmark in 25 | let key = ES256PrivateKey() 26 | let keyCollection = JWTKeyCollection() 27 | keyCollection.add(ecdsa: key) 28 | for _ in benchmark.scaledIterations { 29 | _ = try await keyCollection.sign(payload) 30 | } 31 | } 32 | 33 | Benchmark("RSA") { benchmark in 34 | let key = try Insecure.RSA.PrivateKey(pem: rsaPrivateKey) 35 | let keyCollection = JWTKeyCollection() 36 | keyCollection.add(rsa: key, digestAlgorithm: .sha256) 37 | for _ in benchmark.scaledIterations { 38 | _ = try await keyCollection.sign(payload) 39 | } 40 | } 41 | 42 | Benchmark("EdDSA") { benchmark in 43 | let key = try EdDSA.PrivateKey() 44 | let keyCollection = JWTKeyCollection() 45 | keyCollection.add(eddsa: key) 46 | for _ in benchmark.scaledIterations { 47 | _ = try await keyCollection.sign(payload) 48 | } 49 | } 50 | } 51 | 52 | struct Payload: JWTPayload { 53 | let name: String 54 | let admin: Bool 55 | 56 | func verify(using signer: some JWTAlgorithm) async throws { 57 | // nothing to verify 58 | } 59 | } 60 | 61 | let payload = Payload(name: "Kyle", admin: true) 62 | 63 | let ecdsaPrivateKey = """ 64 | -----BEGIN PRIVATE KEY----- 65 | MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQg2sD+kukkA8GZUpmm 66 | jRa4fJ9Xa/JnIG4Hpi7tNO66+OGgCgYIKoZIzj0DAQehRANCAATZp0yt0btpR9kf 67 | ntp4oUUzTV0+eTELXxJxFvhnqmgwGAm1iVW132XLrdRG/ntlbQ1yzUuJkHtYBNve 68 | y+77Vzsd 69 | -----END PRIVATE KEY----- 70 | """ 71 | 72 | let rsaPrivateKey = """ 73 | -----BEGIN PRIVATE KEY----- 74 | MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDL8W1D9w5zHpmD 75 | JqpTngIRJ+Sm21e42cRnTudhdejzKUiJQWkHSvQV5yC/+0iEXUsUJYEdSyrKhJFD 76 | PT+IFGdjIiwb7IX+rUreWXlD/YYBL3/byMG4kYoO4oiPp2A+WvfeyLpuN549OXhk 77 | 7o5kXEZjKfjHTfmnAbCMoYW5BEpiHQC3HAeJZ5EiwAn8HZn5UY6lxJcf7H9hR83x 78 | D0W7IZTNyxUu4aLNuihFIxJKgP/L/y95Y6ddZsyyHQopM43/7JOYBwufa07MWaxi 79 | AdBdq1bR/ZeOt2aZaXhV+J6QUoUO8Z6fG6b2cQmvMgk4ybqoeciLII0DfFsyqavu 80 | ip4hRr59AgMBAAECggEAUIw994XwMw922hG/W98gOd5jtHMVJnD73UGQqTGEm+VG 81 | PM+Ux8iWtr/ec3Svo3elW4OkhwlVET9ikAf0u64zVzf769ty4K9YzpDQEEZlUrqL 82 | 6SZVPKxetppKDVKx9G7BT0BAQZ+947h7EIIXwxOeyTOeijkFzSwhqqlwwy4qoqzV 83 | FTQS20QHE62hxzwuS5HBqw8ds183qAg9NbzR0Cp4za9qTiBB6C8KEcLqeatO+q+d 84 | VCDsJcAMZOvW14N6BozKgbQ/WXZQ/3kNUPBndZLzzqaILFNmB1Zf2DVVJ9gU7+EK 85 | xOac60StIfG81NllCTBrmRVq8yitNqwmutHMlxrIkQKBgQDvp39MkEHtNunFGkI5 86 | R8IB5BZjtx5OdRBKkmPasmNU8U0XoQAJUKY/9piIpCtRi87tMXv8WWmlbULi66pu 87 | 4BnMIisw78xlIWRZTSizFrkFcEoVgEnbZBtSrOg/J5PAcjLEGCQoAdmMXAekR2/m 88 | htv7FPijHPNUjyIFLaxwjl9izwKBgQDZ2mQeKNRHjIb5ZBzB0ZCvUy2y4+kaLrhZ 89 | +CWMN1flL4dd1KuZKvCEfHY9kWOjqw6XneN4yT0aPmbBft4fihiiNW0Sm8i+fSpy 90 | g0klw2HJl49wnwctBpRgTdMKGo9n14OGeu0xKOAy7I4j1tKrUXiRWnP9R583Ti7c 91 | w7YHgdHM8wKBgEV147SaPzF08A6bzMPzY2zO4hpmsdcFoQIsKdryR04QXkrR9EO+ 92 | 52C0pYM9Kf0Jq6Ed7ZS3iaJT58YDjjNyqqd648/cQP6yzfYAIiK+HERSRnay5zU6 93 | b5zn1qyvWOi3cLVbVedumdJPvjtEJU/ImKvOaT5FntVMYwzjLw60hTsLAoGAZJnt 94 | UeAY51GFovUQMpDL96q5l7qXknewuhtVe4KzHCrun+3tsDWcDBJNp/DTymjbvDg1 95 | KzoC9XOLkB8+A+KJrZ5uWAGImi7Cw07NIJsxNR7AJonJjolTS4Wkxy2su49SNW/e 96 | yKzPm7SRjwtNDb/5pWXX2kaQx8Fa8qeOD7lrYPECgYAwQ6o0vYmr+L1tOZZgMVv9 97 | Jusa8beVUH5hyduJjmxbYOtFTkggAozdx7rs4BgyRsmDlV48cEmcVf/7IH4gMJLb 98 | O+bbERwCYUChe+piANhnwfwDHzbRd8mmQus54P06X7bWu6Rmi7gbQGVN/Z6VhbIm 99 | D2cOo0w4bk/3yb01xz1MEw== 100 | -----END PRIVATE KEY----- 101 | """ 102 | 103 | let eddsaPublicKeyBase64Url = "0ZcEvMCSYqSwR8XIkxOoaYjRQSAO8frTMSCpNbUl4lE" 104 | let eddsaPrivateKeyBase64Url = "d1H3_dcg0V3XyAuZW2TE5Z3rhY20M-4YAfYu_HUQd8w" 105 | -------------------------------------------------------------------------------- /Benchmarks/Thresholds/Signing.ES256.p90.json: -------------------------------------------------------------------------------- 1 | { 2 | "mallocCountTotal": 271, 3 | "peakMemoryResident": 27000000 4 | } 5 | -------------------------------------------------------------------------------- /Benchmarks/Thresholds/Signing.EdDSA.p90.json: -------------------------------------------------------------------------------- 1 | { 2 | "mallocCountTotal": 117, 3 | "peakMemoryResident": 27000000 4 | } 5 | -------------------------------------------------------------------------------- /Benchmarks/Thresholds/Signing.RSA.p90.json: -------------------------------------------------------------------------------- 1 | { 2 | "mallocCountTotal": 248, 3 | "peakMemoryResident": 27000000 4 | } 5 | -------------------------------------------------------------------------------- /Benchmarks/Thresholds/TokenLifecycle.ES256_Generated.p90.json: -------------------------------------------------------------------------------- 1 | { 2 | "mallocCountTotal": 456, 3 | "peakMemoryResident": 57000000 4 | } 5 | -------------------------------------------------------------------------------- /Benchmarks/Thresholds/TokenLifecycle.ES256_PEM.p90.json: -------------------------------------------------------------------------------- 1 | { 2 | "mallocCountTotal": 372, 3 | "peakMemoryResident": 52500000 4 | } 5 | -------------------------------------------------------------------------------- /Benchmarks/Thresholds/TokenLifecycle.EdDSA_Coordinates.p90.json: -------------------------------------------------------------------------------- 1 | { 2 | "mallocCountTotal": 323, 3 | "peakMemoryResident": 53000000 4 | } 5 | -------------------------------------------------------------------------------- /Benchmarks/Thresholds/TokenLifecycle.EdDSA_Generated.p90.json: -------------------------------------------------------------------------------- 1 | { 2 | "mallocCountTotal": 291, 3 | "peakMemoryResident": 54000000 4 | } 5 | -------------------------------------------------------------------------------- /Benchmarks/Thresholds/TokenLifecycle.RSA_PEM.p90.json: -------------------------------------------------------------------------------- 1 | { 2 | "mallocCountTotal": 445, 3 | "peakMemoryResident": 54500000 4 | } 5 | -------------------------------------------------------------------------------- /Benchmarks/Thresholds/Verifying.ES256.p90.json: -------------------------------------------------------------------------------- 1 | { 2 | "mallocCountTotal": 263, 3 | "peakMemoryResident": 27000000 4 | } 5 | -------------------------------------------------------------------------------- /Benchmarks/Thresholds/Verifying.EdDSA.p90.json: -------------------------------------------------------------------------------- 1 | { 2 | "mallocCountTotal": 251, 3 | "peakMemoryResident": 27000000 4 | } 5 | -------------------------------------------------------------------------------- /Benchmarks/Thresholds/Verifying.RS256.p90.json: -------------------------------------------------------------------------------- 1 | { 2 | "mallocCountTotal": 257, 3 | "peakMemoryResident": 27000000 4 | } 5 | -------------------------------------------------------------------------------- /Benchmarks/TokenLifecycle/TokenLifecycle.swift: -------------------------------------------------------------------------------- 1 | import Benchmark 2 | import Foundation 3 | import JWTKit 4 | 5 | let benchmarks = { 6 | Benchmark.defaultConfiguration = .init( 7 | metrics: [.peakMemoryResident, .mallocCountTotal], 8 | thresholds: [ 9 | .peakMemoryResident: .init( 10 | /// Tolerate up to 4% of difference compared to the threshold. 11 | relative: [.p90: 4], 12 | /// Tolerate up to one million bytes of difference compared to the threshold. 13 | absolute: [.p90: 1_100_000] 14 | ), 15 | .mallocCountTotal: .init( 16 | /// Tolerate up to 1% of difference compared to the threshold. 17 | relative: [.p90: 1], 18 | /// Tolerate up to 2 malloc calls of difference compared to the threshold. 19 | absolute: [.p90: 2] 20 | ), 21 | ] 22 | ) 23 | 24 | Benchmark("ES256 Generated") { benchmark in 25 | for _ in benchmark.scaledIterations { 26 | let key = ES256PrivateKey() 27 | let keyCollection = JWTKeyCollection() 28 | keyCollection.add(ecdsa: key) 29 | let token = try await keyCollection.sign(payload) 30 | _ = try await keyCollection.verify(token, as: Payload.self) 31 | } 32 | } 33 | 34 | Benchmark("ES256 PEM") { benchmark in 35 | for _ in benchmark.scaledIterations { 36 | let key = try ES256PrivateKey(pem: ecdsaPrivateKey) 37 | let keyCollection = JWTKeyCollection() 38 | keyCollection.add(ecdsa: key) 39 | let token = try await keyCollection.sign(payload) 40 | _ = try await keyCollection.verify(token, as: Payload.self) 41 | } 42 | } 43 | 44 | Benchmark("RSA PEM") { benchmark in 45 | for _ in benchmark.scaledIterations { 46 | let key = try Insecure.RSA.PrivateKey(pem: rsaPrivateKey) 47 | let keyCollection = JWTKeyCollection() 48 | keyCollection.add(rsa: key, digestAlgorithm: .sha256) 49 | let token = try await keyCollection.sign(payload) 50 | _ = try await keyCollection.verify(token, as: Payload.self) 51 | } 52 | } 53 | 54 | Benchmark("EdDSA Generated") { benchmark in 55 | for _ in benchmark.scaledIterations { 56 | let key = try EdDSA.PrivateKey() 57 | let keyCollection = JWTKeyCollection() 58 | keyCollection.add(eddsa: key) 59 | let token = try await keyCollection.sign(payload) 60 | _ = try await keyCollection.verify(token, as: Payload.self) 61 | } 62 | } 63 | 64 | Benchmark("EdDSA Coordinates") { benchmark in 65 | for _ in benchmark.scaledIterations { 66 | let key = try EdDSA.PrivateKey(d: eddsaPrivateKeyBase64Url, curve: .ed25519) 67 | let keyCollection = JWTKeyCollection() 68 | keyCollection.add(eddsa: key) 69 | let token = try await keyCollection.sign(payload) 70 | _ = try await keyCollection.verify(token, as: Payload.self) 71 | } 72 | } 73 | } 74 | 75 | struct Payload: JWTPayload { 76 | let name: String 77 | let admin: Bool 78 | 79 | func verify(using signer: some JWTAlgorithm) async throws { 80 | // nothing to verify 81 | } 82 | } 83 | 84 | let payload = Payload(name: "Kyle", admin: true) 85 | 86 | let ecdsaPrivateKey = """ 87 | -----BEGIN PRIVATE KEY----- 88 | MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQg2sD+kukkA8GZUpmm 89 | jRa4fJ9Xa/JnIG4Hpi7tNO66+OGgCgYIKoZIzj0DAQehRANCAATZp0yt0btpR9kf 90 | ntp4oUUzTV0+eTELXxJxFvhnqmgwGAm1iVW132XLrdRG/ntlbQ1yzUuJkHtYBNve 91 | y+77Vzsd 92 | -----END PRIVATE KEY----- 93 | """ 94 | 95 | let rsaPrivateKey = """ 96 | -----BEGIN PRIVATE KEY----- 97 | MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDL8W1D9w5zHpmD 98 | JqpTngIRJ+Sm21e42cRnTudhdejzKUiJQWkHSvQV5yC/+0iEXUsUJYEdSyrKhJFD 99 | PT+IFGdjIiwb7IX+rUreWXlD/YYBL3/byMG4kYoO4oiPp2A+WvfeyLpuN549OXhk 100 | 7o5kXEZjKfjHTfmnAbCMoYW5BEpiHQC3HAeJZ5EiwAn8HZn5UY6lxJcf7H9hR83x 101 | D0W7IZTNyxUu4aLNuihFIxJKgP/L/y95Y6ddZsyyHQopM43/7JOYBwufa07MWaxi 102 | AdBdq1bR/ZeOt2aZaXhV+J6QUoUO8Z6fG6b2cQmvMgk4ybqoeciLII0DfFsyqavu 103 | ip4hRr59AgMBAAECggEAUIw994XwMw922hG/W98gOd5jtHMVJnD73UGQqTGEm+VG 104 | PM+Ux8iWtr/ec3Svo3elW4OkhwlVET9ikAf0u64zVzf769ty4K9YzpDQEEZlUrqL 105 | 6SZVPKxetppKDVKx9G7BT0BAQZ+947h7EIIXwxOeyTOeijkFzSwhqqlwwy4qoqzV 106 | FTQS20QHE62hxzwuS5HBqw8ds183qAg9NbzR0Cp4za9qTiBB6C8KEcLqeatO+q+d 107 | VCDsJcAMZOvW14N6BozKgbQ/WXZQ/3kNUPBndZLzzqaILFNmB1Zf2DVVJ9gU7+EK 108 | xOac60StIfG81NllCTBrmRVq8yitNqwmutHMlxrIkQKBgQDvp39MkEHtNunFGkI5 109 | R8IB5BZjtx5OdRBKkmPasmNU8U0XoQAJUKY/9piIpCtRi87tMXv8WWmlbULi66pu 110 | 4BnMIisw78xlIWRZTSizFrkFcEoVgEnbZBtSrOg/J5PAcjLEGCQoAdmMXAekR2/m 111 | htv7FPijHPNUjyIFLaxwjl9izwKBgQDZ2mQeKNRHjIb5ZBzB0ZCvUy2y4+kaLrhZ 112 | +CWMN1flL4dd1KuZKvCEfHY9kWOjqw6XneN4yT0aPmbBft4fihiiNW0Sm8i+fSpy 113 | g0klw2HJl49wnwctBpRgTdMKGo9n14OGeu0xKOAy7I4j1tKrUXiRWnP9R583Ti7c 114 | w7YHgdHM8wKBgEV147SaPzF08A6bzMPzY2zO4hpmsdcFoQIsKdryR04QXkrR9EO+ 115 | 52C0pYM9Kf0Jq6Ed7ZS3iaJT58YDjjNyqqd648/cQP6yzfYAIiK+HERSRnay5zU6 116 | b5zn1qyvWOi3cLVbVedumdJPvjtEJU/ImKvOaT5FntVMYwzjLw60hTsLAoGAZJnt 117 | UeAY51GFovUQMpDL96q5l7qXknewuhtVe4KzHCrun+3tsDWcDBJNp/DTymjbvDg1 118 | KzoC9XOLkB8+A+KJrZ5uWAGImi7Cw07NIJsxNR7AJonJjolTS4Wkxy2su49SNW/e 119 | yKzPm7SRjwtNDb/5pWXX2kaQx8Fa8qeOD7lrYPECgYAwQ6o0vYmr+L1tOZZgMVv9 120 | Jusa8beVUH5hyduJjmxbYOtFTkggAozdx7rs4BgyRsmDlV48cEmcVf/7IH4gMJLb 121 | O+bbERwCYUChe+piANhnwfwDHzbRd8mmQus54P06X7bWu6Rmi7gbQGVN/Z6VhbIm 122 | D2cOo0w4bk/3yb01xz1MEw== 123 | -----END PRIVATE KEY----- 124 | """ 125 | 126 | let eddsaPublicKeyBase64Url = "0ZcEvMCSYqSwR8XIkxOoaYjRQSAO8frTMSCpNbUl4lE" 127 | let eddsaPrivateKeyBase64Url = "d1H3_dcg0V3XyAuZW2TE5Z3rhY20M-4YAfYu_HUQd8w" 128 | -------------------------------------------------------------------------------- /Benchmarks/Verifying/Verifying.swift: -------------------------------------------------------------------------------- 1 | import Benchmark 2 | import Foundation 3 | import JWTKit 4 | 5 | let benchmarks = { 6 | Benchmark.defaultConfiguration = .init( 7 | metrics: [.peakMemoryResident, .mallocCountTotal], 8 | thresholds: [ 9 | .peakMemoryResident: .init( 10 | /// Tolerate up to 4% of difference compared to the threshold. 11 | relative: [.p90: 4], 12 | /// Tolerate up to one million bytes of difference compared to the threshold. 13 | absolute: [.p90: 1_100_000] 14 | ), 15 | .mallocCountTotal: .init( 16 | /// Tolerate up to 1% of difference compared to the threshold. 17 | relative: [.p90: 1], 18 | /// Tolerate up to 2 malloc calls of difference compared to the threshold. 19 | absolute: [.p90: 2] 20 | ), 21 | ] 22 | ) 23 | 24 | Benchmark("ES256") { benchmark in 25 | let pem = """ 26 | -----BEGIN PUBLIC KEY----- 27 | MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEVs/o5+uQbTjL3chynL4wXgUg2R9 28 | q9UU8I5mEovUf86QZ7kOBIjJwqnzD1omageEHWwHdBO6B+dFabmdT9POxg== 29 | -----END PUBLIC KEY----- 30 | """ 31 | let token = 32 | "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSm9obiBEb2UiLCJhZG1pbiI6dHJ1ZX0.vY4BbTLWcVbA4sS_EnaSV-exTZT3mRpH6JNc5C7XiUDA1PfbTO6LdObMFYPEcKZMydfHy6SJz1eJySq2uYBLAA" 33 | let key = try ES256PublicKey(pem: pem) 34 | let keyCollection = JWTKeyCollection().add(ecdsa: key) 35 | for _ in benchmark.scaledIterations { 36 | _ = try await keyCollection.verify(token, as: Payload.self) 37 | } 38 | } 39 | 40 | Benchmark("RS256") { benchmark in 41 | let pem = """ 42 | -----BEGIN PUBLIC KEY----- 43 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu1SU1LfVLPHCozMxH2Mo 44 | 4lgOEePzNm0tRgeLezV6ffAt0gunVTLw7onLRnrq0/IzW7yWR7QkrmBL7jTKEn5u 45 | +qKhbwKfBstIs+bMY2Zkp18gnTxKLxoS2tFczGkPLPgizskuemMghRniWaoLcyeh 46 | kd3qqGElvW/VDL5AaWTg0nLVkjRo9z+40RQzuVaE8AkAFmxZzow3x+VJYKdjykkJ 47 | 0iT9wCS0DRTXu269V264Vf/3jvredZiKRkgwlL9xNAwxXFg0x/XFw005UWVRIkdg 48 | cKWTjpBP2dPwVZ4WWC+9aGVd+Gyn1o0CLelf4rEjGoXbAAEgAqeGUxrcIlbjXfbc 49 | mwIDAQAB 50 | -----END PUBLIC KEY----- 51 | """ 52 | let token = 53 | "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSm9obiBEb2UiLCJhZG1pbiI6dHJ1ZX0.AJCCTYfWKXUPf6dztCbYoVdB4E7FjvmD9WogWtZv20mL6Urt-fFU2DntUIBmoFXGJ5424ubslBt6sk5yBxS_DMnXKfhj2R-J6xDkT0vlldfFzrrDSQEIsbiErfmfVK40Fr9MW4XFKBZdKEI6X35SCmLx9s5RsQCejIo9pdHyx6jGbfXqN_04RWprx6pcqqOn6_Gm4jkofAd1duZ_IUlojUBKX56OgEweR_2glQ8uumb-oklwYl699ZF9DmTKRHHE2RMMT2QVy0RWl1R7HIvUOY0EzxeuKDiiOQC1bFxIH_EZpqBp5FbfW0iemK6Tm5v7_8UzEOmIVrFUIpqxwrI3Sg" 54 | let key = try Insecure.RSA.PublicKey(pem: pem) 55 | let keyCollection = JWTKeyCollection().add(rsa: key, digestAlgorithm: .sha256) 56 | for _ in benchmark.scaledIterations { 57 | _ = try await keyCollection.verify(token, as: Payload.self) 58 | } 59 | } 60 | 61 | Benchmark("EdDSA") { benchmark in 62 | let eddsaPublicKeyBase64Url = "0ZcEvMCSYqSwR8XIkxOoaYjRQSAO8frTMSCpNbUl4lE" 63 | let eddsaPrivateKeyBase64Url = "d1H3_dcg0V3XyAuZW2TE5Z3rhY20M-4YAfYu_HUQd8w" 64 | let keyCollection = try JWTKeyCollection() 65 | .add(eddsa: EdDSA.PrivateKey(d: eddsaPrivateKeyBase64Url, curve: .ed25519)) 66 | let token = 67 | "eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSJ9.eyJuYW1lIjoiSm9obiBEb2UiLCJhZG1pbiI6dHJ1ZX0.UzwX3znh-Ne180bcZqjbTk8Dx-BmHR_IL6b8wR2K2AG5f8ny-vThSL0b9IUvR8ybDkUiubpqlKKQXrRtbKQzAA" 68 | for _ in benchmark.scaledIterations { 69 | _ = try await keyCollection.verify(token, as: Payload.self) 70 | } 71 | } 72 | } 73 | 74 | struct Payload: JWTPayload { 75 | let name: String 76 | let admin: Bool 77 | 78 | func verify(using signer: some JWTAlgorithm) async throws { 79 | // nothing to verify 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Qutheory, LLC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /NOTICES.txt: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Vapor open source project 4 | // 5 | // Copyright (c) 2017-2020 Vapor project authors 6 | // Licensed under MIT 7 | // 8 | // See LICENSE for license information 9 | // 10 | // SPDX-License-Identifier: MIT 11 | // 12 | //===----------------------------------------------------------------------===// 13 | 14 | This product contains a derivation of the Wycheproof tests from the Swift Crypto package. 15 | 16 | * LICENSE (Apache License 2.0): 17 | * https://www.apache.org/licenses/LICENSE-2.0 18 | * HOMEPAGE: 19 | * https://github.com/apple/swift-crypto 20 | 21 | This product contains the SubjectPublicKeyInfo.swift file from the Swift ASN1 package. 22 | 23 | * LICENSE (Apache License 2.0): 24 | * https://www.apache.org/licenses/LICENSE-2.0 25 | * HOMEPAGE: 26 | * https://github.com/apple/swift-asn1 27 | 28 | This product contains the NIOCompression and NIOCompression namespaces from the SwiftNIO Extras package. 29 | 30 | * LICENSE (Apache License 2.0): 31 | * https://www.apache.org/licenses/LICENSE-2.0 32 | * HOMEPAGE: 33 | * https://github.com/apple/swift-nio-extras 34 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:6.0 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "jwt-kit", 6 | platforms: [ 7 | .macOS(.v13), 8 | .iOS(.v15), 9 | .tvOS(.v15), 10 | .watchOS(.v8), 11 | ], 12 | products: [ 13 | .library(name: "JWTKit", targets: ["JWTKit"]) 14 | ], 15 | dependencies: [ 16 | .package(url: "https://github.com/apple/swift-crypto.git", "3.8.0"..<"5.0.0"), 17 | .package(url: "https://github.com/apple/swift-certificates.git", from: "1.2.0"), 18 | .package(url: "https://github.com/apple/swift-log.git", from: "1.0.0"), 19 | ], 20 | targets: [ 21 | .target( 22 | name: "JWTKit", 23 | dependencies: [ 24 | .product(name: "Crypto", package: "swift-crypto"), 25 | .product(name: "_CryptoExtras", package: "swift-crypto"), 26 | .product(name: "X509", package: "swift-certificates"), 27 | .product(name: "Logging", package: "swift-log"), 28 | ] 29 | ), 30 | .testTarget( 31 | name: "JWTKitTests", 32 | dependencies: [ 33 | "JWTKit" 34 | ] 35 | ), 36 | ] 37 | ) 38 | -------------------------------------------------------------------------------- /Snippets/JWKExamples.swift: -------------------------------------------------------------------------------- 1 | import JWTKit 2 | 3 | #if !canImport(Darwin) 4 | import FoundationEssentials 5 | #else 6 | import Foundation 7 | #endif 8 | 9 | let rsaModulus = "..." 10 | 11 | let json = """ 12 | { 13 | "keys": [ 14 | {"kty": "RSA", "alg": "RS256", "kid": "a", "n": "\(rsaModulus)", "e": "AQAB"}, 15 | {"kty": "RSA", "alg": "RS512", "kid": "b", "n": "\(rsaModulus)", "e": "AQAB"}, 16 | ] 17 | } 18 | """ 19 | 20 | // Create key collection and add JWKS 21 | let keys = try await JWTKeyCollection().add(jwksJSON: json) 22 | -------------------------------------------------------------------------------- /Snippets/JWTKitExamples.swift: -------------------------------------------------------------------------------- 1 | // snippet.KEY_COLLECTION 2 | import JWTKit 3 | 4 | #if !canImport(Darwin) 5 | import FoundationEssentials 6 | #else 7 | import Foundation 8 | #endif 9 | 10 | // Signs and verifies JWTs 11 | let keys = JWTKeyCollection() 12 | 13 | // snippet.EXAMPLE_PAYLOAD 14 | struct ExamplePayload: JWTPayload { 15 | var sub: SubjectClaim 16 | var exp: ExpirationClaim 17 | var admin: BoolClaim 18 | 19 | func verify(using _: some JWTAlgorithm) throws { 20 | try self.exp.verifyNotExpired() 21 | } 22 | } 23 | 24 | // snippet.KEY_COLLECTION_ADD_HS256 25 | // Registers an HS256 (HMAC-SHA-256) signer. 26 | await keys.add(hmac: "secret", digestAlgorithm: .sha256) 27 | 28 | // snippet.KEY_COLLECTION_ADD_HS256_KID 29 | // Registers an HS256 (HMAC-SHA-256) signer with a key identifier. 30 | await keys.add(hmac: "secret", digestAlgorithm: .sha256, kid: "my-key") 31 | 32 | // snippet.KEY_COLLECTION_CREATE_ES256 33 | let ecdsaPublicKey = "-----BEGIN PUBLIC KEY----- ... -----END PUBLIC KEY-----" 34 | 35 | // Initialize an ECDSA key with public pem. 36 | let key = try ES256PublicKey(pem: ecdsaPublicKey) 37 | 38 | // snippet.KEY_COLLECTION_ADD_ES256 39 | await keys.add(ecdsa: key) 40 | 41 | // snippet.end 42 | do { 43 | // Create a new instance of our JWTPayload 44 | let payload = ExamplePayload( 45 | sub: "vapor", 46 | exp: .init(value: .distantFuture), 47 | admin: true 48 | ) 49 | 50 | // snippet.EXAMPLE_PAYLOAD_SIGN 51 | // Sign the payload, returning the JWT as String 52 | let jwt = try await keys.sign(payload, header: ["kid": "my-key"]) 53 | print(jwt) 54 | // snippet.end 55 | } 56 | 57 | do { 58 | // snippet.VERIFYING 59 | let exampleJWT = """ 60 | eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ2YXBvciIsImV4cCI6NjQwOTIyMTEyMDAsImFkbWluIjp0cnVlfQ.lS5lpwfRNSZDvpGQk6x5JI1g40gkYCOWqbc3J_ghowo 61 | """ 62 | 63 | // snippet.VERIFYING_PAYLOAD 64 | // Parse the JWT, verifies its signature, and decodes its content 65 | let payload = try await keys.verify(exampleJWT, as: ExamplePayload.self) 66 | print(payload) 67 | // snippet.end 68 | } 69 | 70 | do { 71 | // snippet.EDDSA 72 | // Initialize an EdDSA key with public PEM 73 | let publicKey = try EdDSA.PublicKey(x: "...", curve: .ed25519) 74 | 75 | // Initialize an EdDSA key with private PEM 76 | let privateKey = try EdDSA.PrivateKey(d: "...", curve: .ed25519) 77 | 78 | // Add public key to the key collection 79 | await keys.add(eddsa: publicKey) 80 | 81 | // Add private key to the key collection 82 | await keys.add(eddsa: privateKey) 83 | // snippet.end 84 | } 85 | 86 | do { 87 | // snippet.RSA 88 | // Initialize an RSA key with components. 89 | let key = try Insecure.RSA.PrivateKey( 90 | modulus: "...", 91 | exponent: "...", 92 | privateExponent: "..." 93 | ) 94 | // snippet.end 95 | _ = key 96 | } 97 | 98 | do { 99 | // snippet.RSA_FROM_PEM 100 | let rsaPublicKey = "-----BEGIN PUBLIC KEY----- ... -----END PUBLIC KEY-----" 101 | 102 | // Initialize an RSA key with public PEM 103 | let key = try Insecure.RSA.PublicKey(pem: rsaPublicKey) 104 | 105 | // snippet.RSA_ADD 106 | // Add RSA with SHA-256 algorithm 107 | await keys.add(rsa: key, digestAlgorithm: .sha256) 108 | 109 | // Add RSA with SHA-512 and PSS padding algorithm 110 | await keys.add(pss: key, digestAlgorithm: .sha512) 111 | // snippet.end 112 | } 113 | 114 | extension DataProtocol { 115 | func base64URLDecodedBytes() -> [UInt8] { 116 | let string = String(decoding: self, as: UTF8.self) 117 | .replacingOccurrences(of: "-", with: "+") 118 | .replacingOccurrences(of: "_", with: "/") 119 | let padding = string.count % 4 == 0 ? "" : String(repeating: "=", count: 4 - string.count % 4) 120 | return [UInt8](Data(base64Encoded: string + padding) ?? Data()) 121 | } 122 | 123 | func base64URLEncodedBytes() -> [UInt8] { 124 | Data(self).base64EncodedString() 125 | .replacingOccurrences(of: "+", with: "-") 126 | .replacingOccurrences(of: "/", with: "_") 127 | .replacingOccurrences(of: "=", with: "") 128 | .utf8 129 | .map { UInt8($0) } 130 | } 131 | } 132 | 133 | // snippet.CUSTOM_SERIALIZER 134 | struct CustomSerializer: JWTSerializer { 135 | var jsonEncoder: JWTJSONEncoder = .defaultForJWT 136 | 137 | func serialize(_ payload: some JWTPayload, header: JWTHeader) throws -> Data { 138 | if header.b64?.asBool == true { 139 | try Data(self.jsonEncoder.encode(payload).base64URLEncodedBytes()) 140 | } else { 141 | try self.jsonEncoder.encode(payload) 142 | } 143 | } 144 | } 145 | 146 | struct CustomParser: JWTParser { 147 | var jsonDecoder: JWTJSONDecoder = .defaultForJWT 148 | 149 | func parse(_ token: some DataProtocol, as _: Payload.Type) throws -> ( 150 | header: JWTHeader, payload: Payload, signature: Data 151 | ) where Payload: JWTPayload { 152 | let (encodedHeader, encodedPayload, encodedSignature) = try getTokenParts(token) 153 | 154 | let header = try jsonDecoder.decode( 155 | JWTHeader.self, from: .init(encodedHeader.base64URLDecodedBytes())) 156 | 157 | let payload = 158 | if header.b64?.asBool ?? true { 159 | try self.jsonDecoder.decode(Payload.self, from: .init(encodedPayload.base64URLDecodedBytes())) 160 | } else { 161 | try self.jsonDecoder.decode(Payload.self, from: .init(encodedPayload)) 162 | } 163 | 164 | let signature = Data(encodedSignature.base64URLDecodedBytes()) 165 | 166 | return (header: header, payload: payload, signature: signature) 167 | } 168 | } 169 | 170 | // snippet.end 171 | 172 | do { 173 | // snippet.CUSTOM_SIGNING 174 | let keyCollection = await JWTKeyCollection() 175 | .add(hmac: "secret", digestAlgorithm: .sha256, parser: CustomParser(), serializer: CustomSerializer()) 176 | 177 | let payload = ExamplePayload(sub: "vapor", exp: .init(value: .init(timeIntervalSince1970: 2_000_000_000)), admin: false) 178 | 179 | let token = try await keyCollection.sign(payload, header: ["b64": true]) 180 | // snippet.end 181 | _ = token 182 | } 183 | 184 | do { 185 | // snippet.CUSTOM_ENCODING 186 | let encoder = JSONEncoder() 187 | encoder.dateEncodingStrategy = .iso8601 188 | let decoder = JSONDecoder() 189 | decoder.dateDecodingStrategy = .iso8601 190 | 191 | let parser = DefaultJWTParser(jsonDecoder: decoder) 192 | let serializer = DefaultJWTSerializer(jsonEncoder: encoder) 193 | 194 | let keyCollection = await JWTKeyCollection() 195 | .add(hmac: "secret", digestAlgorithm: .sha256, parser: parser, serializer: serializer) 196 | // snippet.end 197 | _ = keyCollection 198 | } 199 | -------------------------------------------------------------------------------- /Sources/JWTKit/Claims/AudienceClaim.swift: -------------------------------------------------------------------------------- 1 | /// The "aud" (audience) claim identifies the recipients that the JWT is 2 | /// intended for. Each principal intended to process the JWT MUST 3 | /// identify itself with a value in the audience claim. If the principal 4 | /// processing the claim does not identify itself with a value in the 5 | /// "aud" claim when this claim is present, then the JWT MUST be 6 | /// rejected. In the general case, the "aud" value is an array of case- 7 | /// sensitive strings, each containing a StringOrURI value. In the 8 | /// special case when the JWT has one audience, the "aud" value MAY be a 9 | /// single case-sensitive string containing a StringOrURI value. The 10 | /// interpretation of audience values is generally application specific. 11 | /// Use of this claim is OPTIONAL. 12 | public struct AudienceClaim: JWTMultiValueClaim, Equatable, ExpressibleByStringLiteral { 13 | /// See ``JWTClaim``. 14 | public var value: [String] 15 | 16 | /// See ``JWTClaim``. 17 | public init(value: [String]) { 18 | precondition(!value.isEmpty, "An audience claim must have at least one audience.") 19 | self.value = value 20 | } 21 | 22 | /// See `ExpressibleByStringLiteral`. 23 | public init(stringLiteral value: String) { 24 | self.init(value: value) 25 | } 26 | 27 | /// Verify that the given audience is included as one of the claim's 28 | /// intended audiences by simple string comparison. 29 | public func verifyIntendedAudience(includes audience: String) throws { 30 | guard self.value.contains(audience) else { 31 | throw JWTError.claimVerificationFailure(failedClaim: self, reason: "not intended for \(audience)") 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/JWTKit/Claims/BoolClaim.swift: -------------------------------------------------------------------------------- 1 | /// A claim which represents a bool 2 | /// 3 | /// If a string is provided, and the string doesn't represent a bool, then `false` will be used. 4 | public struct BoolClaim: JWTClaim, Equatable, ExpressibleByStringLiteral, ExpressibleByBooleanLiteral { 5 | /// See ``JWTClaim``. 6 | public var value: Bool 7 | 8 | /// See ``JWTClaim``. 9 | public init(value: Bool) { 10 | self.value = value 11 | } 12 | 13 | public init(stringLiteral value: String) { 14 | self.value = Bool(value) ?? false 15 | } 16 | 17 | public init(booleanLiteral value: Bool) { 18 | self.value = value 19 | } 20 | 21 | public init(from decoder: Decoder) throws { 22 | let single = try decoder.singleValueContainer() 23 | 24 | do { 25 | try self.init(value: single.decode(Bool.self)) 26 | } catch { 27 | let str = try single.decode(String.self) 28 | guard let bool = Bool(str) else { 29 | throw JWTError.invalidBool(str) 30 | } 31 | 32 | self.init(value: bool) 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/JWTKit/Claims/ExpirationClaim.swift: -------------------------------------------------------------------------------- 1 | #if !canImport(Darwin) 2 | import FoundationEssentials 3 | #else 4 | import Foundation 5 | #endif 6 | 7 | /// The "exp" (expiration time) claim identifies the expiration time on 8 | /// or after which the JWT MUST NOT be accepted for processing. The 9 | /// processing of the "exp" claim requires that the current date/time 10 | /// MUST be before the expiration date/time listed in the "exp" claim. 11 | /// Implementers MAY provide for some small leeway, usually no more than 12 | /// a few minutes, to account for clock skew. Its value MUST be a number 13 | /// containing a NumericDate value. Use of this claim is OPTIONAL. 14 | public struct ExpirationClaim: JWTUnixEpochClaim, Equatable { 15 | /// See ``JWTClaim``. 16 | public var value: Date 17 | 18 | /// See ``JWTClaim``. 19 | public init(value: Date) { 20 | self.value = value 21 | } 22 | 23 | /// Throws an error if the claim's date is later than current date. 24 | public func verifyNotExpired(currentDate: Date = .init()) throws { 25 | switch self.value.compare(currentDate) { 26 | case .orderedAscending, .orderedSame: 27 | throw JWTError.claimVerificationFailure(failedClaim: self, reason: "expired") 28 | case .orderedDescending: 29 | break 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/JWTKit/Claims/GoogleHostedDomainClaim.swift: -------------------------------------------------------------------------------- 1 | public struct GoogleHostedDomainClaim: JWTClaim, Equatable, ExpressibleByStringLiteral { 2 | /// See ``JWTClaim``. 3 | public var value: String 4 | 5 | /// See ``JWTClaim``. 6 | public init(value: String) { 7 | self.value = value 8 | } 9 | 10 | public func verify(domain: String) throws { 11 | guard value == domain else { 12 | throw JWTError.claimVerificationFailure(failedClaim: self, reason: "\(value) is invalid") 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Sources/JWTKit/Claims/IDClaim.swift: -------------------------------------------------------------------------------- 1 | /// The "jti" (JWT ID) claim provides a unique identifier for the JWT. 2 | /// The identifier value MUST be assigned in a manner that ensures that 3 | /// there is a negligible probability that the same value will be 4 | /// accidentally assigned to a different data object; if the application 5 | /// uses multiple issuers, collisions MUST be prevented among values 6 | /// produced by different issuers as well. The "jti" claim can be used 7 | /// to prevent the JWT from being replayed. The "jti" value is a case- 8 | /// sensitive string. Use of this claim is OPTIONAL. 9 | public struct IDClaim: JWTClaim, Equatable, ExpressibleByStringLiteral { 10 | /// See ``JWTClaim``. 11 | public var value: String 12 | 13 | /// See ``JWTClaim``. 14 | public init(value: String) { 15 | self.value = value 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/JWTKit/Claims/IssuedAtClaim.swift: -------------------------------------------------------------------------------- 1 | #if !canImport(Darwin) 2 | import FoundationEssentials 3 | #else 4 | import Foundation 5 | #endif 6 | 7 | /// The "iat" (issued at) claim identifies the time at which the JWT was 8 | /// issued. This claim can be used to determine the age of the JWT. Its 9 | /// value MUST be a number containing a NumericDate value. Use of this 10 | /// claim is OPTIONAL. 11 | public struct IssuedAtClaim: JWTUnixEpochClaim, Equatable { 12 | /// See ``JWTClaim``. 13 | public var value: Date 14 | 15 | /// See ``JWTClaim``. 16 | public init(value: Date) { 17 | self.value = value 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/JWTKit/Claims/IssuerClaim.swift: -------------------------------------------------------------------------------- 1 | /// The "iss" (issuer) claim identifies the principal that issued the 2 | /// JWT. The processing of this claim is generally application specific. 3 | /// The "iss" value is a case-sensitive string containing a StringOrURI 4 | /// value. Use of this claim is OPTIONAL. 5 | public struct IssuerClaim: JWTClaim, Equatable, ExpressibleByStringLiteral { 6 | /// See ``JWTClaim``. 7 | public var value: String 8 | 9 | /// See ``JWTClaim``. 10 | public init(value: String) { 11 | self.value = value 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Sources/JWTKit/Claims/JWTClaim.swift: -------------------------------------------------------------------------------- 1 | /// A claim is a codable, top-level property of a JWT payload. Multiple claims form a payload. 2 | /// Some claims, such as expiration claims, are inherently verifiable. Each claim able to verify 3 | /// itself provides an appropriate method for doing so, depending on the specific claim. 4 | public protocol JWTClaim: Codable, Sendable { 5 | /// The associated value type. 6 | associatedtype Value: Codable 7 | 8 | /// The claim's value. 9 | var value: Value { get set } 10 | 11 | /// Initializes the claim with its value. 12 | init(value: Value) 13 | } 14 | 15 | extension JWTClaim where Value == String, Self: ExpressibleByStringLiteral { 16 | /// See `ExpressibleByStringLiteral`. 17 | public init(stringLiteral string: String) { 18 | self.init(value: string) 19 | } 20 | } 21 | 22 | extension JWTClaim { 23 | /// See `Decodable`. 24 | public init(from decoder: Decoder) throws { 25 | let single = try decoder.singleValueContainer() 26 | try self.init(value: single.decode(Value.self)) 27 | } 28 | 29 | /// See `Encodable`. 30 | public func encode(to encoder: Encoder) throws { 31 | var single = encoder.singleValueContainer() 32 | try single.encode(value) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/JWTKit/Claims/JWTMultiValueClaim.swift: -------------------------------------------------------------------------------- 1 | #if !canImport(Darwin) 2 | import FoundationEssentials 3 | #else 4 | import Foundation 5 | #endif 6 | 7 | public protocol JWTMultiValueClaim: JWTClaim where Value: Collection, Value.Element: Codable { 8 | init(value: Value.Element) 9 | } 10 | 11 | extension JWTMultiValueClaim { 12 | /// Single-element initializer. Uses the `CollectionOfOneDecoder` to work 13 | /// around the lack of an initializer on the `Collection` protocol. Not 14 | /// spectacularly efficient, but it works. 15 | public init(value: Value.Element) { 16 | self.init(value: try! CollectionOfOneDecoder.decode(value)) 17 | } 18 | 19 | /// Because multi-value claims can take either singular or plural form in 20 | /// JSON, the default conformance to `Decodable` from ``JWTClaim`` isn't good 21 | /// enough. 22 | /// 23 | /// - Note: The spec is mute on what multi-value claims like `aud` with an 24 | /// empty list of values would be considered to represent - whether it 25 | /// would be the same as having no claim at all, or represent a token 26 | /// making the claim but with zero values. For maximal flexibility, this 27 | /// implementation accepts an empty unkeyed container (in JSON, `[]`) 28 | /// silently. 29 | /// 30 | /// - Note: It would be preferable to be able to safely decode the empty 31 | /// array from a lack of _any_ encoded value. This is precluded by the way 32 | /// `Codable` works, as either the claim would have to be marked 33 | /// optional in the payload, leading to the ambiguity of having both `nil` 34 | /// and `[]` representations, each payload type would have to manually 35 | /// implement `init(from decoder:)` to use `decodeIfPresent(_:forKey:)` 36 | /// and a fallback value, or we would have to export extensions on 37 | /// `KeyedEncodingContainer` and `KeyedEncodingContainerProtocol` to 38 | /// explicitly override behavior for types confroming to 39 | /// ``JWTMultiValueClaim``, a tricky and error-prone approach relying on 40 | /// poorly-understood mechanics of static versus dynamic dispatch. 41 | /// 42 | /// - Note: The spec is also mute regarding the behavior of duplicate values 43 | /// in a list of more than one. This implementation behaves according to 44 | /// the semantics of the particular `Collection` type used as its value; 45 | /// `Array` will preserve ordering and duplicates, `Set` will not. 46 | public init(from decoder: Decoder) throws { 47 | let container = try decoder.singleValueContainer() 48 | 49 | do { 50 | try self.init(value: container.decode(Value.Element.self)) 51 | } catch DecodingError.typeMismatch(let type, let context) 52 | where type == Value.Element.self 53 | && context.codingPath.count == container.codingPath.count 54 | { 55 | // Unfortunately, `typeMismatch()` doesn't let us explicitly look for what type found, 56 | // only what type was expected, so we have to match the coding path depth instead. 57 | try self.init(value: container.decode(Value.self)) 58 | } 59 | } 60 | 61 | /// This claim can take either singular or plural form in JSON, with the 62 | /// singular being overwhelmingly more common, so when there is only one 63 | /// value, ensure it is encoded as a scalar, not an array. 64 | /// 65 | /// - Note: As in decoding, the implementation takes a conservative approach 66 | /// with regards to the importance of ordering and the handling of 67 | /// duplicate values by simply encoding what's there without further 68 | /// analysis or filtering. 69 | /// 70 | /// - Warning: If the claim has zero values, this implementation will encode 71 | /// an inefficient zero-element representation. See the notes regarding 72 | /// this on `init(from decoder:)` above. 73 | public func encode(to encoder: Encoder) throws { 74 | var container = encoder.singleValueContainer() 75 | 76 | switch self.value.first { 77 | case .some(let value) where self.value.count == 1: 78 | try container.encode(value) 79 | default: 80 | try container.encode(self.value) 81 | } 82 | } 83 | } 84 | 85 | /// An extremely specialized `Decoder` whose only purpose is to spoon-feed the 86 | /// type being decoded a single unkeyed element. This ridiculously intricate 87 | /// workaround is used to get around the problem of `Collection` not having any 88 | /// initializers for the single-value initializer of ``JWTMultiValueClaim``. The 89 | /// other workaround would be to require conformance to 90 | /// `ExpressibleByArrayLiteral`, but what fun would that be? 91 | private struct CollectionOfOneDecoder: Decoder, UnkeyedDecodingContainer where T: Collection, T: Codable, T.Element: Codable { 92 | static func decode(_ element: T.Element) throws -> T { 93 | try T(from: self.init(value: element)) 94 | } 95 | 96 | /// The single value we're returning. 97 | var value: T.Element 98 | 99 | /// The `currentIndex` for ``UnkeyedDecodingContainer``. 100 | var currentIndex: Int = 0 101 | 102 | /// We are our own unkeyed decoding container. 103 | func unkeyedContainer() throws -> UnkeyedDecodingContainer { 104 | return self 105 | } 106 | 107 | /// Standard `decodeNil()` implementation. We could ask the value for its 108 | /// `nil`-ness, but returning `false` unconditionally will cause `Codable` 109 | /// to just defer to `Optional`'s decodable implementation anyway. 110 | mutating func decodeNil() throws -> Bool { 111 | return false 112 | } 113 | 114 | /// Standard `decode(_:)` implementation. If the type is correct, we 115 | /// return our singular value, otherwise error. We throw nice errors instead 116 | /// of using `fatalError()` mostly just in case someone implemented a 117 | /// ``Collection`` with a really weird `Decodable` conformance. 118 | mutating func decode(_: U.Type) throws -> U where U: Decodable { 119 | guard !self.isAtEnd else { 120 | throw DecodingError.valueNotFound(U.self, .init(codingPath: [], debugDescription: "Unkeyed container went past the end?")) 121 | } 122 | 123 | guard U.self == T.Element.self else { 124 | throw DecodingError.typeMismatch(U.self, .init(codingPath: [], debugDescription: "Asked for the wrong type!")) 125 | } 126 | 127 | self.currentIndex += 1 128 | return value as! U 129 | } 130 | 131 | /// The error we throw for all operations we don't support (which is most of them). 132 | private var unsupportedError: DecodingError { 133 | return DecodingError.typeMismatch(Any.self, .init(codingPath: [], debugDescription: "This decoder doesn't support most things.")) 134 | } 135 | 136 | // ``Decoder`` and ``UnkeyedDecodingContainer`` conformance requirements. We don't bother tracking any coding path or 137 | // user info and we just fail instantly if asked for anything other than an unnested unkeyed container. The count 138 | // of the unkeyed container is always exactly one. 139 | 140 | var codingPath: [CodingKey] = [] 141 | var userInfo: [CodingUserInfoKey: Any] = [:] 142 | var isAtEnd: Bool { currentIndex != 0 } 143 | var count: Int? = 1 144 | 145 | func container(keyedBy _: Key.Type) throws -> KeyedDecodingContainer where Key: CodingKey { 146 | throw self.unsupportedError 147 | } 148 | 149 | func singleValueContainer() throws -> SingleValueDecodingContainer { 150 | throw self.unsupportedError 151 | } 152 | 153 | mutating func nestedContainer(keyedBy _: N.Type) throws -> KeyedDecodingContainer where N: CodingKey { 154 | throw self.unsupportedError 155 | } 156 | 157 | mutating func nestedUnkeyedContainer() throws -> UnkeyedDecodingContainer { 158 | throw self.unsupportedError 159 | } 160 | 161 | mutating func superDecoder() throws -> Decoder { 162 | throw self.unsupportedError 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /Sources/JWTKit/Claims/JWTUnixEpochClaim.swift: -------------------------------------------------------------------------------- 1 | #if !canImport(Darwin) 2 | import FoundationEssentials 3 | #else 4 | import Foundation 5 | #endif 6 | 7 | public protocol JWTUnixEpochClaim: JWTClaim where Value == Date {} 8 | 9 | extension JWTUnixEpochClaim { 10 | /// See `Decodable`. 11 | public init(from decoder: Decoder) throws { 12 | let container = try decoder.singleValueContainer() 13 | try self.init(value: container.decode(Date.self)) 14 | } 15 | 16 | /// See `Encodable`. 17 | public func encode(to encoder: Encoder) throws { 18 | var container = encoder.singleValueContainer() 19 | try container.encode(self.value) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/JWTKit/Claims/LocaleClaim.swift: -------------------------------------------------------------------------------- 1 | #if !canImport(Darwin) 2 | import FoundationEssentials 3 | #else 4 | import Foundation 5 | #endif 6 | 7 | public struct LocaleClaim: JWTClaim, Equatable, ExpressibleByStringLiteral { 8 | /// See ``JWTClaim``. 9 | public var value: Locale 10 | 11 | /// See ``JWTClaim``. 12 | public init(value: Locale) { 13 | self.value = value 14 | } 15 | 16 | public init(stringLiteral value: String) { 17 | self.value = Locale(identifier: value) 18 | } 19 | 20 | /// See `Decodable`. 21 | public init(from decoder: Decoder) throws { 22 | let container = try decoder.singleValueContainer() 23 | try self.init(value: Locale(identifier: container.decode(String.self))) 24 | } 25 | 26 | /// See `Encodable`. 27 | public func encode(to encoder: Encoder) throws { 28 | var container = encoder.singleValueContainer() 29 | try container.encode(value.identifier) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/JWTKit/Claims/NotBeforeClaim.swift: -------------------------------------------------------------------------------- 1 | #if !canImport(Darwin) 2 | import FoundationEssentials 3 | #else 4 | import Foundation 5 | #endif 6 | 7 | /// The "nbf" (not before) claim identifies the time before which the JWT 8 | /// MUST NOT be accepted for processing. The processing of the "nbf" 9 | /// claim requires that the current date/time MUST be after or equal to 10 | /// the not-before date/time listed in the "nbf" claim. Implementers MAY 11 | /// provide for some small leeway, usually no more than a few minutes, to 12 | /// account for clock skew. Its value MUST be a number containing a 13 | /// NumericDate value. Use of this claim is OPTIONAL. 14 | public struct NotBeforeClaim: JWTUnixEpochClaim, Equatable { 15 | /// See ``JWTClaim``. 16 | public var value: Date 17 | 18 | /// See ``JWTClaim``. 19 | public init(value: Date) { 20 | self.value = value 21 | } 22 | 23 | /// Throws an error if the claim's date is earlier than current date. 24 | public func verifyNotBefore(currentDate: Date = .init()) throws { 25 | switch value.compare(currentDate) { 26 | case .orderedDescending: 27 | throw JWTError.claimVerificationFailure(failedClaim: self, reason: "too soon") 28 | case .orderedAscending, .orderedSame: 29 | break 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/JWTKit/Claims/SubjectClaim.swift: -------------------------------------------------------------------------------- 1 | /// The "sub" (subject) claim identifies the principal that is the 2 | /// subject of the JWT. The claims in a JWT are normally statements 3 | /// about the subject. The subject value MUST either be scoped to be 4 | /// locally unique in the context of the issuer or be globally unique. 5 | /// The processing of this claim is generally application specific. The 6 | /// "sub" value is a case-sensitive string containing a StringOrURI 7 | /// value. Use of this claim is OPTIONAL. 8 | public struct SubjectClaim: JWTClaim, Equatable, ExpressibleByStringLiteral { 9 | /// See ``JWTClaim``. 10 | public var value: String 11 | 12 | /// See ``JWTClaim``. 13 | public init(value: String) { 14 | self.value = value 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Sources/JWTKit/Claims/TenantIDClaim.swift: -------------------------------------------------------------------------------- 1 | /// The "tid" (tenant ID) claim represents the unique identifier of the Azure AD tenant 2 | /// that the token was issued by. This claim is present in tokens when a user signs in to an 3 | /// application through Azure Active Directory. The tenant ID is a key piece of information 4 | /// for identifying the tenant realm and is essential for applications that are multi-tenant aware. 5 | /// The tenant ID is a GUID that is immutable and uniquely identifies an Azure AD tenant. This 6 | /// claim is crucial for applications that need to enforce tenant-specific access control, and for 7 | /// logging or auditing the tenant context of the authenticated user. The value of "tid" is a 8 | /// case-sensitive string representing a GUID. The presence of this claim and its proper validation 9 | /// are critical for the security of multi-tenant applications. 10 | public struct TenantIDClaim: JWTClaim, Equatable, ExpressibleByStringLiteral, ExpressibleByNilLiteral { 11 | // See `JWTClaim.value`. 12 | public var value: String? 13 | 14 | // See `JWTClaim.init(value:)`. 15 | public init(value: String?) { 16 | self.value = value 17 | } 18 | 19 | public init(stringLiteral value: String) { 20 | self.init(value: value) 21 | } 22 | 23 | public init(nilLiteral: ()) { 24 | self.init(value: nil) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/JWTKit/Docs.docc/images/vapor-jwtkit-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /Sources/JWTKit/Docs.docc/theme-settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "theme": { 3 | "aside": { "border-radius": "6px", "border-style": "double", "border-width": "3px" }, 4 | "border-radius": "0", 5 | "button": { "border-radius": "16px", "border-width": "1px", "border-style": "solid" }, 6 | "code": { "border-radius": "16px", "border-width": "1px", "border-style": "solid" }, 7 | "color": { 8 | "jwtkit": "hsl(0, 55%, 57%)", 9 | "documentation-intro-fill": "radial-gradient(circle at top, var(--color-jwtkit) 30%, #000 100%)", 10 | "documentation-intro-accent": "var(--color-jwtkit)", 11 | "logo-base": { "dark": "#fff", "light": "#000" }, 12 | "logo-shape": { "dark": "#000", "light": "#fff" }, 13 | "fill": { "dark": "#000", "light": "#fff" } 14 | }, 15 | "icons": { "technology": "/jwtkit/images/vapor-jwtkit-logo.svg" } 16 | }, 17 | "features": { 18 | "quickNavigation": { "enable": true }, 19 | "i18n": { "enable": true } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/JWTKit/ECDSA/ECDSA.swift: -------------------------------------------------------------------------------- 1 | import Crypto 2 | import X509 3 | 4 | #if !canImport(Darwin) 5 | import FoundationEssentials 6 | #else 7 | import Foundation 8 | #endif 9 | 10 | public enum ECDSA: Sendable {} 11 | 12 | public protocol ECDSAKey: Sendable { 13 | associatedtype Curve: ECDSACurveType 14 | 15 | var curve: ECDSACurve { get } 16 | var parameters: ECDSAParameters? { get } 17 | } 18 | 19 | extension ECDSA { 20 | public struct PublicKey: ECDSAKey, Equatable where Curve: ECDSACurveType { 21 | typealias Signature = Curve.Signature 22 | typealias PublicKey = Curve.PrivateKey.PublicKey 23 | 24 | public private(set) var curve: ECDSACurve = Curve.curve 25 | 26 | public var parameters: ECDSAParameters? { 27 | // 0x04 || x || y 28 | let x = self.backing.x963Representation[Curve.byteRanges.x].base64EncodedString() 29 | let y = self.backing.x963Representation[Curve.byteRanges.y].base64EncodedString() 30 | return (x, y) 31 | } 32 | 33 | var backing: PublicKey 34 | 35 | /// The current public key as a PEM encoded string. 36 | /// 37 | /// - Returns: A PEM encoded string representation of the key. 38 | public var pemRepresentation: String { 39 | self.backing.pemRepresentation 40 | } 41 | 42 | /// Creates an ``ECDSA.PublicKey`` instance from SwiftCrypto PublicKey. 43 | /// 44 | /// - Parameter backing: The SwiftCrypto PublicKey. 45 | /// - Throws: If there is a problem parsing the public key. 46 | public init(backing: some ECDSAPublicKey) throws { 47 | self.backing = try PublicKey(rawRepresentation: backing.rawRepresentation) 48 | } 49 | 50 | /// Creates an ``ECDSA.PublicKey`` instance from a PEM encoded certificate string. 51 | /// 52 | /// - Parameter pem: The PEM encoded certificate string. 53 | /// - Throws: If there is a problem parsing the certificate or deriving the public key. 54 | /// - Returns: A new ``ECDSAKey`` instance with the public key from the certificate. 55 | public init(certificate pem: String) throws { 56 | let certificate = try X509.Certificate(pemEncoded: pem) 57 | guard let publicKey = PublicKey(certificate.publicKey) else { 58 | throw ECDSAError.generateKeyFailure 59 | } 60 | self.backing = publicKey 61 | } 62 | 63 | /// Creates an ``ECDSA.PublicKey`` instance from a PEM encoded certificate data. 64 | /// 65 | /// - Parameter pem: The PEM encoded certificate data. 66 | /// - Throws: If there is a problem parsing the certificate or deriving the public key. 67 | /// - Returns: A new ``ECDSA.PublicKey`` instance with the public key from the certificate. 68 | public init(certificate pem: some DataProtocol) throws { 69 | try self.init(certificate: String(decoding: pem, as: UTF8.self)) 70 | } 71 | 72 | /// Creates an ``ECDSA.PublicKey`` instance from a PEM encoded public key string. 73 | /// 74 | /// - Parameter pem: The PEM encoded public key string. 75 | /// - Throws: If there is a problem parsing the public key. 76 | /// - Returns: A new ``ECDSA.PublicKey`` instance with the public key from the certificate. 77 | public init(pem string: String) throws { 78 | self.backing = try PublicKey(pemRepresentation: string) 79 | } 80 | 81 | /// Creates an ``ECDSA.PublicKey`` instance from a PEM encoded public key data. 82 | /// 83 | /// - Parameter pem: The PEM encoded public key data. 84 | /// - Throws: If there is a problem parsing the public key. 85 | /// - Returns: A new ``ECDSA.PublicKey`` instance with the public key from the certificate. 86 | public init(pem data: some DataProtocol) throws { 87 | try self.init(pem: String(decoding: data, as: UTF8.self)) 88 | } 89 | 90 | /// Initializes a new ``ECDSA.PublicKey` with ECDSA parameters. 91 | /// 92 | /// - Parameters: 93 | /// - parameters: The ``ECDSAParameters`` tuple containing the x and y coordinates of the public key. These coordinates should be base64 URL encoded strings. 94 | /// 95 | /// - Throws: 96 | /// - ``JWTError/generic`` with the identifier `ecCoordinates` if the x and y coordinates from `parameters` cannot be interpreted as base64 encoded data. 97 | /// - ``JWTError/generic`` with the identifier `ecPrivateKey` if the provided `privateKey` is non-nil but cannot be interpreted as a valid `PrivateKey`. 98 | /// 99 | /// - Note: 100 | /// The ``ECDSAParameters`` tuple is assumed to have x and y properties that are base64 URL encoded strings representing the respective coordinates of an ECDSA public key. 101 | public init(parameters: ECDSAParameters) throws { 102 | guard 103 | let x = parameters.x.base64URLDecodedData(), 104 | let y = parameters.y.base64URLDecodedData() 105 | else { 106 | throw JWTError.generic(identifier: "ecCoordinates", reason: "Unable to interpret x or y as base64 encoded data") 107 | } 108 | self.backing = try PublicKey(x963Representation: [0x04] + x + y) 109 | } 110 | 111 | init(backing: PublicKey) { 112 | self.backing = backing 113 | } 114 | 115 | public static func == (lhs: Self, rhs: Self) -> Bool { 116 | lhs.parameters?.x == rhs.parameters?.x && lhs.parameters?.y == rhs.parameters?.y 117 | } 118 | } 119 | } 120 | 121 | extension ECDSA { 122 | public struct PrivateKey: ECDSAKey, Equatable where Curve: ECDSACurveType { 123 | typealias PrivateKey = Curve.PrivateKey 124 | typealias Signature = PrivateKey.Signature 125 | 126 | public private(set) var curve: ECDSACurve = Curve.curve 127 | 128 | public var parameters: ECDSAParameters? { 129 | self.publicKey.parameters 130 | } 131 | 132 | var backing: PrivateKey 133 | 134 | public var publicKey: PublicKey { 135 | .init(backing: self.backing.publicKey) 136 | } 137 | 138 | /// The current private key as a PEM encoded string. 139 | /// 140 | /// - Returns: A PEM encoded string representation of the key. 141 | public var pemRepresentation: String { 142 | self.backing.pemRepresentation 143 | } 144 | 145 | /// Creates an ``ECDSA.PrivateKey`` instance from SwiftCrypto PrivateKey. 146 | /// 147 | /// - Parameter backing: The SwiftCrypto PrivateKey. 148 | /// - Throws: If there is a problem parsing the private key. 149 | public init(backing: some ECDSAPrivateKey) throws { 150 | self.backing = try PrivateKey(rawRepresentation: backing.rawRepresentation) 151 | } 152 | 153 | /// Creates an ``ECDSA.PrivateKey`` instance from a PEM encoded private key string. 154 | /// 155 | /// - Parameter pem: The PEM encoded private key string. 156 | /// - Throws: If there is a problem parsing the private key. 157 | /// - Returns: A new ``ECDSA.PrivateKey`` instance with the private key. 158 | public init(pem string: String) throws { 159 | self.backing = try PrivateKey(pemRepresentation: string) 160 | } 161 | 162 | /// Creates an ``ECDSA.PrivateKey`` instance from a PEM encoded private key data. 163 | /// 164 | /// - Parameter pem: The PEM encoded private key data. 165 | /// - Throws: If there is a problem parsing the private key. 166 | /// - Returns: A new ``ECDSA.PrivateKey`` instance with the private key. 167 | public init(pem data: some DataProtocol) throws { 168 | try self.init(pem: String(decoding: data, as: UTF8.self)) 169 | } 170 | 171 | /// Initializes a new ``ECDSA.PrivateKey`` with ECDSA parameters. 172 | /// 173 | /// - Parameters: 174 | /// - parameters: The ``ECDSAParameters`` tuple containing the x and y coordinates of the public key. These coordinates should be base64 URL encoded strings. 175 | /// - privateKey: A base64 URL encoded string representation of the private key. If provided, it is used to create the private key for the instance. Defaults to `nil`. 176 | /// 177 | /// - Throws: 178 | /// - ``JWTError/generic`` with the identifier `ecCoordinates` if the x and y coordinates from `parameters` cannot be interpreted as base64 encoded data. 179 | /// - ``JWTError/generic`` with the identifier `ecPrivateKey` if the provided `privateKey` is non-nil but cannot be interpreted as a valid `PrivateKey`. 180 | /// 181 | /// - Note: 182 | /// The ``ECDSAParameters`` tuple is assumed to have x and y properties that are base64 URL encoded strings representing the respective coordinates of an ECDSA public key. 183 | public init(key: String) throws { 184 | guard let keyData = key.base64URLDecodedData() else { 185 | throw JWTError.generic(identifier: "ECDSAKey Creation", reason: "Unable to interpret private key data as base64URL") 186 | } 187 | 188 | self.backing = try PrivateKey(rawRepresentation: [UInt8](keyData)) 189 | } 190 | 191 | /// Generates a new ECDSA key. 192 | /// 193 | /// - Returns: A new ``ECDSA.PrivateKey`` instance with the generated key. 194 | public init() { 195 | self.backing = PrivateKey() 196 | } 197 | 198 | public static func == (lhs: Self, rhs: Self) -> Bool { 199 | lhs.parameters?.x == rhs.parameters?.x && lhs.parameters?.y == rhs.parameters?.y 200 | } 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /Sources/JWTKit/ECDSA/ECDSACurve.swift: -------------------------------------------------------------------------------- 1 | /// A struct representing an Elliptic Curve used in Elliptic Curve Digital Signature Algorithm (ECDSA). 2 | /// 3 | /// ``ECDSACurve`` encapsulates the different types of elliptic curves used in cryptographic operations, 4 | /// particularly in signing and verifying digital signatures with ECDSA. Each instance of ``ECDSACurve`` 5 | /// represents a specific elliptic curve, identified by its standardized curve name. 6 | /// 7 | /// The struct provides predefined static properties for common elliptic curves, such as P-256, P-384, P-521, 8 | /// and others. These are widely used curves, each offering different levels of security and performance characteristics. 9 | /// 10 | /// The use of ``ECDSACurve`` in cryptographic operations allows for easy specification and interchange of 11 | /// the elliptic curves based on security requirements and application needs. 12 | public struct ECDSACurve: Codable, RawRepresentable, Sendable { 13 | let backing: Backing 14 | 15 | /// Textual representation of the elliptic curve. 16 | public var rawValue: String { 17 | backing.rawValue 18 | } 19 | 20 | /// Represents the P-256 elliptic curve. 21 | public static let p256 = Self(.p256) 22 | 23 | /// Represents the P-384 elliptic curve. 24 | public static let p384 = Self(.p384) 25 | 26 | /// Represents the P-521 elliptic curve. 27 | public static let p521 = Self(.p521) 28 | 29 | enum Backing: String, Codable { 30 | case p256 = "P-256" 31 | case p384 = "P-384" 32 | case p521 = "P-521" 33 | } 34 | 35 | init(_ backing: Backing) { 36 | self.backing = backing 37 | } 38 | 39 | public init?(rawValue: String) { 40 | guard let backing = Backing(rawValue: rawValue) else { 41 | return nil 42 | } 43 | self.init(backing) 44 | } 45 | 46 | public init(from decoder: any Decoder) throws { 47 | try self.init(decoder.singleValueContainer().decode(Backing.self)) 48 | } 49 | 50 | public func encode(to encoder: any Encoder) throws { 51 | var container = encoder.singleValueContainer() 52 | try container.encode(self.backing) 53 | } 54 | } 55 | 56 | extension ECDSACurve: Equatable {} 57 | -------------------------------------------------------------------------------- /Sources/JWTKit/ECDSA/ECDSACurveType.swift: -------------------------------------------------------------------------------- 1 | /// A protocol defining the requirements for elliptic curve types used in ECDSA (Elliptic Curve Digital Signature Algorithm). 2 | /// 3 | /// This protocol specifies the necessary components and characteristics for an elliptic curve to be used in ECDSA. 4 | /// Implementations of this protocol should provide specific types and values associated with a particular elliptic curve, 5 | /// allowing for a more generic handling of ECDSA operations across different curves. 6 | /// 7 | /// Types conforming to this protocol are used to define the characteristics of specific elliptic curves, 8 | /// such as the curve used (represented by ``ECDSACurve``), and the byte ranges for the x and y coordinates 9 | /// that are crucial in elliptic curve cryptography. 10 | /// 11 | /// Conformance to this protocol requires specifying: 12 | /// - ``Signature``: The type representing a signature produced using the ECDSA algorithm with the specific curve. 13 | /// - ``PrivateKey``: The type representing a private key compatible with the specific curve. It must conform to ``ECDSAPrivateKey``. 14 | /// - ``curve``: A static property providing the ``ECDSACurve`` instance associated with the specific curve. 15 | /// - ``byteRanges``: A static property specifying the byte ranges for the x and y coordinates on the curve. 16 | /// 17 | /// Types conforming to this protocol can be used to abstract ECDSA cryptographic operations across various elliptic curves, 18 | /// allowing for flexible and modular cryptographic code. 19 | public protocol ECDSACurveType: Sendable { 20 | associatedtype Signature: ECDSASignature 21 | associatedtype PrivateKey: ECDSAPrivateKey 22 | associatedtype SigningAlgorithm: ECDSASigningAlgorithm 23 | 24 | static var curve: ECDSACurve { get } 25 | static var byteRanges: (x: Range, y: Range) { get } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/JWTKit/ECDSA/ECDSAError.swift: -------------------------------------------------------------------------------- 1 | enum ECDSAError: Error { 2 | case newKeyByCurveFailure 3 | case generateKeyFailure 4 | case signFailure 5 | case noPublicKey 6 | case noPrivateKey 7 | } 8 | -------------------------------------------------------------------------------- /Sources/JWTKit/ECDSA/ECDSAKeyTypes.swift: -------------------------------------------------------------------------------- 1 | import Crypto 2 | import X509 3 | 4 | #if !canImport(Darwin) 5 | import FoundationEssentials 6 | #else 7 | import Foundation 8 | #endif 9 | 10 | /// A typealias representing the parameters of an ECDSA (Elliptic Curve Digital Signature Algorithm) key. 11 | /// 12 | /// This tuple consists of two strings representing the x and y coordinates on the elliptic curve. 13 | /// These coordinates are crucial in defining the public key in ECDSA cryptography. 14 | /// They are typically encoded in Base64 or a similar encoding format. 15 | /// 16 | /// The `x` and `y` coordinates are represented as strings for easier handling and conversion, 17 | /// especially when dealing with different encoding and serialization formats like PEM, DER, or others commonly used in cryptographic operations. 18 | /// 19 | /// - Parameters: 20 | /// - x: A `String` representing the x-coordinate on the elliptic curve. 21 | /// - y: A `String` representing the y-coordinate on the elliptic curve. 22 | public typealias ECDSAParameters = (x: String, y: String) 23 | 24 | public protocol ECDSASignature: Sendable { 25 | var rawRepresentation: Data { get set } 26 | } 27 | 28 | public protocol ECDSAPrivateKey: Sendable { 29 | associatedtype PublicKey: ECDSAPublicKey 30 | associatedtype Signature: ECDSASignature 31 | init(compactRepresentable: Bool) 32 | init(x963Representation: some ContiguousBytes) throws 33 | init(rawRepresentation: some ContiguousBytes) throws 34 | init(pemRepresentation: String) throws 35 | init(derRepresentation: Bytes) throws where Bytes: RandomAccessCollection, Bytes.Element == UInt8 36 | var publicKey: PublicKey { get } 37 | var rawRepresentation: Data { get } 38 | var x963Representation: Data { get } 39 | var derRepresentation: Data { get } 40 | var pemRepresentation: String { get } 41 | func signature(for data: some Digest) throws -> Signature 42 | } 43 | 44 | public protocol ECDSAPublicKey: Sendable { 45 | init(rawRepresentation: some ContiguousBytes) throws 46 | init(compactRepresentation: some ContiguousBytes) throws 47 | init(x963Representation: some ContiguousBytes) throws 48 | @available(iOS 16.0, macOS 13.0, watchOS 9.0, tvOS 16.0, *) 49 | init(compressedRepresentation: some ContiguousBytes) throws 50 | init(pemRepresentation: String) throws 51 | init(derRepresentation: Bytes) throws where Bytes: RandomAccessCollection, Bytes.Element == UInt8 52 | init?(_ key: X509.Certificate.PublicKey) 53 | var compactRepresentation: Data? { get } 54 | var rawRepresentation: Data { get } 55 | var x963Representation: Data { get } 56 | @available(iOS 16.0, macOS 13.0, watchOS 9.0, tvOS 16.0, *) 57 | var compressedRepresentation: Data { get } 58 | var derRepresentation: Data { get } 59 | var pemRepresentation: String { get } 60 | func isValidSignature(_ signature: some DataProtocol, for data: some Digest) throws -> Bool 61 | } 62 | 63 | extension ECDSAPrivateKey { 64 | init(compactRepresentable: Bool = true) { 65 | self.init(compactRepresentable: compactRepresentable) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Sources/JWTKit/ECDSA/ECDSASigner.swift: -------------------------------------------------------------------------------- 1 | #if !canImport(Darwin) 2 | import FoundationEssentials 3 | #else 4 | import Foundation 5 | #endif 6 | 7 | struct ECDSASigner: JWTAlgorithm, CryptoSigner { 8 | let privateKey: ECDSA.PrivateKey? 9 | let publicKey: ECDSA.PublicKey 10 | let algorithm: DigestAlgorithm = Key.Curve.SigningAlgorithm.digestAlgorithm 11 | let name: String = Key.Curve.SigningAlgorithm.name 12 | 13 | init(key: Key) { 14 | switch key { 15 | case let privateKey as ECDSA.PrivateKey: 16 | self.privateKey = privateKey 17 | self.publicKey = privateKey.publicKey 18 | case let publicKey as ECDSA.PublicKey: 19 | self.publicKey = publicKey 20 | self.privateKey = nil 21 | default: 22 | // This should never happen 23 | fatalError("Unexpected key type: \(type(of: key))") 24 | } 25 | } 26 | 27 | func sign(_ plaintext: some DataProtocol) throws -> [UInt8] { 28 | let digest = try self.digest(plaintext) 29 | guard let privateKey else { 30 | throw JWTError.signingAlgorithmFailure(ECDSAError.noPrivateKey) 31 | } 32 | let signature = try privateKey.backing.signature(for: digest) 33 | return [UInt8](signature.rawRepresentation) 34 | } 35 | 36 | public func verify(_ signature: some DataProtocol, signs plaintext: some DataProtocol) throws 37 | -> Bool 38 | { 39 | let digest = try self.digest(plaintext) 40 | return try publicKey.backing.isValidSignature(signature, for: digest) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Sources/JWTKit/ECDSA/ECDSASigningAlgorithm.swift: -------------------------------------------------------------------------------- 1 | public protocol ECDSASigningAlgorithm { 2 | static var name: String { get } 3 | static var digestAlgorithm: DigestAlgorithm { get } 4 | } 5 | -------------------------------------------------------------------------------- /Sources/JWTKit/ECDSA/JWTKeyCollection+ECDSA.swift: -------------------------------------------------------------------------------- 1 | import Crypto 2 | 3 | extension JWTKeyCollection { 4 | /// Adds an ECDSA key to the collection. 5 | /// 6 | /// Example Usage: 7 | /// ``` 8 | /// let collection = await JWTKeyCollection() 9 | /// .addECDSA(key: myECDSAKey) 10 | /// ``` 11 | /// 12 | /// - Parameters: 13 | /// - key: The ``ECDSAKey`` to be used for signing. This key should be securely stored and not exposed. 14 | /// - kid: An optional ``JWKIdentifier`` (Key ID). If provided, this identifier will be used in the JWT `kid` 15 | /// header field to identify the key. 16 | /// - jsonEncoder: An optional custom JSON encoder conforming to ``JWTJSONEncoder``, used for encoding JWTs. 17 | /// If `nil`, a default encoder is used. 18 | /// - jsonDecoder: An optional custom JSON decoder conforming to ``JWTJSONDecoder``, used for decoding JWTs. 19 | /// If `nil`, a default decoder is used. 20 | /// - Returns: The same instance of the collection (`Self`), which allows for method chaining. 21 | @discardableResult 22 | public func add( 23 | ecdsa key: some ECDSAKey, 24 | kid: JWKIdentifier? = nil, 25 | parser: some JWTParser = DefaultJWTParser(), 26 | serializer: some JWTSerializer = DefaultJWTSerializer() 27 | ) -> Self { 28 | add( 29 | .init( 30 | algorithm: ECDSASigner(key: key), 31 | parser: parser, 32 | serializer: serializer 33 | ), for: kid 34 | ) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/JWTKit/ECDSA/P256+CurveType.swift: -------------------------------------------------------------------------------- 1 | import Crypto 2 | 3 | #if !canImport(Darwin) 4 | import FoundationEssentials 5 | #else 6 | import Foundation 7 | #endif 8 | 9 | // TODO: Remove @unchecked Sendable when Crypto is updated to use Sendable 10 | extension P256: ECDSACurveType { 11 | public typealias Signature = P256.Signing.ECDSASignature 12 | public typealias PrivateKey = P256.Signing.PrivateKey 13 | 14 | public static let curve: ECDSACurve = .p256 15 | 16 | /// Specifies the byte ranges in which the X and Y coordinates of an ECDSA public key appear for the P256 curve. 17 | /// For P256, the public key is typically 65 bytes long: a single byte prefix (usually 0x04 for uncompressed keys), followed by 18 | /// 32 bytes for the X coordinate, and then 32 bytes for the Y coordinate. 19 | /// 20 | /// Thus: 21 | /// - The X coordinate spans bytes 1 through 32 (byte 0 is for the prefix). 22 | /// - The Y coordinate spans bytes 33 through 64. 23 | public static let byteRanges: (x: Range, y: Range) = (1..<33, 33..<65) 24 | 25 | public struct SigningAlgorithm: ECDSASigningAlgorithm { 26 | public static let name = "ES256" 27 | public static let digestAlgorithm: DigestAlgorithm = .sha256 28 | } 29 | } 30 | 31 | // TODO: Remove @unchecked Sendable when Crypto is updated to use Sendable 32 | extension P256.Signing.PublicKey: ECDSAPublicKey { 33 | /// Verifies that the P256 key signature is valid for the given digest. 34 | /// 35 | /// - Parameters: 36 | /// - signature: The signature to verify. 37 | /// - digest: The digest to verify the signature against. 38 | /// - Returns: True if the signature is valid for the given digest, false otherwise. 39 | /// - Throws: If there is a problem verifying the signature. 40 | public func isValidSignature(_ signature: some DataProtocol, for data: some Digest) throws -> Bool { 41 | let signature = try P256.Signing.ECDSASignature(rawRepresentation: signature) 42 | return isValidSignature(signature, for: data) 43 | } 44 | } 45 | 46 | // TODO: Remove @unchecked Sendable when Crypto is updated to use Sendable 47 | extension P256.Signing.PrivateKey: ECDSAPrivateKey, @unchecked @retroactive Sendable {} 48 | extension P256.Signing.ECDSASignature: ECDSASignature, @unchecked @retroactive Sendable {} 49 | extension P256.Signing.PublicKey: @unchecked @retroactive Sendable {} 50 | extension P256: @unchecked @retroactive Sendable {} 51 | 52 | public typealias ES256PublicKey = ECDSA.PublicKey 53 | public typealias ES256PrivateKey = ECDSA.PrivateKey 54 | -------------------------------------------------------------------------------- /Sources/JWTKit/ECDSA/P384+CurveType.swift: -------------------------------------------------------------------------------- 1 | import Crypto 2 | 3 | #if !canImport(Darwin) 4 | import FoundationEssentials 5 | #else 6 | import Foundation 7 | #endif 8 | 9 | // TODO: Remove @unchecked Sendable when Crypto is updated to use Sendable 10 | extension P384: ECDSACurveType { 11 | public typealias Signature = P384.Signing.ECDSASignature 12 | public typealias PrivateKey = P384.Signing.PrivateKey 13 | 14 | public static let curve: ECDSACurve = .p384 15 | 16 | /// Specifies the byte ranges in which the X and Y coordinates of an ECDSA public key appear for the P384 curve. 17 | /// For P384, the public key is typically 97 bytes long: a single byte prefix (usually 0x04 for uncompressed keys), followed by 18 | /// 48 bytes for the X coordinate, and then 48 bytes for the Y coordinate. 19 | /// 20 | /// Thus: 21 | /// - The X coordinate spans bytes 1 through 48. 22 | /// - The Y coordinate spans bytes 49 through 96. 23 | public static let byteRanges: (x: Range, y: Range) = (1..<49, 49..<97) 24 | 25 | public enum SigningAlgorithm: ECDSASigningAlgorithm { 26 | public static let name = "ES384" 27 | public static let digestAlgorithm: DigestAlgorithm = .sha384 28 | } 29 | } 30 | 31 | // TODO: Remove @unchecked Sendable when Crypto is updated to use Sendable 32 | extension P384.Signing.PublicKey: ECDSAPublicKey { 33 | /// Verifies that the P384 key signature is valid for the given digest. 34 | /// 35 | /// - Parameters: 36 | /// - signature: The signature to verify. 37 | /// - digest: The digest to verify the signature against. 38 | /// - Returns: True if the signature is valid for the given digest, false otherwise. 39 | /// - Throws: If there is a problem verifying the signature. 40 | public func isValidSignature(_ signature: some DataProtocol, for data: some Digest) throws -> Bool { 41 | let signature = try P384.Signing.ECDSASignature(rawRepresentation: signature) 42 | return isValidSignature(signature, for: data) 43 | } 44 | } 45 | 46 | // TODO: Remove @unchecked Sendable when Crypto is updated to use Sendable 47 | extension P384.Signing.PrivateKey: ECDSAPrivateKey, @unchecked @retroactive Sendable {} 48 | extension P384.Signing.ECDSASignature: ECDSASignature, @unchecked @retroactive Sendable {} 49 | extension P384.Signing.PublicKey: @unchecked @retroactive Sendable {} 50 | extension P384: @unchecked @retroactive Sendable {} 51 | 52 | public typealias ES384PublicKey = ECDSA.PublicKey 53 | public typealias ES384PrivateKey = ECDSA.PrivateKey 54 | -------------------------------------------------------------------------------- /Sources/JWTKit/ECDSA/P521+CurveType.swift: -------------------------------------------------------------------------------- 1 | import Crypto 2 | 3 | #if !canImport(Darwin) 4 | import FoundationEssentials 5 | #else 6 | import Foundation 7 | #endif 8 | 9 | // TODO: Remove @unchecked Sendable when Crypto is updated to use Sendable 10 | extension P521: ECDSACurveType { 11 | public typealias Signature = P521.Signing.ECDSASignature 12 | public typealias PrivateKey = P521.Signing.PrivateKey 13 | 14 | public static let curve: ECDSACurve = .p521 15 | 16 | /// Specifies the byte ranges in which the X and Y coordinates of an ECDSA public key appear for the P521 curve. 17 | /// For P521, the public key is a bit tricky because 521 bits is not a multiple of 8, but it's typically represented as 66 bytes 18 | /// for each coordinate with leading zeros as needed. The public key is hence 133 bytes long: a single byte prefix (usually 0x04 19 | /// for uncompressed keys), followed by 66 bytes for the X coordinate, and then 66 bytes for the Y coordinate. 20 | /// 21 | /// Thus: 22 | /// - The X coordinate spans bytes 1 through 66. 23 | /// - The Y coordinate spans bytes 67 through 132. 24 | public static let byteRanges: (x: Range, y: Range) = (1..<67, 67..<133) 25 | 26 | public enum SigningAlgorithm: ECDSASigningAlgorithm { 27 | public static let name = "ES512" 28 | public static let digestAlgorithm: DigestAlgorithm = .sha512 29 | } 30 | } 31 | 32 | // TODO: Remove @unchecked Sendable when Crypto is updated to use Sendable 33 | extension P521.Signing.PublicKey: ECDSAPublicKey { 34 | /// Verifies that the P256 key signature is valid for the given digest. 35 | /// 36 | /// - Parameters: 37 | /// - signature: The signature to verify. 38 | /// - digest: The digest to verify the signature against. 39 | /// - Returns: True if the signature is valid for the given digest, false otherwise. 40 | /// - Throws: If there is a problem verifying the signature. 41 | public func isValidSignature(_ signature: some DataProtocol, for data: some Digest) throws -> Bool { 42 | let signature = try P521.Signing.ECDSASignature(rawRepresentation: signature) 43 | return isValidSignature(signature, for: data) 44 | } 45 | } 46 | 47 | extension P521.Signing.PrivateKey: ECDSAPrivateKey, @unchecked @retroactive Sendable {} 48 | extension P521.Signing.ECDSASignature: ECDSASignature, @unchecked @retroactive Sendable {} 49 | extension P521.Signing.PublicKey: @unchecked @retroactive Sendable {} 50 | extension P521: @unchecked @retroactive Sendable {} 51 | 52 | public typealias ES512PublicKey = ECDSA.PublicKey 53 | public typealias ES512PrivateKey = ECDSA.PrivateKey 54 | -------------------------------------------------------------------------------- /Sources/JWTKit/EdDSA/EdDSA.swift: -------------------------------------------------------------------------------- 1 | import Crypto 2 | 3 | #if !canImport(Darwin) 4 | import FoundationEssentials 5 | #else 6 | import Foundation 7 | #endif 8 | 9 | /// Namespace for the EdDSA (Edwards-curve Digital Signature Algorithm) signing algorithm. 10 | /// EdDSA is a modern signing algorithm that is efficient and fast. 11 | public enum EdDSA: Sendable {} 12 | 13 | /// This protocol represents a key that can be used for signing and verifying EdDSA signatures. 14 | /// Both ``EdDSA.PublicKey`` and ``EdDSA.PrivateKey`` conform to this protocol. 15 | public protocol EdDSAKey: Sendable {} 16 | 17 | extension EdDSA { 18 | /// A struct representing a public key used in EdDSA (Edwards-curve Digital Signature Algorithm). 19 | /// 20 | /// In JWT, EdDSA public keys are represented as a single x-coordinate and are used for verifying signatures. 21 | /// Currently, only the ``EdDSACurve/ed25519`` curve is supported. 22 | public struct PublicKey: EdDSAKey { 23 | let backing: Curve25519.Signing.PublicKey 24 | let curve: EdDSACurve 25 | 26 | /// Creates an ``EdDSA.PublicKey`` instance using the provided public key. 27 | /// 28 | /// This init constructs an ``EdDSA.PublicKey`` based on the corresponding SwiftCrypto 29 | /// ``Curve25519.Signing.PublicKey``. 30 | /// - Parameter backing: The SwiftCrypto ``Curve25519.Signing.PublicKey`` 31 | public init(backing: Curve25519.Signing.PublicKey) { 32 | self.backing = backing 33 | self.curve = .ed25519 34 | } 35 | 36 | /// Creates an ``EdDSA.PublicKey`` instance using the public key x-coordinate and specified curve. 37 | /// 38 | /// This init allows for the creation of an ``EdDSA.PublicKey`` using the x-coordinate of the public key. 39 | /// The provided x-coordinate should be a Base64 URL encoded string. 40 | /// 41 | /// - Parameters: 42 | /// - x: A `String` representing the x-coordinate of the public key. This should be a Base64 URL encoded string. 43 | /// - curve: The ``EdDSACurve`` representing the elliptic curve used for the EdDSA public key. 44 | /// 45 | /// - Throws: 46 | /// - ``EdDSAError/publicKeyMissing`` if the x-coordinate data is missing or cannot be properly converted. 47 | public init(x: String, curve: EdDSACurve) throws { 48 | guard 49 | let xData = x.base64URLDecodedData(), 50 | !xData.isEmpty 51 | else { 52 | throw EdDSAError.publicKeyMissing 53 | } 54 | 55 | let key = 56 | switch curve.backing { 57 | case .ed25519: 58 | try Curve25519.Signing.PublicKey(rawRepresentation: xData) 59 | } 60 | 61 | self.init(backing: key) 62 | } 63 | 64 | var rawRepresentation: Data { 65 | self.backing.rawRepresentation 66 | } 67 | } 68 | } 69 | 70 | extension EdDSA { 71 | /// A struct representing a private key used in EdDSA (Edwards-curve Digital Signature Algorithm). 72 | /// 73 | /// In JWT, EdDSA private keys are represented as a pair of x-coordinate and private key (d) and are used for signing. 74 | /// Currently, only the ``Curve/ed25519`` curve is supported. 75 | public struct PrivateKey: EdDSAKey { 76 | let backing: Curve25519.Signing.PrivateKey 77 | let curve: EdDSACurve 78 | 79 | /// Generates a new ``EdDSAKey`` instance with both public and private key components. 80 | /// 81 | /// This method generates a new key pair suitable for signing and verifying signatures. 82 | /// The generated keys use the specified curve, currently limited to ``Curve/ed25519``. 83 | /// 84 | /// - Parameter curve: The curve to be used for key generation. 85 | /// - Throws: An error if key generation fails. 86 | /// - Returns: A new ``EdDSA.PrivateKey`` instance with a freshly generated key pair. 87 | public init(curve: EdDSACurve = .ed25519) throws { 88 | let key = 89 | switch curve.backing { 90 | case .ed25519: 91 | Curve25519.Signing.PrivateKey() 92 | } 93 | 94 | self.init(backing: key) 95 | } 96 | 97 | /// Creates an ``EdDSA.PrivateKey`` instance using the provided private key. 98 | /// 99 | /// This init constructs an ``EdDSA.PrivateKey`` based on the corresponding SwiftCrypto 100 | /// ``Curve25519.Signing.PrivateKey``. 101 | /// - Parameter privateKey: The SwiftCrypto ``Curve25519.Signing.PrivateKey`` 102 | public init(backing: Curve25519.Signing.PrivateKey) { 103 | self.backing = backing 104 | self.curve = .ed25519 105 | } 106 | 107 | /// Creates an ``EdDSA.PrivateKey`` instance using both the public and private key components along with the specified curve. 108 | /// 109 | /// This init constructs an ``EdDSA.PrivateKey`` from the provided private key (d). 110 | /// `d` is expected to be a Base64 URL encoded string. 111 | /// 112 | /// - Parameters: 113 | /// - d: A `String` representing the private key, encoded in Base64 URL format. 114 | /// - curve: The ``EdDSACurve`` representing the elliptic curve used for the EdDSA key. 115 | /// 116 | /// - Throws: 117 | /// - ``EdDSAError/privateKeyMissing`` if the private key data is missing or cannot be properly converted. 118 | public init(d: String, curve: EdDSACurve) throws { 119 | guard 120 | let dData = d.base64URLDecodedData(), 121 | !dData.isEmpty 122 | else { 123 | throw EdDSAError.privateKeyMissing 124 | } 125 | 126 | let key = 127 | switch curve.backing { 128 | case .ed25519: 129 | try Curve25519.Signing.PrivateKey(rawRepresentation: dData) 130 | } 131 | 132 | self.init(backing: key) 133 | } 134 | 135 | var publicKey: PublicKey { 136 | .init(backing: self.backing.publicKey) 137 | } 138 | 139 | var rawRepresentation: Data { 140 | self.backing.rawRepresentation 141 | } 142 | } 143 | } 144 | 145 | // TODO: Remove @unchecked Sendable when Crypto is updated to use Sendable 146 | extension Curve25519.Signing.PublicKey: @unchecked @retroactive Sendable {} 147 | extension Curve25519.Signing.PrivateKey: @unchecked @retroactive Sendable {} 148 | -------------------------------------------------------------------------------- /Sources/JWTKit/EdDSA/EdDSACurve.swift: -------------------------------------------------------------------------------- 1 | /// A struct representing an Elliptic Curve used in EdDSA (Edwards-curve Digital Signature Algorithm). 2 | /// 3 | /// ``EdDSACurve`` encapsulates different types of elliptic curves specifically used in EdDSA cryptographic operations. 4 | /// It allows for representing and working with EdDSA curves. 5 | /// The struct provides a static property for the Ed25519 curve, a widely used curve known for its 6 | /// balance of security and efficiency. This makes ``EdDSACurve`` suitable for operations requiring Ed25519, 7 | /// such as generating digital signatures or key pairs. 8 | public struct EdDSACurve: Codable, Equatable, RawRepresentable, Sendable { 9 | let backing: Backing 10 | 11 | /// Textual representation of the curve. 12 | public var rawValue: String { 13 | backing.rawValue 14 | } 15 | 16 | /// Represents the Ed25519 curve. 17 | public static let ed25519 = Self(.ed25519) 18 | 19 | enum Backing: String, Codable { 20 | case ed25519 = "Ed25519" 21 | } 22 | 23 | init(_ backing: Backing) { 24 | self.backing = backing 25 | } 26 | 27 | public init?(rawValue: String) { 28 | guard let backing = Backing(rawValue: rawValue) else { 29 | return nil 30 | } 31 | self.init(backing) 32 | } 33 | 34 | public init(from decoder: any Decoder) throws { 35 | try self.init(decoder.singleValueContainer().decode(Backing.self)) 36 | } 37 | 38 | public func encode(to encoder: any Encoder) throws { 39 | var container = encoder.singleValueContainer() 40 | try container.encode(self.backing) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Sources/JWTKit/EdDSA/EdDSAError.swift: -------------------------------------------------------------------------------- 1 | enum EdDSAError: Error { 2 | case publicAndPrivateKeyMissing 3 | case privateKeyMissing 4 | case publicKeyMissing 5 | } 6 | -------------------------------------------------------------------------------- /Sources/JWTKit/EdDSA/EdDSASigner.swift: -------------------------------------------------------------------------------- 1 | import Crypto 2 | 3 | #if !canImport(Darwin) 4 | import FoundationEssentials 5 | #else 6 | import Foundation 7 | #endif 8 | 9 | struct EdDSASigner: JWTAlgorithm, Sendable { 10 | let publicKey: EdDSA.PublicKey 11 | let privateKey: EdDSA.PrivateKey? 12 | let name = "EdDSA" 13 | 14 | init(key: some EdDSAKey) { 15 | switch key { 16 | case let key as EdDSA.PrivateKey: 17 | self.privateKey = key 18 | self.publicKey = key.publicKey 19 | case let key as EdDSA.PublicKey: 20 | self.publicKey = key 21 | self.privateKey = nil 22 | default: 23 | // This should never happen 24 | fatalError("Unexpected key type: \(type(of: key))") 25 | } 26 | } 27 | 28 | func sign(_ plaintext: some DataProtocol) throws -> [UInt8] { 29 | guard let privateKey else { 30 | throw JWTError.signingAlgorithmFailure(EdDSAError.privateKeyMissing) 31 | } 32 | 33 | switch privateKey.curve.backing { 34 | case .ed25519: 35 | return try Curve25519.Signing.PrivateKey(rawRepresentation: privateKey.rawRepresentation) 36 | .signature(for: plaintext).copyBytes() 37 | } 38 | } 39 | 40 | func verify(_ signature: some DataProtocol, signs plaintext: some DataProtocol) throws -> Bool { 41 | switch publicKey.curve.backing { 42 | case .ed25519: 43 | try Curve25519.Signing.PublicKey(rawRepresentation: publicKey.rawRepresentation) 44 | .isValidSignature(signature, for: plaintext) 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Sources/JWTKit/EdDSA/JWTKeyCollection+EdDSA.swift: -------------------------------------------------------------------------------- 1 | import Crypto 2 | 3 | #if !canImport(Darwin) 4 | import FoundationEssentials 5 | #else 6 | import Foundation 7 | #endif 8 | 9 | extension JWTKeyCollection { 10 | /// Adds an EdDSA key to the collection using an ``EdDSAKey``. 11 | /// 12 | /// This method incorporates an EdDSA (Edwards-curve Digital Signature Algorithm) signer into the collection. 13 | /// 14 | /// Usage Example: 15 | /// ``` 16 | /// let collection = await JWTKeyCollection() 17 | /// .addEdDSA(key: myEdDSAKey) 18 | /// ``` 19 | /// 20 | /// - Parameters: 21 | /// - key: The ``EdDSAKey`` used for EdDSA signing. EdDSA keys are known for their short signature and key sizes, 22 | /// which contribute to their efficiency and speed. 23 | /// - kid: An optional ``JWKIdentifier`` (Key ID). Providing this identifier allows the JWT `kid` header field 24 | /// to reference this specific signer. 25 | /// - jsonEncoder: An optional custom JSON encoder that conforms to ``JWTJSONEncoder``. If not specified, 26 | /// a default encoder is used for encoding JWT payloads. 27 | /// - jsonDecoder: An optional custom JSON decoder that conforms to ``JWTJSONDecoder``. If not specified, 28 | /// a default decoder is used for decoding JWT payloads. 29 | /// - Returns: The same instance of the collection (`Self`), useful for chaining multiple configuration calls. 30 | @discardableResult 31 | public func add( 32 | eddsa key: some EdDSAKey, 33 | kid: JWKIdentifier? = nil, 34 | parser: some JWTParser = DefaultJWTParser(), 35 | serializer: some JWTSerializer = DefaultJWTSerializer() 36 | ) -> Self { 37 | add( 38 | .init( 39 | algorithm: EdDSASigner(key: key), 40 | parser: parser, 41 | serializer: serializer 42 | ), for: kid) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Sources/JWTKit/HMAC/HMAC.swift: -------------------------------------------------------------------------------- 1 | @preconcurrency import Crypto 2 | 3 | #if !canImport(Darwin) 4 | import FoundationEssentials 5 | #else 6 | import Foundation 7 | #endif 8 | 9 | public struct HMACKey: Sendable { 10 | let key: SymmetricKey 11 | 12 | public init(from string: some StringProtocol) { 13 | self.init(from: [UInt8](string.utf8)) 14 | } 15 | 16 | public init(from data: some DataProtocol) { 17 | self.key = .init(data: data.copyBytes()) 18 | } 19 | 20 | public init(key: SymmetricKey) { 21 | self.key = key 22 | } 23 | } 24 | 25 | extension HMACKey: ExpressibleByStringLiteral { 26 | public init(stringLiteral value: StringLiteralType) { 27 | self.init(from: value) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/JWTKit/HMAC/HMACError.swift: -------------------------------------------------------------------------------- 1 | internal enum HMACError: Error { 2 | case initializationFailure 3 | case updateFailure 4 | case finalizationFailure 5 | } 6 | -------------------------------------------------------------------------------- /Sources/JWTKit/HMAC/HMACSigner.swift: -------------------------------------------------------------------------------- 1 | @preconcurrency import Crypto 2 | 3 | #if !canImport(Darwin) 4 | import FoundationEssentials 5 | #else 6 | import Foundation 7 | #endif 8 | 9 | struct HMACSigner: JWTAlgorithm where SHAType: HashFunction { 10 | let key: SymmetricKey 11 | let name: String 12 | 13 | init(key: SymmetricKey) { 14 | self.key = key 15 | switch SHAType.self { 16 | case is SHA256.Type: 17 | self.name = "HS256" 18 | case is SHA384.Type: 19 | self.name = "HS384" 20 | case is SHA512.Type: 21 | self.name = "HS512" 22 | default: 23 | fatalError("Unsupported hash function: \(SHAType.self)") 24 | } 25 | } 26 | 27 | func sign(_ plaintext: some DataProtocol) throws -> [UInt8] { 28 | Array(HMAC.authenticationCode(for: plaintext, using: self.key)) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Sources/JWTKit/HMAC/JWTKeyCollection+HMAC.swift: -------------------------------------------------------------------------------- 1 | import Crypto 2 | 3 | #if !canImport(Darwin) 4 | import FoundationEssentials 5 | #else 6 | import Foundation 7 | #endif 8 | 9 | extension JWTKeyCollection { 10 | /// Adds an HMAC key to the collection. 11 | /// 12 | /// Example Usage: 13 | /// ``` 14 | /// let collection = await JWTKeyCollection() 15 | /// .addHS256(key: "mySecretKey") 16 | /// ``` 17 | /// 18 | /// - Parameters: 19 | /// - key: The `SymmetricKey` used for HMAC signing. This key should be kept confidential 20 | /// and secure, as it can be used for both signing and verification. 21 | /// - kid: An optional ``JWKIdentifier`` (Key ID). If given, it is used in the JWT `kid` header field 22 | /// to identify this key. 23 | /// - jsonEncoder: An optional custom JSON encoder conforming to ``JWTJSONEncoder``, used for encoding JWTs. 24 | /// If `nil`, a default encoder is employed. 25 | /// - jsonDecoder: An optional custom JSON decoder conforming to ``JWTJSONDecoder``, used for decoding JWTs. 26 | /// If `nil`, a default decoder is used. 27 | /// - Returns: The same instance of the collection (`Self`), enabling method chaining. 28 | @discardableResult 29 | public func add( 30 | hmac key: HMACKey, 31 | digestAlgorithm: DigestAlgorithm, 32 | kid: JWKIdentifier? = nil, 33 | parser: some JWTParser = DefaultJWTParser(), 34 | serializer: some JWTSerializer = DefaultJWTSerializer() 35 | ) -> Self { 36 | let signer: any JWTAlgorithm = 37 | switch digestAlgorithm.backing { 38 | case .sha256: 39 | HMACSigner(key: key.key) 40 | case .sha384: 41 | HMACSigner(key: key.key) 42 | case .sha512: 43 | HMACSigner(key: key.key) 44 | } 45 | return add(.init(algorithm: signer, parser: parser, serializer: serializer), for: kid) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Sources/JWTKit/Insecure/Insecure.swift: -------------------------------------------------------------------------------- 1 | /// A container for older, cryptographically insecure algorithms. 2 | /// 3 | /// - Important: These algorithms aren’t considered cryptographically secure, 4 | /// but the framework provides them for backward compatibility with older 5 | /// services that require them. For new services, avoid these algorithms. 6 | public enum Insecure {} 7 | -------------------------------------------------------------------------------- /Sources/JWTKit/JWK/JWKIdentifier.swift: -------------------------------------------------------------------------------- 1 | public struct JWKIdentifier: Hashable, Equatable, Sendable { 2 | public let string: String 3 | 4 | public init(string: String) { 5 | self.string = string 6 | } 7 | } 8 | 9 | extension JWKIdentifier: Codable { 10 | public init(from decoder: Decoder) throws { 11 | let container = try decoder.singleValueContainer() 12 | try self.init(string: container.decode(String.self)) 13 | } 14 | 15 | public func encode(to encoder: Encoder) throws { 16 | var container = encoder.singleValueContainer() 17 | try container.encode(self.string) 18 | } 19 | } 20 | 21 | extension JWKIdentifier: ExpressibleByStringLiteral { 22 | public init(stringLiteral value: String) { 23 | self.init(string: value) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Sources/JWTKit/JWK/JWKS.swift: -------------------------------------------------------------------------------- 1 | /// A JSON Web Key Set. 2 | /// 3 | /// A JSON object that represents a set of JWKs. 4 | /// Read specification (RFC 7517) https://tools.ietf.org/html/rfc7517. 5 | public struct JWKS: Codable, Sendable { 6 | /// All JSON Web Keys 7 | public var keys: [JWK] 8 | 9 | public init(keys: [JWK]) { 10 | self.keys = keys 11 | } 12 | 13 | /// Retrieves the desired key from the JSON Web Key Set 14 | /// - Parameters: 15 | /// - identifier: The `kid` value to lookup. 16 | /// - type: The `kty` value. 17 | public func find(identifier: String, type: JWK.KeyType) -> JWK? { 18 | self.keys.first(where: { $0.keyType == type && $0.keyIdentifier?.string == identifier }) 19 | } 20 | 21 | /// Retrieves the desired key from the JSON Web Key Set 22 | /// - Parameters: 23 | /// - identifier: The `kid` value to lookup. 24 | /// - type: The `kty` value. 25 | public func find(identifier: JWKIdentifier, type: JWK.KeyType) -> JWK? { 26 | self.find(identifier: identifier.string, type: type) 27 | } 28 | 29 | /// Retrieves the desired keys from the JSON Web Key Set 30 | /// Multiple keys can have the same `kid` if they have different `kty` values. 31 | /// - Parameter identifier: The `kid` value to lookup. 32 | public func find(identifier: JWKIdentifier) -> [JWK]? { 33 | self.find(identifier: identifier.string) 34 | } 35 | 36 | /// Retrieves the desired keys from the JSON Web Key Set 37 | /// Multiple keys can have the same `kid` if they have different `kty` values. 38 | /// - Parameter identifier: The `kid` value to lookup. 39 | public func find(identifier: String) -> [JWK]? { 40 | self.keys.filter { $0.keyIdentifier?.string == identifier } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Sources/JWTKit/JWK/JWKSigner.swift: -------------------------------------------------------------------------------- 1 | actor JWKSigner: Sendable { 2 | let jwk: JWK 3 | let parser: any JWTParser 4 | let serializer: any JWTSerializer 5 | private(set) var signer: JWTSigner? 6 | 7 | init( 8 | jwk: JWK, 9 | parser: some JWTParser = DefaultJWTParser(), 10 | serializer: some JWTSerializer = DefaultJWTSerializer() 11 | ) throws { 12 | self.jwk = jwk 13 | if let algorithm = try jwk.getKey() { 14 | self.signer = .init(algorithm: algorithm, parser: parser, serializer: serializer) 15 | } else { 16 | self.signer = nil 17 | } 18 | self.parser = parser 19 | self.serializer = serializer 20 | } 21 | 22 | func makeSigner(for algorithm: JWK.Algorithm) throws -> JWTSigner { 23 | guard let key = try jwk.getKey(for: algorithm) else { 24 | throw JWTError.invalidJWK(reason: "Unable to create signer with given algorithm") 25 | } 26 | 27 | let signer = JWTSigner(algorithm: key, parser: parser, serializer: serializer) 28 | self.signer = signer 29 | return signer 30 | } 31 | } 32 | 33 | extension JWK { 34 | func getKey(for alg: JWK.Algorithm? = nil) throws -> (any JWTAlgorithm)? { 35 | switch self.keyType.backing { 36 | case .rsa: 37 | guard 38 | let modulus = self.modulus, 39 | let exponent = self.exponent 40 | else { 41 | throw JWTError.invalidJWK(reason: "Missing RSA primitives") 42 | } 43 | 44 | let rsaKey: RSAKey = 45 | if let privateExponent = self.privateExponent { 46 | if let prime1, let prime2 { 47 | try Insecure.RSA.PrivateKey( 48 | modulus: modulus, 49 | exponent: exponent, 50 | privateExponent: privateExponent, 51 | prime1: prime1, 52 | prime2: prime2 53 | ) 54 | } else { 55 | try Insecure.RSA.PrivateKey( 56 | modulus: modulus, 57 | exponent: exponent, 58 | privateExponent: privateExponent 59 | ) 60 | } 61 | } else { 62 | try Insecure.RSA.PublicKey(modulus: modulus, exponent: exponent) 63 | } 64 | 65 | let algorithm = alg ?? self.algorithm 66 | 67 | switch algorithm { 68 | case .rs256: 69 | return RSASigner( 70 | key: rsaKey, algorithm: .sha256, name: "RS256", padding: .insecurePKCS1v1_5) 71 | case .rs384: 72 | return RSASigner( 73 | key: rsaKey, algorithm: .sha384, name: "RS384", padding: .insecurePKCS1v1_5) 74 | case .rs512: 75 | return RSASigner( 76 | key: rsaKey, algorithm: .sha512, name: "RS512", padding: .insecurePKCS1v1_5) 77 | case .ps256: 78 | return RSASigner(key: rsaKey, algorithm: .sha256, name: "PS256", padding: .PSS) 79 | case .ps384: 80 | return RSASigner(key: rsaKey, algorithm: .sha384, name: "PS384", padding: .PSS) 81 | case .ps512: 82 | return RSASigner(key: rsaKey, algorithm: .sha512, name: "PS512", padding: .PSS) 83 | default: 84 | return nil 85 | } 86 | 87 | // ECDSA 88 | 89 | case .ecdsa: 90 | guard 91 | let x = self.x, 92 | let y = self.y 93 | else { 94 | throw JWTError.invalidJWK(reason: "Missing ECDSA coordinates") 95 | } 96 | 97 | let algorithm = alg ?? self.algorithm 98 | 99 | switch algorithm { 100 | case .es256: 101 | if let privateExponent = self.privateExponent { 102 | return try ECDSASigner(key: ES256PrivateKey(key: privateExponent)) 103 | } else { 104 | return try ECDSASigner(key: ES256PublicKey(parameters: (x, y))) 105 | } 106 | case .es384: 107 | if let privateExponent = self.privateExponent { 108 | return try ECDSASigner(key: ES384PrivateKey(key: privateExponent)) 109 | } else { 110 | return try ECDSASigner(key: ES384PublicKey(parameters: (x, y))) 111 | } 112 | case .es512: 113 | if let privateExponent = self.privateExponent { 114 | return try ECDSASigner(key: ES512PrivateKey(key: privateExponent)) 115 | } else { 116 | return try ECDSASigner(key: ES512PublicKey(parameters: (x, y))) 117 | } 118 | default: 119 | return nil 120 | } 121 | 122 | // EdDSA 123 | 124 | case .octetKeyPair: 125 | guard let curve = self.curve.flatMap({ EdDSACurve(rawValue: $0.rawValue) }) else { 126 | throw JWTError.invalidJWK(reason: "Invalid EdDSA curve") 127 | } 128 | 129 | let algorithm = alg ?? self.algorithm 130 | 131 | switch (algorithm, self.x, self.privateExponent) { 132 | case (.eddsa, .some(_), .some(let d)): 133 | let key = try EdDSA.PrivateKey(d: d, curve: curve) 134 | return EdDSASigner(key: key) 135 | 136 | case (.eddsa, .some(let x), .none): 137 | let key = try EdDSA.PublicKey(x: x, curve: curve) 138 | return EdDSASigner(key: key) 139 | 140 | default: 141 | return nil 142 | } 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /Sources/JWTKit/JWTAlgorithm.swift: -------------------------------------------------------------------------------- 1 | #if !canImport(Darwin) 2 | import FoundationEssentials 3 | #else 4 | import Foundation 5 | #endif 6 | 7 | /// A protocol defining the necessary functionality for a JWT (JSON Web Token) algorithm. 8 | /// All algorithms conform to ``JWTAlgorithm`` to provide custom signing and verification logic for JWT tokens. 9 | public protocol JWTAlgorithm: Sendable { 10 | /// Unique JWT-standard name for this algorithm. 11 | var name: String { get } 12 | 13 | /// Creates a signature from the supplied plaintext. 14 | /// 15 | /// let sig = try alg.sign("hello") 16 | /// 17 | /// - parameters: 18 | /// - plaintext: Plaintext data to sign. 19 | /// - returns: Signature unique to the supplied data. 20 | func sign(_ plaintext: some DataProtocol) throws -> [UInt8] 21 | 22 | /// Returns `true` if the signature was creating by signing the plaintext. 23 | /// 24 | /// let sig = try alg.sign("hello") 25 | /// 26 | /// if alg.verify(sig, signs: "hello") { 27 | /// print("signature is valid") 28 | /// } else { 29 | /// print("signature is invalid") 30 | /// } 31 | /// 32 | /// The above snippet should print `"signature is valid"`. 33 | /// 34 | /// - parameters: 35 | /// - signature: Signature data resulting from a previous call to `sign(:_)`. 36 | /// - plaintext: Plaintext data to check signature against. 37 | /// - returns: Returns `true` if the signature was created by the supplied plaintext data. 38 | func verify(_ signature: some DataProtocol, signs plaintext: some DataProtocol) throws -> Bool 39 | } 40 | 41 | extension JWTAlgorithm { 42 | /// See ``JWTAlgorithm``. 43 | func verify(_ signature: some DataProtocol, signs plaintext: some DataProtocol) throws -> Bool { 44 | // create test signature 45 | let check = try sign(plaintext) 46 | 47 | // byte-by-byte comparison to avoid timing attacks 48 | var match = true 49 | for (a, b) in zip(check, signature) { 50 | if a != b { 51 | match = false 52 | } 53 | } 54 | 55 | // finally, if the counts match then we can accept the result 56 | if check.count == signature.count { 57 | return match 58 | } else { 59 | return false 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Sources/JWTKit/JWTError.swift: -------------------------------------------------------------------------------- 1 | #if !canImport(Darwin) 2 | import FoundationEssentials 3 | #else 4 | import Foundation 5 | #endif 6 | 7 | /// JWT error type. 8 | public struct JWTError: Error, Sendable, Equatable { 9 | public struct ErrorType: Sendable, Hashable, CustomStringConvertible, Equatable { 10 | enum Base: String, Sendable, Equatable { 11 | case claimVerificationFailure 12 | case signingAlgorithmFailure 13 | case malformedToken 14 | case signatureVerificationFailed 15 | case missingKIDHeader 16 | case unknownKID 17 | case invalidJWK 18 | case invalidBool 19 | case noKeyProvided 20 | case missingX5CHeader 21 | case invalidX5CChain 22 | case invalidHeaderField 23 | case unsupportedCurve 24 | case generic 25 | } 26 | 27 | let base: Base 28 | 29 | private init(_ base: Base) { 30 | self.base = base 31 | } 32 | 33 | public static let claimVerificationFailure = Self(.claimVerificationFailure) 34 | public static let signingAlgorithmFailure = Self(.signingAlgorithmFailure) 35 | public static let signatureVerificationFailed = Self(.signatureVerificationFailed) 36 | public static let missingKIDHeader = Self(.missingKIDHeader) 37 | public static let malformedToken = Self(.malformedToken) 38 | public static let unknownKID = Self(.unknownKID) 39 | public static let invalidJWK = Self(.invalidJWK) 40 | public static let invalidBool = Self(.invalidBool) 41 | public static let noKeyProvided = Self(.noKeyProvided) 42 | public static let missingX5CHeader = Self(.missingX5CHeader) 43 | public static let invalidX5CChain = Self(.invalidX5CChain) 44 | public static let invalidHeaderField = Self(.invalidHeaderField) 45 | public static let unsupportedCurve = Self(.unsupportedCurve) 46 | public static let generic = Self(.generic) 47 | 48 | public var description: String { 49 | base.rawValue 50 | } 51 | } 52 | 53 | private struct Backing: Sendable, Equatable { 54 | fileprivate let errorType: ErrorType 55 | fileprivate let name: String? 56 | fileprivate let reason: String? 57 | fileprivate let underlying: Error? 58 | fileprivate let kid: JWKIdentifier? 59 | fileprivate let identifier: String? 60 | fileprivate let failedClaim: (any JWTClaim)? 61 | fileprivate var curve: (any ECDSACurveType)? 62 | 63 | init( 64 | errorType: ErrorType, 65 | name: String? = nil, 66 | reason: String? = nil, 67 | underlying: Error? = nil, 68 | kid: JWKIdentifier? = nil, 69 | identifier: String? = nil, 70 | failedClaim: (any JWTClaim)? = nil, 71 | curve: (any ECDSACurveType)? = nil 72 | ) { 73 | self.errorType = errorType 74 | self.name = name 75 | self.reason = reason 76 | self.underlying = underlying 77 | self.kid = kid 78 | self.identifier = identifier 79 | self.failedClaim = failedClaim 80 | self.curve = curve 81 | } 82 | 83 | static func == (lhs: JWTError.Backing, rhs: JWTError.Backing) -> Bool { 84 | lhs.errorType == rhs.errorType 85 | } 86 | } 87 | 88 | private var backing: Backing 89 | 90 | public var errorType: ErrorType { backing.errorType } 91 | public var name: String? { backing.name } 92 | public var reason: String? { backing.reason } 93 | public var underlying: (any Error)? { backing.underlying } 94 | public var kid: JWKIdentifier? { backing.kid } 95 | public var identifier: String? { backing.identifier } 96 | public var failedClaim: (any JWTClaim)? { backing.failedClaim } 97 | public var curve: (any ECDSACurveType)? { backing.curve } 98 | 99 | private init(backing: Backing) { 100 | self.backing = backing 101 | } 102 | 103 | private init(errorType: ErrorType) { 104 | self.backing = .init(errorType: errorType) 105 | } 106 | 107 | public static func claimVerificationFailure(failedClaim: (any JWTClaim)?, reason: String) -> Self { 108 | .init(backing: .init(errorType: .claimVerificationFailure, reason: reason, failedClaim: failedClaim)) 109 | } 110 | 111 | public static func signingAlgorithmFailure(_ error: Error) -> Self { 112 | .init(backing: .init(errorType: .signingAlgorithmFailure, underlying: error)) 113 | } 114 | 115 | public static func malformedToken(reason: String) -> Self { 116 | .init(backing: .init(errorType: .malformedToken, reason: reason)) 117 | } 118 | 119 | public static let signatureVerificationFailed = Self(errorType: .signatureVerificationFailed) 120 | 121 | public static let missingKIDHeader = Self(errorType: .missingKIDHeader) 122 | 123 | public static func unknownKID(_ kid: JWKIdentifier) -> Self { 124 | .init(backing: .init(errorType: .unknownKID, kid: kid)) 125 | } 126 | 127 | public static func invalidJWK(reason: String) -> Self { 128 | .init(backing: .init(errorType: .invalidJWK, reason: reason)) 129 | } 130 | 131 | public static func invalidBool(_ name: String) -> Self { 132 | .init(backing: .init(errorType: .invalidBool, name: name)) 133 | } 134 | 135 | public static let noKeyProvided = Self(errorType: .noKeyProvided) 136 | 137 | public static let missingX5CHeader = Self(errorType: .missingX5CHeader) 138 | 139 | public static func invalidX5CChain(reason: String) -> Self { 140 | .init(backing: .init(errorType: .invalidX5CChain, reason: reason)) 141 | } 142 | 143 | public static func invalidHeaderField(reason: String) -> Self { 144 | .init(backing: .init(errorType: .invalidHeaderField, reason: reason)) 145 | } 146 | 147 | public static func unsupportedCurve(curve: any ECDSACurveType) -> Self { 148 | .init(backing: .init(errorType: .unsupportedCurve, curve: curve)) 149 | } 150 | 151 | public static func generic(identifier: String, reason: String) -> Self { 152 | .init(backing: .init(errorType: .generic, reason: reason)) 153 | } 154 | 155 | public static func == (lhs: JWTError, rhs: JWTError) -> Bool { 156 | lhs.backing == rhs.backing 157 | } 158 | } 159 | 160 | extension JWTError: CustomStringConvertible { 161 | public var description: String { 162 | var result = #"JWTKitError(errorType: \#(self.errorType)"# 163 | 164 | if let name { 165 | result.append(", name: \(String(reflecting: name))") 166 | } 167 | 168 | if let failedClaim { 169 | result.append(", failedClaim: \(String(reflecting: failedClaim))") 170 | } 171 | 172 | if let reason { 173 | result.append(", reason: \(String(reflecting: reason))") 174 | } 175 | 176 | if let underlying { 177 | result.append(", underlying: \(String(reflecting: underlying))") 178 | } 179 | 180 | if let kid { 181 | result.append(", kid: \(String(reflecting: kid))") 182 | } 183 | 184 | if let identifier { 185 | result.append(", identifier: \(String(reflecting: identifier))") 186 | } 187 | 188 | result.append(")") 189 | 190 | return result 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /Sources/JWTKit/JWTHeader+CommonFields.swift: -------------------------------------------------------------------------------- 1 | extension JWTHeader { 2 | /// The `alg` (Algorithm) Header Parameter identifies the cryptographic algorithm used to secure the JWT. 3 | /// Common values include `HS256`, `RS256`, etc. 4 | public var alg: String? { 5 | get { self[dynamicMember: "alg"]?.asString } 6 | set { self[dynamicMember: "alg"] = newValue.map { .string($0) } } 7 | } 8 | 9 | /// The `kid` (Key ID) Header Parameter is a hint indicating which key was used to secure the JWT. 10 | /// This parameter allows originators to explicitly signal a change of key to recipients. 11 | public var kid: String? { 12 | get { self[dynamicMember: "kid"]?.asString } 13 | set { self[dynamicMember: "kid"] = newValue.map { .string($0) } } 14 | } 15 | 16 | /// The `typ` (Type) Header Parameter is used to declare the media type of the JWT. 17 | /// While optional, it's typically set to `JWT`. 18 | public var typ: String? { 19 | get { self[dynamicMember: "typ"]?.asString } 20 | set { self[dynamicMember: "typ"] = newValue.map { .string($0) } } 21 | } 22 | 23 | /// The `cty` (Content Type) Header Parameter is used to declare the media type of the payload 24 | /// when the JWT is nested (e.g., encrypted JWT inside a JWT). 25 | public var cty: String? { 26 | get { self[dynamicMember: "cty"]?.asString } 27 | set { self[dynamicMember: "cty"] = newValue.map { .string($0) } } 28 | } 29 | 30 | /// The `crit` (Critical) Header Parameter indicates that extensions to standard JWT specifications 31 | /// are being used and must be understood and processed. 32 | public var crit: [String]? { 33 | get { 34 | if case .array(let array) = self[dynamicMember: "crit"] { 35 | return array.compactMap { $0.asString } 36 | } 37 | return nil 38 | } 39 | set { 40 | let arrayField = newValue?.map { JWTHeaderField.string($0) } 41 | self[dynamicMember: "crit"] = arrayField.map { .array($0) } 42 | } 43 | } 44 | 45 | /// The `jku` (JWK Set URL) Header Parameter is a URI that refers to a resource for a set of JSON-encoded public keys, 46 | /// one of which corresponds to the key used to digitally sign the JWT. 47 | public var jku: String? { 48 | get { self[dynamicMember: "jku"]?.asString } 49 | set { self[dynamicMember: "jku"] = newValue.map { .string($0) } } 50 | } 51 | 52 | /// The `jwk` (JSON Web Key) Header Parameter is a JSON object that represents a cryptographic key. 53 | /// This parameter is used to transmit a key to be used in securing the JWT. 54 | public var jwk: [String: JWTHeaderField]? { 55 | get { self[dynamicMember: "jwk"]?.asObject } 56 | set { self[dynamicMember: "jwk"] = newValue.map { .object($0) } } 57 | } 58 | 59 | /// The `x5c` (X.509 Certificate Chain) Header Parameter contains a chain of one or more PKIX certificates. 60 | /// Each string in the array is a base64-encoded (Section 4 of [RFC4648] - not base64url-encoded) DER [ITU.X690.1994] PKIX certificate value. 61 | public var x5c: [String]? { 62 | get { 63 | if case .array(let array) = self[dynamicMember: "x5c"] { 64 | return array.compactMap { $0.asString } 65 | } 66 | return nil 67 | } 68 | set { 69 | let arrayField = newValue?.map { JWTHeaderField.string($0) } 70 | self[dynamicMember: "x5c"] = arrayField.map { .array($0) } 71 | } 72 | } 73 | 74 | /// The `x5u` (X.509 URL) Header Parameter is a URI that refers to a resource for the X.509 public key certificate 75 | /// or certificate chain corresponding to the key used to digitally sign the JWT. 76 | public var x5u: String? { 77 | get { self[dynamicMember: "x5u"]?.asString } 78 | set { self[dynamicMember: "x5u"] = newValue.map { .string($0) } } 79 | } 80 | 81 | /// The `x5t` (X.509 Certificate SHA-1 Thumbprint) Header Parameter is a base64url-encoded SHA-1 thumbprint 82 | /// (a.k.a. digest) of the DER encoding of the X.509 certificate [RFC5280]. 83 | public var x5t: String? { 84 | get { self[dynamicMember: "x5t"]?.asString } 85 | set { self[dynamicMember: "x5t"] = newValue.map { .string($0) } } 86 | } 87 | 88 | /// The `x5t#S256` (X.509 Certificate SHA-256 Thumbprint) Header Parameter is a base64url-encoded SHA-256 thumbprint 89 | /// of the DER encoding of the X.509 certificate [RFC5280]. 90 | public var x5tS256: String? { 91 | get { self[dynamicMember: "x5t#S256"]?.asString } 92 | set { self[dynamicMember: "x5t#S256"] = newValue.map { .string($0) } } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /Sources/JWTKit/JWTHeader.swift: -------------------------------------------------------------------------------- 1 | /// The header (details) used for signing and processing the JWT. 2 | @dynamicMemberLookup 3 | public struct JWTHeader: Sendable { 4 | public var fields: [String: JWTHeaderField] 5 | 6 | public init(fields: [String: JWTHeaderField] = [:]) { 7 | self.fields = fields 8 | } 9 | 10 | public subscript(dynamicMember member: String) -> JWTHeaderField? { 11 | get { self.fields[member] } 12 | set { 13 | if let newValue = newValue { 14 | self.fields[member] = newValue 15 | } else { 16 | self.fields[member] = .null 17 | } 18 | } 19 | } 20 | 21 | public mutating func remove(_ field: String) { 22 | self.fields.removeValue(forKey: field) 23 | } 24 | } 25 | 26 | extension JWTHeader: ExpressibleByDictionaryLiteral { 27 | public init(dictionaryLiteral elements: (String, JWTHeaderField)...) { 28 | self.init(fields: Dictionary(uniqueKeysWithValues: elements)) 29 | } 30 | } 31 | 32 | extension JWTHeader: Codable { 33 | public func encode(to encoder: Encoder) throws { 34 | var container = encoder.container(keyedBy: CodingKeys.self) 35 | for (key, value) in self.fields { 36 | try container.encode(value, forKey: .custom(name: key)) 37 | } 38 | } 39 | 40 | public init(from decoder: Decoder) throws { 41 | let container = try decoder.container(keyedBy: CodingKeys.self) 42 | 43 | self.fields = try Set(container.allKeys) 44 | .reduce(into: [String: JWTHeaderField]()) { result, key in 45 | result[key.stringValue] = try container.decode(JWTHeaderField.self, forKey: key) 46 | } 47 | } 48 | 49 | private enum CodingKeys: CodingKey, Equatable, Hashable { 50 | case custom(name: String) 51 | 52 | var stringValue: String { 53 | switch self { 54 | case .custom(let name): 55 | return name 56 | } 57 | } 58 | 59 | var intValue: Int? { nil } 60 | 61 | init?(stringValue: String) { 62 | self = .custom(name: stringValue) 63 | } 64 | 65 | init?(intValue _: Int) { nil } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Sources/JWTKit/JWTHeaderField.swift: -------------------------------------------------------------------------------- 1 | #if !canImport(Darwin) 2 | import FoundationEssentials 3 | #else 4 | import Foundation 5 | #endif 6 | 7 | public indirect enum JWTHeaderField: Hashable, Sendable, Codable { 8 | case null 9 | case bool(Bool) 10 | case int(Int) 11 | case decimal(Double) 12 | case string(String) 13 | case array([JWTHeaderField]) 14 | case object([String: JWTHeaderField]) 15 | 16 | public init(from decoder: any Decoder) throws { 17 | let container: any SingleValueDecodingContainer 18 | 19 | do { 20 | container = try decoder.singleValueContainer() 21 | } catch DecodingError.typeMismatch { 22 | self = .null 23 | return 24 | } 25 | 26 | if container.decodeNil() { 27 | self = .null 28 | return 29 | } 30 | 31 | do { 32 | self = try .bool(container.decode(Bool.self)) 33 | return 34 | } catch DecodingError.typeMismatch {} 35 | 36 | // This is a bit of a hack to correctly differentiate between integers and doubles 37 | do { 38 | let doubleValue = try container.decode(Double.self) 39 | if doubleValue.truncatingRemainder(dividingBy: 1) == 0 { 40 | self = .int(Int(doubleValue)) 41 | } else { 42 | self = .decimal(doubleValue) 43 | } 44 | return 45 | } catch DecodingError.typeMismatch {} 46 | 47 | do { 48 | self = try .string(container.decode(String.self)) 49 | return 50 | } catch DecodingError.typeMismatch {} 51 | 52 | do { 53 | self = try .array(container.decode([Self].self)) 54 | return 55 | } catch DecodingError.typeMismatch {} 56 | 57 | do { 58 | self = try .object(container.decode([String: Self].self)) 59 | return 60 | } catch DecodingError.typeMismatch {} 61 | 62 | throw DecodingError.dataCorruptedError( 63 | in: container, debugDescription: "No valid JSON type found.") 64 | } 65 | 66 | public func encode(to encoder: any Encoder) throws { 67 | var container = encoder.singleValueContainer() 68 | switch self { 69 | case .null: try container.encodeNil() 70 | case .bool(let value): try container.encode(value) 71 | case .int(let value): try container.encode(value) 72 | case .decimal(let value): try container.encode(value) 73 | case .string(let value): try container.encode(value) 74 | case .array(let value): try container.encode(value) 75 | case .object(let value): try container.encode(value) 76 | } 77 | } 78 | } 79 | 80 | extension JWTHeaderField { 81 | public var isNull: Bool { if case .null = self { true } else { false } } 82 | public var asBool: Bool? { if case .bool(let b) = self { b } else { nil } } 83 | public var asInt: Int? { if case .int(let i) = self { i } else { nil } } 84 | public var asDecimal: Double? { if case .decimal(let d) = self { d } else { nil } } 85 | public var asString: String? { if case .string(let s) = self { s } else { nil } } 86 | public var asArray: [Self]? { if case .array(let a) = self { a } else { nil } } 87 | public var asObject: [String: Self]? { if case .object(let o) = self { o } else { nil } } 88 | } 89 | 90 | extension JWTHeaderField { 91 | public func asObject(of _: T.Type) throws -> [String: T] { 92 | guard let object = self.asObject else { 93 | throw JWTError.invalidHeaderField(reason: "Element is not an object") 94 | } 95 | let values: [String: T]? = 96 | switch T.self { 97 | case is Bool.Type: object.compactMapValues { $0.asBool } as? [String: T] 98 | case is Int.Type: object.compactMapValues { $0.asInt } as? [String: T] 99 | case is Double.Type: object.compactMapValues { $0.asDecimal } as? [String: T] 100 | case is String.Type: object.compactMapValues { $0.asString } as? [String: T] 101 | default: nil 102 | } 103 | guard let values, object.count == values.count else { 104 | throw JWTError.invalidHeaderField(reason: "Object is not homogeneous") 105 | } 106 | return values 107 | } 108 | 109 | public func asArray(of _: T.Type) throws -> [T] { 110 | guard let array = self.asArray else { 111 | throw JWTError.invalidHeaderField(reason: "Element is not an array") 112 | } 113 | let values: [T]? = 114 | switch T.self { 115 | case is Bool.Type: array.compactMap { $0.asBool } as? [T] 116 | case is Int.Type: array.compactMap { $0.asInt } as? [T] 117 | case is Double.Type: array.compactMap { $0.asDecimal } as? [T] 118 | case is String.Type: array.compactMap { $0.asString } as? [T] 119 | default: nil 120 | } 121 | guard let values, array.count == values.count else { 122 | throw JWTError.invalidHeaderField(reason: "Array is not homogeneous") 123 | } 124 | return values 125 | } 126 | } 127 | 128 | extension JWTHeaderField: ExpressibleByNilLiteral { 129 | public init(nilLiteral _: ()) { 130 | self = .null 131 | } 132 | } 133 | 134 | extension JWTHeaderField: ExpressibleByStringLiteral { 135 | public init(stringLiteral value: StringLiteralType) { 136 | self = .string(value) 137 | } 138 | } 139 | 140 | extension JWTHeaderField: ExpressibleByIntegerLiteral { 141 | public init(integerLiteral value: IntegerLiteralType) { 142 | self = .int(value) 143 | } 144 | } 145 | 146 | extension JWTHeaderField: ExpressibleByBooleanLiteral { 147 | public init(booleanLiteral value: BooleanLiteralType) { 148 | self = .bool(value) 149 | } 150 | } 151 | 152 | extension JWTHeaderField: ExpressibleByFloatLiteral { 153 | public init(floatLiteral value: FloatLiteralType) { 154 | self = .decimal(value) 155 | } 156 | } 157 | 158 | extension JWTHeaderField: ExpressibleByArrayLiteral { 159 | public init(arrayLiteral elements: JWTHeaderField...) { 160 | self = .array(elements) 161 | } 162 | } 163 | 164 | extension JWTHeaderField: ExpressibleByDictionaryLiteral { 165 | public init(dictionaryLiteral elements: (String, JWTHeaderField)...) { 166 | self = .object(Dictionary(uniqueKeysWithValues: elements)) 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /Sources/JWTKit/JWTParser.swift: -------------------------------------------------------------------------------- 1 | #if !canImport(Darwin) 2 | import FoundationEssentials 3 | #else 4 | import Foundation 5 | #endif 6 | 7 | public protocol JWTParser: Sendable { 8 | var jsonDecoder: JWTJSONDecoder { get set } 9 | func parse(_ token: some DataProtocol, as: Payload.Type) throws -> ( 10 | header: JWTHeader, payload: Payload, signature: Data 11 | ) where Payload: JWTPayload 12 | } 13 | 14 | extension JWTParser { 15 | public func getTokenParts(_ token: some DataProtocol) throws -> ( 16 | header: ArraySlice, payload: ArraySlice, signature: ArraySlice 17 | ) { 18 | let tokenParts = token.copyBytes().split( 19 | separator: .period, omittingEmptySubsequences: false 20 | ) 21 | 22 | guard tokenParts.count == 3 else { 23 | throw JWTError.malformedToken(reason: "Token is not split in 3 parts") 24 | } 25 | 26 | return (tokenParts[0], tokenParts[1], tokenParts[2]) 27 | } 28 | } 29 | 30 | extension JWTParser { 31 | func parseHeader(_ token: some DataProtocol) throws -> JWTHeader { 32 | let tokenParts = token.copyBytes().split(separator: .period, omittingEmptySubsequences: false) 33 | 34 | guard tokenParts.count == 3 else { 35 | throw JWTError.malformedToken(reason: "Token parts count is not 3.") 36 | } 37 | 38 | do { 39 | return try jsonDecoder.decode(JWTHeader.self, from: .init(tokenParts[0].base64URLDecodedBytes())) 40 | } catch { 41 | throw JWTError.malformedToken(reason: "Couldn't decode header from JWT with error: \(String(describing: error)).") 42 | } 43 | } 44 | } 45 | 46 | public struct DefaultJWTParser: JWTParser { 47 | public var jsonDecoder: JWTJSONDecoder = .defaultForJWT 48 | 49 | public init(jsonDecoder: JWTJSONDecoder = .defaultForJWT) { 50 | self.jsonDecoder = jsonDecoder 51 | } 52 | 53 | public func parse(_ token: some DataProtocol, as: Payload.Type) throws -> ( 54 | header: JWTHeader, payload: Payload, signature: Data 55 | ) where Payload: JWTPayload { 56 | let (encodedHeader, encodedPayload, encodedSignature) = try getTokenParts(token) 57 | 58 | let header: JWTHeader 59 | let payload: Payload 60 | let signature: Data 61 | 62 | func isUTF8(_ bytes: [UInt8]) -> Bool { 63 | String(bytes: bytes, encoding: .utf8) != nil 64 | } 65 | 66 | let headerBytes = encodedHeader.base64URLDecodedBytes() 67 | let payloadBytes = encodedPayload.base64URLDecodedBytes() 68 | 69 | guard isUTF8(headerBytes) && isUTF8(payloadBytes) else { 70 | throw JWTError.malformedToken(reason: "Header and payload must be UTF-8 encoded.") 71 | } 72 | 73 | do { 74 | header = try jsonDecoder.decode(JWTHeader.self, from: .init(headerBytes)) 75 | payload = try jsonDecoder.decode(Payload.self, from: .init(payloadBytes)) 76 | signature = Data(encodedSignature.base64URLDecodedBytes()) 77 | } catch { 78 | throw JWTError.malformedToken(reason: "Couldn't decode JWT with error: \(String(describing: error))") 79 | } 80 | 81 | return (header: header, payload: payload, signature: signature) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Sources/JWTKit/JWTPayload.swift: -------------------------------------------------------------------------------- 1 | /// A JWT payload is a Publically Readable set of claims. 2 | /// Each variable represents a claim. 3 | public protocol JWTPayload: Codable, Sendable { 4 | /// Verifies that the payload's claims are correct or throws an error. 5 | func verify(using algorithm: some JWTAlgorithm) async throws 6 | } 7 | -------------------------------------------------------------------------------- /Sources/JWTKit/JWTSerializer.swift: -------------------------------------------------------------------------------- 1 | import X509 2 | 3 | #if !canImport(Darwin) 4 | import FoundationEssentials 5 | #else 6 | import Foundation 7 | #endif 8 | 9 | public protocol JWTSerializer: Sendable { 10 | var jsonEncoder: any JWTJSONEncoder { get } 11 | func serialize(_ payload: some JWTPayload, header: JWTHeader) throws -> Data 12 | } 13 | 14 | extension JWTSerializer { 15 | public func makeHeader(from header: JWTHeader, key: JWTAlgorithm) async throws -> JWTHeader { 16 | var newHeader = header 17 | 18 | newHeader.alg = newHeader.alg ?? key.name 19 | newHeader.typ = newHeader.typ ?? "JWT" 20 | 21 | if let x5c = newHeader.x5c, !x5c.isEmpty { 22 | let verifier = try X5CVerifier(rootCertificates: [x5c[0]]) 23 | let certs = try x5c.map { try Certificate(pemEncoded: $0) } 24 | _ = try await verifier.verifyChain(certificates: certs) 25 | 26 | newHeader.x5c = try x5c.map { cert in 27 | let certificate = try Certificate(pemEncoded: cert) 28 | let derBytes = try Data(certificate.serializeAsPEM().derBytes) 29 | return derBytes.base64EncodedString() 30 | } 31 | } 32 | 33 | return newHeader 34 | } 35 | 36 | func makeSigningInput(payload: some JWTPayload, header: JWTHeader, key: some JWTAlgorithm) async throws -> Data { 37 | let header = try await self.makeHeader(from: header, key: key) 38 | let encodedHeader = try jsonEncoder.encode(header).base64URLEncodedBytes() 39 | 40 | let encodedPayload = try self.serialize(payload, header: header) 41 | 42 | return encodedHeader + [.period] + encodedPayload 43 | } 44 | 45 | func sign(_ payload: some JWTPayload, with header: JWTHeader = JWTHeader(), using key: some JWTAlgorithm) async throws -> String { 46 | let signingInput = try await makeSigningInput(payload: payload, header: header, key: key) 47 | 48 | let signatureData = try key.sign(signingInput) 49 | 50 | let bytes = signingInput + [.period] + signatureData.base64URLEncodedBytes() 51 | return String(decoding: bytes, as: UTF8.self) 52 | } 53 | } 54 | 55 | public struct DefaultJWTSerializer: JWTSerializer { 56 | public var jsonEncoder: JWTJSONEncoder = .defaultForJWT 57 | 58 | public init(jsonEncoder: JWTJSONEncoder = .defaultForJWT) { 59 | self.jsonEncoder = jsonEncoder 60 | } 61 | 62 | public func serialize(_ payload: some JWTPayload, header: JWTHeader = JWTHeader()) throws -> Data { 63 | try Data(jsonEncoder.encode(payload).base64URLEncodedBytes()) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Sources/JWTKit/JWTSigner.swift: -------------------------------------------------------------------------------- 1 | #if !canImport(Darwin) 2 | import FoundationEssentials 3 | #else 4 | import Foundation 5 | #endif 6 | 7 | /// A JWT signer. 8 | final class JWTSigner: Sendable { 9 | let algorithm: JWTAlgorithm 10 | 11 | let parser: any JWTParser 12 | let serializer: any JWTSerializer 13 | 14 | init( 15 | algorithm: some JWTAlgorithm, 16 | parser: any JWTParser = DefaultJWTParser(), 17 | serializer: any JWTSerializer = DefaultJWTSerializer() 18 | ) { 19 | self.algorithm = algorithm 20 | self.parser = parser 21 | self.serializer = serializer 22 | } 23 | 24 | func sign(_ payload: some JWTPayload, with header: JWTHeader = .init()) async throws -> String { 25 | try await serializer.sign(payload, with: header, using: self.algorithm) 26 | } 27 | 28 | func verify(_ token: some DataProtocol) async throws -> Payload where Payload: JWTPayload { 29 | let (encodedHeader, encodedPayload, encodedSignature) = try parser.getTokenParts(token) 30 | let data = encodedHeader + [.period] + encodedPayload 31 | let signature = encodedSignature.base64URLDecodedBytes() 32 | 33 | guard try algorithm.verify(signature, signs: data) else { 34 | throw JWTError.signatureVerificationFailed 35 | } 36 | 37 | let (_, payload, _) = try parser.parse(token, as: Payload.self) 38 | 39 | try await payload.verify(using: algorithm) 40 | return payload 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Sources/JWTKit/None/JWTKeyCollection+UnsecuredNone.swift: -------------------------------------------------------------------------------- 1 | extension JWTKeyCollection { 2 | /// Adds a configuration for JWTs without a signature. 3 | /// 4 | /// This method configures JWT processing to accept tokens with the 'none' algorithm, indicating that the JWT 5 | /// is not secured by a signature. Use this with caution, as it means the token's integrity and authenticity 6 | /// are not verified through cryptographic means. 7 | /// 8 | /// Tokens without a signature ('none' algorithm) are typically used in trusted environments or for specific 9 | /// use cases where security is not a primary concern, such as testing environments. 10 | /// 11 | /// Usage Example: 12 | /// ``` 13 | /// let collection = await JWTKeyCollection() 14 | /// .addUnsecuredNone() 15 | /// ``` 16 | /// 17 | /// - Parameters: 18 | /// - kid: An optional `JWKIdentifier` (Key ID). If provided, it is used in the JWT `kid` header field to 19 | /// identify this key. While the key is unsecured, the `kid` can still be useful for 20 | /// consistent token structure or for routing purposes. 21 | /// - jsonEncoder: An optional custom JSON encoder conforming to `JWTJSONEncoder`. This encoder is used 22 | /// for encoding JWTs. If not provided, a default encoder is used. 23 | /// - jsonDecoder: An optional custom JSON decoder conforming to `JWTJSONDecoder`. This decoder is used 24 | /// for decoding JWTs. If not provided, a default decoder is used. 25 | /// - Returns: The same instance of the collection (`Self`), facilitating method chaining. 26 | /// 27 | /// Note: As this configuration does not secure the JWT, ensure its use is appropriate for the security 28 | /// requirements of your system. It is not recommended for scenarios where data integrity and authentication 29 | /// are critical. 30 | @discardableResult 31 | public func addUnsecuredNone( 32 | kid: JWKIdentifier? = nil, 33 | parser: some JWTParser = DefaultJWTParser(), 34 | serializer: some JWTSerializer = DefaultJWTSerializer() 35 | ) -> Self { 36 | add( 37 | .init(algorithm: UnsecuredNoneSigner(), parser: parser, serializer: serializer), 38 | for: kid) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/JWTKit/None/UnsecuredNoneSigner.swift: -------------------------------------------------------------------------------- 1 | #if !canImport(Darwin) 2 | import FoundationEssentials 3 | #else 4 | import Foundation 5 | #endif 6 | 7 | struct UnsecuredNoneSigner: JWTAlgorithm { 8 | var name: String { 9 | "none" 10 | } 11 | 12 | func sign(_: some DataProtocol) throws -> [UInt8] { 13 | [] 14 | } 15 | 16 | func verify(_ signature: some DataProtocol, signs _: some DataProtocol) throws -> Bool { 17 | signature.isEmpty 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/JWTKit/RSA/JWTKeyCollection+RSA.swift: -------------------------------------------------------------------------------- 1 | import _CryptoExtras 2 | 3 | extension JWTKeyCollection { 4 | /// Adds an RSA key to the collection. 5 | /// 6 | /// This method configures and adds an RSA key to the collection. The key is used for signing JWTs 7 | /// 8 | /// Example Usage: 9 | /// ``` 10 | /// let collection = await JWTKeyCollection() 11 | /// .addRSA(key: myRSAKey) 12 | /// ``` 13 | /// 14 | /// - Parameters: 15 | /// - key: The ``RSAKey`` to use for signing. This key should be kept secure and not exposed. 16 | /// - kid: An optional ``JWKIdentifier`` (Key ID). If provided, it will be used to identify this key 17 | /// in the JWT `kid` header field. 18 | /// - jsonEncoder: An optional custom JSON encoder conforming to ``JWTJSONEncoder`` used for encoding JWTs. 19 | /// If `nil`, a default encoder is used. 20 | /// - jsonDecoder: An optional custom JSON decoder conforming to ``JWTJSONDecoder`` used for decoding JWTs. 21 | /// If `nil`, a default decoder is used. 22 | /// - Returns: The same instance of the collection (`Self`), enabling method chaining. 23 | @discardableResult 24 | public func add( 25 | rsa key: some RSAKey, 26 | digestAlgorithm: DigestAlgorithm, 27 | kid: JWKIdentifier? = nil, 28 | parser: some JWTParser = DefaultJWTParser(), 29 | serializer: some JWTSerializer = DefaultJWTSerializer() 30 | ) -> Self { 31 | let name = 32 | switch digestAlgorithm.backing { 33 | case .sha256: 34 | "RS256" 35 | case .sha384: 36 | "RS384" 37 | case .sha512: 38 | "RS512" 39 | } 40 | 41 | return add( 42 | .init( 43 | algorithm: RSASigner( 44 | key: key, algorithm: digestAlgorithm, name: name, padding: .insecurePKCS1v1_5), 45 | parser: parser, 46 | serializer: serializer 47 | ), 48 | for: kid) 49 | } 50 | 51 | /// Adds a PSS key to the collection. 52 | /// 53 | /// This method configures and adds a PSS (RSA PSS Signature) key to the collection. PSS 54 | /// uses RSASSA-PSS for the RSA signature, which is considered more secure than PKCS#1 v1.5 55 | /// padding used in RSA. 56 | /// 57 | /// Example Usage: 58 | /// ``` 59 | /// let collection = await JWTKeyCollection() 60 | /// .addPSS(key: myRSAKey) 61 | /// ``` 62 | /// 63 | /// - Parameters: 64 | /// - key: The ``RSAKey`` to use for signing. This key should be kept secure and not exposed. 65 | /// - kid: An optional ``JWKIdentifier`` (Key ID). If provided, it will be used to identify this key 66 | /// in the JWT `kid` header field. 67 | /// - jsonEncoder: An optional custom JSON encoder conforming to ``JWTJSONEncoder`` used for encoding JWTs. 68 | /// If `nil`, a default encoder is used. 69 | /// - jsonDecoder: An optional custom JSON decoder conforming to ``JWTJSONDecoder`` used for decoding JWTs. 70 | /// If `nil`, a default decoder is used. 71 | /// - Returns: The same instance of the collection (`Self`), enabling method chaining. 72 | @discardableResult 73 | public func add( 74 | pss key: some RSAKey, 75 | digestAlgorithm: DigestAlgorithm, 76 | kid: JWKIdentifier? = nil, 77 | parser: some JWTParser = DefaultJWTParser(), 78 | serializer: some JWTSerializer = DefaultJWTSerializer() 79 | ) -> Self { 80 | let name = 81 | switch digestAlgorithm.backing { 82 | case .sha256: 83 | "PS256" 84 | case .sha384: 85 | "PS384" 86 | case .sha512: 87 | "PS512" 88 | } 89 | 90 | return add( 91 | .init( 92 | algorithm: RSASigner( 93 | key: key, algorithm: digestAlgorithm, name: name, padding: .PSS), 94 | parser: parser, 95 | serializer: serializer 96 | ), 97 | for: kid) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /Sources/JWTKit/RSA/RSAError.swift: -------------------------------------------------------------------------------- 1 | enum RSAError: Error { 2 | case privateKeyRequired 3 | case publicKeyRequired 4 | case signFailure(_ error: Error) 5 | case keyInitializationFailure 6 | case keySizeTooSmall 7 | } 8 | -------------------------------------------------------------------------------- /Sources/JWTKit/RSA/RSASigner.swift: -------------------------------------------------------------------------------- 1 | import _CryptoExtras 2 | 3 | #if !canImport(Darwin) 4 | import FoundationEssentials 5 | #else 6 | import Foundation 7 | #endif 8 | 9 | struct RSASigner: JWTAlgorithm, CryptoSigner { 10 | let publicKey: Insecure.RSA.PublicKey 11 | let privateKey: Insecure.RSA.PrivateKey? 12 | var algorithm: DigestAlgorithm 13 | let name: String 14 | let padding: _RSA.Signing.Padding 15 | 16 | init(key: some RSAKey, algorithm: DigestAlgorithm, name: String, padding: _RSA.Signing.Padding) { 17 | switch key { 18 | case let key as Insecure.RSA.PrivateKey: 19 | self.privateKey = key 20 | self.publicKey = key.publicKey 21 | case let key as Insecure.RSA.PublicKey: 22 | self.publicKey = key 23 | self.privateKey = nil 24 | default: 25 | // This should never happen 26 | fatalError("Unexpected key type: \(type(of: key))") 27 | } 28 | self.algorithm = algorithm 29 | self.name = name 30 | self.padding = padding 31 | } 32 | 33 | func sign(_ plaintext: some DataProtocol) throws -> [UInt8] { 34 | guard let privateKey else { 35 | throw JWTError.signingAlgorithmFailure(RSAError.privateKeyRequired) 36 | } 37 | 38 | let digest = try self.digest(plaintext) 39 | 40 | do { 41 | let signature = try privateKey.signature(for: digest, padding: padding) 42 | return [UInt8](signature.rawRepresentation) 43 | } catch { 44 | throw JWTError.signingAlgorithmFailure(RSAError.signFailure(error)) 45 | } 46 | } 47 | 48 | func verify(_ signature: some DataProtocol, signs plaintext: some DataProtocol) throws -> Bool { 49 | let digest = try self.digest(plaintext) 50 | let signature = _RSA.Signing.RSASignature(rawRepresentation: signature) 51 | 52 | return publicKey.isValidSignature(signature, for: digest, padding: padding) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Sources/JWTKit/Utilities/Base64URL.swift: -------------------------------------------------------------------------------- 1 | #if !canImport(Darwin) 2 | import FoundationEssentials 3 | #else 4 | import Foundation 5 | #endif 6 | 7 | extension String { 8 | package func base64URLDecodedData() -> Data? { 9 | var base64URL = replacingOccurrences(of: "-", with: "+") 10 | .replacingOccurrences(of: "_", with: "/") 11 | 12 | base64URL.append(contentsOf: "===".prefix((4 - (base64URL.count & 3)) & 3)) 13 | 14 | return Data(base64Encoded: base64URL) 15 | } 16 | } 17 | 18 | extension DataProtocol { 19 | package func base64URLDecodedBytes() -> [UInt8] { 20 | Data(base64Encoded: Data(copyBytes()).base64URLUnescaped())?.copyBytes() ?? [] 21 | } 22 | 23 | package func base64URLEncodedBytes() -> [UInt8] { 24 | Data(copyBytes()).base64EncodedData().base64URLEscaped().copyBytes() 25 | } 26 | } 27 | 28 | // MARK: Data Escape 29 | 30 | extension Data { 31 | /// Converts base64-url encoded data to a base64 encoded data. 32 | /// 33 | /// https://tools.ietf.org/html/rfc4648#page-7 34 | fileprivate mutating func base64URLUnescape() { 35 | for idx in self.indices { 36 | switch self[idx] { 37 | case 0x2D: // - 38 | self[idx] = 0x2B // + 39 | case 0x5F: // _ 40 | self[idx] = 0x2F // / 41 | default: break 42 | } 43 | } 44 | /// https://stackoverflow.com/questions/43499651/decode-base64url-to-base64-swift 45 | let padding = count % 4 46 | if padding > 0 { 47 | self += Data(repeating: 0x3D, count: 4 - count % 4) 48 | } 49 | } 50 | 51 | /// Converts base64 encoded data to a base64-url encoded data. 52 | /// 53 | /// https://tools.ietf.org/html/rfc4648#page-7 54 | fileprivate mutating func base64URLEscape() { 55 | for idx in self.indices { 56 | switch self[idx] { 57 | case 0x2B: // + 58 | self[idx] = 0x2D // - 59 | case 0x2F: // / 60 | self[idx] = 0x5F // _ 61 | default: break 62 | } 63 | } 64 | self = split(separator: 0x3D).first ?? .init() 65 | } 66 | 67 | /// Converts base64-url encoded data to a base64 encoded data. 68 | /// 69 | /// https://tools.ietf.org/html/rfc4648#page-7 70 | fileprivate func base64URLUnescaped() -> Data { 71 | var data = self 72 | data.base64URLUnescape() 73 | return data 74 | } 75 | 76 | /// Converts base64 encoded data to a base64-url encoded data. 77 | /// 78 | /// https://tools.ietf.org/html/rfc4648#page-7 79 | fileprivate func base64URLEscaped() -> Data { 80 | var data = self 81 | data.base64URLEscape() 82 | return data 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Sources/JWTKit/Utilities/CryptoSigner.swift: -------------------------------------------------------------------------------- 1 | import Crypto 2 | 3 | #if !canImport(Darwin) 4 | import FoundationEssentials 5 | #else 6 | import Foundation 7 | #endif 8 | 9 | public struct DigestAlgorithm: Sendable, Equatable { 10 | enum Backing { 11 | case sha256 12 | case sha384 13 | case sha512 14 | } 15 | 16 | let backing: Backing 17 | 18 | public static let sha256 = Self(backing: .sha256) 19 | public static let sha384 = Self(backing: .sha384) 20 | public static let sha512 = Self(backing: .sha512) 21 | } 22 | 23 | protocol CryptoSigner: Sendable { 24 | var algorithm: DigestAlgorithm { get } 25 | } 26 | 27 | private enum CryptoError: Error { 28 | case digestInitializationFailure 29 | case digestUpdateFailure 30 | case digestFinalizationFailure 31 | case bioPutsFailure 32 | case bioConversionFailure 33 | } 34 | 35 | extension CryptoSigner { 36 | func digest(_ plaintext: some DataProtocol) throws -> any Digest { 37 | switch algorithm.backing { 38 | case .sha256: 39 | SHA256.hash(data: plaintext) 40 | case .sha384: 41 | SHA384.hash(data: plaintext) 42 | case .sha512: 43 | SHA512.hash(data: plaintext) 44 | } 45 | } 46 | } 47 | 48 | enum KeyType { 49 | case `public` 50 | case `private` 51 | } 52 | -------------------------------------------------------------------------------- /Sources/JWTKit/Utilities/CustomizedJSONCoders.swift: -------------------------------------------------------------------------------- 1 | #if !canImport(Darwin) 2 | import FoundationEssentials 3 | #else 4 | import Foundation 5 | #endif 6 | 7 | public protocol JWTJSONDecoder: Sendable { 8 | func decode(_: T.Type, from string: Data) throws -> T 9 | } 10 | 11 | public protocol JWTJSONEncoder: Sendable { 12 | func encode(_ value: T) throws -> Data 13 | } 14 | 15 | extension JSONDecoder: JWTJSONDecoder {} 16 | extension JSONEncoder: JWTJSONEncoder {} 17 | 18 | extension JSONDecoder.DateDecodingStrategy { 19 | public static var integerSecondsSince1970: Self { 20 | .custom { decoder in 21 | let container = try decoder.singleValueContainer() 22 | return try Date(timeIntervalSince1970: Double(container.decode(Int.self))) 23 | } 24 | } 25 | } 26 | 27 | extension JSONEncoder.DateEncodingStrategy { 28 | public static var integerSecondsSince1970: Self { 29 | .custom { date, encoder in 30 | var container = encoder.singleValueContainer() 31 | try container.encode(Int(date.timeIntervalSince1970.rounded(.towardZero))) 32 | } 33 | } 34 | } 35 | 36 | extension JWTJSONEncoder where Self == JSONEncoder { 37 | public static var defaultForJWT: any JWTJSONEncoder { 38 | let encoder = JSONEncoder() 39 | encoder.dateEncodingStrategy = .secondsSince1970 40 | return encoder 41 | } 42 | } 43 | 44 | extension JWTJSONDecoder where Self == JSONDecoder { 45 | public static var defaultForJWT: any JWTJSONDecoder { 46 | let decoder = JSONDecoder() 47 | decoder.dateDecodingStrategy = .secondsSince1970 48 | return decoder 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Sources/JWTKit/Utilities/Utilities.swift: -------------------------------------------------------------------------------- 1 | #if !canImport(Darwin) 2 | import FoundationEssentials 3 | #else 4 | import Foundation 5 | #endif 6 | 7 | extension DataProtocol { 8 | public func copyBytes() -> [UInt8] { 9 | if let array = self.withContiguousStorageIfAvailable({ buffer in 10 | [UInt8](buffer) 11 | }) { 12 | return array 13 | } else { 14 | let buffer = UnsafeMutableBufferPointer.allocate(capacity: self.count) 15 | self.copyBytes(to: buffer) 16 | defer { buffer.deallocate() } 17 | return [UInt8](buffer) 18 | } 19 | } 20 | } 21 | 22 | extension UInt8 { 23 | static var period: UInt8 { 24 | return Character(".").asciiValue! 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/JWTKit/Vendor/AppleIdentityToken.swift: -------------------------------------------------------------------------------- 1 | /// - See Also: 2 | /// [Retrieve the User’s Information from Apple ID Servers](https://developer.apple.com/documentation/signinwithapplerestapi/authenticating_users_with_sign_in_with_apple) 3 | public struct AppleIdentityToken: JWTPayload { 4 | enum CodingKeys: String, CodingKey { 5 | case nonce, email 6 | case issuer = "iss" 7 | case subject = "sub" 8 | case audience = "aud" 9 | case issuedAt = "iat" 10 | case expires = "exp" 11 | case emailVerified = "email_verified" 12 | case isPrivateEmail = "is_private_email" 13 | case nonceSupported = "nonce_supported" 14 | case orgId = "org_id" 15 | case realUserStatus = "real_user_status" 16 | } 17 | 18 | /// The issuer-registered claim key, which has the value https://appleid.apple.com. 19 | public let issuer: IssuerClaim 20 | 21 | /// Your `client_id` in your Apple Developer account. 22 | public let audience: AudienceClaim 23 | 24 | /// The expiry time for the token. This value is typically set to 5 minutes. 25 | public let expires: ExpirationClaim 26 | 27 | /// The time the token was issued. 28 | public let issuedAt: IssuedAtClaim 29 | 30 | /// The unique identifier for the user. 31 | public let subject: SubjectClaim 32 | 33 | /// A Boolean value that indicates whether the transaction is on a nonce-supported platform. If you sent a nonce in the authorization 34 | /// request but do not see the nonce claim in the ID token, check this claim to determine how to proceed. If this claim returns true you 35 | /// should treat nonce as mandatory and fail the transaction; otherwise, you can proceed treating the nonce as optional. 36 | public let nonceSupported: BoolClaim? 37 | 38 | /// A string value used to associate a client session and an ID token. This value is used to mitigate replay attacks and is present only 39 | /// if passed during the authorization request. 40 | public let nonce: String? 41 | 42 | /// The user's email address. 43 | public let email: String? 44 | 45 | /// Managed Apple ID organization (see https://developer.apple.com/documentation/rosterapi/integrating_with_roster_api_and_sign_in_with_apple) 46 | public let orgId: String? 47 | 48 | /// A Boolean value that indicates whether the service has verified the email. The value of this claim is always true because the servers only return verified email addresses. 49 | public let emailVerified: BoolClaim? 50 | 51 | /// A Boolean value that indicates whether the email shared by the user is the proxy address. It is absent (nil) if the user is not using a proxy email address. 52 | public let isPrivateEmail: BoolClaim? 53 | 54 | /// A value that indicates whether the user appears to be a real person. 55 | public let realUserStatus: UserDetectionStatus? 56 | 57 | public init( 58 | issuer: IssuerClaim, 59 | audience: AudienceClaim, 60 | expires: ExpirationClaim, 61 | issuedAt: IssuedAtClaim, 62 | subject: SubjectClaim, 63 | nonceSupported: BoolClaim? = nil, 64 | nonce: String? = nil, 65 | email: String? = nil, 66 | orgId: String? = nil, 67 | emailVerified: BoolClaim? = nil, 68 | isPrivateEmail: BoolClaim? = nil, 69 | realUserStatus: UserDetectionStatus? = nil 70 | ) { 71 | self.issuer = issuer 72 | self.audience = audience 73 | self.expires = expires 74 | self.issuedAt = issuedAt 75 | self.subject = subject 76 | self.nonceSupported = nonceSupported 77 | self.nonce = nonce 78 | self.email = email 79 | self.orgId = orgId 80 | self.emailVerified = emailVerified 81 | self.isPrivateEmail = isPrivateEmail 82 | self.realUserStatus = realUserStatus 83 | } 84 | 85 | public func verify(using _: some JWTAlgorithm) throws { 86 | guard self.issuer.value == "https://appleid.apple.com" else { 87 | throw JWTError.claimVerificationFailure(failedClaim: issuer, reason: "Token not provided by Apple") 88 | } 89 | 90 | try self.expires.verifyNotExpired() 91 | } 92 | } 93 | 94 | extension AppleIdentityToken { 95 | /// Taken from https://developer.apple.com/documentation/authenticationservices/asuserdetectionstatus 96 | /// With slight modification to make adding new cases non-breaking. 97 | public struct UserDetectionStatus: OptionSet, Codable, Sendable { 98 | /// Used for decoding/encoding 99 | private enum Status: Int, Codable { 100 | case unsupported 101 | case unknown 102 | case likelyReal 103 | } 104 | 105 | /// Not supported on current platform, ignore the value 106 | public static let unsupported = UserDetectionStatus([]) // 0 was giving a warning 107 | 108 | /// We could not determine the value. New users in the ecosystem will get this value as well, so you should not block these users, but instead treat them as any new user through standard email sign up flows 109 | public static let unknown = UserDetectionStatus(rawValue: 1) 110 | 111 | /// A hint that we have high confidence that the user is real 112 | public static let likelyReal = UserDetectionStatus(rawValue: 2) 113 | 114 | public let rawValue: Int 115 | 116 | public init(rawValue: Int) { 117 | self.rawValue = rawValue 118 | } 119 | 120 | public init(from decoder: Decoder) throws { 121 | let value = try decoder.singleValueContainer().decode(Status.self) 122 | switch value { 123 | case .unsupported: self = .unsupported 124 | case .unknown: self = .unknown 125 | case .likelyReal: self = .likelyReal 126 | } 127 | } 128 | 129 | public func encode(to encoder: Encoder) throws { 130 | var container = encoder.singleValueContainer() 131 | switch self { 132 | case .unsupported: try container.encode(Status.unsupported) 133 | case .unknown: try container.encode(Status.unknown) 134 | case .likelyReal: try container.encode(Status.likelyReal) 135 | default: 136 | let context = EncodingError.Context(codingPath: encoder.codingPath, debugDescription: "Invalid enum value: \(self)") 137 | throw EncodingError.invalidValue(self, context) 138 | } 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /Sources/JWTKit/Vendor/FirebaseAuthIdentityToken.swift: -------------------------------------------------------------------------------- 1 | #if !canImport(Darwin) 2 | import FoundationEssentials 3 | #else 4 | import Foundation 5 | #endif 6 | 7 | public struct FirebaseAuthIdentityToken: JWTPayload { 8 | /// Additional Firebase-specific claims 9 | public struct Firebase: Codable, Sendable { 10 | enum CodingKeys: String, CodingKey { 11 | case identities 12 | case signInProvider = "sign_in_provider" 13 | case signInSecondFactor = "sign_in_second_factor" 14 | case secondFactorIdentifier = "second_factor_identifier" 15 | case tenant 16 | } 17 | 18 | public init( 19 | identities: [String: [String]], 20 | signInProvider: String, 21 | signInSecondFactor: String? = nil, 22 | secondFactorIdentifier: String? = nil, 23 | tenant: String? = nil 24 | ) { 25 | self.identities = identities 26 | self.signInProvider = signInProvider 27 | self.signInSecondFactor = signInSecondFactor 28 | self.secondFactorIdentifier = secondFactorIdentifier 29 | self.tenant = tenant 30 | } 31 | 32 | public let identities: [String: [String]] 33 | public let signInProvider: String 34 | public let signInSecondFactor: String? 35 | public let secondFactorIdentifier: String? 36 | public let tenant: String? 37 | } 38 | 39 | enum CodingKeys: String, CodingKey { 40 | case email, name, picture, firebase 41 | case issuer = "iss" 42 | case subject = "sub" 43 | case audience = "aud" 44 | case issuedAt = "iat" 45 | case expires = "exp" 46 | case emailVerified = "email_verified" 47 | case userID = "user_id" 48 | case authTime = "auth_time" 49 | case phoneNumber = "phone_number" 50 | } 51 | 52 | /// Issuer. It must be "https://securetoken.google.com/", where is the same project ID used for aud 53 | public let issuer: IssuerClaim 54 | 55 | /// Issued-at time. It must be in the past. The time is measured in seconds since the UNIX epoch. 56 | public let issuedAt: IssuedAtClaim 57 | 58 | /// Expiration time. It must be in the future. The time is measured in seconds since the UNIX epoch. 59 | public let expires: ExpirationClaim 60 | 61 | /// The audience that this ID token is intended for. It must be your Firebase project ID, the unique identifier for your Firebase project, which can be found in the URL of that project's console. 62 | public let audience: AudienceClaim 63 | 64 | /// Subject. It must be a non-empty string and must be the uid of the user or device. 65 | public let subject: SubjectClaim 66 | 67 | /// Authentication time. It must be in the past. The time when the user authenticated. 68 | public let authTime: Date? 69 | 70 | public let userID: String 71 | 72 | /// The user's email address. 73 | public let email: String? 74 | 75 | /// The URL of the user's profile picture. 76 | public let picture: String? 77 | 78 | /// The user's full name, in a displayable form. 79 | public let name: String? 80 | 81 | /// `True` if the user's e-mail address has been verified; otherwise `false`. 82 | public let emailVerified: Bool? 83 | 84 | /// The user's phone number. 85 | public let phoneNumber: String? 86 | 87 | /// Additional Firebase-specific claims 88 | public let firebase: Firebase? 89 | 90 | // TODO: support custom claims 91 | 92 | public init( 93 | issuer: IssuerClaim, 94 | subject: SubjectClaim, 95 | audience: AudienceClaim, 96 | issuedAt: IssuedAtClaim, 97 | expires: ExpirationClaim, 98 | authTime: Date? = nil, 99 | userID: String, 100 | email: String? = nil, 101 | emailVerified: Bool? = nil, 102 | phoneNumber: String? = nil, 103 | name: String? = nil, 104 | picture: String? = nil, 105 | firebase: FirebaseAuthIdentityToken.Firebase? = nil 106 | ) { 107 | self.issuer = issuer 108 | self.issuedAt = issuedAt 109 | self.expires = expires 110 | self.audience = audience 111 | self.subject = subject 112 | self.authTime = authTime 113 | self.userID = userID 114 | self.email = email 115 | self.picture = picture 116 | self.name = name 117 | self.emailVerified = emailVerified 118 | self.phoneNumber = phoneNumber 119 | self.firebase = firebase 120 | } 121 | 122 | public func verify(using _: some JWTAlgorithm) throws { 123 | guard let projectId = self.audience.value.first else { 124 | throw JWTError.claimVerificationFailure(failedClaim: audience, reason: "Token not provided by Firebase") 125 | } 126 | 127 | guard self.issuer.value == "https://securetoken.google.com/\(projectId)" else { 128 | throw JWTError.claimVerificationFailure(failedClaim: issuer, reason: "Token not provided by Firebase") 129 | } 130 | 131 | guard self.subject.value.count <= 255 else { 132 | throw JWTError.claimVerificationFailure(failedClaim: subject, reason: "Subject claim beyond 255 ASCII characters long.") 133 | } 134 | 135 | try self.expires.verifyNotExpired() 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /Sources/JWTKit/Vendor/GoogleIdentityToken.swift: -------------------------------------------------------------------------------- 1 | /// - See Also: 2 | /// [An ID token's payload](https://developers.google.com/identity/protocols/OpenIDConnect#an-id-tokens-payload) 3 | public struct GoogleIdentityToken: JWTPayload { 4 | enum CodingKeys: String, CodingKey { 5 | case email, name, picture, locale, nonce, profile 6 | case issuer = "iss" 7 | case subject = "sub" 8 | case audience = "aud" 9 | case authorizedPresenter = "azp" 10 | case issuedAt = "iat" 11 | case expires = "exp" 12 | case hostedDomain = "hd" 13 | case emailVerified = "email_verified" 14 | case givenName = "given_name" 15 | case familyName = "family_name" 16 | case atHash = "at_hash" 17 | } 18 | 19 | /// The Issuer Identifier for the Issuer of the response. Always https://accounts.google.com or accounts.google.com for Google ID tokens. 20 | public let issuer: IssuerClaim 21 | 22 | /// An identifier for the user, unique among all Google accounts and never reused. 23 | /// 24 | /// A Google account can have multiple email addresses at different 25 | /// points in time, but the sub value is never changed. Use sub within your application as the unique-identifier key for the user. Maximum length of 26 | /// 255 case-sensitive ASCII characters. 27 | public let subject: SubjectClaim 28 | 29 | /// The audience that this ID token is intended for. It must be one of the OAuth 2.0 client IDs of your application. 30 | public let audience: AudienceClaim 31 | 32 | /// The client_id of the authorized presenter. 33 | /// 34 | /// This claim is only needed when the party requesting the ID token is not the same as the audience of the ID token. This may be the case at 35 | /// Google for hybrid apps where a web application and Android app have a different OAuth 2.0 client_id but share the same Google APIs project. 36 | public let authorizedPresenter: String 37 | 38 | /// The time the ID token was issued. 39 | public let issuedAt: IssuedAtClaim 40 | 41 | /// Expiration time on or after which the ID token must not be accepted. 42 | public let expires: ExpirationClaim 43 | 44 | /// Access token hash. 45 | /// 46 | /// Provides validation that the access token is tied to the identity token. If the ID token is issued with an access_token value in 47 | /// the server flow, this claim is always included. This claim can be used as an alternate mechanism to protect against cross-site request forgery 48 | /// attacks. 49 | public let atHash: String? 50 | 51 | /// The hosted G Suite domain of the user. Provided only if the user belongs to a hosted domain. 52 | public let hostedDomain: GoogleHostedDomainClaim? 53 | 54 | /// The user's email address. 55 | /// 56 | /// This value may not be unique to this user and is not suitable for use as a primary key. Provided only if your scope included the email scope value. 57 | public let email: String? 58 | 59 | /// `True` if the user's e-mail address has been verified; otherwise `false`. 60 | public let emailVerified: BoolClaim? 61 | 62 | /// The user's full name, in a displayable form. 63 | /// 64 | /// **Might** be provided when: 65 | /// - The request scope included the string "profile" 66 | /// - The ID token is returned from a token refresh 67 | /// 68 | /// When `name` claims are present, you can use them to update your app's user records. 69 | public let name: String? 70 | 71 | /// The URL of the user's profile picture. 72 | /// 73 | /// **Might** be provided when: 74 | /// - The request scope included the string "profile" 75 | /// - The ID token is returned from a token refresh 76 | /// 77 | /// When `picture` claims are present, you can use them to update your app's user records. 78 | public let picture: String? 79 | 80 | /// The URL of the user's profile picture. 81 | /// 82 | /// **Might** be provided when: 83 | /// - The request scope included the string "profile" 84 | /// - The ID token is returned from a token refresh 85 | /// 86 | /// When `profile` claims are present, you can use them to update your app's user records. 87 | public let profile: String? 88 | 89 | /// The user's given name(s) or first name(s). Might be provided when a `name` claim is present. 90 | public let givenName: String? 91 | 92 | /// The user's surname(s) or last name(s). Might be provided when a `name` claim is present. 93 | public let familyName: String? 94 | 95 | /// The user's locale, represented by a [BCP 47](https://tools.ietf.org/html/bcp47) language tag. Might be provided when a name claim is present. 96 | public let locale: LocaleClaim? 97 | 98 | /// The value of the nonce supplied by your app in the authentication request. You should enforce protection against replay attacks by ensuring it is presented only once. 99 | public let nonce: String? 100 | 101 | public init( 102 | issuer: IssuerClaim, 103 | subject: SubjectClaim, 104 | audience: AudienceClaim, 105 | authorizedPresenter: String, 106 | issuedAt: IssuedAtClaim, 107 | expires: ExpirationClaim, 108 | atHash: String? = nil, 109 | hostedDomain: GoogleHostedDomainClaim? = nil, 110 | email: String? = nil, 111 | emailVerified: BoolClaim? = nil, 112 | name: String? = nil, 113 | picture: String? = nil, 114 | profile: String? = nil, 115 | givenName: String? = nil, 116 | familyName: String? = nil, 117 | locale: LocaleClaim? = nil, 118 | nonce: String? = nil 119 | ) { 120 | self.issuer = issuer 121 | self.subject = subject 122 | self.audience = audience 123 | self.authorizedPresenter = authorizedPresenter 124 | self.issuedAt = issuedAt 125 | self.expires = expires 126 | self.atHash = atHash 127 | self.hostedDomain = hostedDomain 128 | self.email = email 129 | self.emailVerified = emailVerified 130 | self.name = name 131 | self.picture = picture 132 | self.profile = profile 133 | self.givenName = givenName 134 | self.familyName = familyName 135 | self.locale = locale 136 | self.nonce = nonce 137 | } 138 | 139 | public func verify(using _: some JWTAlgorithm) throws { 140 | guard ["accounts.google.com", "https://accounts.google.com"].contains(self.issuer.value) else { 141 | throw JWTError.claimVerificationFailure(failedClaim: issuer, reason: "Token not provided by Google") 142 | } 143 | 144 | guard self.subject.value.count <= 255 else { 145 | throw JWTError.claimVerificationFailure(failedClaim: subject, reason: "Subject claim beyond 255 ASCII characters long.") 146 | } 147 | 148 | try self.expires.verifyNotExpired() 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /Sources/JWTKit/Vendor/MicrosoftIdentityToken.swift: -------------------------------------------------------------------------------- 1 | /// - See Also: 2 | /// [Retrieve the User’s Information from Microsoft Servers](https://docs.microsoft.com/pl-pl/azure/active-directory/develop/id-tokens) 3 | public struct MicrosoftIdentityToken: JWTPayload { 4 | enum CodingKeys: String, CodingKey { 5 | case nonce, email, name, roles 6 | case audience = "aud" 7 | case issuer = "iss" 8 | case issuedAt = "iat" 9 | case identityProvider = "idp" 10 | case notBefore = "nbf" 11 | case expires = "exp" 12 | case codeHash = "c_hash" 13 | case accessTokenHash = "at_hash" 14 | case preferredUsername = "preferred_username" 15 | case objectId = "oid" 16 | case subject = "sub" 17 | case tenantId = "tid" 18 | case uniqueName = "unique_name" 19 | case version = "ver" 20 | } 21 | 22 | /// Identifies the intended recipient of the token. In id_tokens, the audience is your app's Application ID, assigned to your app 23 | /// in the Azure portal. Your app should validate this value, and reject the token if the value does not match. 24 | public let audience: AudienceClaim 25 | 26 | /// Identifies the security token service (STS) that constructs and returns the token, and the Azure AD tenant in which the user 27 | /// was authenticated. If the token was issued by the v2.0 endpoint, the URI will end in /v2.0. The GUID that indicates that the 28 | /// user is a consumer user from a Microsoft account is 9188040d-6c67-4c5b-b112-36a304b66dad. Your app should use the 29 | /// GUID portion of the claim to restrict the set of tenants that can sign in to the app, if applicable. 30 | public let issuer: IssuerClaim 31 | 32 | /// "Issued At" indicates when the authentication for this token occurred. 33 | public let issuedAt: IssuedAtClaim 34 | 35 | /// Records the identity provider that authenticated the subject of the token. This value is identical to the value of the Issuer claim 36 | /// unless the user account not in the same tenant as the issuer - guests, for instance. If the claim isn't present, it means that the 37 | /// value of iss can be used instead. For personal accounts being used in an organizational context (for instance, a personal account 38 | /// invited to an Azure AD tenant), the idp claim may be 'live.com' or an STS URI containing the Microsoft account 39 | /// tenant 9188040d-6c67-4c5b-b112-36a304b66dad. 40 | public let identityProvider: String? 41 | 42 | /// The "nbf" (not before) claim identifies the time before which the JWT MUST NOT be accepted for processing. 43 | public let notBefore: NotBeforeClaim 44 | 45 | /// The "exp" (expiration time) claim identifies the expiration time on or after which the JWT MUST NOT be accepted for 46 | /// processing. It's important to note that a resource may reject the token before this time as well - if, for example, 47 | /// a change in authentication is required or a token revocation has been detected. 48 | public let expires: ExpirationClaim 49 | 50 | /// The code hash is included in ID tokens only when the ID token is issued with an OAuth 2.0 authorization code. 51 | /// It can be used to validate the authenticity of an authorization code. For details about performing this validation, 52 | /// see the [OpenID Connect specification](https://openid.net/specs/openid-connect-core-1_0.html). 53 | public let codeHash: String? 54 | 55 | /// The access token hash is included in ID tokens only when the ID token is issued with an OAuth 2.0 access token. 56 | /// It can be used to validate the authenticity of an access token. For details about performing this validation, 57 | /// see the [OpenID Connect specification](https://openid.net/specs/openid-connect-core-1_0.html). 58 | public let accessTokenHash: String? 59 | 60 | /// The primary username that represents the user. It could be an email address, phone number, or a generic username 61 | /// without a specified format. Its value is mutable and might change over time. Since it is mutable, this value must not be 62 | /// used to make authorization decisions. The profile scope is required to receive this claim. 63 | public let preferredUsername: String? 64 | 65 | /// The email claim is present by default for guest accounts that have an email address. Your app can request the email 66 | /// claim for managed users (those from the same tenant as the resource) using the email optional claim. On the v2.0 endpoint, 67 | /// your app can also request the email OpenID Connect scope - you don't need to request both the optional claim and the scope 68 | /// to get the claim. The email claim only supports addressable mail from the user's profile information. 69 | public let email: String? 70 | 71 | /// The name claim provides a human-readable value that identifies the subject of the token. The value isn't guaranteed 72 | /// to be unique, it is mutable, and it's designed to be used only for display purposes. The profile scope is required to receive this claim. 73 | public let name: String? 74 | 75 | /// The nonce matches the parameter included in the original /authorize request to the IDP. If it does not match, 76 | /// your application should reject the token. 77 | public let nonce: String? 78 | 79 | /// The immutable identifier for an object in the Microsoft identity system, in this case, a user account. This ID uniquely identifies 80 | /// the user across applications - two different applications signing in the same user will receive the same value in the oid claim. 81 | /// The Microsoft Graph will return this ID as the id property for a given user account. Because the oid allows multiple apps to 82 | /// correlate users, the profile scope is required to receive this claim. Note that if a single user exists in multiple tenants, the user 83 | /// will contain a different object ID in each tenant - they're considered different accounts, even though the user logs into each 84 | /// account with the same credentials. The oid claim is a GUID and cannot be reused. 85 | public let objectId: String 86 | 87 | /// The set of roles that were assigned to the user who is logging in. 88 | public let roles: [String]? 89 | 90 | /// The principal about which the token asserts information, such as the user of an app. This value is immutable and cannot 91 | /// be reassigned or reused. The subject is a pairwise identifier - it is unique to a particular application ID. If a single user signs 92 | /// into two different apps using two different client IDs, those apps will receive two different values for the subject claim. 93 | /// This may or may not be wanted depending on your architecture and privacy requirements. 94 | public let subject: SubjectClaim 95 | 96 | /// A GUID that represents the Azure AD tenant that the user is from. For work and school accounts, the GUID is the 97 | /// immutable tenant ID of the organization that the user belongs to. For personal accounts, the value is 98 | /// 9188040d-6c67-4c5b-b112-36a304b66dad. The profile scope is required to receive this claim. 99 | public let tenantId: TenantIDClaim 100 | 101 | /// Provides a human readable value that identifies the subject of the token. This value is unique at any given point in time 102 | /// but as emails and other identifiers can be reused, this value can reappear on other accounts, and should therefore be 103 | /// used only for display purposes. Only issued in v1.0 id_tokens. 104 | public let uniqueName: String? 105 | 106 | /// Indicates the version of the id_token. 107 | public let version: String? 108 | 109 | public init( 110 | audience: AudienceClaim, 111 | issuer: IssuerClaim, 112 | issuedAt: IssuedAtClaim, 113 | identityProvider: String?, 114 | notBefore: NotBeforeClaim, 115 | expires: ExpirationClaim, 116 | codeHash: String?, 117 | accessTokenHash: String?, 118 | preferredUsername: String?, 119 | email: String?, 120 | name: String?, 121 | nonce: String?, 122 | objectId: String, 123 | roles: [String]?, 124 | subject: SubjectClaim, 125 | tenantId: TenantIDClaim, 126 | uniqueName: String?, 127 | version: String? 128 | ) { 129 | self.audience = audience 130 | self.issuer = issuer 131 | self.issuedAt = issuedAt 132 | self.identityProvider = identityProvider 133 | self.notBefore = notBefore 134 | self.expires = expires 135 | self.codeHash = codeHash 136 | self.accessTokenHash = accessTokenHash 137 | self.preferredUsername = preferredUsername 138 | self.email = email 139 | self.name = name 140 | self.nonce = nonce 141 | self.objectId = objectId 142 | self.roles = roles 143 | self.subject = subject 144 | self.tenantId = tenantId 145 | self.uniqueName = uniqueName 146 | self.version = version 147 | } 148 | 149 | public func verify(using _: some JWTAlgorithm) throws { 150 | guard let tenantId = self.tenantId.value else { 151 | throw JWTError.claimVerificationFailure(failedClaim: tenantId, reason: "Token must contain tenant Id") 152 | } 153 | 154 | guard self.issuer.value == "https://login.microsoftonline.com/\(tenantId)/v2.0" else { 155 | throw JWTError.claimVerificationFailure(failedClaim: issuer, reason: "Token not provided by Microsoft") 156 | } 157 | 158 | try self.expires.verifyNotExpired() 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /Sources/JWTKit/X5C/ValidationTimePayload.swift: -------------------------------------------------------------------------------- 1 | #if !canImport(Darwin) 2 | import FoundationEssentials 3 | #else 4 | import Foundation 5 | #endif 6 | 7 | /// A protocol defining the requirements for payloads that include a validation time. 8 | /// 9 | /// This protocol extends `JWTPayload` to include an additional `signedDate` property. 10 | /// It is used to represent JWT payloads that require a date to validate the date the token was signed. 11 | public protocol ValidationTimePayload: JWTPayload { 12 | var signedDate: Date { get } 13 | } 14 | -------------------------------------------------------------------------------- /Sources/JWTKit/X5C/X5CVerifier.swift: -------------------------------------------------------------------------------- 1 | import X509 2 | 3 | #if !canImport(Darwin) 4 | import FoundationEssentials 5 | #else 6 | import Foundation 7 | #endif 8 | 9 | /// An object for verifying JWS tokens that contain the `x5c` header parameter 10 | /// with a set of known root certificates. 11 | /// 12 | /// Usage: 13 | /// ``` 14 | /// let verifier = try X5CVerifier(rootCertificates: myRoots) 15 | /// let payload = try await verifier.verifyJWS(myJWS, as: MyPayload.self) 16 | /// // payload is now known to be valid! 17 | /// ``` 18 | /// 19 | /// See [RFC 7515](https://www.rfc-editor.org/rfc/rfc7515#section-4.1.6) 20 | /// for details on the `x5c` header parameter. 21 | public struct X5CVerifier: Sendable { 22 | private let trustedStore: X509.CertificateStore 23 | 24 | /// Create a new X5CVerifier trusting `rootCertificates`. 25 | /// 26 | /// - Parameter rootCertificates: The root certificates to be trusted. 27 | /// - Throws: ``JWTError/invalidX5CChain(reason:)`` if no root certificates are provided. 28 | public init(rootCertificates: [Certificate]) throws { 29 | guard !rootCertificates.isEmpty else { 30 | throw JWTError.invalidX5CChain(reason: "No root certificates provided") 31 | } 32 | trustedStore = X509.CertificateStore(rootCertificates) 33 | } 34 | 35 | /// Create a new X5CVerifier trusting `rootCertificates`. 36 | /// 37 | /// - Parameter rootCertificates: The root certificates to be trusted. 38 | /// - Throws: ``JWTError/invalidX5CChain(reason:)`` if no root certificates are provided. 39 | public init(rootCertificates: [String]) throws { 40 | guard !rootCertificates.isEmpty else { 41 | throw JWTError.invalidX5CChain(reason: "No root certificates provided") 42 | } 43 | try self.init(rootCertificates: rootCertificates.map { try X509.Certificate(pemEncoded: $0) }) 44 | } 45 | 46 | /// Create a new X5CVerifier trusting `rootCertificates`. 47 | /// 48 | /// - Parameter rootCertificates: The root certificates to be trusted. 49 | /// - Throws: ``JWTError/invalidX5CChain(reason:)`` if no root certificates are provided. 50 | public init(rootCertificates: [some DataProtocol]) throws { 51 | guard !rootCertificates.isEmpty else { 52 | throw JWTError.invalidX5CChain(reason: "No root certificates provided") 53 | } 54 | try self.init(rootCertificates: rootCertificates.map { try X509.Certificate(derEncoded: [UInt8]($0)) }) 55 | } 56 | 57 | /// Verify a chain of certificates against the trusted root certificates. 58 | /// 59 | /// - Parameter certificates: The certificates to verify. 60 | /// - Returns: A `X509.VerificationResult` indicating the result of the verification. 61 | public func verifyChain( 62 | certificates: [String], 63 | policy: () throws -> some VerifierPolicy = { RFC5280Policy(validationTime: Date()) } 64 | ) async throws -> X509.VerificationResult { 65 | let certificates = try certificates.map { try Certificate(pemEncoded: $0) } 66 | return try await verifyChain(certificates: certificates, policy: policy) 67 | } 68 | 69 | /// Verify a chain of certificates against the trusted root certificates. 70 | /// 71 | /// - Parameters: 72 | /// - certificates: The certificates to verify. 73 | /// - policy: The policy to use for verification. 74 | /// - Returns: A `X509.VerificationResult` indicating the result of the verification. 75 | public func verifyChain( 76 | certificates: [Certificate], 77 | @PolicyBuilder policy: () throws -> some VerifierPolicy = { RFC5280Policy(validationTime: Date()) } 78 | ) async throws -> X509.VerificationResult { 79 | let untrustedChain = CertificateStore(certificates) 80 | var verifier = try Verifier(rootCertificates: trustedStore, policy: policy) 81 | let result = await verifier.validate( 82 | leafCertificate: certificates[0], intermediates: untrustedChain) 83 | return result 84 | } 85 | 86 | /// Verify a JWS with the `x5c` header parameter against the trusted root 87 | /// certificates. 88 | /// 89 | /// - Parameters: 90 | /// - token: The JWS to verify. 91 | /// - payload: The type to decode from the token payload. 92 | /// - Returns: The decoded payload, if verified. 93 | public func verifyJWS( 94 | _ token: String, 95 | as _: Payload.Type = Payload.self 96 | ) async throws -> Payload { 97 | try await verifyJWS(token, as: Payload.self, jsonDecoder: .defaultForJWT) 98 | } 99 | 100 | /// Verify a JWS with the `x5c` header parameter against the trusted root 101 | /// certificates, overriding the default JSON decoder. 102 | /// 103 | /// - Parameters: 104 | /// - token: The JWS to verify. 105 | /// - payload: The type to decode from the token payload. 106 | /// - jsonDecoder: The JSON decoder to use for decoding the token. 107 | /// - Returns: The decoded payload, if verified. 108 | public func verifyJWS( 109 | _ token: String, 110 | as _: Payload.Type = Payload.self, 111 | jsonDecoder: any JWTJSONDecoder 112 | ) async throws -> Payload { 113 | try await verifyJWS(Array(token.utf8), as: Payload.self, jsonDecoder: jsonDecoder) 114 | } 115 | 116 | /// Verify a JWS with `x5c` claims against the 117 | /// trusted root certificates. 118 | /// 119 | /// - Parameters: 120 | /// - token: The JWS to verify. 121 | /// - payload: The type to decode from the token payload. 122 | /// - Returns: The decoded payload, if verified. 123 | public func verifyJWS( 124 | _ token: some DataProtocol, 125 | as _: Payload.Type = Payload.self 126 | ) async throws -> Payload 127 | where Payload: JWTPayload { 128 | try await verifyJWS(token, as: Payload.self, jsonDecoder: .defaultForJWT) 129 | } 130 | 131 | /// Verify a JWS with `x5c` claims against the 132 | /// trusted root certificates, overriding the default JSON decoder. 133 | /// 134 | /// - Parameters: 135 | /// - token: The JWS to verify. 136 | /// - payload: The type to decode from the token payload. 137 | /// - jsonDecoder: The JSON decoder to use for dcoding the token. 138 | /// - policy: The policy to use for verification. 139 | /// - Returns: The decoded payload, if verified. 140 | public func verifyJWS( 141 | _ token: some DataProtocol, 142 | as _: Payload.Type = Payload.self, 143 | jsonDecoder: any JWTJSONDecoder, 144 | @PolicyBuilder policy: () throws -> some VerifierPolicy = { RFC5280Policy(validationTime: Date()) } 145 | ) async throws -> Payload 146 | where Payload: JWTPayload { 147 | // Parse the JWS header to get the header 148 | let parser = DefaultJWTParser(jsonDecoder: jsonDecoder) 149 | let (header, payload, _) = try parser.parse(token, as: Payload.self) 150 | 151 | // Ensure the algorithm used is ES256, as it's the only supported one (for now) 152 | guard let headerAlg = header.alg, headerAlg == "ES256" else { 153 | throw JWTError.invalidX5CChain(reason: "Unsupported algorithm: \(String(describing: header.alg))") 154 | } 155 | 156 | // Ensure the x5c header parameter is present and not empty 157 | guard let x5c = header.x5c, !x5c.isEmpty else { 158 | throw JWTError.missingX5CHeader 159 | } 160 | 161 | // Decode the x5c certificates 162 | let certificateData = try x5c.map { 163 | guard let data = Data(base64Encoded: $0) else { 164 | throw JWTError.invalidX5CChain(reason: "Invalid x5c certificate: \($0)") 165 | } 166 | return data 167 | } 168 | 169 | let certificates = try certificateData.map { 170 | try Certificate(derEncoded: [UInt8]($0)) 171 | } 172 | 173 | // Setup an untrusted chain using the intermediate certificates 174 | let untrustedChain = CertificateStore(certificates.dropFirst()) 175 | 176 | let date: Date 177 | // Some JWT implementations have the sign date in the payload. 178 | // If it's such a payload, we'll use that date for validation 179 | if let validationTimePayload = payload as? ValidationTimePayload { 180 | date = validationTimePayload.signedDate 181 | } else { 182 | date = Date() 183 | } 184 | 185 | // Setup the verifier using the predefined trusted store 186 | var verifier = try Verifier( 187 | rootCertificates: trustedStore, 188 | policy: { 189 | try policy() 190 | RFC5280Policy(validationTime: date) 191 | }) 192 | 193 | // Validate the leaf certificate against the trusted store 194 | let result = await verifier.validate( 195 | leafCertificate: certificates[0], 196 | intermediates: untrustedChain 197 | ) 198 | 199 | if case .couldNotValidate(let failures) = result { 200 | throw JWTError.invalidX5CChain(reason: "\(failures)") 201 | } 202 | 203 | // Assuming the chain is valid, verify the token was signed by the valid certificate 204 | let ecdsaKey = try ES256PublicKey(certificate: certificates[0].serializeAsPEM().pemString) 205 | let signer = JWTSigner(algorithm: ECDSASigner(key: ecdsaKey), parser: parser) 206 | 207 | return try await signer.verify(token) 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /Tests/JWTKitTests/ClaimTests.swift: -------------------------------------------------------------------------------- 1 | #if canImport(Testing) 2 | import Testing 3 | import JWTKit 4 | 5 | #if !canImport(Darwin) 6 | import FoundationEssentials 7 | #else 8 | import Foundation 9 | #endif 10 | 11 | @Suite("Claim Tests") 12 | struct ClaimTests { 13 | @Test("Test Claim with Boolean") 14 | func boolClaim() async throws { 15 | let payload = #"{"trueStr":"true","trueBool":true,"falseStr":"false","falseBool":false}"# 16 | var data = Data(payload.utf8) 17 | let decoded = try! JSONDecoder().decode(BoolPayload.self, from: data) 18 | 19 | #expect(decoded.trueStr.value == true) 20 | #expect(decoded.trueBool.value == true) 21 | #expect(decoded.falseBool.value == false) 22 | #expect(decoded.falseStr.value == false) 23 | 24 | data = Data(#"{"bad":"Not boolean"}"#.utf8) 25 | #expect(throws: DecodingError.self) { 26 | try JSONDecoder().decode(BoolPayload.self, from: data) 27 | } 28 | } 29 | 30 | @Test("Test Claim with Locale") 31 | func localeClaim() async throws { 32 | let ptBR = #"{"locale":"pt-BR"}"# 33 | 34 | let plainEnglish = try LocalePayload.from(#"{"locale":"en"}"#) 35 | let brazillianPortugese = try LocalePayload.from(ptBR) 36 | let nadizaDialectSlovenia = try LocalePayload.from(#"{"locale":"sl-nedis"}"#) 37 | let germanSwissPost1996 = try LocalePayload.from(#"{"locale":"de-CH-1996"}"#) 38 | let chineseTraditionalTwoPrivate = try LocalePayload.from( 39 | #"{"locale":"zh-Hant-CN-x-private1-private2"}"# 40 | ) 41 | 42 | #expect(plainEnglish.locale.value.identifier == "en") 43 | #expect(brazillianPortugese.locale.value.identifier == "pt-BR") 44 | #expect(nadizaDialectSlovenia.locale.value.identifier == "sl-nedis") 45 | #expect(germanSwissPost1996.locale.value.identifier == "de-CH-1996") 46 | #expect(chineseTraditionalTwoPrivate.locale.value.identifier == "zh-Hant-CN-x-private1-private2") 47 | 48 | let encoded = try JSONEncoder().encode(brazillianPortugese) 49 | let string = String(bytes: encoded, encoding: .utf8)! 50 | #expect(string == ptBR) 51 | } 52 | 53 | @Test("Test Claim with Sindle Audience") 54 | func singleAudienceClaim() async throws { 55 | let id = UUID() 56 | let str = "{\"audience\":\"\(id.uuidString)\"}" 57 | let data = Data(str.utf8) 58 | let decoded = try! JSONDecoder().decode(AudiencePayload.self, from: data) 59 | 60 | #expect(decoded.audience.value == [id.uuidString]) 61 | #expect(throws: Never.self) { 62 | try decoded.audience.verifyIntendedAudience(includes: id.uuidString) 63 | } 64 | #expect { 65 | try decoded.audience.verifyIntendedAudience(includes: UUID().uuidString) 66 | } throws: { error in 67 | guard let jwtError = error as? JWTError else { return false } 68 | return jwtError.errorType == .claimVerificationFailure 69 | && jwtError.failedClaim is AudienceClaim 70 | && (jwtError.failedClaim as? AudienceClaim)?.value == [id.uuidString] 71 | } 72 | } 73 | 74 | @Test("Test Claim with Multiple Audiences") 75 | func multipleAudienceClaims() async throws { 76 | let id1 = UUID() 77 | let id2 = UUID() 78 | let str = "{\"audience\":[\"\(id1.uuidString)\", \"\(id2.uuidString)\"]}" 79 | let data = Data(str.utf8) 80 | let decoded = try! JSONDecoder().decode(AudiencePayload.self, from: data) 81 | 82 | #expect(decoded.audience.value == [id1.uuidString, id2.uuidString]) 83 | #expect(throws: Never.self) { 84 | try decoded.audience.verifyIntendedAudience(includes: id1.uuidString) 85 | } 86 | #expect(throws: Never.self) { 87 | try decoded.audience.verifyIntendedAudience(includes: id2.uuidString) 88 | } 89 | #expect { 90 | try decoded.audience.verifyIntendedAudience(includes: UUID().uuidString) 91 | } throws: { error in 92 | guard let jwtError = error as? JWTError else { return false } 93 | return jwtError.errorType == .claimVerificationFailure 94 | && jwtError.failedClaim is AudienceClaim 95 | && (jwtError.failedClaim as? AudienceClaim)?.value == [ 96 | id1.uuidString, id2.uuidString, 97 | ] 98 | } 99 | } 100 | 101 | @Test("Test Expiration Encoding") 102 | func expirationEncoding() async throws { 103 | let exp = ExpirationClaim(value: Date(timeIntervalSince1970: 2_000_000_000)) 104 | let parser = DefaultJWTParser() 105 | let keyCollection = await JWTKeyCollection() 106 | .add(hmac: .init(from: "secret".bytes), digestAlgorithm: .sha256, parser: parser) 107 | let jwt = try await keyCollection.sign(ExpirationPayload(exp: exp)) 108 | let parsed = try parser.parse(jwt.bytes, as: ExpirationPayload.self) 109 | let header = parsed.header 110 | 111 | let typ = try #require(header.typ) 112 | #expect(typ == "JWT") 113 | let alg = try #require(header.alg) 114 | #expect(alg == "HS256") 115 | #expect(parsed.payload.exp == exp) 116 | _ = try await keyCollection.verify(jwt, as: ExpirationPayload.self) 117 | } 118 | } 119 | #endif // canImport(Testing) 120 | -------------------------------------------------------------------------------- /Tests/JWTKitTests/EdDSATests.swift: -------------------------------------------------------------------------------- 1 | #if canImport(Testing) 2 | import Testing 3 | import JWTKit 4 | 5 | @Suite("EdDSA Tests") 6 | struct EdDSATests { 7 | 8 | @Test("Test EdDSA Generate") 9 | func edDSAGenerate() async throws { 10 | let payload = TestPayload( 11 | sub: "vapor", 12 | name: "Foo", 13 | admin: false, 14 | exp: .init(value: .init(timeIntervalSince1970: 2_000_000_000)) 15 | ) 16 | 17 | let keyCollection = try await JWTKeyCollection() 18 | .add(eddsa: EdDSA.PrivateKey(curve: .ed25519)) 19 | 20 | let token = try await keyCollection.sign(payload) 21 | let verifiedPayload = try await keyCollection.verify(token, as: TestPayload.self) 22 | #expect(verifiedPayload == payload) 23 | } 24 | 25 | @Test("Test EdDSA Public and Private") 26 | func edDSAPublicPrivate() async throws { 27 | let keys = try await JWTKeyCollection() 28 | .add(eddsa: EdDSA.PublicKey(x: eddsaPublicKeyBase64, curve: .ed25519), kid: "public") 29 | .add(eddsa: EdDSA.PrivateKey(d: eddsaPrivateKeyBase64, curve: .ed25519), kid: "private") 30 | 31 | let payload = TestPayload( 32 | sub: "vapor", 33 | name: "Foo", 34 | admin: false, 35 | exp: .init(value: .init(timeIntervalSince1970: 2_000_000_000)) 36 | ) 37 | 38 | for _ in 0..<1000 { 39 | let token = try await keys.sign(payload, kid: "private") 40 | // test public signer decoding 41 | let verifiedPayload = try await keys.verify(token, as: TestPayload.self) 42 | #expect(verifiedPayload == payload) 43 | } 44 | } 45 | 46 | @Test("Test Verifying EdDSA Key Using JWK") 47 | func verifyingEdDSAKeyUsingJWK() async throws { 48 | struct Foo: JWTPayload { 49 | var bar: Int 50 | func verify(using _: some JWTAlgorithm) throws {} 51 | } 52 | 53 | // ecdsa key in base64 format 54 | let x = eddsaPublicKeyBase64 55 | let d = eddsaPrivateKeyBase64 56 | 57 | // sign JWT 58 | let keyCollection = try await JWTKeyCollection() 59 | .add(eddsa: EdDSA.PrivateKey(d: d, curve: .ed25519), kid: "vapor") 60 | 61 | let jwt = try await keyCollection.sign(Foo(bar: 42)) 62 | 63 | // verify using jwks 64 | let jwksString = """ 65 | { 66 | "keys": [ 67 | { 68 | "kty": "OKP", 69 | "crv": "Ed25519", 70 | "use": "sig", 71 | "kid": "vapor", 72 | "x": "\(x)", 73 | "d": "\(d)" 74 | } 75 | ] 76 | } 77 | """ 78 | 79 | try await keyCollection.add(jwksJSON: jwksString) 80 | let foo = try await keyCollection.verify(jwt, as: Foo.self) 81 | #expect(foo.bar == 42) 82 | } 83 | 84 | @Test("Test Verifying EdDSA Key Using JWK Base64URL") 85 | func verifyingEdDSAKeyUsingJWKBase64URL() async throws { 86 | struct Foo: JWTPayload { 87 | var bar: Int 88 | func verify(using _: some JWTAlgorithm) throws {} 89 | } 90 | 91 | let x = eddsaPublicKeyBase64Url 92 | let d = eddsaPrivateKeyBase64Url 93 | 94 | // sign JWT 95 | let keyCollection = try await JWTKeyCollection() 96 | .add(eddsa: EdDSA.PrivateKey(d: d, curve: .ed25519), kid: "vapor") 97 | 98 | let jwt = try await keyCollection.sign(Foo(bar: 42)) 99 | 100 | // verify using jwks 101 | let jwksString = """ 102 | { 103 | "keys": [ 104 | { 105 | "kty": "OKP", 106 | "crv": "Ed25519", 107 | "use": "sig", 108 | "kid": "vapor", 109 | "x": "\(x)", 110 | "d": "\(d)" 111 | } 112 | ] 113 | } 114 | """ 115 | 116 | try await keyCollection.add(jwksJSON: jwksString) 117 | let foo = try await keyCollection.verify(jwt, as: Foo.self) 118 | #expect(foo.bar == 42) 119 | } 120 | 121 | @Test("Test Verifying EdDSA Key Using JWK with Mixed Base64 Formats") 122 | func verifyingEdDSAKeyUsingJWKWithMixedBase64Formats() async throws { 123 | struct Foo: JWTPayload { 124 | var bar: Int 125 | func verify(using _: some JWTAlgorithm) throws {} 126 | } 127 | 128 | // eddsa key in base64url format 129 | let x = eddsaPublicKeyBase64Url 130 | let d = eddsaPrivateKeyBase64 131 | 132 | // sign JWT 133 | let keyCollection = try await JWTKeyCollection() 134 | .add(eddsa: EdDSA.PrivateKey(d: d, curve: .ed25519), kid: "vapor") 135 | 136 | let jwt = try await keyCollection.sign(Foo(bar: 42), kid: "vapor") 137 | 138 | // verify using jwks 139 | let jwksString = """ 140 | { 141 | "keys": [ 142 | { 143 | "kty": "OKP", 144 | "crv": "Ed25519", 145 | "use": "sig", 146 | "kid": "vapor", 147 | "x": "\(x)", 148 | "d": "\(d)" 149 | } 150 | ] 151 | } 152 | """ 153 | 154 | try await keyCollection.add(jwksJSON: jwksString) 155 | let foo = try await keyCollection.verify(jwt, as: Foo.self) 156 | #expect(foo.bar == 42) 157 | } 158 | } 159 | 160 | let eddsaPublicKeyBase64 = "0ZcEvMCSYqSwR8XIkxOoaYjRQSAO8frTMSCpNbUl4lE=" 161 | let eddsaPrivateKeyBase64 = "d1H3/dcg0V3XyAuZW2TE5Z3rhY20M+4YAfYu/HUQd8w=" 162 | let eddsaPublicKeyBase64Url = "0ZcEvMCSYqSwR8XIkxOoaYjRQSAO8frTMSCpNbUl4lE" 163 | let eddsaPrivateKeyBase64Url = "d1H3_dcg0V3XyAuZW2TE5Z3rhY20M-4YAfYu_HUQd8w" 164 | #endif // canImport(Testing) 165 | -------------------------------------------------------------------------------- /Tests/JWTKitTests/Helpers/String+bytes.swift: -------------------------------------------------------------------------------- 1 | extension String { 2 | var bytes: [UInt8] { 3 | .init(utf8) 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /Tests/JWTKitTests/Types/AudiencePayload.swift: -------------------------------------------------------------------------------- 1 | import JWTKit 2 | 3 | struct AudiencePayload: Codable { 4 | var audience: AudienceClaim 5 | } 6 | -------------------------------------------------------------------------------- /Tests/JWTKitTests/Types/BadBoolPayload.swift: -------------------------------------------------------------------------------- 1 | import JWTKit 2 | 3 | struct BadBoolPayload: Decodable { 4 | var bad: BoolClaim 5 | } 6 | -------------------------------------------------------------------------------- /Tests/JWTKitTests/Types/BoolPayload.swift: -------------------------------------------------------------------------------- 1 | import JWTKit 2 | 3 | struct BoolPayload: Decodable { 4 | var trueStr: BoolClaim 5 | var trueBool: BoolClaim 6 | var falseStr: BoolClaim 7 | var falseBool: BoolClaim 8 | } 9 | -------------------------------------------------------------------------------- /Tests/JWTKitTests/Types/ExpirationPayload.swift: -------------------------------------------------------------------------------- 1 | import JWTKit 2 | 3 | struct ExpirationPayload: JWTPayload { 4 | var exp: ExpirationClaim 5 | 6 | func verify(using _: some JWTAlgorithm) throws { 7 | try self.exp.verifyNotExpired() 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Tests/JWTKitTests/Types/LocalePayload.swift: -------------------------------------------------------------------------------- 1 | import JWTKit 2 | 3 | #if !canImport(Darwin) 4 | import FoundationEssentials 5 | #else 6 | import Foundation 7 | #endif 8 | 9 | struct LocalePayload: Codable { 10 | var locale: LocaleClaim 11 | } 12 | 13 | extension LocalePayload { 14 | static func from(_ string: String) throws -> LocalePayload { 15 | let data = string.data(using: .utf8)! 16 | return try JSONDecoder().decode(LocalePayload.self, from: data) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Tests/JWTKitTests/Types/TestPayload.swift: -------------------------------------------------------------------------------- 1 | import JWTKit 2 | 3 | struct TestPayload: JWTPayload, Equatable { 4 | var sub: SubjectClaim 5 | var name: String 6 | var admin: Bool 7 | var exp: ExpirationClaim 8 | 9 | func verify(using _: some JWTAlgorithm) throws { 10 | try exp.verifyNotExpired() 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /scripts/generate-certificates.sh: -------------------------------------------------------------------------------- 1 | # This gets called by the `generateTokens.swift` script. Do not modify! 2 | 3 | #!/usr/bin/env bash 4 | set -e -o pipefail 5 | 6 | # Configuration variables 7 | COUNTRY="US" 8 | STATE="New York" 9 | CITY="New York" 10 | ORGANIZATION="Vapor" 11 | ORGANIZATIONAL_UNIT="Engineering" 12 | EMAIL="admin@vapor.example.com" 13 | 14 | mkdir -p x5c_test_certs 15 | pushd x5c_test_certs 16 | 17 | # Function to generate subject string 18 | generate_subject() { 19 | local prefix="$1" 20 | echo "/C=$COUNTRY/ST=$STATE/L=$CITY/O=$ORGANIZATION/OU=$ORGANIZATIONAL_UNIT/CN=$ORGANIZATION $prefix/emailAddress=$EMAIL" 21 | } 22 | 23 | # Create Root Certificate 24 | openssl ecparam -name prime256v1 -genkey -noout -out root_key.pem 25 | openssl req -new -x509 -key root_key.pem -out root_cert.pem -days 3650 \ 26 | -subj "$(generate_subject 'Root CA')" \ 27 | -nodes 28 | 29 | # Create Intermediate Certificate 30 | openssl ecparam -name prime256v1 -genkey -noout -out intermediate_key.pem 31 | openssl req -new -key intermediate_key.pem -out intermediate_csr.pem \ 32 | -subj "$(generate_subject 'Intermediate CA')" \ 33 | -nodes 34 | 35 | echo 'basicConstraints=CA:TRUE' > intermediate_ext.txt 36 | openssl x509 -req \ 37 | -in intermediate_csr.pem \ 38 | -CA root_cert.pem -CAkey root_key.pem \ 39 | -CAcreateserial \ 40 | -out intermediate_cert.pem \ 41 | -days 1825 \ 42 | -extfile intermediate_ext.txt 43 | 44 | # Create Valid Leaf Certificate 45 | openssl ecparam -name prime256v1 -genkey -noout -out leaf_key.pem 46 | openssl req -new -key leaf_key.pem -out leaf_csr.pem \ 47 | -subj "$(generate_subject 'Leaf')" \ 48 | -nodes 49 | openssl x509 -req -in leaf_csr.pem \ 50 | -CA intermediate_cert.pem -CAkey intermediate_key.pem \ 51 | -CAcreateserial \ 52 | -out leaf_cert.pem -days 365 53 | 54 | # Create Expired Leaf Certificate 55 | openssl ecparam -name prime256v1 -genkey -noout -out expired_leaf_key.pem 56 | openssl req -new -key expired_leaf_key.pem -out expired_leaf_csr.pem \ 57 | -subj "$(generate_subject 'Expired Leaf')" \ 58 | -nodes 59 | openssl x509 -req -in expired_leaf_csr.pem \ 60 | -CA intermediate_cert.pem -CAkey intermediate_key.pem \ 61 | -CAcreateserial \ 62 | -out expired_leaf_cert.pem \ 63 | -not_before 200101010000Z \ 64 | -not_after 200102010000Z 65 | 66 | rm -f intermediate_ext.txt 67 | popd 68 | -------------------------------------------------------------------------------- /scripts/generateTokens.swift: -------------------------------------------------------------------------------- 1 | /// This script can be used to regenerate the X5CTests tokens 2 | /// when they don't verify anymore. If you're here, it likely means 3 | /// the certificates are expired and should be updated. 4 | /// This script does just that: generate new certificates and create 5 | /// tokens with x5c chains based on those certs. 6 | /// 7 | /// To run the script, simply run `swift scripts/generateTokens.swift`. 8 | /// The output will be 9 | /// - the new tokens, printed in Swift, which means you 10 | /// just need to copy and paste them replacing the old ones; 11 | /// - the root certificate, which you have to replace too. 12 | /// After creating the tokens, the script will delete certificates. 13 | /// If you want to keep them for some reason, just add `--keep-certs` 14 | /// to the script execution. They will be stored in the `x5c_test_certs`. 15 | /// This directory is in the `.gitignore` so that it doesn't get committed. 16 | import Foundation 17 | 18 | enum ScriptError: Error { 19 | case certificateGenerationFailed(status: Int32) 20 | case fileNotFound(at: String) 21 | case invalidSignature(String) 22 | } 23 | 24 | struct JWTGenerator { 25 | let certificateDirectory = "x5c_test_certs" 26 | let leafKeyFileName = "leaf_key.pem" 27 | let leafCertFileName = "leaf_cert.pem" 28 | let expiredLeafCertFileName = "expired_leaf_cert.pem" 29 | let intermediateCertFileName = "intermediate_cert.pem" 30 | let rootCertFileName = "root_cert.pem" 31 | 32 | func generateCertificates() throws { 33 | let process = Process() 34 | process.launchPath = "/bin/bash" 35 | process.arguments = ["scripts/generate-certificates.sh"] 36 | 37 | try process.run() 38 | process.waitUntilExit() 39 | 40 | if process.terminationStatus != 0 { 41 | throw ScriptError.certificateGenerationFailed(status: process.terminationStatus) 42 | } 43 | 44 | print("✅ Certificates generated successfully") 45 | } 46 | 47 | func readCertificateData(from name: String, stripped: Bool = true) throws -> String { 48 | let path = URL(filePath: certificateDirectory).appending(path: name) 49 | let content = try String(contentsOf: path, encoding: .utf8) 50 | let finalContent = 51 | if stripped { 52 | content 53 | .replacing("-----BEGIN CERTIFICATE-----", with: "") 54 | .replacing("-----END CERTIFICATE-----", with: "") 55 | .replacing("\n", with: "") 56 | } else { 57 | content 58 | } 59 | return finalContent 60 | } 61 | 62 | func generateToken(payload: [String: Any], certificates: [String], signingKeyPath: String) throws -> String { 63 | func base64URLEncode(_ data: Data) -> String { 64 | data.base64EncodedString() 65 | .replacingOccurrences(of: "+", with: "-") 66 | .replacingOccurrences(of: "/", with: "_") 67 | .replacingOccurrences(of: "=", with: "") 68 | } 69 | 70 | let x5cChain = try certificates.map { try readCertificateData(from: $0) } 71 | 72 | let header: [String: Any] = [ 73 | "alg": "ES256", 74 | "typ": "JWT", 75 | "x5c": x5cChain, 76 | ] 77 | let encodedHeader = try base64URLEncode(JSONSerialization.data(withJSONObject: header)) 78 | let encodedBody = try base64URLEncode(JSONSerialization.data(withJSONObject: payload)) 79 | 80 | let message = "\(encodedHeader).\(encodedBody)" 81 | 82 | let task = Process() 83 | task.executableURL = URL(filePath: "/bin/bash") 84 | let command = """ 85 | echo -n "\(message)" | 86 | openssl dgst -sha256 -sign "\(certificateDirectory)/\(signingKeyPath)" | 87 | openssl asn1parse -inform DER | 88 | perl -n -e '/INTEGER :([0-9A-Z]*)$/ && print $1' | 89 | xxd -p -r 90 | """ 91 | task.arguments = ["-c", command] 92 | 93 | let outputPipe = Pipe() 94 | task.standardOutput = outputPipe 95 | 96 | try task.run() 97 | let signature = base64URLEncode(outputPipe.fileHandleForReading.readDataToEndOfFile()) 98 | return "\(message).\(signature)" 99 | } 100 | 101 | func generateTokens(keepingCertificates: Bool = false) throws { 102 | let coolPayload = ["cool": true] 103 | let tokens = try [ 104 | "validToken": generateToken( 105 | payload: coolPayload, certificates: [leafCertFileName, intermediateCertFileName, rootCertFileName], 106 | signingKeyPath: leafKeyFileName 107 | ), 108 | "missingIntermediateToken": generateToken( 109 | payload: coolPayload, certificates: [leafCertFileName, rootCertFileName], signingKeyPath: leafKeyFileName 110 | ), 111 | "missingRootToken": generateToken( 112 | payload: coolPayload, certificates: [leafCertFileName, intermediateCertFileName], signingKeyPath: leafKeyFileName 113 | ), 114 | "missingLeafToken": generateToken( 115 | payload: coolPayload, certificates: [intermediateCertFileName, rootCertFileName], signingKeyPath: leafKeyFileName 116 | ), 117 | "missingLeafAndIntermediateToken": generateToken( 118 | payload: coolPayload, certificates: [rootCertFileName], signingKeyPath: leafKeyFileName 119 | ), 120 | "missingIntermediateAndRootToken": generateToken( 121 | payload: coolPayload, certificates: [leafCertFileName], signingKeyPath: leafKeyFileName 122 | ), 123 | "expiredLeafToken": generateToken( 124 | payload: coolPayload, certificates: [expiredLeafCertFileName, intermediateCertFileName, rootCertFileName], 125 | signingKeyPath: leafKeyFileName 126 | ), 127 | "validButNotCoolToken": generateToken( 128 | payload: ["cool": false], certificates: [leafCertFileName, intermediateCertFileName, rootCertFileName], 129 | signingKeyPath: leafKeyFileName 130 | ), 131 | ] 132 | print("Swift Token Declarations:") 133 | for (name, token) in tokens { 134 | print( 135 | """ 136 | let \(name) = \""" 137 | \(token) 138 | \""" 139 | """) 140 | } 141 | try print(readCertificateData(from: rootCertFileName, stripped: false)) 142 | 143 | if !keepingCertificates { 144 | try? FileManager.default.removeItem(atPath: certificateDirectory) 145 | print("🧹 Certificates cleaned up") 146 | } else { 147 | if let absolutePath = FileManager.default.currentDirectoryPath as NSString? { 148 | print("💾 Certificates saved in \(absolutePath.appendingPathComponent(certificateDirectory))") 149 | } 150 | } 151 | } 152 | } 153 | 154 | let generator = JWTGenerator() 155 | let keepingCertificates = CommandLine.arguments.contains("--keep-certs") 156 | do { 157 | try generator.generateCertificates() 158 | try generator.generateTokens(keepingCertificates: keepingCertificates) 159 | } catch { 160 | print("Error: \(error)") 161 | exit(1) 162 | } 163 | --------------------------------------------------------------------------------