This repository has been archived by the owner on Apr 20, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1 from nodes-vapor/develop
Develop
- Loading branch information
Showing
21 changed files
with
1,054 additions
and
0 deletions.
There are no files selected for viewing
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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), | ||
] | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
110
Sources/JWTKeychain/Controllers/Api/UsersController.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
|
||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
Oops, something went wrong.