Skip to content

Commit f240c5f

Browse files
andtieptoffy
andauthored
Allow configuration of URIs in JWT helpers (#136)
* Expose URIs of jwks-endpoints * Actually use new endpoint * Update cache on Microsoft token * Add test for endpoint switch --------- Co-authored-by: Andreas Tielmann <[email protected]> Co-authored-by: Paul Toffoloni <[email protected]> Co-authored-by: Paul Toffoloni <[email protected]>
1 parent f26ae5d commit f240c5f

File tree

4 files changed

+165
-20
lines changed

4 files changed

+165
-20
lines changed

Sources/JWT/JWT+Apple.swift

+42-5
Original file line numberDiff line numberDiff line change
@@ -49,13 +49,23 @@ public extension Application.JWT {
4949
public let _jwt: Application.JWT
5050

5151
public func keys(on request: Request) async throws -> JWTKeyCollection {
52-
try await JWTKeyCollection().add(jwks: jwks.get(on: request).get())
52+
try await .init().add(jwks: jwks.get(on: request).get())
5353
}
5454

5555
public var jwks: EndpointCache<JWKS> {
5656
self.storage.jwks
5757
}
5858

59+
public var jwksEndpoint: URI {
60+
get {
61+
self.storage.jwksEndpoint
62+
}
63+
nonmutating set {
64+
self.storage.jwksEndpoint = newValue
65+
self.storage.jwks = .init(uri: newValue)
66+
}
67+
}
68+
5969
public var applicationIdentifier: String? {
6070
get {
6171
self.storage.applicationIdentifier
@@ -71,12 +81,39 @@ public extension Application.JWT {
7181

7282
private final class Storage: Sendable {
7383
private struct SendableBox: Sendable {
74-
var applicationIdentifier: String?
84+
var jwks: EndpointCache<JWKS>
85+
var jwksEndpoint: URI
86+
var applicationIdentifier: String? = nil
7587
}
7688

77-
let jwks: EndpointCache<JWKS>
7889
private let sendableBox: NIOLockedValueBox<SendableBox>
7990

91+
var jwks: EndpointCache<JWKS> {
92+
get {
93+
self.sendableBox.withLockedValue { box in
94+
box.jwks
95+
}
96+
}
97+
set {
98+
self.sendableBox.withLockedValue { box in
99+
box.jwks = newValue
100+
}
101+
}
102+
}
103+
104+
var jwksEndpoint: URI {
105+
get {
106+
self.sendableBox.withLockedValue { box in
107+
box.jwksEndpoint
108+
}
109+
}
110+
set {
111+
self.sendableBox.withLockedValue { box in
112+
box.jwksEndpoint = newValue
113+
}
114+
}
115+
}
116+
80117
var applicationIdentifier: String? {
81118
get {
82119
self.sendableBox.withLockedValue { box in
@@ -91,8 +128,8 @@ public extension Application.JWT {
91128
}
92129

93130
init() {
94-
self.jwks = .init(uri: "https://appleid.apple.com/auth/keys")
95-
let box = SendableBox(applicationIdentifier: nil)
131+
let jwksEndpoint: URI = "https://appleid.apple.com/auth/keys"
132+
let box = SendableBox(jwks: .init(uri: jwksEndpoint), jwksEndpoint: jwksEndpoint)
96133
self.sendableBox = .init(box)
97134
}
98135
}

Sources/JWT/JWT+Google.swift

+46-6
Original file line numberDiff line numberDiff line change
@@ -60,13 +60,23 @@ public extension Application.JWT {
6060
public let _jwt: Application.JWT
6161

6262
public func keys(on request: Request) async throws -> JWTKeyCollection {
63-
try await JWTKeyCollection().add(jwks: jwks.get(on: request).get())
63+
try await .init().add(jwks: jwks.get(on: request).get())
6464
}
6565

6666
public var jwks: EndpointCache<JWKS> {
6767
self.storage.jwks
6868
}
6969

70+
public var jwksEndpoint: URI {
71+
get {
72+
self.storage.jwksEndpoint
73+
}
74+
nonmutating set {
75+
self.storage.jwksEndpoint = newValue
76+
self.storage.jwks = .init(uri: newValue)
77+
}
78+
}
79+
7080
public var applicationIdentifier: String? {
7181
get {
7282
self.storage.applicationIdentifier
@@ -91,13 +101,27 @@ public extension Application.JWT {
91101

92102
private final class Storage: Sendable {
93103
private struct SendableBox: Sendable {
94-
var applicationIdentifier: String?
95-
var gSuiteDomainName: String?
104+
var jwks: EndpointCache<JWKS>
105+
var jwksEndpoint: URI
106+
var applicationIdentifier: String? = nil
107+
var gSuiteDomainName: String? = nil
96108
}
97109

98-
let jwks: EndpointCache<JWKS>
99110
private let sendableBox: NIOLockedValueBox<SendableBox>
100111

112+
var jwks: EndpointCache<JWKS> {
113+
get {
114+
self.sendableBox.withLockedValue { box in
115+
box.jwks
116+
}
117+
}
118+
set {
119+
self.sendableBox.withLockedValue { box in
120+
box.jwks = newValue
121+
}
122+
}
123+
}
124+
101125
var applicationIdentifier: String? {
102126
get {
103127
self.sendableBox.withLockedValue { box in
@@ -124,9 +148,25 @@ public extension Application.JWT {
124148
}
125149
}
126150

151+
var jwksEndpoint: URI {
152+
get {
153+
self.sendableBox.withLockedValue { box in
154+
box.jwksEndpoint
155+
}
156+
}
157+
set {
158+
self.sendableBox.withLockedValue { box in
159+
box.jwksEndpoint = newValue
160+
}
161+
}
162+
}
163+
127164
init() {
128-
self.jwks = .init(uri: "https://www.googleapis.com/oauth2/v3/certs")
129-
let box = SendableBox(applicationIdentifier: nil, gSuiteDomainName: nil)
165+
let jwksEndpoint: URI = "https://www.googleapis.com/oauth2/v3/certs"
166+
let box = SendableBox(
167+
jwks: .init(uri: jwksEndpoint),
168+
jwksEndpoint: jwksEndpoint
169+
)
130170
self.sendableBox = .init(box)
131171
}
132172
}

Sources/JWT/JWT+Microsoft.swift

+44-4
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,16 @@ public extension Application.JWT {
5555
public var jwks: EndpointCache<JWKS> {
5656
self.storage.jwks
5757
}
58+
59+
public var jwksEndpoint: URI {
60+
get {
61+
self.storage.jwksEndpoint
62+
}
63+
nonmutating set {
64+
self.storage.jwksEndpoint = newValue
65+
self.storage.jwks = .init(uri: newValue)
66+
}
67+
}
5868

5969
public var applicationIdentifier: String? {
6070
get {
@@ -71,11 +81,25 @@ public extension Application.JWT {
7181

7282
private final class Storage: Sendable {
7383
private struct SendableBox: Sendable {
74-
var applicationIdentifier: String?
84+
var jwks: EndpointCache<JWKS>
85+
var jwksEndpoint: URI
86+
var applicationIdentifier: String? = nil
7587
}
7688

77-
let jwks: EndpointCache<JWKS>
7889
private let sendableBox: NIOLockedValueBox<SendableBox>
90+
91+
var jwks: EndpointCache<JWKS> {
92+
get {
93+
self.sendableBox.withLockedValue { box in
94+
box.jwks
95+
}
96+
}
97+
set {
98+
self.sendableBox.withLockedValue { box in
99+
box.jwks = newValue
100+
}
101+
}
102+
}
79103

80104
var applicationIdentifier: String? {
81105
get {
@@ -89,10 +113,26 @@ public extension Application.JWT {
89113
}
90114
}
91115
}
116+
117+
var jwksEndpoint: URI {
118+
get {
119+
self.sendableBox.withLockedValue { box in
120+
box.jwksEndpoint
121+
}
122+
}
123+
set {
124+
self.sendableBox.withLockedValue { box in
125+
box.jwksEndpoint = newValue
126+
}
127+
}
128+
}
92129

93130
init() {
94-
self.jwks = .init(uri: "https://login.microsoftonline.com/common/discovery/keys")
95-
let box = SendableBox(applicationIdentifier: nil)
131+
let jwksEndpoint: URI = "https://login.microsoftonline.com/common/discovery/keys"
132+
let box = SendableBox(
133+
jwks: .init(uri: jwksEndpoint),
134+
jwksEndpoint: jwksEndpoint
135+
)
96136
self.sendableBox = .init(box)
97137
}
98138
}

Tests/JWTTests/JWTTests.swift

+33-5
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,16 @@ import XCTVapor
44

55
class JWTTests: XCTestCase {
66
var app: Application!
7-
7+
88
override func setUp() async throws {
99
app = try await Application.make(.testing)
1010
XCTAssert(isLoggingConfigured)
1111
}
12-
12+
1313
override func tearDown() async throws {
1414
try await app.asyncShutdown()
1515
}
16-
16+
1717
func testDocs() async throws {
1818
// Add HMAC with SHA-256 signer.
1919
await app.jwt.keys.add(hmac: "secret", digestAlgorithm: .sha256)
@@ -116,7 +116,7 @@ class JWTTests: XCTestCase {
116116
}, afterResponse: { res async in
117117
XCTAssertEqual(res.status, .ok)
118118
})
119-
119+
120120
try await app.test(.POST, "login", beforeRequest: { req in
121121
print(req)
122122
}, afterResponse: { res async throws in
@@ -274,7 +274,7 @@ class JWTTests: XCTestCase {
274274
try await app.test(.GET, "test", headers: headers) { res async in
275275
XCTAssertEqual(res.status, .unauthorized)
276276
}
277-
277+
278278
try await app.test(.GET, "test2", headers: headers) { res async in
279279
XCTAssertEqual(res.status, .unauthorized)
280280
}
@@ -336,6 +336,34 @@ class JWTTests: XCTestCase {
336336
}
337337
}
338338
}
339+
340+
func testMicrosoftEndpointSwitch() async throws {
341+
await app.jwt.keys.add(hmac: "secret", digestAlgorithm: .sha256)
342+
343+
let testUser = TestUser(name: "foo")
344+
let token = try await app.jwt.keys.sign(testUser)
345+
346+
app.jwt.microsoft.applicationIdentifier = ""
347+
app.get("microsoft") { req async throws in
348+
let token = try await req.jwt.microsoft.verify()
349+
return token.name ?? "none"
350+
}
351+
352+
try await app.test(.GET, "microsoft", headers: ["Authorization": "Bearer \(token)"]) { res async in
353+
XCTAssertEqual(res.status, .unauthorized)
354+
}
355+
356+
app.jwt.microsoft.jwksEndpoint = "https://login.microsoftonline.com/common/discovery/v2.0/keys"
357+
try await app.test(.GET, "microsoft", headers: ["Authorization": "Bearer \(token)"]) { res async in
358+
XCTAssertEqual(res.status, .unauthorized)
359+
}
360+
361+
// Use a non-existent endpoint to show that endpoint switching works
362+
app.jwt.microsoft.jwksEndpoint = "https://login.microsoftonline.com/nonexistent/endpoint"
363+
try await app.test(.GET, "microsoft", headers: ["Authorization": "Bearer \(token)"]) { res async in
364+
XCTAssertEqual(res.status, .internalServerError)
365+
}
366+
}
339367
}
340368

341369
extension ByteBuffer {

0 commit comments

Comments
 (0)