Skip to content

Commit

Permalink
fix: Custom subject claim required to be a String (#5)
Browse files Browse the repository at this point in the history
* Add test for UserProfileDelegate
* Add documentation and usage examples for custom claims
  • Loading branch information
djones6 authored Sep 3, 2019
1 parent 4a4e10a commit 7b77c54
Show file tree
Hide file tree
Showing 25 changed files with 1,205 additions and 151 deletions.
82 changes: 80 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: "<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:
```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: "<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.

## Example of JWT authentication for Codable routes

A Kitura Codable route can be authenticated using a JWT by using the `JWT<C: Claims>` type (defined by [Swift-JWT](https://github.com/IBM-Swift/Swift-JWT)) as a Type-Safe Middleware:
Expand Down
103 changes: 96 additions & 7 deletions Sources/CredentialsJWT/CredentialsJWT.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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: "<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:
```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: "<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.
*/
public class CredentialsJWT<C: Claims>: 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?
Expand All @@ -50,7 +134,10 @@ public class CredentialsJWT<C: Claims>: 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
}
Expand Down Expand Up @@ -140,11 +227,13 @@ public class CredentialsJWT<C: Claims>: 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)
Expand Down
Loading

0 comments on commit 7b77c54

Please sign in to comment.