Skip to content

Commit feb252e

Browse files
committed
feat: Introduce a ClaimSet
1 parent 72d693f commit feb252e

File tree

5 files changed

+246
-125
lines changed

5 files changed

+246
-125
lines changed

JWT.xcodeproj/project.pbxproj

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@
1010
2734C6A81D88001F00BFF9F1 /* CryptoSwift.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 66725DAB1C59202E00FC32F4 /* CryptoSwift.framework */; };
1111
2734C6A91D88002900BFF9F1 /* CryptoSwift.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 66725DAB1C59202E00FC32F4 /* CryptoSwift.framework */; };
1212
2734C6AA1D88003000BFF9F1 /* CryptoSwift.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 66725DAB1C59202E00FC32F4 /* CryptoSwift.framework */; };
13+
277794051DF221F800573F3E /* ClaimSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 277794041DF221F800573F3E /* ClaimSet.swift */; };
14+
277794061DF221F800573F3E /* ClaimSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 277794041DF221F800573F3E /* ClaimSet.swift */; };
15+
277794071DF221F800573F3E /* ClaimSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 277794041DF221F800573F3E /* ClaimSet.swift */; };
16+
277794081DF221F800573F3E /* ClaimSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 277794041DF221F800573F3E /* ClaimSet.swift */; };
1317
279D63A21AD07FFF0024E2BC /* JWT.h in Headers */ = {isa = PBXBuildFile; fileRef = 279D63A11AD07FFF0024E2BC /* JWT.h */; settings = {ATTRIBUTES = (Public, ); }; };
1418
279D63A81AD07FFF0024E2BC /* JWT.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 279D639C1AD07FFF0024E2BC /* JWT.framework */; };
1519
279D63AF1AD07FFF0024E2BC /* JWTTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 279D63AE1AD07FFF0024E2BC /* JWTTests.swift */; };
@@ -67,6 +71,7 @@
6771
/* End PBXContainerItemProxy section */
6872

6973
/* Begin PBXFileReference section */
74+
277794041DF221F800573F3E /* ClaimSet.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClaimSet.swift; sourceTree = "<group>"; };
7075
279D639C1AD07FFF0024E2BC /* JWT.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = JWT.framework; sourceTree = BUILT_PRODUCTS_DIR; };
7176
279D63A01AD07FFF0024E2BC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
7277
279D63A11AD07FFF0024E2BC /* JWT.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = JWT.h; sourceTree = "<group>"; };
@@ -196,6 +201,7 @@
196201
520A71121C469F010005C709 /* Sources */ = {
197202
isa = PBXGroup;
198203
children = (
204+
277794041DF221F800573F3E /* ClaimSet.swift */,
199205
520A71131C469F010005C709 /* Base64.swift */,
200206
520A71141C469F010005C709 /* Claims.swift */,
201207
520A71151C469F010005C709 /* Decode.swift */,
@@ -460,6 +466,7 @@
460466
520A71181C469F010005C709 /* Claims.swift in Sources */,
461467
520A711A1C469F010005C709 /* JWT.swift in Sources */,
462468
520A71191C469F010005C709 /* Decode.swift in Sources */,
469+
277794051DF221F800573F3E /* ClaimSet.swift in Sources */,
463470
520A71171C469F010005C709 /* Base64.swift in Sources */,
464471
);
465472
runOnlyForDeploymentPostprocessing = 0;
@@ -479,6 +486,7 @@
479486
CD9B62171C7753D8005D4844 /* Claims.swift in Sources */,
480487
CD9B62181C7753D8005D4844 /* JWT.swift in Sources */,
481488
CD9B62191C7753D8005D4844 /* Decode.swift in Sources */,
489+
277794061DF221F800573F3E /* ClaimSet.swift in Sources */,
482490
CD9B621A1C7753D8005D4844 /* Base64.swift in Sources */,
483491
);
484492
runOnlyForDeploymentPostprocessing = 0;
@@ -490,6 +498,7 @@
490498
CD9B62291C7753EC005D4844 /* Claims.swift in Sources */,
491499
CD9B622A1C7753EC005D4844 /* JWT.swift in Sources */,
492500
CD9B622B1C7753EC005D4844 /* Decode.swift in Sources */,
501+
277794071DF221F800573F3E /* ClaimSet.swift in Sources */,
493502
CD9B622C1C7753EC005D4844 /* Base64.swift in Sources */,
494503
);
495504
runOnlyForDeploymentPostprocessing = 0;
@@ -501,6 +510,7 @@
501510
CD9B623B1C7753FB005D4844 /* Claims.swift in Sources */,
502511
CD9B623C1C7753FB005D4844 /* JWT.swift in Sources */,
503512
CD9B623D1C7753FB005D4844 /* Decode.swift in Sources */,
513+
277794081DF221F800573F3E /* ClaimSet.swift in Sources */,
504514
CD9B623E1C7753FB005D4844 /* Base64.swift in Sources */,
505515
);
506516
runOnlyForDeploymentPostprocessing = 0;

Sources/ClaimSet.swift

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
public struct ClaimSet {
2+
var claims: [String: Any]
3+
4+
public init(claims: [String: Any]? = nil) {
5+
self.claims = claims ?? [:]
6+
}
7+
8+
public subscript(key: String) -> Any? {
9+
get {
10+
return claims[key]
11+
}
12+
13+
set {
14+
if let newValue = newValue, let date = newValue as? Date {
15+
claims[key] = date.timeIntervalSince1970
16+
} else {
17+
claims[key] = newValue
18+
}
19+
}
20+
}
21+
}
22+
23+
24+
// MARK: Accessors
25+
26+
extension ClaimSet {
27+
public var issuer: String? {
28+
get {
29+
return claims["iss"] as? String
30+
}
31+
32+
set {
33+
claims["iss"] = newValue
34+
}
35+
}
36+
37+
public var audience: String? {
38+
get {
39+
return claims["aud"] as? String
40+
}
41+
42+
set {
43+
claims["aud"] = newValue
44+
}
45+
}
46+
47+
public var expiration: Date? {
48+
get {
49+
if let expiration = claims["exp"] as? TimeInterval {
50+
return Date(timeIntervalSince1970: expiration)
51+
}
52+
53+
return nil
54+
}
55+
56+
set {
57+
self["exp"] = newValue
58+
}
59+
}
60+
61+
public var notBefore: Date? {
62+
get {
63+
if let notBefore = claims["nbf"] as? TimeInterval {
64+
return Date(timeIntervalSince1970: notBefore)
65+
}
66+
67+
return nil
68+
}
69+
70+
set {
71+
self["nbf"] = newValue
72+
}
73+
}
74+
75+
public var issuedAt: Date? {
76+
get {
77+
if let issuedAt = claims["iat"] as? TimeInterval {
78+
return Date(timeIntervalSince1970: issuedAt)
79+
}
80+
81+
return nil
82+
}
83+
84+
set {
85+
self["iat"] = newValue
86+
}
87+
}
88+
}
89+
90+
91+
// MARK: Validations
92+
93+
extension ClaimSet {
94+
func validate(audience: String? = nil, issuer: String? = nil) throws {
95+
if let issuer = issuer {
96+
try validateIssuer(issuer)
97+
}
98+
99+
if let audience = audience {
100+
try validateAudience(audience)
101+
}
102+
103+
try validateDate(claims, key: "exp", comparison: .orderedAscending, failure: .expiredSignature, decodeError: "Expiration time claim (exp) must be an integer")
104+
try validateDate(claims, key: "nbf", comparison: .orderedDescending, failure: .immatureSignature, decodeError: "Not before claim (nbf) must be an integer")
105+
try validateDate(claims, key: "iat", comparison: .orderedDescending, failure: .invalidIssuedAt, decodeError: "Issued at claim (iat) must be an integer")
106+
}
107+
108+
func validateAudience(_ audience: String) throws {
109+
if let aud = self["aud"] as? [String] {
110+
if !aud.contains(audience) {
111+
throw InvalidToken.invalidAudience
112+
}
113+
} else if let aud = self["aud"] as? String {
114+
if aud != audience {
115+
throw InvalidToken.invalidAudience
116+
}
117+
} else {
118+
throw InvalidToken.decodeError("Invalid audience claim, must be a string or an array of strings")
119+
}
120+
}
121+
122+
func validateIssuer(_ issuer: String) throws {
123+
if let iss = self["iss"] as? String {
124+
if iss != issuer {
125+
throw InvalidToken.invalidIssuer
126+
}
127+
} else {
128+
throw InvalidToken.invalidIssuer
129+
}
130+
}
131+
}
132+
133+
// MARK: Builder
134+
135+
public class ClaimSetBuilder {
136+
var claims = ClaimSet()
137+
138+
public var issuer: String? {
139+
get {
140+
return claims.issuer
141+
}
142+
143+
set {
144+
claims.issuer = newValue
145+
}
146+
}
147+
148+
public var audience: String? {
149+
get {
150+
return claims.audience
151+
}
152+
153+
set {
154+
claims.audience = newValue
155+
}
156+
}
157+
158+
public var expiration: Date? {
159+
get {
160+
return claims.expiration
161+
}
162+
163+
set {
164+
claims.expiration = newValue
165+
}
166+
}
167+
168+
public var notBefore: Date? {
169+
get {
170+
return claims.notBefore
171+
}
172+
173+
set {
174+
claims.notBefore = newValue
175+
}
176+
}
177+
178+
public var issuedAt: Date? {
179+
get {
180+
return claims.issuedAt
181+
}
182+
183+
set {
184+
claims.issuedAt = newValue
185+
}
186+
}
187+
188+
public subscript(key: String) -> Any? {
189+
get {
190+
return claims[key]
191+
}
192+
193+
set {
194+
claims[key] = newValue
195+
}
196+
}
197+
}
198+
199+
typealias PayloadBuilder = ClaimSetBuilder

Sources/Claims.swift

Lines changed: 0 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,5 @@
11
import Foundation
22

3-
func validateClaims(_ payload: Payload, audience: String?, issuer: String?) throws {
4-
try validateIssuer(payload, issuer: issuer)
5-
try validateAudience(payload, audience: audience)
6-
try validateDate(payload, key: "exp", comparison: .orderedAscending, failure: .expiredSignature, decodeError: "Expiration time claim (exp) must be an integer")
7-
try validateDate(payload, key: "nbf", comparison: .orderedDescending, failure: .immatureSignature, decodeError: "Not before claim (nbf) must be an integer")
8-
try validateDate(payload, key: "iat", comparison: .orderedDescending, failure: .invalidIssuedAt, decodeError: "Issued at claim (iat) must be an integer")
9-
}
10-
11-
func validateAudience(_ payload: Payload, audience: String?) throws {
12-
guard let audience = audience else {
13-
return
14-
}
15-
16-
if let aud = payload["aud"] as? [String] {
17-
if !aud.contains(audience) {
18-
throw InvalidToken.invalidAudience
19-
}
20-
} else if let aud = payload["aud"] as? String {
21-
if aud != audience {
22-
throw InvalidToken.invalidAudience
23-
}
24-
} else {
25-
throw InvalidToken.decodeError("Invalid audience claim, must be a string or an array of strings")
26-
}
27-
}
28-
29-
func validateIssuer(_ payload: Payload, issuer: String?) throws {
30-
if let issuer = issuer {
31-
if let iss = payload["iss"] as? String {
32-
if iss != issuer {
33-
throw InvalidToken.invalidIssuer
34-
}
35-
} else {
36-
throw InvalidToken.invalidIssuer
37-
}
38-
}
39-
}
40-
413
func validateDate(_ payload:Payload, key:String, comparison:ComparisonResult, failure:InvalidToken, decodeError:String) throws {
424
if payload[key] == nil {
435
return

Sources/Decode.swift

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -47,24 +47,33 @@ public enum InvalidToken : CustomStringConvertible, Error {
4747

4848

4949
/// Decode a JWT
50-
public func decode(_ jwt:String, algorithms:[Algorithm], verify:Bool = true, audience:String? = nil, issuer:String? = nil) throws -> Payload {
51-
let (header, payload, signature, signatureInput) = try load(jwt)
50+
func decode(_ jwt: String, algorithms: [Algorithm], verify: Bool = true, audience: String? = nil, issuer: String? = nil) throws -> ClaimSet {
51+
let (header, claims, signature, signatureInput) = try load(jwt)
52+
5253
if verify {
53-
try validateClaims(payload, audience: audience, issuer: issuer)
54+
try claims.validate(audience: audience, issuer: issuer)
5455
try verifySignature(algorithms, header: header, signingInput: signatureInput, signature: signature)
5556
}
5657

57-
return payload
58+
return claims
5859
}
5960

61+
6062
/// Decode a JWT
61-
public func decode(_ jwt:String, algorithm:Algorithm, verify:Bool = true, audience:String? = nil, issuer:String? = nil) throws -> Payload {
62-
return try decode(jwt, algorithms: [algorithm], verify: verify, audience: audience, issuer: issuer)
63+
public func decode(_ jwt: String, algorithms: [Algorithm], verify: Bool = true, audience: String? = nil, issuer: String? = nil) throws -> Payload {
64+
return try decode(jwt, algorithms: algorithms, verify: verify, audience: audience, issuer: issuer).claims
6365
}
6466

67+
68+
/// Decode a JWT
69+
public func decode(_ jwt: String, algorithm: Algorithm, verify: Bool = true, audience: String? = nil, issuer: String? = nil) throws -> Payload {
70+
return try decode(jwt, algorithms: [algorithm], verify: verify, audience: audience, issuer: issuer).claims
71+
}
72+
73+
6574
// MARK: Parsing a JWT
6675

67-
func load(_ jwt:String) throws -> (header: Payload, payload: Payload, signature: Data, signatureInput: String) {
76+
func load(_ jwt:String) throws -> (header: Payload, payload: ClaimSet, signature: Data, signatureInput: String) {
6877
let segments = jwt.components(separatedBy: ".")
6978
if segments.count != 3 {
7079
throw InvalidToken.decodeError("Not enough segments")
@@ -98,7 +107,7 @@ func load(_ jwt:String) throws -> (header: Payload, payload: Payload, signature:
98107
throw InvalidToken.decodeError("Signature is not correctly encoded as base64")
99108
}
100109

101-
return (header: header!, payload: payload!, signature: signature, signatureInput: signatureInput)
110+
return (header: header!, payload: ClaimSet(claims: payload!), signature: signature, signatureInput: signatureInput)
102111
}
103112

104113
// MARK: Signature Verification

0 commit comments

Comments
 (0)