Skip to content

Commit

Permalink
Support JWKs with no alg (#34)
Browse files Browse the repository at this point in the history
* jwks without algorithm

* make jwksigner private

* add msft jwks test
  • Loading branch information
tanner0101 authored Jul 29, 2020
1 parent 8c9f67a commit ee44e77
Show file tree
Hide file tree
Showing 6 changed files with 193 additions and 85 deletions.
26 changes: 20 additions & 6 deletions Sources/JWTKit/Keys/JWK.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import struct Foundation.Data
import class Foundation.JSONDecoder

/// A JSON Web Key.
///
/// Read specification (RFC 7517) https://tools.ietf.org/html/rfc7517.
Expand Down Expand Up @@ -33,21 +36,28 @@ public struct JWK: Decodable {
case rs384
/// RSA with SHA512
case rs512

/// Decodes from a lowercased string.
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let value = try container.decode(String.self).lowercased()
switch value {

init?(string: String) {
switch string.lowercased() {
case "rs256":
self = .rs256
case "rs384":
self = .rs384
case "rs512":
self = .rs512
default:
return nil
}
}

/// Decodes from a lowercased string.
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let string = try container.decode(String.self)
guard let algorithm = Self(string: string) else {
throw JWTError.invalidJWK
}
self = algorithm
}
}

Expand Down Expand Up @@ -87,4 +97,8 @@ public struct JWK: Decodable {
case exponent = "e"
case privateExponent = "d"
}

public init(json: String) throws {
self = try JSONDecoder().decode(JWK.self, from: Data(json.utf8))
}
}
42 changes: 42 additions & 0 deletions Sources/JWTKit/Signing/JWKSigner.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import Foundation

struct JWKSigner {
let jwk: JWK

init(jwk: JWK) {
self.jwk = jwk
}

func signer(for algorithm: JWK.Algorithm? = nil) -> JWTSigner? {
switch self.jwk.keyType {
case .rsa:
guard let modulus = self.jwk.modulus else {
return nil
}
guard let exponent = self.jwk.exponent else {
return nil
}

guard let rsaKey = RSAKey(
modulus: modulus,
exponent: exponent,
privateExponent: self.jwk.privateExponent
) else {
return nil
}

guard let algorithm = algorithm ?? self.jwk.algorithm else {
return nil
}

switch algorithm {
case .rs256:
return JWTSigner.rs256(key: rsaKey)
case .rs384:
return JWTSigner.rs384(key: rsaKey)
case .rs512:
return JWTSigner.rs512(key: rsaKey)
}
}
}
}
64 changes: 0 additions & 64 deletions Sources/JWTKit/Signing/JWTSigner+JWK.swift

This file was deleted.

3 changes: 2 additions & 1 deletion Sources/JWTKit/Signing/JWTSigner.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@ public final class JWTSigner {

public func sign<Payload>(
_ payload: Payload,
kid: JWKIdentifier? = nil,
cty: String? = nil
) throws -> String
where Payload: JWTPayload
{
try JWTSerializer().sign(payload, using: self, kid: nil, cty: cty)
try JWTSerializer().sign(payload, using: self, kid: kid, cty: cty)
}

public func unverified<Payload>(
Expand Down
44 changes: 36 additions & 8 deletions Sources/JWTKit/Signing/JWTSigners.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ import struct Foundation.Data
/// A collection of signers labeled by `kid`.
public final class JWTSigners {
/// Internal storage.
private var storage: [JWKIdentifier: JWTSigner]
private var jwtStorage: [JWKIdentifier: JWTSigner]
private var jwkStorage: [JWKIdentifier: JWKSigner]

private var `default`: JWTSigner?

/// Create a new `JWTSigners`.
public init() {
self.storage = [:]
self.jwtStorage = [:]
self.jwkStorage = [:]
}

/// Adds a new signer.
Expand All @@ -21,24 +23,50 @@ public final class JWTSigners {
isDefault: Bool? = nil
) {
if let kid = kid {
self.storage[kid] = signer
self.jwtStorage[kid] = signer
}
if self.default == nil && isDefault != false {
self.default = signer
}
}

/// Adds a `JWKS` (JSON Web Key Set) to this signers collection
/// by first decoding the JSON string.
public func use(jwksJSON json: String) throws {
let jwks = try JSONDecoder().decode(JWKS.self, from: Data(json.utf8))
try self.use(jwks: jwks)
}

/// Adds a `JWKS` (JSON Web Key Set) to this signers collection.
public func use(jwks: JWKS) throws {
try jwks.keys.forEach { try self.use(jwk: $0) }
}

/// Adds a `JWK` (JSON Web Key) to this signers collection.
public func use(jwk: JWK) throws {
guard let kid = jwk.keyIdentifier else {
throw JWTError.invalidJWK
}
self.jwkStorage[kid] = JWKSigner(jwk: jwk)
}

/// Gets a signer for the supplied `kid`, if one exists.
public func get(kid: JWKIdentifier? = nil) -> JWTSigner? {
public func get(kid: JWKIdentifier? = nil, alg: String? = nil) -> JWTSigner? {
if let kid = kid {
return self.storage[kid]
if let jwt = self.jwtStorage[kid] {
return jwt
} else if let jwk = self.jwkStorage[kid] {
return jwk.signer(for: alg.flatMap(JWK.Algorithm.init))
} else {
return nil
}
} else {
return self.default
}
}

public func require(kid: JWKIdentifier? = nil) throws -> JWTSigner {
guard let signer = self.get(kid: kid) else {
public func require(kid: JWKIdentifier? = nil, alg: String? = nil) throws -> JWTSigner {
guard let signer = self.get(kid: kid, alg: alg) else {
if let kid = kid {
throw JWTError.unknownKID(kid)
} else {
Expand Down Expand Up @@ -84,7 +112,7 @@ public final class JWTSigners {
{
let parser = try JWTParser(token: token)
let header = try parser.header()
return try self.require(kid: header.kid).verify(parser: parser)
return try self.require(kid: header.kid, alg: header.alg).verify(parser: parser)
}

public func sign<Payload>(
Expand Down
99 changes: 93 additions & 6 deletions Tests/JWTKitTests/JWTKitTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ class JWTKitTests: XCTestCase {
let exp = ExpirationClaim(value: Date(timeIntervalSince1970: 2_000_000_000))
let jwt = try JWTSigner.hs256(key: "secret".bytes)
.sign(ExpirationPayload(exp: exp))
var parser = try JWTParser(token: jwt.bytes)
let parser = try JWTParser(token: jwt.bytes)
try XCTAssertEqual(parser.header().typ, "JWT")
try XCTAssertEqual(parser.header().alg, "HS256")
try XCTAssertEqual(parser.payload(as: ExpirationPayload.self).exp, exp)
Expand Down Expand Up @@ -194,20 +194,23 @@ class JWTKitTests: XCTestCase {
}
"""

let privateSigner = try JWTSigner.jwk(json: privateKey)
let publicSigner = try JWTSigner.jwk(json: publicKey)
let publicSigners = JWTSigners()
try publicSigners.use(jwk: .init(json: publicKey))

let privateSigners = JWTSigners()
try privateSigners.use(jwk: .init(json: privateKey))

let payload = TestPayload(
sub: "vapor",
name: "Foo",
admin: false,
exp: .init(value: .init(timeIntervalSince1970: 2_000_000_000))
)
let data = try privateSigner.sign(payload)
let data = try privateSigners.sign(payload, kid: "1234")
// test private signer decoding
try XCTAssertEqual(privateSigner.verify(data, as: TestPayload.self), payload)
try XCTAssertEqual(privateSigners.verify(data, as: TestPayload.self), payload)
// test public signer decoding
try XCTAssertEqual(publicSigner.verify(data, as: TestPayload.self), payload)
try XCTAssertEqual(publicSigners.verify(data, as: TestPayload.self), payload)
}

func testJWKS() throws {
Expand Down Expand Up @@ -332,6 +335,50 @@ class JWTKitTests: XCTestCase {
XCTAssertEqual(name, "aud")
}
}

func testAlgorithmInJWTHeaderOnly() throws {
// rsa key
let modulus = "mSfWGBcXRBPgnwnL_ymDCkBaL6vcMcLpBEomzf-wZPajcQFiq4n4MHScyo85Te6GU-YuErVvHKK0D72JhMNWAQXbiF5Hh7swSYX9QsycWwHBgOBNfp51Fm_HTU7ikDBEdSonrmSep8wNqi_PX2_jVBsoxYNeiCQyDLFLHOAAcbIE4Y6lpJy76GpdHJscMO2RsUznjv5VPOQVa_BlQRIIZ0YoSsq9EEZna9O370wZy8jnOthQIXoegQ7sItS1JMKk4X5DdoRenIfbfWLy88XxKOPlIHA5ekT8TyzeI2Uqkg3YMETTDPrSROVO1Qdl2W1uMdfIZ94DgKpZN2VW-w0fLw"
let exponent = "AQAB"
let privateExponent = "awDmF9aqLqokmXjiydda8mKboArWwP2Ih7K3Ad3Og_u9nUp2gZrXiCMxGGSQiN5Jg3yiW_ffNYaHfyfRWKyQ_g31n4UfPLmPtw6iL3V9GChV5ZDRE9HpxE88U8r1h__xFFrrdnBeWKW8NldI70jg7vY6uiRae4uuXCfSbs4iAUxmRVKWCnV7JE6sObQKUV_EJkBcyND5Y97xsmWD0nPmXCnloQ84gF-eTErJoZBvQhJ4BhmBeUlREHmDKssaxVOCK4l335DKHD1vbuPk9e49M71BK7r2y4Atqk3TEetnwzMs3u-L9RqHaGIBw5u324uGweY7QeD7HFdAUtpjOq_MQQ"

// sign jwt
let privateSigner = JWTSigner.rs256(key: RSAKey(
modulus: modulus,
exponent: exponent,
privateExponent: privateExponent
)!)
struct Foo: JWTPayload {
var bar: Int
func verify(using signer: JWTSigner) throws { }
}
let jwt = try privateSigner.sign(Foo(bar: 42), kid: "vapor")

// verify using jwks without alg
let jwksString = """
{
"keys": [
{
"kty": "RSA",
"use": "sig",
"kid": "vapor",
"n": "\(modulus)",
"e": "\(exponent)"
}
]
}
"""

let signers = JWTSigners()
try signers.use(jwksJSON: jwksString)
let foo = try signers.verify(jwt, as: Foo.self)
XCTAssertEqual(foo.bar, 42)
}

func testMicrosoftJWKs() throws {
let signers = JWTSigners()
try signers.use(jwksJSON: microsoftJWKS)
}
}

struct AudiencePayload: Codable {
Expand Down Expand Up @@ -379,6 +426,46 @@ struct ExpirationPayload: JWTPayload {
}
}

let microsoftJWKS = """
{
"keys":[
{
"kty":"RSA",
"use":"sig",
"kid":"huN95IvPfehq34GzBDZ1GXGirnM",
"x5t":"huN95IvPfehq34GzBDZ1GXGirnM",
"n":"6lldKm5Rc_vMKa1RM_TtUv3tmtj52wLRrJqu13yGM3_h0dwru2ZP53y65wDfz6_tLCjoYuRCuVsjoW37-0zXUORJvZ0L90CAX-58lW7NcE4bAzA1pXv7oR9kQw0X8dp0atU4HnHeaTU8LZxcjJO79_H9cxgwa-clKfGxllcos8TsuurM8xi2dx5VqwzqNMB2s62l3MTN7AzctHUiQCiX2iJArGjAhs-mxS1wmyMIyOSipdodhjQWRAcseW-aFVyRTFVi8okl2cT1HJjPXdx0b1WqYSOzeRdrrLUcA0oR2Tzp7xzOYJZSGNnNLQqa9f6h6h52XbX0iAgxKgEDlRpbJw",
"e":"AQAB",
"x5c":[
"MIIDBTCCAe2gAwIBAgIQPCxFbySVSLZOggeWRzBWOjANBgkqhkiG9w0BAQsFADAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MB4XDTIwMDYwNzAwMDAwMFoXDTI1MDYwNzAwMDAwMFowLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOpZXSpuUXP7zCmtUTP07VL97ZrY+dsC0ayartd8hjN/4dHcK7tmT+d8uucA38+v7Swo6GLkQrlbI6Ft+/tM11DkSb2dC/dAgF/ufJVuzXBOGwMwNaV7+6EfZEMNF/HadGrVOB5x3mk1PC2cXIyTu/fx/XMYMGvnJSnxsZZXKLPE7LrqzPMYtnceVasM6jTAdrOtpdzEzewM3LR1IkAol9oiQKxowIbPpsUtcJsjCMjkoqXaHYY0FkQHLHlvmhVckUxVYvKJJdnE9RyYz13cdG9VqmEjs3kXa6y1HANKEdk86e8czmCWUhjZzS0KmvX+oeoedl219IgIMSoBA5UaWycCAwEAAaMhMB8wHQYDVR0OBBYEFFXP0ODFhjf3RS6oRijM5Tb+yB8CMA0GCSqGSIb3DQEBCwUAA4IBAQB9GtVikLTbJWIu5x9YCUTTKzNhi44XXogP/v8VylRSUHI5YTMdnWwvDIt/Y1sjNonmSy9PrioEjcIiI1U8nicveafMwIq5VLn+gEY2lg6KDJAzgAvA88CXqwfHHvtmYBovN7goolp8TY/kddMTf6TpNzN3lCTM2MK4Ye5xLLVGdp4bqWCOJ/qjwDxpTRSydYIkLUDwqNjv+sYfOElJpYAB4rTL/aw3ChJ1iaA4MtXEt6OjbUtbOa21lShfLzvNRbYK3+ukbrhmRl9lemJEeUls51vPuIe+jg+Ssp43aw7PQjxt4/MpfNMS2BfZ5F8GVSVG7qNb352cLLeJg5rc398Z"
]
},
{
"kty":"RSA",
"use":"sig",
"kid":"jibNbkFSSbmxPYrN9CFqRk4K4gw",
"x5t":"jibNbkFSSbmxPYrN9CFqRk4K4gw",
"n":"2YX-YDuuTzPiaiZKt04IuUzAjCjPLLmBCVA6npKuZyIouMuaSEuM7BP8QctfCprUY16Rq2-KDrAEvaaKJvsD5ZONddt79yFdCs1E8wKlYIPO74fSpePdVDizflr5W-QCFH9tokbZrHBBuluFojgtbvPMXAhHfZTGC4ItZ0i_Lc9eXwtENHJQC4e4m7olweK1ExM-OzsKGzDlOsOUOU5pN2sHY74nXPqQRH1dQKfB0NT0YrfkbnR8fiq8z-soixfECUXkF8FzWnMnqL6X90wngnuIi8OtH2mvDcnsvUVh3K2JgvSgjRWZbsDx6G-mVQL2vEuHXMXoIoe8hd1ZpV16pQ",
"e":"AQAB",
"x5c":[
"MIIDBTCCAe2gAwIBAgIQUUG7iptQUoVA7bYvX2tHlDANBgkqhkiG9w0BAQsFADAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MB4XDTIwMDcxODAwMDAwMFoXDTI1MDcxODAwMDAwMFowLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANmF/mA7rk8z4momSrdOCLlMwIwozyy5gQlQOp6SrmciKLjLmkhLjOwT/EHLXwqa1GNekatvig6wBL2miib7A+WTjXXbe/chXQrNRPMCpWCDzu+H0qXj3VQ4s35a+VvkAhR/baJG2axwQbpbhaI4LW7zzFwIR32UxguCLWdIvy3PXl8LRDRyUAuHuJu6JcHitRMTPjs7Chsw5TrDlDlOaTdrB2O+J1z6kER9XUCnwdDU9GK35G50fH4qvM/rKIsXxAlF5BfBc1pzJ6i+l/dMJ4J7iIvDrR9prw3J7L1FYdytiYL0oI0VmW7A8ehvplUC9rxLh1zF6CKHvIXdWaVdeqUCAwEAAaMhMB8wHQYDVR0OBBYEFFOUEOWLUJOTFTOlr7P+6GxsmM90MA0GCSqGSIb3DQEBCwUAA4IBAQCP+LLZw7SSYnWQmRGWHmksBwwJ4Gy32C6g7+wZZv3ombHW9mwLQuzsir97/PP042i/ZIxePHJavpeLm/z3KMSpGIPmiPtmgNcK4HtLTEDnoTprnllobOAqU0TREFWogjkockNo98AvpsmHxNMXuwDikto9o/d9ACBtpkpatS2xgVOZxZtqyMpwZzSJARD5A4qcKov4zdqntVyjpZGK4N6ZaedRbEVd12m1VI+dtDB9+EJRqtTn8zamPYljVTEPNCbDAFgKBDtrhwBnrrrnKTq4/LEOouNQZuUucBTMOGDn4FEejNh3qbxNdWR6tSZbXUnJ+NIQ99IqZMvvMqm9ndL7"
]
},
{
"kty":"RSA",
"use":"sig",
"kid":"M6pX7RHoraLsprfJeRCjSxuURhc",
"x5t":"M6pX7RHoraLsprfJeRCjSxuURhc",
"n":"xHScZMPo8FifoDcrgncWQ7mGJtiKhrsho0-uFPXg-OdnRKYudTD7-Bq1MDjcqWRf3IfDVjFJixQS61M7wm9wALDj--lLuJJ9jDUAWTA3xWvQLbiBM-gqU0sj4mc2lWm6nPfqlyYeWtQcSC0sYkLlayNgX4noKDaXivhVOp7bwGXq77MRzeL4-9qrRYKjuzHfZL7kNBCsqO185P0NI2Jtmw-EsqYsrCaHsfNRGRrTvUHUq3hWa859kK_5uNd7TeY2ZEwKVD8ezCmSfR59ZzyxTtuPpkCSHS9OtUvS3mqTYit73qcvprjl3R8hpjXLb8oftfpWr3hFRdpxrwuoQEO4QQ",
"e":"AQAB",
"x5c":[
"MIIC8TCCAdmgAwIBAgIQfEWlTVc1uINEc9RBi6qHMjANBgkqhkiG9w0BAQsFADAjMSEwHwYDVQQDExhsb2dpbi5taWNyb3NvZnRvbmxpbmUudXMwHhcNMTgxMDE0MDAwMDAwWhcNMjAxMDE0MDAwMDAwWjAjMSEwHwYDVQQDExhsb2dpbi5taWNyb3NvZnRvbmxpbmUudXMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDEdJxkw+jwWJ+gNyuCdxZDuYYm2IqGuyGjT64U9eD452dEpi51MPv4GrUwONypZF/ch8NWMUmLFBLrUzvCb3AAsOP76Uu4kn2MNQBZMDfFa9AtuIEz6CpTSyPiZzaVabqc9+qXJh5a1BxILSxiQuVrI2BfiegoNpeK+FU6ntvAZervsxHN4vj72qtFgqO7Md9kvuQ0EKyo7Xzk/Q0jYm2bD4SypiysJoex81EZGtO9QdSreFZrzn2Qr/m413tN5jZkTApUPx7MKZJ9Hn1nPLFO24+mQJIdL061S9LeapNiK3vepy+muOXdHyGmNctvyh+1+laveEVF2nGvC6hAQ7hBAgMBAAGjITAfMB0GA1UdDgQWBBQ5TKadw06O0cvXrQbXW0Nb3M3h/DANBgkqhkiG9w0BAQsFAAOCAQEAI48JaFtwOFcYS/3pfS5+7cINrafXAKTL+/+he4q+RMx4TCu/L1dl9zS5W1BeJNO2GUznfI+b5KndrxdlB6qJIDf6TRHh6EqfA18oJP5NOiKhU4pgkF2UMUw4kjxaZ5fQrSoD9omjfHAFNjradnHA7GOAoF4iotvXDWDBWx9K4XNZHWvD11Td66zTg5IaEQDIZ+f8WS6nn/98nAVMDtR9zW7Te5h9kGJGfe6WiHVaGRPpBvqC4iypGHjbRwANwofZvmp5wP08hY1CsnKY5tfP+E2k/iAQgKKa6QoxXToYvP7rsSkglak8N5g/+FJGnq4wP6cOzgZpjdPMwaVt5432GA=="
]
}
]
}
"""

let rsaModulus = """
gWu7yhI35FScdKARYboJoAm-T7yJfJ9JTvAok_RKOJYcL8oLIRSeLqQX83PPZiWdKTdXaiGWntpDu6vW7VAb-HWPF6tNYSLKDSmR3sEu2488ibWijZtNTCKOSb_1iAKAI5BJ80LTqyQtqaKzT0XUBtMsde8vX1nKI05UxujfTX3kqUtkZgLv1Yk1ZDpUoLOWUTtCm68zpjtBrPiN8bU2jqCGFyMyyXys31xFRzz4MyJ5tREHkQCzx0g7AvW0ge_sBTPQ2U6NSkcZvQyDbfDv27cMUHij1Sjx16SY9a2naTuOgamjtUzyClPLVpchX-McNyS0tjdxWY_yRL9MYuw4AQ
"""
Expand Down

0 comments on commit ee44e77

Please sign in to comment.