From 717a1bc9c382cae03b9d2255a5d7f8ed34beb23f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Thu, 27 Sep 2018 10:47:27 -0400 Subject: [PATCH 1/4] Updates Swift 4.2 Newest Vapor Packages are compatabile now --- Dockerfile | 2 +- Sources/App/Configuration/configure.swift | 8 ++++--- Sources/App/Configuration/router.swift | 7 ++++-- Sources/App/Controllers/AuthController.swift | 25 ++++++++++---------- Sources/App/Models/AccessToken.swift | 10 ++++---- Sources/App/Models/Attribute/Attribute.swift | 1 + Sources/App/Models/User/User.swift | 1 + 7 files changed, 31 insertions(+), 23 deletions(-) diff --git a/Dockerfile b/Dockerfile index a95d196..b75eb83 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM swift:4.1 +FROM swift:4.2 ARG ENVIRONMENT ENV ENVIRONMENT ${ENVIRONMENT:-production} diff --git a/Sources/App/Configuration/configure.swift b/Sources/App/Configuration/configure.swift index 6628b97..e6c921e 100644 --- a/Sources/App/Configuration/configure.swift +++ b/Sources/App/Configuration/configure.swift @@ -41,9 +41,11 @@ public func configure( try services.register(jwtProvider) /// Register routes to the router - let router = EngineRouter.default() - try routes(router) - services.register(router, as: Router.self) + services.register(Router.self) { container -> EngineRouter in + let router = EngineRouter.default() + try routes(router, container) + return router + } /// Register middleware var middlewares = MiddlewareConfig() // Create _empty_ middleware config diff --git a/Sources/App/Configuration/router.swift b/Sources/App/Configuration/router.swift index 732ca95..3c550e2 100644 --- a/Sources/App/Configuration/router.swift +++ b/Sources/App/Configuration/router.swift @@ -1,16 +1,19 @@ import Routing import Vapor +import JWTMiddleware /// Register your application's routes here. /// /// [Learn More →](https://docs.vapor.codes/3.0/getting-started/structure/#routesswift) -public func routes(_ router: Router) throws { +public func routes(_ router: Router, _ container: Container) throws { // Create a 'health' route useed by AWS to check if the server needs a re-boot. router.get(any, "users", "health") { _ in return "all good" } - try router.register(collection: AuthController()) + let jwtService = try container.make(JWTService.self) + + try router.register(collection: AuthController(jwtService: jwtService)) try router.register(collection: VersionedCollection()) } diff --git a/Sources/App/Controllers/AuthController.swift b/Sources/App/Controllers/AuthController.swift index 502a6a1..157bf28 100644 --- a/Sources/App/Controllers/AuthController.swift +++ b/Sources/App/Controllers/AuthController.swift @@ -10,6 +10,12 @@ import JWT /// A route controller that handles user authentication with JWT. final class AuthController: RouteCollection { + private let jwtService:JWTService + + init(jwtService: JWTService) { + self.jwtService = jwtService + } + func boot(router: Router) throws { let auth = router.grouped(any, "users") @@ -131,13 +137,10 @@ final class AuthController: RouteCollection { /// A route handler that returns a new access and refresh token for the user. func refreshAccessToken(_ request: Request)throws -> Future<[String: String]> { - let signer = try request.make(JWTService.self) - // Get refresh token from request body and verify it. let refreshToken = try request.content.syncGet(String.self, at: "refreshToken") - let refreshJWT = try JWT(from: refreshToken, verifiedUsing: signer.signer) - try refreshJWT.payload.verify() - + let refreshJWT = try JWT(from: refreshToken, verifiedUsing: self.jwtService.signer) + try refreshJWT.payload.verify(using: self.jwtService.signer) // Get the user with the ID that was just fetched. let userID = refreshJWT.payload.id let user = User.find(userID, on: request).unwrap(or: Abort(.badRequest, reason: "No user found with ID '\(userID)'.")) @@ -148,12 +151,12 @@ final class AuthController: RouteCollection { // Construct the new access token payload let payload = try App.Payload(user: user) - return try request.payloadData(signer.sign(payload), with: ["userId": "\(user.requireID())"], as: JSON.self).and(result: payload) + return try request.payloadData(self.jwtService.sign(payload), with: ["userId": "\(user.requireID())"], as: JSON.self).and(result: payload) }.map(to: [String: String].self) { payloadData in let payload = try payloadData.0.merge(payloadData.1.json()) // Return the signed token with a success status. - let token = try signer.sign(payload) + let token = try self.jwtService.sign(payload) return ["status": "success", "accessToken": token] } } @@ -179,15 +182,13 @@ final class AuthController: RouteCollection { /// The actual authentication is handled by the `JWTAuthenticatableMiddleware`. /// The request's body should contain an email and a password for authenticating. func login(_ request: Request)throws -> Future { - let signer = try request.make(JWTService.self) - let user = try request.requireAuthenticated(User.self) let userPayload = try Payload(user: user) // Create a payload using the standard data // and the data from the registered `DataService`s let remotePayload = try request.payloadData( - signer.sign(userPayload), + self.jwtService.sign(userPayload), with: ["userId": "\(user.requireID())"], as: JSON.self ) @@ -196,8 +197,8 @@ final class AuthController: RouteCollection { return remotePayload.map(to: LoginResponse.self) { remotePayload in let payload = try remotePayload.merge(userPayload.json()) - let accessToken = try signer.sign(payload) - let refreshToken = try signer.sign(RefreshToken(user: user)) + let accessToken = try self.jwtService.sign(payload) + let refreshToken = try self.jwtService.sign(RefreshToken(user: user)) guard user.confirmed else { throw Abort(.badRequest, reason: "User not activated.") } diff --git a/Sources/App/Models/AccessToken.swift b/Sources/App/Models/AccessToken.swift index c94e208..818df7a 100644 --- a/Sources/App/Models/AccessToken.swift +++ b/Sources/App/Models/AccessToken.swift @@ -30,9 +30,9 @@ struct Payload: PermissionedUserPayload { self.id = try user.requireID() } - func verify() throws { + func verify(using signer: JWTSigner) throws { let expiration = Date(timeIntervalSince1970: self.exp) - try ExpirationClaim(value: expiration).verify() + try ExpirationClaim(value: expiration).verifyNotExpired() } } @@ -50,14 +50,14 @@ struct RefreshToken: IdentifiableJWTPayload { self.exp = now + expiration } - func verify() throws { + func verify(using signer: JWTSigner) throws { let expiration = Date(timeIntervalSince1970: self.exp) - try ExpirationClaim(value: expiration).verify() + try ExpirationClaim(value: expiration).verifyNotExpired() } } extension JSON: JWTPayload { - public func verify() throws { + public func verify(using signer: JWTSigner) throws { // Don't do anything // We only conform to `JWTPayload` // so we can sign a JWT with JSON as diff --git a/Sources/App/Models/Attribute/Attribute.swift b/Sources/App/Models/Attribute/Attribute.swift index 6b4e15e..aeb5927 100644 --- a/Sources/App/Models/Attribute/Attribute.swift +++ b/Sources/App/Models/Attribute/Attribute.swift @@ -3,6 +3,7 @@ import Vapor /// An attribute for a `User` to store custom data.. final class Attribute: Content, MySQLModel, Migration, Parameter { + static let entity: String = "attributes" /// The database ID of a class instance. var id: Int? diff --git a/Sources/App/Models/User/User.swift b/Sources/App/Models/User/User.swift index 1d3a962..fefbebb 100644 --- a/Sources/App/Models/User/User.swift +++ b/Sources/App/Models/User/User.swift @@ -13,6 +13,7 @@ import Vapor /// A generic user that can be conected to any service that uses JWT for authentication. final class User: Content, MySQLModel, Migration, Parameter { + static let entity: String = "users" /// The database ID of the class instance. var id: Int? From 1cd4411a99a3bee24c80adeaee3834f5f79e86da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Thu, 27 Sep 2018 10:49:12 -0400 Subject: [PATCH 2/4] Updates README --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index f03987c..c7e0f44 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ The names of the environment variables are the same ones used by Vapor Cloud, so ### JWT -You will also need to create an environment variable named `JWT_SECRET` with the `n` value of the JWK to verify access tokens. This service also signs the access tokens, so you will need another environment variable (called `USER_JWT_D` by default, but that can be changed) that contains the `d` value of the JWK. +You will also need to create an environment variable named `JWT_PUBLIC` with the `n` value of the JWK to verify access tokens. This service also signs the access tokens, so you will need another environment variable (called `JWT_SECRET` by default, but that can be changed) that contains the `d` value of the JWK. ### Email @@ -132,22 +132,22 @@ The easiest way to start the service is by using the Dockerfile. The following c ```bash # Note that you still need to setup the database before this step. docker build . -t users -docker run -e JWT_SECRET='n-value-from-jwt' \ +docker run -e JWT_PUBLIC='n-value-from-jwt' \ -e DATABASE_HOSTNAME='localhost' \ -e DATABASE_USER='users_service' \ -e DATABASE_PASSWORD='users_service' \ -e DATABASE_DB='users_service' \ --e USER_JWT_D='d-value-from-jwt' -p 8080:8080 users +-e JWT_SECRET='d-value-from-jwt' -p 8080:8080 users ``` If you want to run the server without docker the following summary of ENV variables that are needed may be helpful: ```bash -export JWT_SECRET='n-value-from-jwt' +export JWT_PUBLIC='n-value-from-jwt' export DATABASE_HOSTNAME='localhost' export DATABASE_USER='users_service' export DATABASE_PASSWORD='users_service' export DATABASE_DB='users_service' -export USER_JWT_D='d-value-from-jwt' +export JWT_SECRET='d-value-from-jwt' ``` ## More Information about JWT and JWKS From 52fa5e555df695bb5431b0bcdf988275faa51573 Mon Sep 17 00:00:00 2001 From: Ubuntu Deploy Date: Thu, 27 Sep 2018 15:00:53 +0000 Subject: [PATCH 3/4] updates --- Dockerfile | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index a95d196..5da17f2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM swift:4.1 +FROM swift:4.2 ARG ENVIRONMENT ENV ENVIRONMENT ${ENVIRONMENT:-production} @@ -19,6 +19,12 @@ WORKDIR /root/vapor RUN cd /root/vapor && rm -rf .build RUN swift package update RUN swift build --configuration release -EXPOSE 8080 +#EXPOSE 8080 +RUN export JWT_SECRET='ohFcON5JWImUWAa-SCC2yYOsFwlHwj3ZBOqpDNFX6JbOqkGrSaGjQWkieAj1fJhuYpTQq7A__s0G6yujmnE6N-I9UHEqXmKxI87ek9z5uxhzIeIHBS6ToyoXHECMS_jN8MbsM4bjec7FLuO9bVNJALFmCgEwcSzZdP9zFHjlj32ATWuSwXbNHNAJnk2IUk2eYiMNiG1BzZM8OApsCF1ASa9zcXdm2QYtOat7hhP-Uo6y_zflx9Ahg-CUBqPTpfOUUuJoGjeWgbhy0-ISveueGjzj7x5UYKNCRZyCircJ_-v51wFvx1lbgRmqH4eJy0dh8Ra-zmzLsFCDs2Akz8Oy0Q' +RUN export DATABASE_HOSTNAME='users.cpzpcvtsi0py.us-east-1.rds.amazonaws.com' +RUN export DATABASE_USER='users' +RUN export DATABASE_PASSWORD='k3AjY.eHcPVWxWM' +RUN export DATABASE_DB='users' +RUN export USER_JWT_D='IiLd9ex8LnXsFQ52jeK2HYPqf3-o6bT1PR_gM570kT0SkrH6TiwJowFuDTJ14qSIu6L0wPUCxbyRtH8gmqs2xAaXO5Zagj7vaMduAl8NCud_eKePKvxAhKGc9Ip0ApyJZCnCHqhOyZ1P0yyM_bYJLmgvQfQ2K-ByfT5BExLT54EFwUJ63tPQiU0gyycDULZAGTQBPzJNB5yWrVFW6s_VPZo73wd_4r86VErMeMgT0u4Nb5FihOcCjsdHt8X43oU4sf-YnHdzO7reHS8g11JLHrWL_sQlrC-gtJFq88UTzsevdsziDTByuB-Kf8cPATXPhTaisEb-TuURR_61wGLbQQ' CMD .build/release/Run --hostname=0.0.0.0 --port=8080 From d2889466ced3d04d89512fd2cd24eaf775219f02 Mon Sep 17 00:00:00 2001 From: Ralph Kuepper Date: Mon, 26 Nov 2018 11:15:38 -0500 Subject: [PATCH 4/4] Corrected dependency --- Sources/App/Models/User/User+HTTP.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/App/Models/User/User+HTTP.swift b/Sources/App/Models/User/User+HTTP.swift index c5ef08a..e11f275 100644 --- a/Sources/App/Models/User/User+HTTP.swift +++ b/Sources/App/Models/User/User+HTTP.swift @@ -30,7 +30,7 @@ extension User: BasicJWTAuthenticatable { /// The key-path for the property to check against `AuthBody.username` /// when fetching the user form the database to authenticate. - static var usernameKey: KeyPath { + static var usernameKey: WritableKeyPath { return \.email }