Compare commits

...

9 Commits

Author SHA1 Message Date
Ralph Kuepper 5257875a5b Beta 2 Adjustments 2019-12-10 05:30:15 -05:00
Ralph Kuepper d7db87fcd8 version fix 2019-12-06 14:34:31 -05:00
Caleb Kleveter 079814fe35
Gave JWTProvider initializer 'header' paramater a default value of nil 2019-11-06 15:32:23 -06:00
Caleb Kleveter c852499d8a
Changed JWTKit referance to the master branch 2019-11-06 15:29:31 -06:00
Caleb Kleveter 63a01c6c5f
Allow passing in SHA algorithm to JWTProvider.Algorithm enum cases 2019-11-06 11:59:21 -06:00
Caleb Kleveter b1ab5bae59
Added .swiftpm/ directory to .gitignore 2019-11-06 09:37:40 -06:00
Caleb Kleveter 22e244a5e6
Updated for Vapor 4 and JWTKit 2019-11-06 08:17:30 -06:00
Caleb Kleveter 36ca47cd4a
Merge branch 'master' into develop 2019-11-05 16:41:02 -06:00
Caleb Kleveter 2d241ee1c7
Updated swift tools version to 5.0 2019-05-16 08:11:58 -05:00
21 changed files with 455 additions and 464 deletions

1
.gitignore vendored
View File

@ -2,3 +2,4 @@
/.build /.build
/Packages /Packages
/*.xcodeproj /*.xcodeproj
.swiftpm/

View File

@ -2,129 +2,75 @@
"object": { "object": {
"pins": [ "pins": [
{ {
"package": "Auth", "package": "async-http-client",
"repositoryURL": "https://github.com/vapor/auth.git", "repositoryURL": "https://github.com/swift-server/async-http-client.git",
"state": { "state": {
"branch": null, "branch": null,
"revision": "e6f61b403727ec124214beb3e57deff579f31d00", "revision": "51dc885a30ca704b02fa803099b0a9b5b38067b6",
"version": "2.0.4" "version": "1.0.0"
} }
}, },
{ {
"package": "Console", "package": "async-kit",
"repositoryURL": "https://github.com/vapor/console.git", "repositoryURL": "https://github.com/vapor/async-kit.git",
"state": { "state": {
"branch": null, "branch": null,
"revision": "74cfbea629d4aac34a97cead2447a6870af1950b", "revision": "d9fd2be441af6d1428b62ab694848396e7072a14",
"version": "3.1.1" "version": "1.0.0-beta.1"
} }
}, },
{ {
"package": "Core", "package": "console-kit",
"repositoryURL": "https://github.com/vapor/core.git", "repositoryURL": "https://github.com/vapor/console-kit.git",
"state": { "state": {
"branch": null, "branch": null,
"revision": "c64f63cb21631010952f7abfef719d376ab6a441", "revision": "5b91c2dc93781e4b36cb4c667972670eac90e6e7",
"version": "3.9.1" "version": "4.0.0-beta.1"
} }
}, },
{ {
"package": "Crypto", "package": "jwt-kit",
"repositoryURL": "https://github.com/vapor/crypto.git", "repositoryURL": "https://github.com/vapor/jwt-kit.git",
"state": { "state": {
"branch": null, "branch": null,
"revision": "df8eb7d8ae51787b3a0628aa3975e67666da936c", "revision": "0c7e52ab75ddfabad539a4a2a2f9fc003e7700b7",
"version": "3.3.3" "version": null
} }
}, },
{ {
"package": "DatabaseKit", "package": "multipart-kit",
"repositoryURL": "https://github.com/vapor/database-kit.git", "repositoryURL": "https://github.com/vapor/multipart-kit.git",
"state": { "state": {
"branch": null, "branch": null,
"revision": "8f352c8e66dab301ab9bfef912a01ce1361ba1e4", "revision": "a941d7a1d685c83df09077f6190808ff2a7f4dce",
"version": "1.3.3" "version": "4.0.0-beta.1"
} }
}, },
{ {
"package": "Fluent", "package": "open-crypto",
"repositoryURL": "https://github.com/vapor/fluent.git", "repositoryURL": "https://github.com/vapor/open-crypto.git",
"state": { "state": {
"branch": null, "branch": null,
"revision": "b915c321c6f9e83743ee5efa35a30895e1b02e51", "revision": "06d26edb8e28295bb7103b4f950d5ea58d634c1b",
"version": "3.2.0" "version": "4.0.0-alpha.2"
} }
}, },
{ {
"package": "FluentSQLite", "package": "routing-kit",
"repositoryURL": "https://github.com/vapor/fluent-sqlite.git", "repositoryURL": "https://github.com/vapor/routing-kit.git",
"state": { "state": {
"branch": null, "branch": null,
"revision": "c32f5bda84bf4ea691d19afe183d40044f579e11", "revision": "6c7f4b471f9662d05045d82e64e22d5572a16a82",
"version": "3.0.0" "version": "4.0.0-alpha.1"
} }
}, },
{ {
"package": "HTTP", "package": "swift-log",
"repositoryURL": "https://github.com/vapor/http.git", "repositoryURL": "https://github.com/apple/swift-log.git",
"state": { "state": {
"branch": null, "branch": null,
"revision": "3808ed0401379b6e9f4a053f03090ea9d658caa9", "revision": "74d7b91ceebc85daf387ebb206003f78813f71aa",
"version": "3.2.1" "version": "1.2.0"
}
},
{
"package": "JWT",
"repositoryURL": "https://github.com/vapor/jwt.git",
"state": {
"branch": null,
"revision": "2e225c722bf26407c1c4bd11d341e48759f46095",
"version": "3.0.0"
}
},
{
"package": "Multipart",
"repositoryURL": "https://github.com/vapor/multipart.git",
"state": {
"branch": null,
"revision": "f919a01c4d10a281d6236a21b0b1d1759a72b8eb",
"version": "3.0.4"
}
},
{
"package": "Routing",
"repositoryURL": "https://github.com/vapor/routing.git",
"state": {
"branch": null,
"revision": "626190ddd2bd9f967743b60ba6adaf90bbd2651c",
"version": "3.0.2"
}
},
{
"package": "Service",
"repositoryURL": "https://github.com/vapor/service.git",
"state": {
"branch": null,
"revision": "fa5b5de62bd68bcde9a69933f31319e46c7275fb",
"version": "1.0.2"
}
},
{
"package": "SQL",
"repositoryURL": "https://github.com/vapor/sql.git",
"state": {
"branch": null,
"revision": "50eaeb8f52a1ce63f1ff3880e1114dd8757a78a6",
"version": "2.3.2"
}
},
{
"package": "SQLite",
"repositoryURL": "https://github.com/vapor/sqlite.git",
"state": {
"branch": null,
"revision": "314d9cd21165bcf14215e336a23ff8214f40e411",
"version": "3.2.1"
} }
}, },
{ {
@ -132,8 +78,26 @@
"repositoryURL": "https://github.com/apple/swift-nio.git", "repositoryURL": "https://github.com/apple/swift-nio.git",
"state": { "state": {
"branch": null, "branch": null,
"revision": "ba7970fe396e8198b84c6c1b44b38a1d4e2eb6bd", "revision": "ff01888051cd7efceb1bf8319c1dd3986c4bf6fc",
"version": "1.14.1" "version": "2.10.1"
}
},
{
"package": "swift-nio-extras",
"repositoryURL": "https://github.com/apple/swift-nio-extras.git",
"state": {
"branch": null,
"revision": "53808818c2015c45247cad74dc05c7a032c96a2f",
"version": "1.3.2"
}
},
{
"package": "swift-nio-http2",
"repositoryURL": "https://github.com/apple/swift-nio-http2.git",
"state": {
"branch": null,
"revision": "867259332c45d5405efed844f7b3997ebfa94167",
"version": "1.7.2"
} }
}, },
{ {
@ -141,71 +105,26 @@
"repositoryURL": "https://github.com/apple/swift-nio-ssl.git", "repositoryURL": "https://github.com/apple/swift-nio-ssl.git",
"state": { "state": {
"branch": null, "branch": null,
"revision": "0f3999f3e3c359cc74480c292644c3419e44a12f", "revision": "ccf96bbe65ecc7c1558ab0dba7ffabdea5c1d31f",
"version": "1.4.0" "version": "2.4.4"
} }
}, },
{ {
"package": "swift-nio-ssl-support", "package": "vapor",
"repositoryURL": "https://github.com/apple/swift-nio-ssl-support.git",
"state": {
"branch": null,
"revision": "c02eec4e0e6d351cd092938cf44195a8e669f555",
"version": "1.0.0"
}
},
{
"package": "swift-nio-zlib-support",
"repositoryURL": "https://github.com/apple/swift-nio-zlib-support.git",
"state": {
"branch": null,
"revision": "37760e9a52030bb9011972c5213c3350fa9d41fd",
"version": "1.0.0"
}
},
{
"package": "TemplateKit",
"repositoryURL": "https://github.com/vapor/template-kit.git",
"state": {
"branch": null,
"revision": "51405c83e95e8adb09565278a5e9b959c605e56c",
"version": "1.4.0"
}
},
{
"package": "URLEncodedForm",
"repositoryURL": "https://github.com/vapor/url-encoded-form.git",
"state": {
"branch": null,
"revision": "82d8d63bdb76b6dd8febe916c639ab8608dbbaed",
"version": "1.0.6"
}
},
{
"package": "Validation",
"repositoryURL": "https://github.com/vapor/validation.git",
"state": {
"branch": null,
"revision": "4de213cf319b694e4ce19e5339592601d4dd3ff6",
"version": "2.1.1"
}
},
{
"package": "Vapor",
"repositoryURL": "https://github.com/vapor/vapor.git", "repositoryURL": "https://github.com/vapor/vapor.git",
"state": { "state": {
"branch": null, "branch": null,
"revision": "c86ada59b31c69f08a6abd4f776537cba48d5df6", "revision": "3d8bbd5eb2b556debbb51da8ac7357f34b7b0a8e",
"version": "3.3.0" "version": "4.0.0-beta.1"
} }
}, },
{ {
"package": "WebSocket", "package": "websocket-kit",
"repositoryURL": "https://github.com/vapor/websocket.git", "repositoryURL": "https://github.com/vapor/websocket-kit.git",
"state": { "state": {
"branch": null, "branch": null,
"revision": "d85e5b6dce4d04065865f77385fc3324f98178f6", "revision": "66c0ea58398f055b5a0d92b0d5f4c32ef0c02eeb",
"version": "1.1.2" "version": "2.0.0-beta.1"
} }
} }
] ]

View File

@ -1,17 +1,22 @@
// swift-tools-version:4.0 // swift-tools-version:5.0
import PackageDescription import PackageDescription
let package = Package( let package = Package(
name: "JWTVapor", name: "JWTVapor",
platforms: [
.macOS(.v10_14)
],
products: [ products: [
.library(name: "JWTVapor", targets: ["JWTVapor"]), .library(name: "JWTVapor", targets: ["JWTVapor"]),
], ],
dependencies: [ dependencies: [
.package(url: "https://github.com/vapor/vapor.git", from: "3.3.0"), .package(url: "https://github.com/vapor/vapor.git", from: "4.0.0-beta"),
.package(url: "https://github.com/vapor/jwt.git", from: "3.0.0"), .package(url: "https://github.com/vapor/jwt-kit.git", .revision("0c7e52ab75ddfabad539a4a2a2f9fc003e7700b7")),
.package(url: "https://github.com/vapor/auth.git", from: "2.0.4") // .package(url: "https://github.com/vapor/auth.git", .branch("master"))
], ],
targets: [ targets: [
.target(name: "JWTVapor", dependencies: ["Vapor", "Authentication", "JWT"]), .target(name: "JWTVapor", dependencies: ["Vapor", "JWTKit"]),
.testTarget(name: "JWTVaporTests", dependencies: ["JWTVapor"]), .testTarget(name: "JWTVaporTests", dependencies: ["JWTVapor"]),
] ]
) )

View File

@ -0,0 +1,71 @@
//
// File.swift
//
//
// Created by Ralph Küpper on 12/10/19.
//
import Vapor
@_exported import JWTKit
class JWTConfig {
var algorithm: JWTAlgorithm
var header: JWTHeader?
var secretVar: String
var publicVar: String
public init(algorithm: JWTAlgorithm, header: JWTHeader? = nil, secretVar: String? = nil, publicVar: String? = nil) {
self.algorithm = algorithm
self.header = header
self.secretVar = secretVar ?? "JWT_SECRET"
self.publicVar = publicVar ?? "JWT_PUBLIC"
}
}
extension Application {
private struct JWTServiceKey: StorageKey {
typealias Value = JWTService
}
private struct JWTConfigKey: StorageKey {
typealias Value = JWTConfig
}
var jwtConfig: JWTConfig {
if let existing = self.storage[JWTConfigKey.self] {
return existing
} else {
let config = JWTConfig(algorithm: .rsa(.sha256))
self.storage[JWTConfigKey.self] = config
return config
}
}
var jwtService: JWTService {
if let existing = self.storage[JWTServiceKey.self] {
return existing
} else {
var new:JWTService
guard let publicKey = Environment.get(self.jwtConfig.publicVar) else {
print("No value was found at the given public key environmen '\(self.jwtConfig.publicVar)'")
exit(0)
}
switch self.jwtConfig.algorithm {
case let .rsa(alorithm): new = RSAService(n: publicKey, e: "AQAB", d: Environment.get(self.jwtConfig.secretVar), header: self.jwtConfig.header, algorithm: alorithm)
case let .hmac(alorithm): new = HMACService(key: publicKey, header: self.jwtConfig.header, algorithm: alorithm)
case let .cert(alorithm): new = CertService(certificate: Data(publicKey.utf8), header: self.jwtConfig.header, algorithm: alorithm)
case let .custom(closure): new = closure(self.jwtConfig.header, publicKey, Environment.get(self.jwtConfig.secretVar))
}
self.storage[JWTServiceKey.self] = new
return new
}
}
}

View File

@ -1,19 +1,23 @@
import Foundation import Foundation
import Crypto import JWTKit
import JWT
public final class CertService: JWTService { public final class CertService: JWTService {
public var signer: JWTSigner public var signer: JWTSigner
public var header: JWTHeader? public var header: JWTHeader?
public init(certificate: String, header: JWTHeader? = nil, algorithm: DigestAlgorithm = .sha256)throws { public init(certificate: Data, header: JWTHeader? = nil, algorithm: DigestAlgorithm = .sha256) {
let key = try RSAKey.public(certificate: certificate) do {
let key = try RSAKey.public(pem: certificate)
switch algorithm {
case .sha256: self.signer = JWTSigner.rs256(key: key) switch algorithm {
case .sha384: self.signer = JWTSigner.rs384(key: key) case .sha256: self.signer = JWTSigner.rs256(key: key)
case .sha512: self.signer = JWTSigner.rs512(key: key) case .sha384: self.signer = JWTSigner.rs384(key: key)
default: throw JWTProviderError(identifier: "badRSAAlgorithm", reason: "RSA signing requires SHA256, SHA384, or SHA512 algorithm", status: .internalServerError) case .sha512: self.signer = JWTSigner.rs512(key: key)
}
}
catch {
print("We cannot run this service without a key.")
exit(0)
} }
self.header = header self.header = header

View File

@ -1,17 +1,15 @@
import Foundation import Foundation
import Crypto import JWTKit
import JWT
public final class HMACService: JWTService { public final class HMACService: JWTService {
public let signer: JWTSigner public let signer: JWTSigner
public let header: JWTHeader? public let header: JWTHeader?
public init(key: String, header: JWTHeader? = nil, algorithm: DigestAlgorithm = .sha256)throws { public init(key: String, header: JWTHeader? = nil, algorithm: DigestAlgorithm = .sha256) {
switch algorithm { switch algorithm {
case .sha256: self.signer = JWTSigner.hs256(key: Data(key.utf8)) case .sha256: self.signer = JWTSigner.hs256(key: Data(key.utf8))
case .sha384: self.signer = JWTSigner.hs384(key: Data(key.utf8)) case .sha384: self.signer = JWTSigner.hs384(key: Data(key.utf8))
case .sha512: self.signer = JWTSigner.hs512(key: Data(key.utf8)) case .sha512: self.signer = JWTSigner.hs512(key: Data(key.utf8))
default: throw JWTProviderError(identifier: "badHMACAlgorithm", reason: "HMAC signing requires SHA256, SHA384, or SHA512 algorithm", status: .internalServerError)
} }
self.header = header self.header = header

View File

@ -1,4 +1,3 @@
import Foundation
import Vapor import Vapor
/// Configuration required by the `JWKSService`. /// Configuration required by the `JWKSService`.
@ -13,12 +12,12 @@ import Vapor
/// `JWKSConfig` is a `Service` which gets created by the required container and is made available /// `JWKSConfig` is a `Service` which gets created by the required container and is made available
/// to `JWKSService` in its `makeService` method. /// to `JWKSService` in its `makeService` method.
public struct JWKSConfig: Service { public struct JWKSConfig {
public let jwks: String public let jwks: URI
public let following: String public let following: String
public let password: String? public let password: String?
public init(jwks: String, following: String, password: String? = nil) { public init(jwks: URI, following: String, password: String? = nil) {
self.jwks = jwks self.jwks = jwks
self.following = following self.following = following
self.password = password self.password = password

View File

@ -4,19 +4,28 @@ import Vapor
/// Content (Codeable) model to read the response obtained after hitting the url specified in the /// Content (Codeable) model to read the response obtained after hitting the url specified in the
/// `jwks` property of `JWKSConfig`. /// `jwks` property of `JWKSConfig`.
public struct JWKSDocumentRequest: Content { public struct JWKSDocumentRequest: Content {
let jwksUrl: String let jwksUrl: URI
public init(from decoder: Decoder) throws { public init(from decoder: Decoder) throws {
// Get the container
let container = try decoder.container(keyedBy: JWKSDocumentCodingKey.self) let container = try decoder.container(keyedBy: JWKSDocumentCodingKey.self)
// Read the coding key passed to the decoder as part of userinfo // Read the coding key passed to the decoder as part of userinfo
guard let jwksUrlKey = decoder.userInfo[JWKSService.codingUserInfoKey] as? JWKSDocumentCodingKey else { guard let jwksUrlKey = decoder.userInfo[JWKSService.codingUserInfoKey] as? JWKSDocumentCodingKey else {
throw DecodingError.dataCorrupted(.init(codingPath: [], debugDescription: "Failed")) throw DecodingError.dataCorrupted(.init(codingPath: [], debugDescription: "Failed"))
} }
// get the jwks url from the json response. self.jwksUrl = try URI(string: container.decode(String.self, forKey: jwksUrlKey))
self.jwksUrl = try container.decode(String.self, forKey: jwksUrlKey) }
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: JWKSDocumentCodingKey.self)
// Read the coding key passed to the decoder as part of userinfo
guard let jwksUrlKey = encoder.userInfo[JWKSService.codingUserInfoKey] as? JWKSDocumentCodingKey else {
throw DecodingError.dataCorrupted(.init(codingPath: [], debugDescription: "Failed"))
}
try container.encode(self.jwksUrl.string, forKey: jwksUrlKey)
} }
} }

View File

@ -1,15 +1,10 @@
import Foundation import Foundation
import Vapor import Vapor
public struct JWKSService: ServiceType { public struct JWKSService {
static let factory: (Application) throws -> JWKSService = { _ in fatalError() }
/// config: `JWKSConfig` instance which contains all the configurations required by `JWKSService`. static let requestFactory: (Request) throws -> JWKSService = { _ in fatalError() }
private let config: JWKSConfig
/// The service container in which the Services are to be loaded.
private let container: Container
/// The client we need to pull the jwks.
private let client: Client
/// Create a key that gets passed as a key in the `JSONDecoder.userInfo` dictionary and its value is /// Create a key that gets passed as a key in the `JSONDecoder.userInfo` dictionary and its value is
/// value that the user passes in `JWKSConfig`'s `following`. The value passed in the `following` /// value that the user passes in `JWKSConfig`'s `following`. The value passed in the `following`
/// property is converted into a `CodingKey` type before passing in `JSONDecoder`'s userInfo. /// property is converted into a `CodingKey` type before passing in `JSONDecoder`'s userInfo.
@ -18,53 +13,53 @@ public struct JWKSService: ServiceType {
/// ///
/// [codingUserInfoKey: JWKSDocumentCodingKey(stringValue: self.config.following)] /// [codingUserInfoKey: JWKSDocumentCodingKey(stringValue: self.config.following)]
static let codingUserInfoKey = CodingUserInfoKey(rawValue: "jwksUrlKey")! static let codingUserInfoKey = CodingUserInfoKey(rawValue: "jwksUrlKey")!
public init(config: JWKSConfig, container: Container) {
/// config: `JWKSConfig` instance which contains all the configurations required by `JWKSService`.
private let config: JWKSConfig
/// The client we need to pull the jwks.
private let client: Client
public init(config: JWKSConfig, client: Client) {
self.config = config self.config = config
self.container = container self.client = client
self.client = try! container.make(Client.self)
} }
/// Finds and returns a RSAService that matches the provided `tid` from the list of JWKS. /// Finds and returns a RSAService that matches the provided `tid` from the list of JWKS.
/// The `tid` is obtained from the JWT Header of the incoming request, if the signing mechanism used is RSA. /// The `tid` is obtained from the JWT Header of the incoming request, if the signing mechanism used is RSA.
public func rsaService(forKey tid: String) throws -> Future<RSAService> { public func rsaService(forKey tid: String) throws -> EventLoopFuture<RSAService> {
return client.get(self.config.jwks).flatMap { response throws -> Future<JWKSDocumentRequest> in return self.client.get(self.config.jwks).flatMap { response -> EventLoopFuture<ClientResponse> in
/// Create a JSONDecoder in which the codingKey could be passed as part of userInfo.
let jsonDecoder = JSONDecoder() let jsonDecoder = JSONDecoder()
let jwksUrlKey = JWKSDocumentCodingKey(stringValue: self.config.following)! let jwksUrlKey = JWKSDocumentCodingKey(stringValue: self.config.following)!
jsonDecoder.userInfo = [JWKSService.codingUserInfoKey: jwksUrlKey] jsonDecoder.userInfo = [JWKSService.codingUserInfoKey: jwksUrlKey]
/// return the JSON response.
return try response.content.decode(json: JWKSDocumentRequest.self, using: jsonDecoder) do {
let request = try response.content.decode(JWKSDocumentRequest.self, using: jsonDecoder)
}.flatMap { jwksDocumentRequest throws -> Future<Response> in return self.client.get(request.jwksUrl)
/// Make a request and return the JWKS file. } catch let error {
return self.client.get(jwksDocumentRequest.jwksUrl) let eventLoop = self.client.eventLoopGroup.next()
return eventLoop.future(error: error)
}.flatMap { response throws -> Future<JWKSKeys> in }
}.flatMapThrowing { response throws -> RSAService in
/// Read the entire list of JWKS Keys. /// Read the entire list of JWKS Keys.
return try response.content.decode(JWKSKeys.self) let jwksKeys = try response.content.decode(JWKSKeys.self)
}.map { jwksKeys throws -> JWKSKey in
/// Search for JWKSKey that corresponds to the provided `tid` /// Search for JWKSKey that corresponds to the provided `tid`
guard let matchingJWKSKey = jwksKeys.keys.filter({ $0.kid == tid }).first else { guard let matchingJWKSKey = jwksKeys.keys.filter({ $0.kid == tid }).first else {
throw JWTProviderError(identifier: "invalidJWKSKeys", reason: "No matching key found in JWKS file", status: .internalServerError) throw JWTProviderError(identifier: "invalidJWKSKeys", reason: "No matching key found in JWKS file", status: .internalServerError)
} }
return matchingJWKSKey
return try RSAService(n: matchingJWKSKey.n, e: matchingJWKSKey.e, d: matchingJWKSKey.d)
}.map { jwksKey throws -> RSAService in
/// Create the RSAService using the JWKSKey.
return try RSAService(n: jwksKey.n, e: jwksKey.e, d: jwksKey.d)
} }
} }
public static func makeService(for worker: Container) throws -> JWKSService { // public static func makeService(for worker: Container) throws -> JWKSService {
// Load the required services. // // Load the required services.
let config = try worker.make(JWKSConfig.self) // let config = try worker.make(JWKSConfig.self)
return JWKSService(config: config, container: worker) // return JWKSService(config: config, container: worker)
} // }
} }

View File

@ -0,0 +1,16 @@
//
// File.swift
//
//
// Created by Ralph Küpper on 12/10/19.
//
import Vapor
public enum JWTAlgorithm {
case rsa(DigestAlgorithm = .sha256)
case hmac(DigestAlgorithm = .sha256)
case cert(DigestAlgorithm = .sha256)
case custom((JWTHeader?, String, String?) -> JWTService)
}

View File

@ -1,42 +1,47 @@
import Command import ConsoleKit
import Console import Foundation
public final class NewJWTCommand<Payload>: Command where Payload: JWTPayload { public final class NewJWTCommand<Payload>: Command where Payload: JWTPayload {
public var arguments: [CommandArgument] = [] public struct Signature: CommandSignature {
static var jsonHelp: String { """
The payload for the generated token.
If this flag is not use, the payload passed into the command's initializer is used.
""" }
@Option(name: "json", help: Signature.jsonHelp) var json: Data?
public init() { }
}
public var options: [CommandOption] = [ public let help: String = "Creates a JWT token for debugging purposes."
CommandOption.value(name: "json", help: [
"The payload for the generated token",
"If this flag is not use, the payload passed into the command's initializer is used"
])
]
public var help: [String] = ["Creates a JWT token for debugging purposes."]
internal let payloadCreator: () -> Payload internal let payloadCreator: () -> Payload
internal let signer: JWTService
public init(payload: Payload) { public init(payload: Payload, signer: JWTService) {
self.payloadCreator = { payload } self.payloadCreator = { payload }
self.signer = signer
} }
public init(_ handler: @escaping () -> Payload) { public init(signer: JWTService, _ handler: @escaping () -> Payload) {
self.payloadCreator = handler self.payloadCreator = handler
self.signer = signer
} }
public func run(using context: CommandContext) throws -> EventLoopFuture<Void> { public func run(using context: CommandContext, signature: Signature) throws {
let signer = try context.container.make(JWTService.self) let payload: Payload =
let payload: Payload try signature.json.map { try JSONDecoder().decode(Payload.self, from: $0) } ??
if let json = context.options["json"]?.data(using: .utf8) { self.payloadCreator()
payload = try JSONDecoder().decode(Payload.self, from: json)
} else {
payload = self.payloadCreator()
}
let token = try signer.sign(payload) let token = try signer.sign(payload)
context.console.output("JWT Token: ", style: .info, newLine: false) context.console.output("JWT Token: ", style: .info, newLine: false)
context.console.output(token, style: .plain, newLine: true) context.console.output(token, style: .plain, newLine: true)
}
return context.container.future() }
extension Data: LosslessStringConvertible {
public init?(_ description: String) {
self.init(description.utf8)
} }
} }

View File

@ -1,60 +0,0 @@
import Vapor
import Service
@_exported import JWT
public class JWTProvider: Vapor.Provider {
public static var repositoryName: String = "JWTProvider"
public let serviceBuilder: (String, String?)throws -> JWTService
public init (serviceBuilder: @escaping (String, String?)throws -> JWTService) {
self.serviceBuilder = serviceBuilder
}
public init(_ serviceType: JWTService.Type)throws {
self.serviceBuilder = { key, d in
switch serviceType {
case is RSAService.Type:
return try RSAService(n: key, e: "AQAB", d: d)
case is HMACService.Type: return try HMACService(key: key)
case is CertService.Type: return try CertService(certificate: key)
default:
throw JWTProviderError(
identifier: "unsupportedJWTService",
reason: "The registered JWT service is not supported. Maybe you created a custom service?",
status: .internalServerError
)
}
}
}
public func register(_ services: inout Services) throws {
/// Registering a JWKSService.
services.register(JWKSService.self)
let d = Environment.get("JWT_SECRET")
guard let key = Environment.get("JWT_PUBLIC") else {
throw JWTProviderError(identifier: "noPublicFound", reason: "No 'JWT_PUBLIC' environment variable was found", status: .internalServerError)
}
let jwtService = try serviceBuilder(key, d)
if let rsaService = jwtService as? RSAService {
services.register(rsaService, as: JWTService.self)
} else if let hmacService = jwtService as? HMACService {
services.register(hmacService, as: JWTService.self)
} else if let certService = jwtService as? CertService {
services.register(certService, as: JWTService.self)
} else {
throw JWTProviderError(
identifier: "unsupportedJWTService",
reason: "The registered JWT service is not supported. Maybe you created a custom service?",
status: .internalServerError
)
}
}
public func boot(_ worker: Container) throws {}
public func didBoot(_ worker: Container) throws -> EventLoopFuture<Void> { return Future.map(on: worker, { () }) }
}

View File

@ -1,8 +1,11 @@
import Debugging
import Vapor import Vapor
public struct JWTProviderError: Debuggable, AbortError, Error { public struct JWTProviderError: AbortError, Error {
public let identifier: String public let identifier: String
public let reason: String public let reason: String
public let status: HTTPResponseStatus public let status: HTTPResponseStatus
public var description: String {
return "JWTProviderError.\(self.identifier): \(self.status) \(self.reason)"
}
} }

View File

@ -1,9 +1,7 @@
import Foundation import Foundation
import Service import JWTKit
import Crypto
import JWT
public protocol JWTService: Service { public protocol JWTService {
var signer: JWTSigner { get } var signer: JWTSigner { get }
var header: JWTHeader? { get } var header: JWTHeader? { get }
@ -11,33 +9,48 @@ public protocol JWTService: Service {
func verify(_ token: Data)throws -> Bool func verify(_ token: Data)throws -> Bool
} }
public enum DigestAlgorithm {
case sha256, sha384, sha512
}
extension UInt8 {
static let period: UInt8 = 46
}
extension JWTSigner {
public func verify<S, H, P>(_ signature: S, header: H, payload: P) throws -> Bool
where S: DataProtocol, H: DataProtocol, P: DataProtocol
{
let message: Data = Data(header) + Data([.period]) + Data(payload)
guard let signature = Data(base64Encoded: Data(signature)) else {
throw JWTError.malformedToken
}
return try algorithm.verify(signature, signs: message)
}
}
extension JWTService { extension JWTService {
public func sign<T>(_ payload: T) throws -> String where T : JWTPayload { public func sign<T>(_ payload: T) throws -> String where T : JWTPayload {
guard let header = self.header else { guard let header = self.header else {
throw JWTProviderError(identifier: "noHeader", reason: "Cannot sign token with a header", status: .internalServerError) throw JWTProviderError(identifier: "noHeader", reason: "Cannot sign token with a header", status: .internalServerError)
} }
let jwt = JWT(header: header, payload: payload) let jwt = JWT(header: header, payload: payload)
let data = try signer.sign(jwt) let bytes = try jwt.sign(using: self.signer)
guard let token = String(data: data, encoding: .utf8) else { return String(decoding: bytes, as: UTF8.self)
throw JWTProviderError(
identifier: "tokenEncodingFailed",
reason: "Converting access token data to a string failed with UTF-8 encoding",
status: .internalServerError
)
}
return token
} }
public func verify(_ token: Data)throws -> Bool { public func verify(_ token: Data)throws -> Bool {
let parts = token.split(separator: .period) let parts = token.split(separator: .period)
guard parts.count == 3 else { guard parts.count == 3 else {
throw JWTError(identifier: "invalidJWT", reason: "Malformed JWT") throw JWTError.malformedToken
} }
let header = Data(parts[0]) let header = Data(parts[0])
let payload = Data(parts[1]) let payload = Data(parts[1])
let signature = Data(parts[2]) let signature = Data(parts[2])
return try self.signer.verify(signature, header: header, payload: payload) return try self.signer.verify(signature, header: header, payload: payload)
} }
} }

View File

@ -1,32 +1,41 @@
import Foundation import Foundation
import Crypto import CJWTKitCrypto
import JWT import JWTKit
public final class RSAService: JWTService { public final class RSAService: JWTService {
public enum KeyType {
case `public`, `private`
}
public let signer: JWTSigner public let signer: JWTSigner
public let header: JWTHeader? public let header: JWTHeader?
public init(pem: String, header: JWTHeader? = nil, type: RSAKeyType = .private, algorithm: DigestAlgorithm = .sha256)throws { public init(pem: Data, header: JWTHeader? = nil, type: KeyType = .private, algorithm: DigestAlgorithm = .sha256) {
let key: RSAKey let key: RSAKey
switch type { do {
case .public: key = try RSAKey.public(pem: pem) switch type {
case .private: key = try RSAKey.private(pem: pem) case .public: key = try RSAKey.public(pem: pem)
case .private: key = try RSAKey.private(pem: pem)
}
}
catch {
print("We need a key to run this service.")
exit(0)
} }
switch algorithm { switch algorithm {
case .sha256: self.signer = JWTSigner.rs256(key: key) case .sha256: self.signer = JWTSigner.rs256(key: key)
case .sha384: self.signer = JWTSigner.rs384(key: key) case .sha384: self.signer = JWTSigner.rs384(key: key)
case .sha512: self.signer = JWTSigner.rs512(key: key) case .sha512: self.signer = JWTSigner.rs512(key: key)
default: throw JWTProviderError(identifier: "badRSAAlgorithm", reason: "RSA signing requires SHA256, SHA384, or SHA512 algorithm", status: .internalServerError)
} }
self.header = header self.header = header
} }
public init( public init(
secret: String, secret: Data,
header: JWTHeader? = nil, header: JWTHeader? = nil,
keyBuilder: (LosslessDataConvertible)throws -> RSAKey, keyBuilder: (Data) throws -> RSAKey,
signerBuilder: (RSAKey) -> JWTSigner = JWTSigner.rs256 signerBuilder: (RSAKey) -> JWTSigner = JWTSigner.rs256
)throws { )throws {
let key = try keyBuilder(secret) let key = try keyBuilder(secret)
@ -34,17 +43,18 @@ public final class RSAService: JWTService {
self.header = header self.header = header
} }
public init(n: String, e: String, d: String? = nil, header: JWTHeader? = nil, algorithm: DigestAlgorithm = .sha256)throws { public init(n: String, e: String, d: String? = nil, header: JWTHeader? = nil, algorithm: DigestAlgorithm = .sha256) {
let key = try RSAKey.components(n: n, e: e, d: d) guard let key = RSAKey(modulus: n, exponent: e, privateExponent: d) else {
print("RSA key initialization failed")
exit(0)
}
switch algorithm { switch algorithm {
case .sha256: self.signer = JWTSigner.rs256(key: key) case .sha256: self.signer = JWTSigner.rs256(key: key)
case .sha384: self.signer = JWTSigner.rs384(key: key) case .sha384: self.signer = JWTSigner.rs384(key: key)
case .sha512: self.signer = JWTSigner.rs512(key: key) case .sha512: self.signer = JWTSigner.rs512(key: key)
default: throw JWTProviderError(identifier: "badRSAAlgorithm", reason: "RSA signing requires SHA256, SHA384, or SHA512 algorithm", status: .internalServerError)
} }
self.header = header self.header = header
} }
} }

View File

@ -1,17 +1,21 @@
import Authentication
import Vapor import Vapor
import JWT import JWTKit
extension Request {
var jwtService: JWTService {
return self.application.jwtService
}
}
extension Request { extension Request {
public func payload<Payload>(`as` type: Payload.Type = Payload.self)throws -> Payload where Payload: JWTPayload { public func payload<Payload>(`as` type: Payload.Type = Payload.self)throws -> Payload where Payload: JWTPayload {
guard let token = self.http.headers.bearerAuthorization?.token else { guard let token = self.headers.bearerAuthorization?.token else {
throw JWTProviderError(identifier: "missingAuthorizationHeader", reason: "'Authorization' header with bearer token is missing", status: .badRequest) throw JWTProviderError(identifier: "missingAuthorizationHeader", reason: "'Authorization' header with bearer token is missing", status: .badRequest)
} }
let jwt = try self.make(JWTService.self)
let data: Data = Data(token.utf8) let data: Data = Data(token.utf8)
if try jwt.verify(data) { if try self.jwtService.verify(data) {
return try JWT<Payload>.init(from: data, verifiedUsing: jwt.signer).payload return try JWT<Payload>.init(from: data, verifiedBy: self.jwtService.signer).payload
} else { } else {
throw JWTProviderError(identifier: "verificationFailed", reason: "Verification failed for JWT token", status: .forbidden) throw JWTProviderError(identifier: "verificationFailed", reason: "Verification failed for JWT token", status: .forbidden)
} }

View File

@ -1,36 +1,36 @@
import Foundation //import Foundation
import Vapor //import Vapor
//
struct JWKSMockClient: Client, ServiceType { //struct JWKSMockClient: Client, ServiceType {
var container: Container // var container: Container
var requestString: String! // var requestString: String!
//
init(container: Container) { // init(container: Container) {
self.container = container // self.container = container
} // }
//
func send(_ req: Request) -> EventLoopFuture<Response> { // func send(_ req: Request) -> EventLoopFuture<Response> {
var response: String = "" // var response: String = ""
//
switch (req.http.url.absoluteString) { // switch (req.http.url.absoluteString) {
case "https://mockurl.com": // case "https://mockurl.com":
response = JWKSResponseFixture.openIDDocumentResponse // response = JWKSResponseFixture.openIDDocumentResponse
case "https//:mock_jwks_uri.com": // case "https//:mock_jwks_uri.com":
response = JWKSResponseFixture.jwksResponse // response = JWKSResponseFixture.jwksResponse
default: // default:
response = "{}" // response = "{}"
} // }
//
return Future.map(on: req) { // return Future.map(on: req) {
req.makeResponse(response, as: .json) // req.makeResponse(response, as: .json)
} // }
} // }
//
public static var serviceSupports: [Any.Type] { // public static var serviceSupports: [Any.Type] {
return [Client.self] // return [Client.self]
} // }
//
static func makeService(for worker: Container) throws -> JWKSMockClient { // static func makeService(for worker: Container) throws -> JWKSMockClient {
return JWKSMockClient(container: worker) // return JWKSMockClient(container: worker)
} // }
} //}

View File

@ -1,74 +1,73 @@
import XCTest //import XCTest
import Vapor //import Vapor
import JWTVapor //import JWTVapor
import Core //
//final class JWKSServiceTests: XCTestCase {
final class JWKSServiceTests: XCTestCase { //
// static let allTests = [
static let allTests = [ // ("testItThrowsErrorIfJWKSConfigIsNotRegisteredAsAService", testItThrowsErrorIfJWKSConfigIsNotRegisteredAsAService),
("testItThrowsErrorIfJWKSConfigIsNotRegisteredAsAService", testItThrowsErrorIfJWKSConfigIsNotRegisteredAsAService), // ("testItDoesNotThrowErrorIfJWKSConfigIsRegisteredAsAService", testItDoesNotThrowErrorIfJWKSConfigIsRegisteredAsAService),
("testItDoesNotThrowErrorIfJWKSConfigIsRegisteredAsAService", testItDoesNotThrowErrorIfJWKSConfigIsRegisteredAsAService), // ("testItReturnsAnRSAServiceForMentionedTid", testItReturnsAnRSAServiceForMentionedTid),
("testItReturnsAnRSAServiceForMentionedTid", testItReturnsAnRSAServiceForMentionedTid), // ("testItThrowsAnErrorIfNoRSAServiceIsFoundForMentionedTid", testItThrowsAnErrorIfNoRSAServiceIsFoundForMentionedTid)
("testItThrowsAnErrorIfNoRSAServiceIsFoundForMentionedTid", testItThrowsAnErrorIfNoRSAServiceIsFoundForMentionedTid) // ]
] //
// func testItThrowsErrorIfJWKSConfigIsNotRegisteredAsAService() {
func testItThrowsErrorIfJWKSConfigIsNotRegisteredAsAService() { // var services = Services()
var services = Services() // try? services.register(JWTProvider(RSAService.self))
try? services.register(JWTProvider(RSAService.self)) // let app = try? Application(services: services)
let app = try? Application(services: services) //
// XCTAssertThrowsError(try app?.make(JWKSService.self))
XCTAssertThrowsError(try app?.make(JWKSService.self)) // }
} //
// func testItDoesNotThrowErrorIfJWKSConfigIsRegisteredAsAService() {
func testItDoesNotThrowErrorIfJWKSConfigIsRegisteredAsAService() { // var services = Services()
var services = Services() // let jwksMockConfig = JWKSConfig(jwks: "https://mockurl.com", following: "jwks_mock_uri_key")
let jwksMockConfig = JWKSConfig(jwks: "https://mockurl.com", following: "jwks_mock_uri_key") // services.register(jwksMockConfig)
services.register(jwksMockConfig) // try? services.register(JWTProvider(RSAService.self))
try? services.register(JWTProvider(RSAService.self)) // let app = try? Application(services: services)
let app = try? Application(services: services) //
// XCTAssertNoThrow(try app?.make(JWKSService.self))
XCTAssertNoThrow(try app?.make(JWKSService.self)) // }
} //
// func testItReturnsAnRSAServiceForMentionedTid() {
func testItReturnsAnRSAServiceForMentionedTid() { // var services = Services.default()
var services = Services.default() // let jwksMockConfig = JWKSConfig(jwks: "https://mockurl.com", following: "jwks_mock_uri_key")
let jwksMockConfig = JWKSConfig(jwks: "https://mockurl.com", following: "jwks_mock_uri_key") // services.register(jwksMockConfig)
services.register(jwksMockConfig) // services.register(JWKSMockClient.self)
services.register(JWKSMockClient.self) // try? services.register(JWTProvider(RSAService.self))
try? services.register(JWTProvider(RSAService.self)) //
// var config = Config.default()
var config = Config.default() // config.prefer(JWKSMockClient.self, for: Client.self)
config.prefer(JWKSMockClient.self, for: Client.self) //
// let app = try! Application(config: config, services: services)
let app = try! Application(config: config, services: services) //
// do {
do { // let rsaService = try app.make(JWKSService.self)
let rsaService = try app.make(JWKSService.self) // .rsaService(forKey: "7_Zuf1tvkwLxYaHS3q6lUjUYIGw")
.rsaService(forKey: "7_Zuf1tvkwLxYaHS3q6lUjUYIGw") // .wait()
.wait() //
// XCTAssertEqual(rsaService.signer.algorithm.jwtAlgorithmName, "RS256")
XCTAssertEqual(rsaService.signer.algorithm.jwtAlgorithmName, "RS256") //
// } catch {
} catch { //
// XCTFail("Failed with exception \(error)")
XCTFail("Failed with exception \(error)") // }
} // }
} //
// func testItThrowsAnErrorIfNoRSAServiceIsFoundForMentionedTid() {
func testItThrowsAnErrorIfNoRSAServiceIsFoundForMentionedTid() { // var services = Services.default()
var services = Services.default() // let jwksMockConfig = JWKSConfig(jwks: "https://mockurl.com", following: "jwks_mock_uri_key")
let jwksMockConfig = JWKSConfig(jwks: "https://mockurl.com", following: "jwks_mock_uri_key") // services.register(jwksMockConfig)
services.register(jwksMockConfig) // services.register(JWKSMockClient.self)
services.register(JWKSMockClient.self) // try? services.register(JWTProvider(RSAService.self))
try? services.register(JWTProvider(RSAService.self)) //
// var config = Config.default()
var config = Config.default() // config.prefer(JWKSMockClient.self, for: Client.self)
config.prefer(JWKSMockClient.self, for: Client.self) //
// let app = try! Application(config: config, services: services)
let app = try! Application(config: config, services: services) //
// XCTAssertThrowsError(try app.make(JWKSService.self)
XCTAssertThrowsError(try app.make(JWKSService.self) // .rsaService(forKey: "some_unknown_tid")
.rsaService(forKey: "some_unknown_tid") // .wait())
.wait()) // }
} //}
}

View File

@ -1,20 +1,20 @@
import XCTest //import XCTest
import Vapor //import Vapor
import JWTVapor //import JWTVapor
//
final class JWTProviderTests: XCTestCase { //final class JWTProviderTests: XCTestCase {
static let allTests = [("testItRegistersJWKSService", testItRegistersJWKSService)] // static let allTests = [("testItRegistersJWKSService", testItRegistersJWKSService)]
//
func testItRegistersJWKSService() { // func testItRegistersJWKSService() {
var services = Services() // var services = Services()
//
let mockJWKSConfig = JWKSConfig(jwks: "https://mockUrl.com", following: "jwks_mock_uri") // let mockJWKSConfig = JWKSConfig(jwks: "https://mockUrl.com", following: "jwks_mock_uri")
services.register(mockJWKSConfig) // services.register(mockJWKSConfig)
try? services.register(JWTProvider(RSAService.self)) // try? services.register(JWTProvider(RSAService.self))
//
let app = try? Application(services: services) // let app = try? Application(services: services)
let jwksService = try? app?.make(JWKSService.self) // let jwksService = try? app?.make(JWKSService.self)
//
XCTAssertNotNil(jwksService as Any) // XCTAssertNotNil(jwksService as Any)
} // }
} //}

View File

@ -1,6 +1,6 @@
import XCTest import XCTest
import JWTVaporTests import JWTKitVaporTests
var tests = [XCTestCaseEntry]() var tests = [XCTestCaseEntry]()
tests += JWTVaporTests.allTests() tests += JWTVaporTests.allTests()

0
swift Normal file
View File