Skip to content
This repository has been archived by the owner on Apr 20, 2024. It is now read-only.

Commit

Permalink
Merge pull request #1 from nodes-vapor/develop
Browse files Browse the repository at this point in the history
Develop
  • Loading branch information
Casperhr authored Jan 11, 2017
2 parents a67d896 + 99f0c62 commit 7763495
Show file tree
Hide file tree
Showing 21 changed files with 1,054 additions and 0 deletions.
Empty file added .codecov.yml
Empty file.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,6 @@ fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots
fastlane/test_output

JWTKeychain.xcodeproj/
Packages/
10 changes: 10 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
os:
- linux
- osx
language: generic
sudo: required
dist: trusty
osx_image: xcode8
script:
- eval "$(curl -sL https://swift.vapor.sh/ci)"
- eval "$(curl -sL https://swift.vapor.sh/codecov)"
12 changes: 12 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import PackageDescription

let package = Package(
name: "JWTKeychain",
dependencies: [
.Package(url: "https://github.com/vapor/vapor.git", majorVersion: 1),
.Package(url: "https://github.com/vapor/mysql-provider.git", majorVersion: 1, minor: 1),
.Package(url: "https://github.com/nodes-vapor/sugar.git", majorVersion: 0),
.Package(url: "https://github.com/siemensikkema/vapor-jwt.git", majorVersion: 0, minor: 4),
.Package(url: "https://github.com/Skyback/vapor-forms.git", majorVersion:0, minor: 3),
]
)
71 changes: 71 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# JWT Keychain
[![Language](https://img.shields.io/badge/Swift-3-brightgreen.svg)](http://swift.org)
[![Build Status](https://travis-ci.org/nodes-vapor/jwt-keychain.svg?branch=master)](https://travis-ci.org/nodes-vapor/jwt-keychain)
[![codecov](https://codecov.io/gh/nodes-vapor/jwt-keychain/branch/master/graph/badge.svg)](https://codecov.io/gh/nodes-vapor/jwt-keychain)
[![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/nodes-vapor/jwt-keychain/master/LICENSE)


This package aims to provide developer with an easy way to scaffhold their API
using a JWT Keychain.

**ATTENTION:** This is a very raw experiment that needs to be tested and validated.

#Installation

#### Config
Update your `Package.swift` file.
```swift
.Package(url: "https://github.com/nodes-vapor/jwt-keychain", majorVersion: 0)
```

Create config jwt.json

```
{
"secondsToExpire": 3600,
"signatureKey": "our-little-secret"
}
```

### main.swift

```
import Auth
import JWTKeychain
```

Add the AuthMiddleware with the User model

```swift
drop.middleware.append(AuthMiddleware<User>())
```

Add JWTAuthMiddleware to your API groups

```swift
drop.group(JWTAuthMiddleware(drop: drop)) { jwtRoutes in
//Routes
}
```

This package also provides a User model and some user endpoints that can be used out of the box.

To register the existing user routes, add this to the main.swift
```swift
// Setup routes
UserRoutes().register(drop: drop)
```

The aim is to encode the user identifier on the SubjectClaim of the JWT. This way we don't
need to keep track of the user's tokens on the database. The tokens generated are signed by
the key setup on the config file.

We just need to verify the token signature and its claims.

Currently provided endpoints are:

- Login: `POST api/v1/users/login`
- Register: `POST api/v1/users`
- Logout: `GET api/v1/users/logout`
- Token regenerate: `GET api/v1/users/token/regenerate`
- Me: `GET api/v1/users/me`
110 changes: 110 additions & 0 deletions Sources/JWTKeychain/Controllers/Api/UsersController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import Vapor
import Auth
import Foundation
import HTTP
import Turnstile
import TurnstileCrypto
import TurnstileWeb
import VaporForms



/// Controller for user api requests
open class UsersController {


/// Registers a user on the DB
///
/// - Parameter request: current request
/// - Returns: JSON response with User data
/// - Throws: on invalid data or if unable to store data on the DB
func register(request: Request) throws -> ResponseRepresentable {

do{

// Validate request
let requestData = try StoreRequest(validating: request.data)

var user = User(
name: requestData.name,
email: requestData.email,
password: requestData.password
)

try user.save()

return try user.makeJSON(withToken: true)

}catch FormError.validationFailed(let fieldset) {
throw Abort.custom(status: Status.preconditionFailed, message: "Invalid data: \(fieldset.errors)")
}catch {
throw Abort.custom(status: Status.unprocessableEntity, message: "Could not create user")
}

}


/// Logins the user on the system, giving the token back
///
/// - Parameter request: current request
/// - Returns: JSON response with User data
/// - Throws: on invalid data or wrong credentials
func login(request: Request) throws -> ResponseRepresentable {

// Get our credentials
guard let email = request.data["email"]?.string, let password = request.data["password"]?.string else {
throw Abort.custom(status: Status.preconditionFailed, message: "Missing email or password")
}

let credentials = EmailPassword(email: email, password: password)

do {

try request.auth.login(credentials)

return try request.user().makeJSON()

} catch _ {

throw Abort.custom(status: Status.preconditionFailed, message: "Invalid email or password")

}
}


/// Logs the user out of the system
///
/// - Parameter request: current request
/// - Returns: JSON success response
/// - Throws: if not able to find token
func logout(request: Request) throws -> ResponseRepresentable {

// Clear the session
request.subject.logout()

return try JSON(node: ["success": true])
}


/// Generates a new token for the user
///
/// - Parameter request: current request
/// - Returns: JSON with token
/// - Throws: if not able to generate token
func regenerate(request: Request) throws -> ResponseRepresentable {
let user = try request.user()

return try JSON(node: ["token": user.generateToken()])
}


/// Returns the authenticated user data
///
/// - Parameter request: current request
/// - Returns: JSON response with User data
/// - Throws: on no user found
func me(request: Request) throws -> ResponseRepresentable {
return try request.user().makeJSON()
}

}
53 changes: 53 additions & 0 deletions Sources/JWTKeychain/Extensions/Request+User.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import Vapor
import HTTP
import Auth
import Turnstile


// MARK: - User and token functionality
extension Request {

// Base URL returns the hostname, scheme, and port in a URL string form.
var baseURL: String {
return uri.scheme + "://" + uri.host + (uri.port == nil ? "" : ":\(uri.port!)")
}

// Exposes the Turnstile subject, as Vapor has a facade on it.
var subject: Subject {
return storage["subject"] as! Subject
}

/// A helper method to retrieve the authenticated user
///
/// - Returns: Authenticated user
/// - Throws: UnsupportedCredentialsError
func user() throws -> User {

// Try to retrieve authenticated user
guard let user = try auth.user() as? User else {
throw UnsupportedCredentialsError()
}
return user
}


/// Retrieves the access token from the current
/// request authorization header
///
/// - Returns: AccessToken
/// - Throws: No authorization header or invalid bearer authorization
func getAuthorizationBearerToken() throws -> AccessToken {

// Try to get the authorization header
guard let authHeader = self.auth.header else {
throw Auth.AuthError.noAuthorizationHeader
}

// Try to retrieve the bearer token
guard let bearer = authHeader.bearer else {
throw Auth.AuthError.invalidBearerAuthorization
}

return bearer
}
}
52 changes: 52 additions & 0 deletions Sources/JWTKeychain/Extensions/User+JWT.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import Vapor
import Fluent
import Foundation
import Auth
import VaporJWT
import Core
import HTTP

extension User {

/// Generates a token for the user
///
/// - Returns: string with valid token
/// - Throws: unable to generate token
public func generateToken() throws -> String{

// Prepare payload Node
var payload: Node

// Prepare contents for payload
var contents: [Claim] = []

// Create a claim with user ID
guard let userId = self.id else {
throw Abort.custom(status: .internalServerError, message: "Cannot generate tokens for unexisting users")
}

let subClaim = SubjectClaim(String(describing: userId))

contents.append(subClaim)

// Prepare expiration claim if needed
if Configuration.secondsToExpire! > 0 {

contents.append(ExpirationTimeClaim(try Configuration.generateExpirationDate()))

}

payload = Node(contents)

// Generate our Token
let jwt = try JWT(
payload: payload,
signer: HS256(key: Configuration.getTokenSignatureKey())
)

// Return the token string
return try jwt.createToken()

}

}
55 changes: 55 additions & 0 deletions Sources/JWTKeychain/Middleware/JWTAuthMiddleware.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import Vapor
import HTTP
import Turnstile
import Auth
import VaporJWT

/// Middleware to extract and authorize a user via
/// Authorization Bearer Token + JWT
class JWTAuthMiddleware: Middleware {

/// Initiates the middleware logic
///
/// - Parameters:
/// - request: current request
/// - next: next middleware to execute in the chain
/// - Returns: response from the next middleware in the chain
/// - Throws: Unauthorized if auth fails or bad request if authorization is not set
func respond(to request: Request, chainingTo next: Responder) throws -> Response {

// Authorization: Bearer Token
do{

let bearer = try request.getAuthorizationBearerToken()

// Verify the token
if try Configuration.validateToken(token: bearer.string) {

try? request.auth.login(bearer, persist: false)

} else {
throw Abort.custom(
status: .unauthorized,
message: "Please reauthenticate with the server."
)
}

} catch AuthError.noAuthorizationHeader {

throw Abort.custom(
status: .badRequest,
message: "Authorization header not set."
)

} catch AuthError.invalidBearerAuthorization {

throw Abort.custom(
status: .unauthorized,
message: "Invalid bearer token"
)

}

return try next.respond(to: request)
}
}
Loading

0 comments on commit 7763495

Please sign in to comment.