-
Notifications
You must be signed in to change notification settings - Fork 2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Include misc
in JWT.Claims
struct. Use InMemoryKeyManager()
by default in DIDJWK.create()
#45
Changes from 5 commits
6c63dd6
80e0643
9b52b6e
d8e7eb4
9f17182
68e74b0
fc80f9a
b916df5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -26,33 +26,38 @@ public struct JWT { | |
/// or after which the JWT must not be accepted for processing. | ||
/// | ||
/// [Spec](https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.4) | ||
@ISO8601Date private(set) var expiration: Date? | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. are date fields in jwt claims supposed to be a numeric value? is there any value in saving it as a date? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. issue created: #46 |
||
let expiration: Int? | ||
|
||
/// The "nbf" (not before) claim identifies the time before which the JWT | ||
/// must not be accepted for processing. | ||
/// | ||
/// [Spec](https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.5) | ||
@ISO8601Date private(set) var notBefore: Date? | ||
let notBefore: Int? | ||
|
||
/// The "iat" (issued at) claim identifies the time at which the JWT was issued. | ||
/// | ||
/// [Spec](https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.6) | ||
@ISO8601Date private(set) var issuedAt: Date? | ||
let issuedAt: Int? | ||
|
||
/// The "jti" (JWT ID) claim provides a unique identifier for the JWT. | ||
/// | ||
/// [Spec](https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.7) | ||
let jwtID: String? | ||
|
||
/// "misc" Miscellaneous claim is a map to store any additional claims | ||
/// | ||
var miscellaneous: [String: AnyCodable]? | ||
|
||
// Default Initializer | ||
public init( | ||
issuer: String? = nil, | ||
subject: String? = nil, | ||
audience: String? = nil, | ||
expiration: Date? = nil, | ||
notBefore: Date? = nil, | ||
issuedAt: Date? = nil, | ||
jwtID: String? = nil | ||
expiration: Int? = nil, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. let's make these |
||
notBefore: Int? = nil, | ||
issuedAt: Int? = nil, | ||
jwtID: String? = nil, | ||
misc: [String: AnyCodable] = [:] | ||
jiyoonie9 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
) { | ||
self.issuer = issuer | ||
self.subject = subject | ||
|
@@ -61,9 +66,11 @@ public struct JWT { | |
self.notBefore = notBefore | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
self.issuedAt = issuedAt | ||
self.jwtID = jwtID | ||
self.miscellaneous = misc | ||
} | ||
|
||
enum CodingKeys: String, CodingKey { | ||
|
||
enum CodingKeys: String, CodingKey, CaseIterable { | ||
case issuer = "iss" | ||
case subject = "sub" | ||
case audience = "aud" | ||
|
@@ -72,6 +79,63 @@ public struct JWT { | |
case issuedAt = "iat" | ||
case jwtID = "jti" | ||
} | ||
|
||
public init(from decoder: Decoder) throws { | ||
let container = try decoder.container(keyedBy: CodingKeys.self) | ||
|
||
// Decode the known properties | ||
issuer = try container.decodeIfPresent(String.self, forKey: .issuer) | ||
subject = try container.decodeIfPresent(String.self, forKey: .subject) | ||
audience = try container.decodeIfPresent(String.self, forKey: .audience) | ||
expiration = try container.decodeIfPresent(Int.self, forKey: .expiration) | ||
notBefore = try container.decodeIfPresent(Int.self, forKey: .notBefore) | ||
issuedAt = try container.decodeIfPresent(Int.self, forKey: .issuedAt) | ||
jwtID = try container.decodeIfPresent(String.self, forKey: .jwtID) | ||
|
||
// Initialize the miscellaneous dictionary | ||
var misc = [String: AnyCodable]() | ||
|
||
// Extract all rawValues from CodingKeys for comparison | ||
let knownKeysRawValues = CodingKeys.allCases.map { $0.rawValue } | ||
|
||
// Dynamically decode keys not in CodingKeys | ||
let dynamicContainer = try decoder.container(keyedBy: DynamicCodingKey.self) | ||
for key in dynamicContainer.allKeys { | ||
// Convert DynamicCodingKey to String | ||
let keyString = key.stringValue | ||
|
||
// Skip keys that are part of the known CodingKeys | ||
if !knownKeysRawValues.contains(keyString) { | ||
if let value = try? dynamicContainer.decode(AnyCodable.self, forKey: key) { | ||
misc[keyString] = value | ||
} | ||
} | ||
} | ||
|
||
miscellaneous = misc.isEmpty ? nil : misc | ||
} | ||
|
||
public func encode(to encoder: Encoder) throws { | ||
var container = encoder.container(keyedBy: CodingKeys.self) | ||
|
||
// Encode known properties | ||
try container.encodeIfPresent(issuer, forKey: .issuer) | ||
try container.encodeIfPresent(subject, forKey: .subject) | ||
try container.encodeIfPresent(audience, forKey: .audience) | ||
try container.encodeIfPresent(expiration, forKey: .expiration) | ||
try container.encodeIfPresent(notBefore, forKey: .notBefore) | ||
try container.encodeIfPresent(issuedAt, forKey: .issuedAt) | ||
try container.encodeIfPresent(jwtID, forKey: .jwtID) | ||
|
||
// Dynamically encode the miscellaneous properties at the top level | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
So this means the claims body won't look like this, right? {
// existing well-defined claims properties
"misc": {
"something": "else"
}
} and instead will look like this, right? {
// existing well-defined claims properties
"something": "else"
} (idk Swift so apologies in advance) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes thats right! in swift we don't get the affordance of [k:string: any] type like in ts, so we effectively encode/decode to/from a known property that does have a dictionary of [String: Any] so exactly as you said:
while in Swift it will look like
|
||
var dynamicContainer = encoder.container(keyedBy: DynamicCodingKey.self) | ||
if let misc = miscellaneous { | ||
for (key, value) in misc { | ||
let codingKey = DynamicCodingKey(stringValue: key)! | ||
try dynamicContainer.encode(value, forKey: codingKey) | ||
} | ||
} | ||
} | ||
} | ||
|
||
/// Signs the provied JWT claims with the provided BearerDID. | ||
|
@@ -94,11 +158,11 @@ public struct JWT { | |
|
||
public struct ParsedJWT { | ||
let header: JWS.Header | ||
let payload: [String: AnyCodable] | ||
let payload: JWT.Claims | ||
|
||
public init( | ||
header: JWS.Header, | ||
payload: [String: AnyCodable] | ||
payload: JWT.Claims | ||
) { | ||
self.header = header | ||
self.payload = payload | ||
|
@@ -129,9 +193,9 @@ public struct JWT { | |
} | ||
|
||
let jwtPayload = try JSONDecoder().decode( | ||
[String: AnyCodable].self, | ||
from: base64urlEncodedJwtPayload.decodeBase64Url() | ||
) | ||
JWT.Claims.self, | ||
from: try base64urlEncodedJwtPayload.decodeBase64Url()) | ||
|
||
|
||
return ParsedJWT(header: jwtHeader, payload: jwtPayload) | ||
} | ||
|
@@ -151,3 +215,21 @@ extension JWT { | |
} | ||
} | ||
} | ||
|
||
|
||
// MARK: - DynamicCodingKey | ||
// Define DynamicCodingKey to use for dynamic encoding | ||
struct DynamicCodingKey: CodingKey { | ||
var stringValue: String | ||
var intValue: Int? | ||
|
||
init?(stringValue: String) { | ||
self.stringValue = stringValue | ||
self.intValue = nil | ||
} | ||
|
||
init?(intValue: Int) { | ||
self.stringValue = "\(intValue)" | ||
self.intValue = intValue | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -30,7 +30,7 @@ public enum DIDJWK { | |
/// - options: Options configuring how the DIDJWK is created | ||
/// - Returns: `BearerDID` that represents the created DIDJWK | ||
public static func create( | ||
keyManager: KeyManager, | ||
keyManager: KeyManager = InMemoryKeyManager(), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🚀 🚀 🚀 |
||
options: CreateOptions = .default | ||
) throws -> BearerDID { | ||
let keyAlias = try keyManager.generatePrivateKey(algorithm: options.algorithm) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,15 +1,69 @@ | ||
import XCTest | ||
import AnyCodable | ||
|
||
@testable import Web5 | ||
|
||
final class JWTTests: XCTestCase { | ||
|
||
func test_sign() throws { | ||
let did = try DIDJWK.create(keyManager: InMemoryKeyManager()) | ||
let did = try DIDJWK.create() | ||
let future = Int(Date.distantFuture.timeIntervalSince1970) | ||
|
||
let claims = JWT.Claims(issuer: did.identifier) | ||
let claims = JWT.Claims( | ||
issuer: did.identifier, | ||
expiration: future, | ||
misc: ["nonce": 123] | ||
) | ||
let jwt = try JWT.sign(did: did, claims: claims) | ||
|
||
XCTAssertFalse(jwt.isEmpty) | ||
|
||
let decoded = try JWT.parse(jwtString: jwt) | ||
let decodedNonceValue = decoded.payload.miscellaneous?["nonce"]?.value as? Int | ||
XCTAssertEqual(decodedNonceValue, 123) | ||
|
||
} | ||
} | ||
|
||
// todo consider adding more tests to verify encode and decode works as intended | ||
class JWTClaimsTests: XCTestCase { | ||
|
||
func testClaimsEncodingDecoding() { | ||
let originalClaims = JWT.Claims( | ||
issuer: "issuer", | ||
subject: "subject", | ||
audience: "audience", | ||
expiration: Int(Date.distantFuture.timeIntervalSince1970), | ||
notBefore: Int(Date.distantPast.timeIntervalSince1970), | ||
issuedAt: Int(Date.now.timeIntervalSince1970), | ||
jiyoonie9 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
jwtID: "jwtID", | ||
misc: ["foo": AnyCodable("bar")] | ||
) | ||
|
||
do { | ||
let encodedClaims = try JSONEncoder().encode(originalClaims) | ||
let decodedClaims = try JSONDecoder().decode(JWT.Claims.self, from: encodedClaims) | ||
|
||
XCTAssertEqual(originalClaims.issuer, decodedClaims.issuer) | ||
XCTAssertEqual(originalClaims.subject, decodedClaims.subject) | ||
XCTAssertEqual(originalClaims.audience, decodedClaims.audience) | ||
XCTAssertEqual(originalClaims.expiration, decodedClaims.expiration) | ||
XCTAssertEqual(originalClaims.notBefore, decodedClaims.notBefore) | ||
XCTAssertEqual(originalClaims.issuedAt, decodedClaims.issuedAt) | ||
XCTAssertEqual(originalClaims.jwtID, decodedClaims.jwtID) | ||
|
||
// Log and compare custom claims | ||
let originalMiscValue = originalClaims.miscellaneous?["foo"]?.value as? String | ||
let decodedMiscValue = decodedClaims.miscellaneous?["foo"]?.value as? String | ||
|
||
if let originalMiscValue = originalMiscValue, let decodedMiscValue = decodedMiscValue { | ||
XCTAssertEqual(originalMiscValue, decodedMiscValue, "Misc claims did not match.") | ||
} else { | ||
XCTFail("Custom claims could not be found or did not match. Original: \(String(describing: originalMiscValue)), Decoded: \(String(describing: decodedMiscValue))") | ||
} | ||
|
||
} catch { | ||
XCTFail("Encoding or decoding failed with error: \(error)") | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
#47