Skip to content

Commit

Permalink
Allow configuration of URIs in JWT helpers (#136)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
4 people authored Jul 9, 2024
1 parent f26ae5d commit f240c5f
Show file tree
Hide file tree
Showing 4 changed files with 165 additions and 20 deletions.
47 changes: 42 additions & 5 deletions Sources/JWT/JWT+Apple.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,23 @@ public extension Application.JWT {
public let _jwt: Application.JWT

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

public var jwks: EndpointCache<JWKS> {
self.storage.jwks
}

public var jwksEndpoint: URI {
get {
self.storage.jwksEndpoint
}
nonmutating set {
self.storage.jwksEndpoint = newValue
self.storage.jwks = .init(uri: newValue)
}
}

public var applicationIdentifier: String? {
get {
self.storage.applicationIdentifier
Expand All @@ -71,12 +81,39 @@ public extension Application.JWT {

private final class Storage: Sendable {
private struct SendableBox: Sendable {
var applicationIdentifier: String?
var jwks: EndpointCache<JWKS>
var jwksEndpoint: URI
var applicationIdentifier: String? = nil
}

let jwks: EndpointCache<JWKS>
private let sendableBox: NIOLockedValueBox<SendableBox>

var jwks: EndpointCache<JWKS> {
get {
self.sendableBox.withLockedValue { box in
box.jwks
}
}
set {
self.sendableBox.withLockedValue { box in
box.jwks = newValue
}
}
}

var jwksEndpoint: URI {
get {
self.sendableBox.withLockedValue { box in
box.jwksEndpoint
}
}
set {
self.sendableBox.withLockedValue { box in
box.jwksEndpoint = newValue
}
}
}

var applicationIdentifier: String? {
get {
self.sendableBox.withLockedValue { box in
Expand All @@ -91,8 +128,8 @@ public extension Application.JWT {
}

init() {
self.jwks = .init(uri: "https://appleid.apple.com/auth/keys")
let box = SendableBox(applicationIdentifier: nil)
let jwksEndpoint: URI = "https://appleid.apple.com/auth/keys"
let box = SendableBox(jwks: .init(uri: jwksEndpoint), jwksEndpoint: jwksEndpoint)
self.sendableBox = .init(box)
}
}
Expand Down
52 changes: 46 additions & 6 deletions Sources/JWT/JWT+Google.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,23 @@ public extension Application.JWT {
public let _jwt: Application.JWT

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

public var jwks: EndpointCache<JWKS> {
self.storage.jwks
}

public var jwksEndpoint: URI {
get {
self.storage.jwksEndpoint
}
nonmutating set {
self.storage.jwksEndpoint = newValue
self.storage.jwks = .init(uri: newValue)
}
}

public var applicationIdentifier: String? {
get {
self.storage.applicationIdentifier
Expand All @@ -91,13 +101,27 @@ public extension Application.JWT {

private final class Storage: Sendable {
private struct SendableBox: Sendable {
var applicationIdentifier: String?
var gSuiteDomainName: String?
var jwks: EndpointCache<JWKS>
var jwksEndpoint: URI
var applicationIdentifier: String? = nil
var gSuiteDomainName: String? = nil
}

let jwks: EndpointCache<JWKS>
private let sendableBox: NIOLockedValueBox<SendableBox>

var jwks: EndpointCache<JWKS> {
get {
self.sendableBox.withLockedValue { box in
box.jwks
}
}
set {
self.sendableBox.withLockedValue { box in
box.jwks = newValue
}
}
}

var applicationIdentifier: String? {
get {
self.sendableBox.withLockedValue { box in
Expand All @@ -124,9 +148,25 @@ public extension Application.JWT {
}
}

var jwksEndpoint: URI {
get {
self.sendableBox.withLockedValue { box in
box.jwksEndpoint
}
}
set {
self.sendableBox.withLockedValue { box in
box.jwksEndpoint = newValue
}
}
}

init() {
self.jwks = .init(uri: "https://www.googleapis.com/oauth2/v3/certs")
let box = SendableBox(applicationIdentifier: nil, gSuiteDomainName: nil)
let jwksEndpoint: URI = "https://www.googleapis.com/oauth2/v3/certs"
let box = SendableBox(
jwks: .init(uri: jwksEndpoint),
jwksEndpoint: jwksEndpoint
)
self.sendableBox = .init(box)
}
}
Expand Down
48 changes: 44 additions & 4 deletions Sources/JWT/JWT+Microsoft.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,16 @@ public extension Application.JWT {
public var jwks: EndpointCache<JWKS> {
self.storage.jwks
}

public var jwksEndpoint: URI {
get {
self.storage.jwksEndpoint
}
nonmutating set {
self.storage.jwksEndpoint = newValue
self.storage.jwks = .init(uri: newValue)
}
}

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

private final class Storage: Sendable {
private struct SendableBox: Sendable {
var applicationIdentifier: String?
var jwks: EndpointCache<JWKS>
var jwksEndpoint: URI
var applicationIdentifier: String? = nil
}

let jwks: EndpointCache<JWKS>
private let sendableBox: NIOLockedValueBox<SendableBox>

var jwks: EndpointCache<JWKS> {
get {
self.sendableBox.withLockedValue { box in
box.jwks
}
}
set {
self.sendableBox.withLockedValue { box in
box.jwks = newValue
}
}
}

var applicationIdentifier: String? {
get {
Expand All @@ -89,10 +113,26 @@ public extension Application.JWT {
}
}
}

var jwksEndpoint: URI {
get {
self.sendableBox.withLockedValue { box in
box.jwksEndpoint
}
}
set {
self.sendableBox.withLockedValue { box in
box.jwksEndpoint = newValue
}
}
}

init() {
self.jwks = .init(uri: "https://login.microsoftonline.com/common/discovery/keys")
let box = SendableBox(applicationIdentifier: nil)
let jwksEndpoint: URI = "https://login.microsoftonline.com/common/discovery/keys"
let box = SendableBox(
jwks: .init(uri: jwksEndpoint),
jwksEndpoint: jwksEndpoint
)
self.sendableBox = .init(box)
}
}
Expand Down
38 changes: 33 additions & 5 deletions Tests/JWTTests/JWTTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,16 @@ import XCTVapor

class JWTTests: XCTestCase {
var app: Application!

override func setUp() async throws {
app = try await Application.make(.testing)
XCTAssert(isLoggingConfigured)
}

override func tearDown() async throws {
try await app.asyncShutdown()
}

func testDocs() async throws {
// Add HMAC with SHA-256 signer.
await app.jwt.keys.add(hmac: "secret", digestAlgorithm: .sha256)
Expand Down Expand Up @@ -116,7 +116,7 @@ class JWTTests: XCTestCase {
}, afterResponse: { res async in
XCTAssertEqual(res.status, .ok)
})

try await app.test(.POST, "login", beforeRequest: { req in
print(req)
}, afterResponse: { res async throws in
Expand Down Expand Up @@ -274,7 +274,7 @@ class JWTTests: XCTestCase {
try await app.test(.GET, "test", headers: headers) { res async in
XCTAssertEqual(res.status, .unauthorized)
}

try await app.test(.GET, "test2", headers: headers) { res async in
XCTAssertEqual(res.status, .unauthorized)
}
Expand Down Expand Up @@ -336,6 +336,34 @@ class JWTTests: XCTestCase {
}
}
}

func testMicrosoftEndpointSwitch() async throws {
await app.jwt.keys.add(hmac: "secret", digestAlgorithm: .sha256)

let testUser = TestUser(name: "foo")
let token = try await app.jwt.keys.sign(testUser)

app.jwt.microsoft.applicationIdentifier = ""
app.get("microsoft") { req async throws in
let token = try await req.jwt.microsoft.verify()
return token.name ?? "none"
}

try await app.test(.GET, "microsoft", headers: ["Authorization": "Bearer \(token)"]) { res async in
XCTAssertEqual(res.status, .unauthorized)
}

app.jwt.microsoft.jwksEndpoint = "https://login.microsoftonline.com/common/discovery/v2.0/keys"
try await app.test(.GET, "microsoft", headers: ["Authorization": "Bearer \(token)"]) { res async in
XCTAssertEqual(res.status, .unauthorized)
}

// Use a non-existent endpoint to show that endpoint switching works
app.jwt.microsoft.jwksEndpoint = "https://login.microsoftonline.com/nonexistent/endpoint"
try await app.test(.GET, "microsoft", headers: ["Authorization": "Bearer \(token)"]) { res async in
XCTAssertEqual(res.status, .internalServerError)
}
}
}

extension ByteBuffer {
Expand Down

0 comments on commit f240c5f

Please sign in to comment.