From 7b77c54fb6cc3bea6353276b58c277e8aa6b81d7 Mon Sep 17 00:00:00 2001
From: David Jones
Date: Tue, 3 Sep 2019 17:18:09 +0100
Subject: [PATCH] fix: Custom subject claim required to be a String (#5)
* Add test for UserProfileDelegate
* Add documentation and usage examples for custom claims
---
README.md | 82 ++++-
Sources/CredentialsJWT/CredentialsJWT.swift | 103 +++++-
.../CredentialsJWTTests/TestRawRouteJWT.swift | 125 ++++++-
docs/Classes.html | 88 ++++-
docs/Classes/CredentialsJWT.html | 311 +++++++++++++++---
docs/Extensions.html | 4 +-
docs/Extensions/JWT.html | 4 +-
docs/Structs.html | 4 +-
docs/Structs/CredentialsJWTOptions.html | 4 +-
docs/Structs/TypeSafeJWT.html | 4 +-
docs/badge.svg | 16 +-
.../Contents/Resources/Documents/Classes.html | 88 ++++-
.../Documents/Classes/CredentialsJWT.html | 311 +++++++++++++++---
.../Resources/Documents/Extensions.html | 4 +-
.../Resources/Documents/Extensions/JWT.html | 4 +-
.../Contents/Resources/Documents/Structs.html | 4 +-
.../Structs/CredentialsJWTOptions.html | 4 +-
.../Documents/Structs/TypeSafeJWT.html | 4 +-
.../Contents/Resources/Documents/index.html | 90 ++++-
.../Contents/Resources/Documents/search.json | 2 +-
.../Contents/Resources/docSet.dsidx | Bin 12288 -> 12288 bytes
docs/docsets/CredentialsJWT.tgz | Bin 72330 -> 77181 bytes
docs/index.html | 90 ++++-
docs/search.json | 2 +-
docs/undocumented.json | 8 +-
25 files changed, 1205 insertions(+), 151 deletions(-)
diff --git a/README.md b/README.md
index 8c8615e..4726e93 100644
--- a/README.md
+++ b/README.md
@@ -22,14 +22,92 @@
# Kitura-CredentialsJWT
-Plugin for the Credentials framework that supports authentication using JWTs.
+A package enabling Kitura to authenticate users using [JSON Web Tokens](https://jwt.io/).
## Summary
-Plugin for [Kitura-Credentials](https://github.com/IBM-Swift/Kitura-Credentials) framework that supports authentication using [JSON Web Tokens](https://jwt.io/).
+
+This package provides two facilities:
+- `CredentialsJWT`: A plugin for the [Kitura-Credentials](https://github.com/IBM-Swift/Kitura-Credentials) framework that supports JWT (token-based) authentication,
+- A `TypeSafeMiddleware` extension for the `JWT` type, enabling it to be used as authentication for Codable routes.
## Swift version
The latest version of Kitura-CredentialsJWT requires **Swift 4.0** or newer. You can download this version of the Swift binaries by following this [link](https://swift.org/download/). Compatibility with other Swift versions is not guaranteed.
+## Using the `CredentialsJWT` plugin
+
+This plugin requires that the following HTTP headers are present on a request:
+- `X-token-type`: must be `JWT`
+- `Authorization`: the JWT string, optionally prefixed with `Bearer`.
+
+The [Swift-JWT](https://github.com/IBM-Swift/Swift-JWT) library is used to decode the token supplied in the Authorization header. To successfully decode it, you must specify the `Claims` that will be present in the JWT.
+
+One claim (by default, `sub`) will be used to represent the identity of the bearer. You can choose a different claim by supplying the `subject` option when creating an instance of CredentialsJWT, and you can further customize the resulting `UserProfile` by defining a `UserProfileDelegate`.
+
+### Usage Example
+
+To use `CredentialsJWT` using the default options:
+```swift
+import Credentials
+import CredentialsJWT
+import SwiftJWT
+
+// Defines the claims that must be present in a JWT.
+struct MyClaims: Claims {
+ let sub: String
+}
+
+// Defines the method used to verify the signature of a JWT.
+let jwtVerifier = .hs256(key: "".data(using: .utf8)!)
+
+// Create a CredentialsJWT plugin with default options.
+let jwtCredentials = CredentialsJWT(verifier: jwtVerifier)
+
+let authenticationMiddleware = Credentials()
+authenticationMiddleware.register(plugin: jwtCredentials)
+router.get("/myProtectedRoute", middleware: authenticationMiddleware)
+```
+
+Following successful authentication, the `UserProfile` will be minimally populated with the two required fields - `id` and `displayName` - both with the value of the JWT's `sub` claim. The `provider` will be set to `JWT`.
+
+### Usage Example - custom claims
+
+To customize the name of the identity claim, and further populate the UserProfile fields, specify the `subject` and `userProfileDelegate` options as follows:
+```swift
+import Credentials
+import CredentialsJWT
+import SwiftJWT
+
+// Defines the claims that must be present in a JWT.
+struct MyClaims: Claims {
+ let id: Int
+ let fullName: String
+ let email: String
+}
+
+struct MyDelegate: UserProfileDelegate {
+ func update(userProfile: UserProfile, from dictionary: [String:Any]) {
+ // `userProfile.id` already contains `id`
+ userProfile.displayName = dictionary["fullName"]! as! String
+ let email = UserProfile.UserProfileEmail(value: dictionary["email"]! as! String, type: "home")
+ userProfile.emails = [email]
+ }
+}
+
+// Defines the method used to verify the signature of a JWT.
+let jwtVerifier = .hs256(key: "".data(using: .utf8)!)
+
+// Create a CredentialsJWT plugin with default options.
+let jwtCredentials = CredentialsJWT(verifier: jwtVerifier, options: [CredentialsJWTOptions.subject: "id", CredentialsJWTOptions.userProfileDelegate: MyDelegate])
+
+let authenticationMiddleware = Credentials()
+authenticationMiddleware.register(plugin: jwtCredentials)
+router.get("/myProtectedRoute", middleware: authenticationMiddleware)
+```
+Following successful authentication, the `UserProfile` will be populated as follows:
+- `id`: the `id` claim (converted to a String),
+- `displayName`: the `fullName` claim,
+- `emails`: an array with a single element, representing the `email` claim.
+
## Example of JWT authentication for Codable routes
A Kitura Codable route can be authenticated using a JWT by using the `JWT` type (defined by [Swift-JWT](https://github.com/IBM-Swift/Swift-JWT)) as a Type-Safe Middleware:
diff --git a/Sources/CredentialsJWT/CredentialsJWT.swift b/Sources/CredentialsJWT/CredentialsJWT.swift
index 2106ca1..d72c680 100644
--- a/Sources/CredentialsJWT/CredentialsJWT.swift
+++ b/Sources/CredentialsJWT/CredentialsJWT.swift
@@ -23,20 +23,104 @@ import LoggerAPI
// MARK CredentialsJWT
-/// Authentication using a JWT.
+/**
+ A plugin for Kitura-Credentials supporting authentication using [JSON Web Tokens](https://jwt.io/).
+
+ This plugin requires that the following HTTP headers are present on a request:
+ - `X-token-type`: must be `JWT`
+ - `Authorization`: the JWT string, optionally prefixed with `Bearer`.
+
+ The [Swift-JWT](https://github.com/IBM-Swift/Swift-JWT) library is used to
+ decode JWT strings. To successfully decode it, you must specify the `Claims` that will
+ be present in the JWT. One claim (by default, `sub`) will be used to represent the identity of
+ the bearer. You can choose a different claim by supplying the `subject` option when
+ creating an instance of CredentialsJWT, and you can further customize the resulting `UserProfile`
+ by defining a `UserProfileDelegate`.
+
+ ### Usage Example
+
+ To use `CredentialsJWT` using the default options:
+ ```swift
+ import Credentials
+ import CredentialsJWT
+ import SwiftJWT
+
+ // Defines the claims that must be present in a JWT.
+ struct MyClaims: Claims {
+ let sub: String
+ }
+
+ // Defines the method used to verify the signature of a JWT.
+ let jwtVerifier = .hs256(key: "".data(using: .utf8)!)
+
+ // Create a CredentialsJWT plugin with default options.
+ let jwtCredentials = CredentialsJWT(verifier: jwtVerifier)
+
+ let authenticationMiddleware = Credentials()
+ authenticationMiddleware.register(plugin: jwtCredentials)
+ router.get("/myProtectedRoute", middleware: authenticationMiddleware)
+ ```
+
+ Following successful authentication, the `UserProfile` will be minimally populated with the
+ two required fields - `id` and `displayName` - both with the value of the JWT's `sub` claim.
+ The `provider` will be set to `JWT`.
+
+ ### Usage Example - custom claims
+
+ To customize the name of the identity claim, and further populate the UserProfile fields,
+ specify the `subject` and `userProfileDelegate` options as follows:
+ ```swift
+ import Credentials
+ import CredentialsJWT
+ import SwiftJWT
+
+ // Defines the claims that must be present in a JWT.
+ struct MyClaims: Claims {
+ let id: Int
+ let fullName: String
+ let email: String
+ }
+
+ struct MyDelegate: UserProfileDelegate {
+ func update(userProfile: UserProfile, from dictionary: [String:Any]) {
+ // `userProfile.id` already contains `id`
+ userProfile.displayName = dictionary["fullName"]! as! String
+ let email = UserProfile.UserProfileEmail(value: dictionary["email"]! as! String, type: "home")
+ userProfile.emails = [email]
+ }
+ }
+
+ // Defines the method used to verify the signature of a JWT.
+ let jwtVerifier = .hs256(key: "".data(using: .utf8)!)
+
+ // Create a CredentialsJWT plugin with default options.
+ let jwtCredentials = CredentialsJWT(verifier: jwtVerifier, options: [CredentialsJWTOptions.subject: "id", CredentialsJWTOptions.userProfileDelegate: MyDelegate])
+
+ let authenticationMiddleware = Credentials()
+ authenticationMiddleware.register(plugin: jwtCredentials)
+ router.get("/myProtectedRoute", middleware: authenticationMiddleware)
+ ```
+ Following successful authentication, the `UserProfile` will be populated as follows:
+ - `id`: the `id` claim (converted to a String),
+ - `displayName`: the `fullName` claim,
+ - `emails`: an array with a single element, representing the `email` claim.
+*/
public class CredentialsJWT: CredentialsPluginProtocol {
- /// The name of the plugin.
+ /// The name of the plugin: `JWT`.
public var name: String {
return "JWT"
}
- /// An indication as to whether the plugin is redirecting or not.
+ /// An indication as to whether the plugin is redirecting or not. This plugin is not redirecting.
public var redirecting: Bool {
return false
}
- /// The time in seconds since the user profile was generated that the access token will be considered valid.
+ /// The time in seconds since the user profile was generated that the access token will be considered valid
+ /// and remain in the `usersCache`.
+ ///
+ /// By default, this value is `nil`, which means that tokens will be cached indefinitely.
public let tokenTimeToLive: TimeInterval?
private var delegate: UserProfileDelegate?
@@ -50,7 +134,10 @@ public class CredentialsJWT: CredentialsPluginProtocol {
/// Token variable used after formatting.
var token = ""
- /// A delegate for `UserProfile` manipulation.
+ /// A delegate for `UserProfile` manipulation. Use this to further populate the profile using
+ /// any fields from the `Claims` that you have defined.
+ ///
+ /// This field can be set by passing the `userProfileDelegate` option during initialization.
public var userProfileDelegate: UserProfileDelegate? {
return delegate
}
@@ -140,11 +227,13 @@ public class CredentialsJWT: CredentialsPluginProtocol {
Log.error("Couldn't decode claims")
return onFailure(nil, nil)
}
- guard let userid = dictionary[subject] as? String else {
+ // Ensure claims contain the expected subject claim (default `sub`)
+ guard let subjectClaim = dictionary[subject] else {
Log.warning("Unable to create user profile: JWT claims do not contain '\(subject)'")
return onFailure(nil, nil)
}
-
+ // Convert subject claim value to a String
+ let userid = String("\(subjectClaim)")
let userProfile = UserProfile(id: userid , displayName: userid, provider: "JWT")
delegate?.update(userProfile: userProfile, from: dictionary)
diff --git a/Tests/CredentialsJWTTests/TestRawRouteJWT.swift b/Tests/CredentialsJWTTests/TestRawRouteJWT.swift
index 8339462..9af1696 100644
--- a/Tests/CredentialsJWTTests/TestRawRouteJWT.swift
+++ b/Tests/CredentialsJWTTests/TestRawRouteJWT.swift
@@ -26,7 +26,8 @@ import Dispatch
@testable import CredentialsJWT
-// A claims structure that will be used for the tests. The `sub` claim holds the users name.
+// A claims structure that will be used for the tests.
+// The `sub` claim holds the user's identity.
struct TestClaims: Claims, Equatable {
var sub: String
@@ -38,7 +39,8 @@ struct TestClaims: Claims, Equatable {
}
}
-// An alternate claims structure that will be used for the tests. The `username` holds the users name.
+// An alternate claims structure that will be used for the tests.
+// The `username` holds the user's identity.
struct TestAlternateClaims: Claims, Equatable {
var username: String
@@ -50,6 +52,32 @@ struct TestAlternateClaims: Claims, Equatable {
}
}
+// A claims structure containing custom claims that should be extracted as part of the
+// UserProfile. The `id` holds the user's identity.
+struct TestDelegateClaims: Claims, Equatable {
+ let id: Int
+ let fullName: String
+ let email: String
+
+ // Testing requirement: Equatable
+ static func == (lhs: TestDelegateClaims, rhs: TestDelegateClaims) -> Bool {
+ return
+ lhs.id == rhs.id && lhs.fullName == rhs.fullName && lhs.email == rhs.email
+ }
+}
+
+// A UserProfileDelegate for the route accessed in testDelegateToken. Custom claims
+// 'fullName' and 'email' are applied to the UserProfile 'displayName' and 'emails'
+// fields.
+struct MyDelegate: UserProfileDelegate {
+ func update(userProfile: UserProfile, from dictionary: [String:Any]) {
+ // `userProfile.id` already contains `id`
+ userProfile.displayName = dictionary["fullName"]! as! String
+ let email = UserProfile.UserProfileEmail(value: dictionary["email"]! as! String, type: "home")
+ userProfile.emails = [email]
+ }
+}
+
// Sets option for CredentialsJWT to allow username to be used instead of sub, used in the alternate
// credentials tests.
let jwtOptions: [String:Any] = [CredentialsJWTOptions.subject: "username"]
@@ -58,11 +86,21 @@ let jwtOptions: [String:Any] = [CredentialsJWTOptions.subject: "username"]
let jwtCredentials = CredentialsJWT(verifier: .hs256(key: "".data(using: .utf8)!), tokenTimeToLive: 1)
let altCredentials = CredentialsJWT(verifier: .hs256(key: "".data(using: .utf8)!), options: jwtOptions)
+// Sets options for CredentialsJWT to perform UserProfile manipulation using
+// a delegate, making use of custom Claims.
+let delegateOptions: [String:Any] = [CredentialsJWTOptions.subject: "id", CredentialsJWTOptions.userProfileDelegate: MyDelegate()]
+let delegateCredentials = CredentialsJWT(verifier: .hs256(key: "".data(using: .utf8)!), options: delegateOptions)
+
class TestRawRouteJWT : XCTestCase {
// A User structure that can be passed into the generate jwt route.
struct User: Codable {
- var name: String
+ let name: String
+ let email: String?
+ init(name: String, email: String? = nil) {
+ self.name = name
+ self.email = email
+ }
}
// Initiliasting the 3 users names and token variables.
@@ -77,6 +115,9 @@ class TestRawRouteJWT : XCTestCase {
var altUser = User(name: "Alternate")
var jwtString3 = ""
+ var delegateUser = User(name: "Mr Delegate", email: "mr_delegate@foo.xyz")
+ var jwtString4 = ""
+
// Key used in generation and decoding of JWT strings.
let key = "".data(using: .utf8)
@@ -183,6 +224,31 @@ class TestRawRouteJWT : XCTestCase {
}
})
}
+
+ // User 4 JWT created. User 4 uses the delegate test claims.
+ performServerTest(router: router) { expectation in
+ self.performRequest(method: "post", path: "/generateRawJwtDelegate", contentType: "application/json", callback: { response in
+ do {
+ guard let body = try response?.readString() else {
+ XCTFail("Couldn't read response")
+ return expectation.fulfill()
+ }
+ self.jwtString4 = body
+ print(self.jwtString4)
+ } catch {
+ XCTFail("Couldn't read string from response")
+ expectation.fulfill()
+ }
+ expectation.fulfill()
+ }, requestModifier: { request in
+ do {
+ try request.write(from: JSONEncoder().encode(self.delegateUser))
+ } catch {
+ XCTFail("Failed to send data")
+ expectation.fulfill()
+ }
+ })
+ }
}
// Tests that when a correct token is supplied a valid UserProfile is created.
@@ -231,6 +297,32 @@ class TestRawRouteJWT : XCTestCase {
}
}
+ // Tests that when a JWT is supplied that contains custom claims, to a route
+ // whose CredentialsJWT is configured with a suitable delegate, the profile
+ // contains the values of those custom claims (fullName and email).
+ func testDelegateToken() {
+ performServerTest(router: router) { expectation in
+ self.performRequest(method: "get", path: "/rawTokenAuthDelegate", callback: { response in
+ XCTAssertNotNil(response, "ERROR!!! ClientRequest response object was nil")
+ XCTAssertEqual(response?.statusCode, HTTPStatusCode.OK, "HTTP Status code was \(String(describing: response?.statusCode))")
+ do {
+ guard let body = try response?.readString() else {
+ XCTFail("No response body")
+ return expectation.fulfill()
+ }
+ let profile = body
+ let expectedProfile = "Mr Delegate,mr_delegate@foo.xyz"
+ XCTAssertEqual(profile, expectedProfile)
+ //TODO
+ } catch {
+ XCTFail("Could not decode response: \(error)")
+ expectation.fulfill()
+ }
+ expectation.fulfill()
+ }, headers: ["X-Token-Type" : "JWT", "Authorization" : "Bearer " + self.jwtString4])
+ }
+ }
+
// Tests that when an incorrect token is supplied an invalid token is supplied, user is unauthorized.
func testInvalidToken() {
performServerTest(router: router) { expectation in
@@ -417,10 +509,12 @@ class TestRawRouteJWT : XCTestCase {
let jwtSigner = JWTSigner.hs256(key: key!)
let tokenCredentials = Credentials()
let altTokenCredentials = Credentials()
+ let delegateTokenCredentials = Credentials()
tokenCredentials.register(plugin: jwtCredentials)
tokenCredentials.register(plugin: CredentialsGoogleToken())
altTokenCredentials.register(plugin: altCredentials)
+ delegateTokenCredentials.register(plugin: delegateCredentials)
router.get("/rawtokenauth", middleware: tokenCredentials)
router.get("/rawtokenauth") { request, response, next in
@@ -446,6 +540,20 @@ class TestRawRouteJWT : XCTestCase {
next()
}
+ router.get("/rawTokenAuthDelegate", middleware: delegateTokenCredentials)
+ router.get("/rawTokenAuthDelegate") { request, response, next in
+ guard let userProfile = request.userProfile else {
+ Log.verbose("Failed raw token authentication")
+ return try response.status(.unauthorized).end()
+ }
+ guard let email = userProfile.emails?.first?.value else {
+ Log.verbose("UserProfile e-mail was not populated")
+ return try response.status(.unauthorized).end()
+ }
+ response.send("\(userProfile.displayName),\(email)")
+ next()
+ }
+
// Route that generates a jwt from a given User's name.
router.post("/generaterawjwt") { request, response, next in
let credentials = try request.read(as: User.self)
@@ -468,6 +576,17 @@ class TestRawRouteJWT : XCTestCase {
next()
}
+ // Route that generates a jwt from a given User's name.
+ router.post("/generateRawJwtDelegate") { request, response, next in
+ let credentials = try request.read(as: User.self)
+ // Users credentials are authenticated
+ let myClaims = TestDelegateClaims(id: 123, fullName: credentials.name, email: credentials.email!)
+ var myJWT = JWT(claims: myClaims)
+ let signedJWT = try myJWT.sign(using: jwtSigner)
+ response.send(signedJWT)
+ next()
+ }
+
return router
}
diff --git a/docs/Classes.html b/docs/Classes.html
index 18477da..8eacff9 100644
--- a/docs/Classes.html
+++ b/docs/Classes.html
@@ -23,7 +23,7 @@
- (94% documented)
+ (100% documented)
Classes
-
Undocumented
+
A plugin for Kitura-Credentials supporting authentication using JSON Web Tokens.
+
+
This plugin requires that the following HTTP headers are present on a request:
+
+
+X-token-type
: must be JWT
+Authorization
: the JWT string, optionally prefixed with Bearer
.
+
+
+
The Swift-JWT library is used to
+decode JWT strings. To successfully decode it, you must specify the Claims
that will
+be present in the JWT. One claim (by default, sub
) will be used to represent the identity of
+the bearer. You can choose a different claim by supplying the subject
option when
+creating an instance of CredentialsJWT, and you can further customize the resulting UserProfile
+by defining a UserProfileDelegate
.
+
Usage Example
+
+
To use CredentialsJWT
using the default options:
+
import Credentials
+import CredentialsJWT
+import SwiftJWT
+
+// Defines the claims that must be present in a JWT.
+struct MyClaims: Claims {
+ let sub: String
+}
+
+// Defines the method used to verify the signature of a JWT.
+let jwtVerifier = .hs256(key: "<PrivateKey>".data(using: .utf8)!)
+
+// Create a CredentialsJWT plugin with default options.
+let jwtCredentials = CredentialsJWT<MyClaims>(verifier: jwtVerifier)
+
+let authenticationMiddleware = Credentials()
+authenticationMiddleware.register(plugin: jwtCredentials)
+router.get("/myProtectedRoute", middleware: authenticationMiddleware)
+
+
+
Following successful authentication, the UserProfile
will be minimally populated with the
+two required fields - id
and displayName
- both with the value of the JWT’s sub
claim.
+The provider
will be set to JWT
.
+
Usage Example - custom claims
+
+
To customize the name of the identity claim, and further populate the UserProfile fields,
+specify the subject
and userProfileDelegate
options as follows:
+
import Credentials
+import CredentialsJWT
+import SwiftJWT
+
+// Defines the claims that must be present in a JWT.
+struct MyClaims: Claims {
+ let id: Int
+ let fullName: String
+ let email: String
+}
+
+struct MyDelegate: UserProfileDelegate {
+ func update(userProfile: UserProfile, from dictionary: [String:Any]) {
+ // `userProfile.id` already contains `id`
+ userProfile.displayName = dictionary["fullName"]! as! String
+ let email = UserProfile.UserProfileEmail(value: dictionary["email"]! as! String, type: "home")
+ userProfile.emails = [email]
+ }
+}
+
+// Defines the method used to verify the signature of a JWT.
+let jwtVerifier = .hs256(key: "<PrivateKey>".data(using: .utf8)!)
+
+// Create a CredentialsJWT plugin with default options.
+let jwtCredentials = CredentialsJWT<MyClaims>(verifier: jwtVerifier, options: [CredentialsJWTOptions.subject: "id", CredentialsJWTOptions.userProfileDelegate: MyDelegate])
+
+let authenticationMiddleware = Credentials()
+authenticationMiddleware.register(plugin: jwtCredentials)
+router.get("/myProtectedRoute", middleware: authenticationMiddleware)
+
+
+
Following successful authentication, the UserProfile
will be populated as follows:
+
+
+id
: the id
claim (converted to a String),
+displayName
: the fullName
claim,
+emails
: an array with a single element, representing the email
claim.
+
See more
@@ -129,7 +211,7 @@ Declaration