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
/Packages
/*.xcodeproj
.swiftpm/

View File

@ -2,129 +2,75 @@
"object": {
"pins": [
{
"package": "Auth",
"repositoryURL": "https://github.com/vapor/auth.git",
"package": "async-http-client",
"repositoryURL": "https://github.com/swift-server/async-http-client.git",
"state": {
"branch": null,
"revision": "e6f61b403727ec124214beb3e57deff579f31d00",
"version": "2.0.4"
"revision": "51dc885a30ca704b02fa803099b0a9b5b38067b6",
"version": "1.0.0"
}
},
{
"package": "Console",
"repositoryURL": "https://github.com/vapor/console.git",
"package": "async-kit",
"repositoryURL": "https://github.com/vapor/async-kit.git",
"state": {
"branch": null,
"revision": "74cfbea629d4aac34a97cead2447a6870af1950b",
"version": "3.1.1"
"revision": "d9fd2be441af6d1428b62ab694848396e7072a14",
"version": "1.0.0-beta.1"
}
},
{
"package": "Core",
"repositoryURL": "https://github.com/vapor/core.git",
"package": "console-kit",
"repositoryURL": "https://github.com/vapor/console-kit.git",
"state": {
"branch": null,
"revision": "c64f63cb21631010952f7abfef719d376ab6a441",
"version": "3.9.1"
"revision": "5b91c2dc93781e4b36cb4c667972670eac90e6e7",
"version": "4.0.0-beta.1"
}
},
{
"package": "Crypto",
"repositoryURL": "https://github.com/vapor/crypto.git",
"package": "jwt-kit",
"repositoryURL": "https://github.com/vapor/jwt-kit.git",
"state": {
"branch": null,
"revision": "df8eb7d8ae51787b3a0628aa3975e67666da936c",
"version": "3.3.3"
"revision": "0c7e52ab75ddfabad539a4a2a2f9fc003e7700b7",
"version": null
}
},
{
"package": "DatabaseKit",
"repositoryURL": "https://github.com/vapor/database-kit.git",
"package": "multipart-kit",
"repositoryURL": "https://github.com/vapor/multipart-kit.git",
"state": {
"branch": null,
"revision": "8f352c8e66dab301ab9bfef912a01ce1361ba1e4",
"version": "1.3.3"
"revision": "a941d7a1d685c83df09077f6190808ff2a7f4dce",
"version": "4.0.0-beta.1"
}
},
{
"package": "Fluent",
"repositoryURL": "https://github.com/vapor/fluent.git",
"package": "open-crypto",
"repositoryURL": "https://github.com/vapor/open-crypto.git",
"state": {
"branch": null,
"revision": "b915c321c6f9e83743ee5efa35a30895e1b02e51",
"version": "3.2.0"
"revision": "06d26edb8e28295bb7103b4f950d5ea58d634c1b",
"version": "4.0.0-alpha.2"
}
},
{
"package": "FluentSQLite",
"repositoryURL": "https://github.com/vapor/fluent-sqlite.git",
"package": "routing-kit",
"repositoryURL": "https://github.com/vapor/routing-kit.git",
"state": {
"branch": null,
"revision": "c32f5bda84bf4ea691d19afe183d40044f579e11",
"version": "3.0.0"
"revision": "6c7f4b471f9662d05045d82e64e22d5572a16a82",
"version": "4.0.0-alpha.1"
}
},
{
"package": "HTTP",
"repositoryURL": "https://github.com/vapor/http.git",
"package": "swift-log",
"repositoryURL": "https://github.com/apple/swift-log.git",
"state": {
"branch": null,
"revision": "3808ed0401379b6e9f4a053f03090ea9d658caa9",
"version": "3.2.1"
}
},
{
"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"
"revision": "74d7b91ceebc85daf387ebb206003f78813f71aa",
"version": "1.2.0"
}
},
{
@ -132,8 +78,26 @@
"repositoryURL": "https://github.com/apple/swift-nio.git",
"state": {
"branch": null,
"revision": "ba7970fe396e8198b84c6c1b44b38a1d4e2eb6bd",
"version": "1.14.1"
"revision": "ff01888051cd7efceb1bf8319c1dd3986c4bf6fc",
"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",
"state": {
"branch": null,
"revision": "0f3999f3e3c359cc74480c292644c3419e44a12f",
"version": "1.4.0"
"revision": "ccf96bbe65ecc7c1558ab0dba7ffabdea5c1d31f",
"version": "2.4.4"
}
},
{
"package": "swift-nio-ssl-support",
"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",
"package": "vapor",
"repositoryURL": "https://github.com/vapor/vapor.git",
"state": {
"branch": null,
"revision": "c86ada59b31c69f08a6abd4f776537cba48d5df6",
"version": "3.3.0"
"revision": "3d8bbd5eb2b556debbb51da8ac7357f34b7b0a8e",
"version": "4.0.0-beta.1"
}
},
{
"package": "WebSocket",
"repositoryURL": "https://github.com/vapor/websocket.git",
"package": "websocket-kit",
"repositoryURL": "https://github.com/vapor/websocket-kit.git",
"state": {
"branch": null,
"revision": "d85e5b6dce4d04065865f77385fc3324f98178f6",
"version": "1.1.2"
"revision": "66c0ea58398f055b5a0d92b0d5f4c32ef0c02eeb",
"version": "2.0.0-beta.1"
}
}
]

View File

@ -1,17 +1,22 @@
// swift-tools-version:4.0
// swift-tools-version:5.0
import PackageDescription
let package = Package(
name: "JWTVapor",
platforms: [
.macOS(.v10_14)
],
products: [
.library(name: "JWTVapor", targets: ["JWTVapor"]),
],
dependencies: [
.package(url: "https://github.com/vapor/vapor.git", from: "3.3.0"),
.package(url: "https://github.com/vapor/jwt.git", from: "3.0.0"),
.package(url: "https://github.com/vapor/auth.git", from: "2.0.4")
.package(url: "https://github.com/vapor/vapor.git", from: "4.0.0-beta"),
.package(url: "https://github.com/vapor/jwt-kit.git", .revision("0c7e52ab75ddfabad539a4a2a2f9fc003e7700b7")),
// .package(url: "https://github.com/vapor/auth.git", .branch("master"))
],
targets: [
.target(name: "JWTVapor", dependencies: ["Vapor", "Authentication", "JWT"]),
.target(name: "JWTVapor", dependencies: ["Vapor", "JWTKit"]),
.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 Crypto
import JWT
import JWTKit
public final class CertService: JWTService {
public var signer: JWTSigner
public var header: JWTHeader?
public init(certificate: String, header: JWTHeader? = nil, algorithm: DigestAlgorithm = .sha256)throws {
let key = try RSAKey.public(certificate: certificate)
public init(certificate: Data, header: JWTHeader? = nil, algorithm: DigestAlgorithm = .sha256) {
do {
let key = try RSAKey.public(pem: certificate)
switch algorithm {
case .sha256: self.signer = JWTSigner.rs256(key: key)
case .sha384: self.signer = JWTSigner.rs384(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)
}
}
catch {
print("We cannot run this service without a key.")
exit(0)
}
self.header = header

View File

@ -1,17 +1,15 @@
import Foundation
import Crypto
import JWT
import JWTKit
public final class HMACService: JWTService {
public let signer: JWTSigner
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 {
case .sha256: self.signer = JWTSigner.hs256(key: Data(key.utf8))
case .sha384: self.signer = JWTSigner.hs384(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

View File

@ -1,4 +1,3 @@
import Foundation
import Vapor
/// 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
/// to `JWKSService` in its `makeService` method.
public struct JWKSConfig: Service {
public let jwks: String
public struct JWKSConfig {
public let jwks: URI
public let following: 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.following = following
self.password = password

View File

@ -4,10 +4,9 @@ import Vapor
/// Content (Codeable) model to read the response obtained after hitting the url specified in the
/// `jwks` property of `JWKSConfig`.
public struct JWKSDocumentRequest: Content {
let jwksUrl: String
let jwksUrl: URI
public init(from decoder: Decoder) throws {
// Get the container
let container = try decoder.container(keyedBy: JWKSDocumentCodingKey.self)
// Read the coding key passed to the decoder as part of userinfo
@ -15,8 +14,18 @@ public struct JWKSDocumentRequest: Content {
throw DecodingError.dataCorrupted(.init(codingPath: [], debugDescription: "Failed"))
}
// get the jwks url from the json response.
self.jwksUrl = try container.decode(String.self, forKey: jwksUrlKey)
self.jwksUrl = try URI(string: 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,14 +1,9 @@
import Foundation
import Vapor
public struct JWKSService: ServiceType {
/// config: `JWKSConfig` instance which contains all the configurations required by `JWKSService`.
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
public struct JWKSService {
static let factory: (Application) throws -> JWKSService = { _ in fatalError() }
static let requestFactory: (Request) throws -> JWKSService = { _ in fatalError() }
/// 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`
@ -19,52 +14,52 @@ public struct JWKSService: ServiceType {
/// [codingUserInfoKey: JWKSDocumentCodingKey(stringValue: self.config.following)]
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.container = container
self.client = try! container.make(Client.self)
self.client = client
}
/// 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.
public func rsaService(forKey tid: String) throws -> Future<RSAService> {
return client.get(self.config.jwks).flatMap { response throws -> Future<JWKSDocumentRequest> in
/// Create a JSONDecoder in which the codingKey could be passed as part of userInfo.
public func rsaService(forKey tid: String) throws -> EventLoopFuture<RSAService> {
return self.client.get(self.config.jwks).flatMap { response -> EventLoopFuture<ClientResponse> in
let jsonDecoder = JSONDecoder()
let jwksUrlKey = JWKSDocumentCodingKey(stringValue: self.config.following)!
jsonDecoder.userInfo = [JWKSService.codingUserInfoKey: jwksUrlKey]
/// return the JSON response.
return try response.content.decode(json: JWKSDocumentRequest.self, using: jsonDecoder)
}.flatMap { jwksDocumentRequest throws -> Future<Response> in
/// Make a request and return the JWKS file.
return self.client.get(jwksDocumentRequest.jwksUrl)
}.flatMap { response throws -> Future<JWKSKeys> in
do {
let request = try response.content.decode(JWKSDocumentRequest.self, using: jsonDecoder)
return self.client.get(request.jwksUrl)
} catch let error {
let eventLoop = self.client.eventLoopGroup.next()
return eventLoop.future(error: error)
}
}.flatMapThrowing { response throws -> RSAService in
/// 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`
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)
}
return matchingJWKSKey
}.map { jwksKey throws -> RSAService in
/// Create the RSAService using the JWKSKey.
return try RSAService(n: jwksKey.n, e: jwksKey.e, d: jwksKey.d)
return try RSAService(n: matchingJWKSKey.n, e: matchingJWKSKey.e, d: matchingJWKSKey.d)
}
}
public static func makeService(for worker: Container) throws -> JWKSService {
// Load the required services.
let config = try worker.make(JWKSConfig.self)
return JWKSService(config: config, container: worker)
}
// public static func makeService(for worker: Container) throws -> JWKSService {
// // Load the required services.
// let config = try worker.make(JWKSConfig.self)
// 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 Console
import ConsoleKit
import Foundation
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.
""" }
public var options: [CommandOption] = [
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"
])
]
@Option(name: "json", help: Signature.jsonHelp) var json: Data?
public var help: [String] = ["Creates a JWT token for debugging purposes."]
public init() { }
}
public let help: String = "Creates a JWT token for debugging purposes."
internal let payloadCreator: () -> Payload
internal let signer: JWTService
public init(payload: Payload) {
public init(payload: Payload, signer: JWTService) {
self.payloadCreator = { payload }
self.signer = signer
}
public init(_ handler: @escaping () -> Payload) {
public init(signer: JWTService, _ handler: @escaping () -> Payload) {
self.payloadCreator = handler
self.signer = signer
}
public func run(using context: CommandContext) throws -> EventLoopFuture<Void> {
let signer = try context.container.make(JWTService.self)
let payload: Payload
if let json = context.options["json"]?.data(using: .utf8) {
payload = try JSONDecoder().decode(Payload.self, from: json)
} else {
payload = self.payloadCreator()
}
public func run(using context: CommandContext, signature: Signature) throws {
let payload: Payload =
try signature.json.map { try JSONDecoder().decode(Payload.self, from: $0) } ??
self.payloadCreator()
let token = try signer.sign(payload)
context.console.output("JWT Token: ", style: .info, newLine: false)
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
public struct JWTProviderError: Debuggable, AbortError, Error {
public struct JWTProviderError: AbortError, Error {
public let identifier: String
public let reason: String
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 Service
import Crypto
import JWT
import JWTKit
public protocol JWTService: Service {
public protocol JWTService {
var signer: JWTSigner { get }
var header: JWTHeader? { get }
@ -11,27 +9,42 @@ public protocol JWTService: Service {
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 {
public func sign<T>(_ payload: T) throws -> String where T : JWTPayload {
guard let header = self.header else {
throw JWTProviderError(identifier: "noHeader", reason: "Cannot sign token with a header", status: .internalServerError)
}
let jwt = JWT(header: header, payload: payload)
let data = try signer.sign(jwt)
guard let token = String(data: data, encoding: .utf8) else {
throw JWTProviderError(
identifier: "tokenEncodingFailed",
reason: "Converting access token data to a string failed with UTF-8 encoding",
status: .internalServerError
)
}
return token
let bytes = try jwt.sign(using: self.signer)
return String(decoding: bytes, as: UTF8.self)
}
public func verify(_ token: Data)throws -> Bool {
let parts = token.split(separator: .period)
guard parts.count == 3 else {
throw JWTError(identifier: "invalidJWT", reason: "Malformed JWT")
throw JWTError.malformedToken
}
let header = Data(parts[0])

View File

@ -1,32 +1,41 @@
import Foundation
import Crypto
import JWT
import CJWTKitCrypto
import JWTKit
public final class RSAService: JWTService {
public enum KeyType {
case `public`, `private`
}
public let signer: JWTSigner
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
do {
switch type {
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 {
case .sha256: self.signer = JWTSigner.rs256(key: key)
case .sha384: self.signer = JWTSigner.rs384(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
}
public init(
secret: String,
secret: Data,
header: JWTHeader? = nil,
keyBuilder: (LosslessDataConvertible)throws -> RSAKey,
keyBuilder: (Data) throws -> RSAKey,
signerBuilder: (RSAKey) -> JWTSigner = JWTSigner.rs256
)throws {
let key = try keyBuilder(secret)
@ -34,17 +43,18 @@ public final class RSAService: JWTService {
self.header = header
}
public init(n: String, e: String, d: String? = nil, header: JWTHeader? = nil, algorithm: DigestAlgorithm = .sha256)throws {
let key = try RSAKey.components(n: n, e: e, d: d)
public init(n: String, e: String, d: String? = nil, header: JWTHeader? = nil, algorithm: DigestAlgorithm = .sha256) {
guard let key = RSAKey(modulus: n, exponent: e, privateExponent: d) else {
print("RSA key initialization failed")
exit(0)
}
switch algorithm {
case .sha256: self.signer = JWTSigner.rs256(key: key)
case .sha384: self.signer = JWTSigner.rs384(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
}
}

View File

@ -1,17 +1,21 @@
import Authentication
import Vapor
import JWT
import JWTKit
extension Request {
var jwtService: JWTService {
return self.application.jwtService
}
}
extension Request {
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)
}
let jwt = try self.make(JWTService.self)
let data: Data = Data(token.utf8)
if try jwt.verify(data) {
return try JWT<Payload>.init(from: data, verifiedUsing: jwt.signer).payload
if try self.jwtService.verify(data) {
return try JWT<Payload>.init(from: data, verifiedBy: self.jwtService.signer).payload
} else {
throw JWTProviderError(identifier: "verificationFailed", reason: "Verification failed for JWT token", status: .forbidden)
}

View File

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

View File

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

View File

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

View File

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

0
swift Normal file
View File