Skip to content

Commit

Permalink
feat(jwt): add dsl claims builder
Browse files Browse the repository at this point in the history
  • Loading branch information
beatt83 committed May 29, 2024
1 parent b8e8133 commit 5a03a65
Show file tree
Hide file tree
Showing 25 changed files with 1,161 additions and 44 deletions.
75 changes: 74 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ This library provides comprehensive support for the Jose suite of standards, inc
| iat |:white_check_mark:|
| typ |:white_check_mark:|
| cty |:white_check_mark:|
| DSL Claims Builder|:white_check_mark:|

</td></tr> </table>

Expand Down Expand Up @@ -386,8 +387,12 @@ JWT is a compact, URL-safe means of representing claims to be transferred betwee
3. **Nested JWT (JWS + JWE)**:
- Implements Nested JWTs where a JWT is signed and then encrypted, providing both the benefits of JWS and JWE.
- Ensures that a token is first authenticated (JWS) and then secured for privacy (JWE).

4. **Domain-specific language (DSL) for Claim Creation**:
- Allows for a more declarative approach to creating claims using a domain-specific language (DSL).
- Facilitates the creation of both standard and custom claims in a readable and structured manner.

4. **Claim Validation**:
5. **Claim Validation**:
- Offers extensive capabilities to validate JWT claims.
- Includes standard claims like issuer (`iss`), subject (`sub`), audience (`aud`), expiration (`exp`), not before (`nbf`), and issued at (`iat`).
- Custom claim validation to meet specific security requirements.
Expand Down Expand Up @@ -440,6 +445,74 @@ let verifiedJWT = try JWT<DefaultJWTClaims>.verify(jwtString: jwtString, recipie
let verifiedPayload = verifiedJWT.payload
```

- DSL for Creating Claims
- Standard Claims on signing a JWT

```swift
let key = JWK.testingES256Pair

let jwt = try JWT.signed(
payload: {
IssuerClaim(value: "testIssuer")
SubjectClaim(value: "testSubject")
ExpirationTimeClaim(value: Date())
IssuedAtClaim(value: Date())
NotBeforeClaim(value: Date())
JWTIdentifierClaim(value: "ThisIdentifier")
AudienceClaim(value: "testAud")
},
protectedHeader: DefaultJWSHeaderImpl(algorithm: .ES256),
key: key
).jwtString
```

- Custom Claims

```swift
let jsonClaimsObject = JWTClaimsBuilder.build {
StringClaim(key: "testStr1", value: "value1")
NumberClaim(key: "testN1", value: 0)
NumberClaim(key: "testN2", value: 1.1)
NumberClaim(key: "testN3", value: Double(1.233232))
BoolClaim(key: "testBool1", value: true)
ArrayClaim(key: "testArray") {
ArrayElementClaim.string("valueArray1")
ArrayElementClaim.string("valueArray2")
ArrayElementClaim.bool(true)
ArrayElementClaim.array {
ArrayElementClaim.string("nestedNestedArray1")
}
ArrayElementClaim.object {
StringClaim(key: "nestedNestedObject", value: "nestedNestedValue")
}
}
ObjectClaim(key: "testObject") {
StringClaim(key: "testDicStr1", value: "valueDic1")
}
}

// Output
// {
// "testBool1":true,
// "testArray":[
// "valueArray1",
// "valueArray2",
// true,
// ["nestedNestedArray1"],
// {
// "nestedNestedObject":"nestedNestedValue"
// }
// ],
// "testObject":{
// "testDicStr1":"valueDic1"
// },
// "testN1":0,
// "testStr1":"value1",
// "testN3":1.233232,
// "testN2":1.1
// }
```

### JWA (JSON Web Algorithms)
JWA specifies cryptographic algorithms used in the context of Jose to perform digital signing and content encryption, as detailed in [RFC 7518](https://datatracker.ietf.org/doc/html/rfc7518). It includes standards for various types of algorithms like RSA, AES, HMAC, and more.

Expand Down
103 changes: 103 additions & 0 deletions Sources/JSONWebToken/Claims/ArrayClaim.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/*
* Copyright 2024 Gonçalo Frade
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import Foundation

/// Represents an array claim within a JWT.
public struct ArrayClaim: Claim {
public var value: ClaimElement

/// A result builder for constructing array claims.
@resultBuilder
public struct ArrayClaimBuilder {
/// Builds an array of `ArrayElementClaim` from the provided components.
/// - Parameter components: The array element claims to include in the array.
/// - Returns: An array of `ArrayElementClaim`.
public static func buildBlock(_ components: ArrayElementClaim...) -> [ArrayElementClaim] {
components
}

/// Builds an array of `ArrayElementClaim` from the provided components.
/// - Parameter components: The array element claims to include in the array.
/// - Returns: An array of `ArrayElementClaim`.
public static func buildBlock(_ components: Claim...) -> [Claim] {
components
}

/// Builds an array of `StringClaim` from the provided components.
/// - Parameter components: The string claims to include in the array.
/// - Returns: An array of `StringClaim`.
public static func buildBlock(_ components: StringClaim...) -> [StringClaim] {
components
}
}

/// Initializes an `ArrayClaim` with a key and a builder for the array elements.
/// - Parameters:
/// - key: The key for the claim.
/// - claims: A closure that returns an array of `ArrayElementClaim` using the result builder.
public init(key: String, @ArrayClaimBuilder claims: () -> [ArrayElementClaim]) {
self.value = .init(key: key, element: .array(claims().map(\.value)))
}

/// Initializes an `ArrayClaim` with a key and a builder for the array elements.
/// - Parameters:
/// - key: The key for the claim.
/// - claims: A closure that returns an array of `Claim` using the result builder.
public init(key: String, @ArrayClaimBuilder claims: () -> [Claim]) {
self.value = .init(key: key, element: .array(claims().map(\.value)))
}
}

/// Represents an element within an array claim.
public struct ArrayElementClaim {
let value: ClaimElement

/// Creates an `ArrayElementClaim` with a string value.
/// - Parameter str: The string value for the claim.
/// - Returns: An `ArrayElementClaim` containing the string value.
public static func string(_ str: String) -> ArrayElementClaim {
.init(value: StringClaim(key: "", value: str).value)
}

/// Creates an `ArrayElementClaim` with a numeric value.
/// - Parameter number: The numeric value for the claim.
/// - Returns: An `ArrayElementClaim` containing the numeric value.
public static func number<N: Numeric & Codable>(_ number: N) -> ArrayElementClaim {
.init(value: NumberClaim(key: "", value: number).value)
}

/// Creates an `ArrayElementClaim` with a boolean value.
/// - Parameter boolean: The boolean value for the claim.
/// - Returns: An `ArrayElementClaim` containing the boolean value.
public static func bool(_ boolean: Bool) -> ArrayElementClaim {
.init(value: BoolClaim(key: "", value: boolean).value)
}

/// Creates an `ArrayElementClaim` with an array of claims.
/// - Parameter claims: A closure that returns an array of `ArrayElementClaim` using the result builder.
/// - Returns: An `ArrayElementClaim` containing the array of claims.
public static func array(@ArrayClaim.ArrayClaimBuilder claims: () -> [ArrayElementClaim]) -> ArrayElementClaim {
.init(value: ArrayClaim(key: "", claims: claims).value)
}

/// Creates an `ArrayElementClaim` with an object of claims.
/// - Parameter claims: A closure that returns an array of `Claim` using the result builder.
/// - Returns: An `ArrayElementClaim` containing the object of claims.
public static func object(@ObjectClaim.ObjectClaimBuilder claims: () -> [Claim]) -> ArrayElementClaim {
.init(value: ObjectClaim(key: "", claims: claims).value)
}
}
37 changes: 37 additions & 0 deletions Sources/JSONWebToken/Claims/AudienceClaim.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright 2024 Gonçalo Frade
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import Foundation

/// A type alias for `AudienceClaim`.
typealias AudClaim = AudienceClaim

/// Represents the "aud" (audience) claim in a JWT.
public struct AudienceClaim: JWTRegisteredClaim {
public var value: ClaimElement

/// Initializes an `AudienceClaim` with a string value.
/// - Parameter value: The audience value for the claim.
public init(value: String) {
self.value = ClaimElement(key: "aud", element: .codable(value))
}

/// Initializes an `AudienceClaim` with an array of audience values using a result builder.
/// - Parameter claims: A closure that returns an array of `StringClaim` using the result builder.
init(@ArrayClaim.ArrayClaimBuilder claims: () -> [StringClaim]) {
self.value = .init(key: "aud", element: .array(claims().map(\.value)))
}
}
30 changes: 30 additions & 0 deletions Sources/JSONWebToken/Claims/BoolClaim.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Copyright 2024 Gonçalo Frade
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import Foundation

/// Represents a boolean claim within a JWT.
public struct BoolClaim: Claim {
public var value: ClaimElement

/// Initializes a `BoolClaim` with a key and a boolean value.
/// - Parameters:
/// - key: The key for the claim.
/// - value: The boolean value for the claim.
public init(key: String, value: Bool) {
self.value = ClaimElement(key: key, element: .codable(value))
}
}
88 changes: 88 additions & 0 deletions Sources/JSONWebToken/Claims/Claims+Codable.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
* Copyright 2024 Gonçalo Frade
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import Foundation

extension ClaimElement: Encodable {
public func encode(to encoder: any Encoder) throws {
var container = encoder.container(keyedBy: DynamicCodingKey.self)
switch element {
case .codable(let obj):
try container.encode(obj, forKey: .init(stringValue: key)!)
case .element(let element):
try container.encode(element, forKey: .init(stringValue: key)!)
case .array(let elements):
var nested = container.nestedUnkeyedContainer(forKey: .init(stringValue: key)!)
try encodeArrayElements(container: &nested, elements: elements)
case .object(let elements):
var nested: KeyedEncodingContainer<DynamicCodingKey>
if key.isEmpty {
nested = container
} else {
nested = container.nestedContainer(keyedBy: DynamicCodingKey.self, forKey: .init(stringValue: key)!)
}

try encodeObjectElement(container: &nested, elements: elements)
}
}

private func encodeArrayElements(container: inout UnkeyedEncodingContainer, elements: [ClaimElement]) throws {
try elements.forEach {
switch $0.element {
case .codable(let obj):
try container.encode(obj)
case .element(let element):
try container.encode(element)
case .array(let elements):
var nested = container.nestedUnkeyedContainer()
try encodeArrayElements(container: &nested, elements: elements)
case .object(let elements):
var nested = container.nestedContainer(keyedBy: DynamicCodingKey.self)
try encodeObjectElement(container: &nested, elements: elements)
}
}
}

private func encodeObjectElement(container: inout KeyedEncodingContainer<DynamicCodingKey>, elements: [ClaimElement]) throws {
try elements.forEach {
switch $0.element {
case .codable(let obj):
try container.encode(obj, forKey: .init(stringValue: $0.key)!)
case .element(let element):
try container.encode(element, forKey: .init(stringValue: $0.key)!)
case .array(let elements):
var nested = container.nestedUnkeyedContainer(forKey: .init(stringValue: $0.key)!)
try encodeArrayElements(container: &nested, elements: elements)
case .object(let elements):
var nested = container.nestedContainer(keyedBy: DynamicCodingKey.self, forKey: .init(stringValue: $0.key)!)
try encodeObjectElement(container: &nested, elements: elements)
}
}
}
}

struct DynamicCodingKey: CodingKey {
var stringValue: String
init?(stringValue: String) {
self.stringValue = stringValue
}

var intValue: Int?
init?(intValue: Int) {
self.stringValue = "\(intValue)"
self.intValue = intValue
}
}
Loading

0 comments on commit 5a03a65

Please sign in to comment.