From d802013dc01af717174c8df554163a122f1cb2a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlio=20Cotta?= Date: Thu, 1 Jun 2017 19:30:15 -0300 Subject: [PATCH 1/2] includes JWT provider --- Config/jwt.json | 7 ++++ Package.pins | 10 ++++- Package.swift | 2 +- README.md | 40 ++++++++++++++++++++ Sources/App/Config+Setup.swift | 4 ++ Sources/App/Controllers/UserController.swift | 9 +++-- Sources/App/Droplet+JWT.swift | 20 ++++++++++ Sources/App/Droplet+Setup.swift | 2 +- Sources/App/Models/User.swift | 25 ++++++++++++ Sources/App/Routes.swift | 16 +++++--- 10 files changed, 122 insertions(+), 13 deletions(-) create mode 100644 Config/jwt.json create mode 100644 Sources/App/Droplet+JWT.swift diff --git a/Config/jwt.json b/Config/jwt.json new file mode 100644 index 0000000..f948f15 --- /dev/null +++ b/Config/jwt.json @@ -0,0 +1,7 @@ +{ + "signer": { + "type": "rsa", + "key": "MIIEowIBAAKCAQEA2MBhga9j06SCZcYr9JLfQg3TnUZnJfN6fl5c/nAoTx8kv/iVP2EtxQt89KcXyKNLnl2z5HQfQgAGnioPkDHcqd2XVyNxxXa2DAtfL34EeNnl/4gV6WXPw6hZ8s2X+OkTTTStqdxfgtxXCKbRRcOBTJa/F3eY9jBW16jujygkyt+WwD28XcQSs4enLhHOyljslhLv6ebqJ3cNI7+j3St4JYmB+SAAK2di1tzU2OaMlcUo7bEtyqzDiZKsXBtLKTBVQ9TbwFiFQ+9IfkegOMEC7P02N9iQ/11urMU/rTrb6fKB6/G7KfFCQhWj+QKZGewXC+PYOMYJgUMk1WRsDcQScwIDAQABAoIBAF1Hja7t+Bwg9C0wd8ItYv9eS++nWMSwX8r6eTLWucIzOPGU3UYFYFkodIIlVsr125kv4jcy8jDJKg/vMftwOfKwdmz9x/ye9gGA81nQ9cO8ooqx2hwzwJIHZY5khD6Or8vOG966BDCg+qOyhuVrGb4IMfy7b4yjiPwOq3vYXt0fSU/zA8U6akCFrIAFICH1rmWqwSsz9CtN5Bz261+seqEtEFLRDtdSxzrYEn1GnWni8DayOaB35fHdDuVRsaxKtSfQ7yIOrPXlRkqTe/sNz891TlwS0G6NHas6E+ZPQljtGBef/AIrtTEHmUdc5wXS0Mmgu+yvzSgVRntNhfX4QiECgYEA7ugr2NDDRgg7el0CspJAzPWH/JotoVb715lZJJ8tbu7NTQfL6bnUzQj6kF78DXOyXYp11lTiEpRzLOKXUKy1VdJ5Du0XkKC/ogR4/5QEdk7RZ1In9DwdE2AXX57hUri3ObMsVpF0+nZWmF1wRs+JYUh2fTBeo1ZWPA/DvHFGTTcCgYEA6EJo60gug7z6zVBorZBgTGZCfg8DwW7CvTGQ785sK5TYcKHgXM2PQhDXEqai1rNoIgmRYtYHDDeS39kR6UuJCRPZbnkDwEcXbpCE5rqmTa+7yO4s0E5di57z9Jos31jPqMFbYU73cHPy8XnlMD8KiHwR/krGgacukK+pdfXlIqUCgYBnlVSFgiZYc/NN34vu3sin1QEr/bExFeTFmuByp21sfq+W6X15DjB84Zq6A+Tm9DXuprzmvBD1G1ZArNIMkYVh+4qvdQ7Vj0znM2c+8O9qWEwkrxNRqsq0fuJDfECXvCz9IHll41VDzxFGqKSonw0il+d/6fvud92V1wP37WkcywKBgQChfKM0jBCDWl9LZ9AQdaTvGd67hTcIRDm0kAUFJ5JATxKaZYL5I5eqyMixWBk6jK0nlV13yfZGgVFmwKfafMGABUQVsqBwDT32ixdM0ZQVyc0YLLoN757NGCzo8lWmyTpBTId7xgr3LjdJvIYlIH/zW8iq9VTGCvaudOSvdtPlXQKBgHmHsipMpFlZeJDtFnlyAGJTte/lowVN9rHm8q8gYioWgLaCxY2bqQRlBzCiVnvhBfKY04QbvwGMBriFhQisaV/0tdr7NgTVSPbFog3+LmlZ7EGGoYgXqmyHAPibHAScbdHrjOGP3lPasJmKqtVgN11dtJoNRj8GkQle2s1Hljnf", + "algorithm": "rs256" + } +} diff --git a/Package.pins b/Package.pins index c33419f..e3f27ea 100644 --- a/Package.pins +++ b/Package.pins @@ -85,6 +85,12 @@ "repositoryURL": "https://github.com/vapor/jwt.git", "version": "2.1.0" }, + { + "package": "JWTProvider", + "reason": null, + "repositoryURL": "https://github.com/vapor/jwt-provider.git", + "version": "1.0.0" + }, { "package": "Multipart", "reason": null, @@ -125,13 +131,13 @@ "package": "TLS", "reason": null, "repositoryURL": "https://github.com/vapor/tls.git", - "version": "2.0.1" + "version": "2.0.3" }, { "package": "Vapor", "reason": null, "repositoryURL": "https://github.com/vapor/vapor.git", - "version": "2.0.4" + "version": "2.0.5" } ], "version": 1 diff --git a/Package.swift b/Package.swift index 4e15180..92bcd76 100644 --- a/Package.swift +++ b/Package.swift @@ -10,7 +10,7 @@ let package = Package( .Package(url: "https://github.com/vapor/vapor.git", majorVersion: 2), .Package(url: "https://github.com/vapor/fluent-provider.git", majorVersion: 1), .Package(url: "https://github.com/vapor/auth-provider.git", majorVersion: 1), - .Package(url: "https://github.com/vapor/jwt.git", majorVersion: 2) + .Package(url: "https://github.com/vapor/jwt-provider.git", majorVersion: 1) ], exclude: [ "Config", diff --git a/README.md b/README.md index 9c707cf..094cb54 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,43 @@ + +## Generate JWT key +``` +ssh-keygen -t rsa -b 2048 -f jwtRS256.key +# Don't add passphrase +openssl rsa -in jwtRS256.key -pubout -outform PEM -out jwtRS256.key.pub +cat jwtRS256.key +``` +It will print something like this +``` +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEA2MBhga9j06SCZcYr9JLfQg3TnUZnJfN6fl5c/nAoTx8kv/iV +P2EtxQt89KcXyKNLnl2z5HQfQgAGnioPkDHcqd2XVyNxxXa2DAtfL34EeNnl/4gV +6WXPw6hZ8s2X+OkTTTStqdxfgtxXCKbRRcOBTJa/F3eY9jBW16jujygkyt+WwD28 +XcQSs4enLhHOyljslhLv6ebqJ3cNI7+j3St4JYmB+SAAK2di1tzU2OaMlcUo7bEt +yqzDiZKsXBtLKTBVQ9TbwFiFQ+9IfkegOMEC7P02N9iQ/11urMU/rTrb6fKB6/G7 +KfFCQhWj+QKZGewXC+PYOMYJgUMk1WRsDcQScwIDAQABAoIBAF1Hja7t+Bwg9C0w +d8ItYv9eS++nWMSwX8r6eTLWucIzOPGU3UYFYFkodIIlVsr125kv4jcy8jDJKg/v +MftwOfKwdmz9x/ye9gGA81nQ9cO8ooqx2hwzwJIHZY5khD6Or8vOG966BDCg+qOy +huVrGb4IMfy7b4yjiPwOq3vYXt0fSU/zA8U6akCFrIAFICH1rmWqwSsz9CtN5Bz2 +61+seqEtEFLRDtdSxzrYEn1GnWni8DayOaB35fHdDuVRsaxKtSfQ7yIOrPXlRkqT +e/sNz891TlwS0G6NHas6E+ZPQljtGBef/AIrtTEHmUdc5wXS0Mmgu+yvzSgVRntN +hfX4QiECgYEA7ugr2NDDRgg7el0CspJAzPWH/JotoVb715lZJJ8tbu7NTQfL6bnU +zQj6kF78DXOyXYp11lTiEpRzLOKXUKy1VdJ5Du0XkKC/ogR4/5QEdk7RZ1In9Dwd +E2AXX57hUri3ObMsVpF0+nZWmF1wRs+JYUh2fTBeo1ZWPA/DvHFGTTcCgYEA6EJo +60gug7z6zVBorZBgTGZCfg8DwW7CvTGQ785sK5TYcKHgXM2PQhDXEqai1rNoIgmR +YtYHDDeS39kR6UuJCRPZbnkDwEcXbpCE5rqmTa+7yO4s0E5di57z9Jos31jPqMFb +YU73cHPy8XnlMD8KiHwR/krGgacukK+pdfXlIqUCgYBnlVSFgiZYc/NN34vu3sin +1QEr/bExFeTFmuByp21sfq+W6X15DjB84Zq6A+Tm9DXuprzmvBD1G1ZArNIMkYVh ++4qvdQ7Vj0znM2c+8O9qWEwkrxNRqsq0fuJDfECXvCz9IHll41VDzxFGqKSonw0i +l+d/6fvud92V1wP37WkcywKBgQChfKM0jBCDWl9LZ9AQdaTvGd67hTcIRDm0kAUF +J5JATxKaZYL5I5eqyMixWBk6jK0nlV13yfZGgVFmwKfafMGABUQVsqBwDT32ixdM +0ZQVyc0YLLoN757NGCzo8lWmyTpBTId7xgr3LjdJvIYlIH/zW8iq9VTGCvaudOSv +dtPlXQKBgHmHsipMpFlZeJDtFnlyAGJTte/lowVN9rHm8q8gYioWgLaCxY2bqQRl +BzCiVnvhBfKY04QbvwGMBriFhQisaV/0tdr7NgTVSPbFog3+LmlZ7EGGoYgXqmyH +APibHAScbdHrjOGP3lPasJmKqtVgN11dtJoNRj8GkQle2s1Hljnf +-----END RSA PRIVATE KEY----- +``` +Remove the BEGIN and END delimiters and the line breaks before adding it to jwt.json +

MySQL
diff --git a/Sources/App/Config+Setup.swift b/Sources/App/Config+Setup.swift index 2c9e295..9dcd176 100644 --- a/Sources/App/Config+Setup.swift +++ b/Sources/App/Config+Setup.swift @@ -1,4 +1,6 @@ import FluentProvider +import AuthProvider +import JWTProvider extension Config { public func setup() throws { @@ -13,6 +15,8 @@ extension Config { /// Configure providers private func setupProviders() throws { try addProvider(FluentProvider.Provider.self) + try addProvider(AuthProvider.Provider.self) + try addProvider(JWTProvider.Provider.self) } /// Add all models that should have their diff --git a/Sources/App/Controllers/UserController.swift b/Sources/App/Controllers/UserController.swift index 9905e55..ad117db 100644 --- a/Sources/App/Controllers/UserController.swift +++ b/Sources/App/Controllers/UserController.swift @@ -12,6 +12,10 @@ import AuthProvider import JWT final class UserController { + var droplet: Droplet + init(_ droplet: Droplet) { + self.droplet = droplet + } func register(request: Request) throws -> ResponseRepresentable { // Get our credentials @@ -70,10 +74,7 @@ final class UserController { // MARK: JWT Token Generation func generateJWTToken(_ userId: Int) throws -> String { - let time = ExpirationTimeClaim(date: Date().addingTimeInterval(60 * 5)) // 5 minutes - let payload: [Claim] = [time, SubjectClaim(string: "\(userId)")] - let jwt = try JWT(payload: JSON(payload), signer: HS256(key: "SIGNING_KEY".makeBytes())) - return try jwt.createToken() + return try droplet.createJwtToken(String(userId)) } } diff --git a/Sources/App/Droplet+JWT.swift b/Sources/App/Droplet+JWT.swift new file mode 100644 index 0000000..f68045e --- /dev/null +++ b/Sources/App/Droplet+JWT.swift @@ -0,0 +1,20 @@ +import Vapor +import HTTP +import JWT + + +extension Droplet { + func createJwtToken(_ userId: String) throws -> String { + + guard let sig = self.signer else { + throw Abort.unauthorized + } + + let timeToLive = Seconds(5 * 60) // 5 min + let claims:[Claim] = [ExpirationTimeClaim(date: Date(), leeway: timeToLive), SubjectClaim(string: userId)] + let payload = JSON(claims) + let jwt = try JWT(payload: payload, signer: sig) + + return try jwt.createToken() + } +} diff --git a/Sources/App/Droplet+Setup.swift b/Sources/App/Droplet+Setup.swift index 886c6d6..3a5c1ae 100644 --- a/Sources/App/Droplet+Setup.swift +++ b/Sources/App/Droplet+Setup.swift @@ -4,6 +4,6 @@ extension Droplet { public func setup() throws { // Do any additional droplet setup - try collection(GenealRoutes.self) + try collection(GeneralRoutes(self)) } } diff --git a/Sources/App/Models/User.swift b/Sources/App/Models/User.swift index a144156..ea04c6e 100644 --- a/Sources/App/Models/User.swift +++ b/Sources/App/Models/User.swift @@ -9,6 +9,7 @@ import Vapor import FluentProvider import AuthProvider +import JWTProvider import JWT import HTTP @@ -137,6 +138,30 @@ extension User: TokenAuthenticatable { } } + +extension SubjectClaim: JSONInitializable { + public init(json: JSON) throws { + guard let value = try json.get(SubjectClaim.name) as String? else { + throw AuthenticationError.invalidCredentials + } + self.value = value + } +} +extension User: PayloadAuthenticatable { + typealias PayloadType = SubjectClaim + static func authenticate(_ payload: SubjectClaim) throws -> User { + let userId = payload.value + guard let user = try User.makeQuery() + .filter(idKey, userId) + .first() + else { + throw AuthenticationError.invalidCredentials + } + + return user + } +} + // MARK: JSON extension User: JSONConvertible { diff --git a/Sources/App/Routes.swift b/Sources/App/Routes.swift index dae81b8..91623f5 100644 --- a/Sources/App/Routes.swift +++ b/Sources/App/Routes.swift @@ -1,21 +1,27 @@ import Vapor import HTTP import AuthProvider +import JWTProvider -final class GenealRoutes: RouteCollection { +final class GeneralRoutes: RouteCollection { + var droplet: Droplet + init(_ droplet: Droplet) { + self.droplet = droplet + } func build(_ builder: RouteBuilder) throws { let api = builder.grouped("api") let v1 = api.grouped("v1") - let userController = UserController() + let userController = UserController(self.droplet) v1.post("register", handler: userController.register) v1.post("login", handler: userController.login) v1.post("logout", handler: userController.logout) - let secured = v1.grouped(TokenAuthenticationMiddleware(User.self)) + //NOTE: TokenAuthenticationMiddleware should be used only to fluent token auth, not JWT + //let secured = v1.grouped(TokenAuthenticationMiddleware(User.self)) + let tokenMiddleware = PayloadAuthenticationMiddleware(self.droplet.signer!,[], User.self) + let secured = v1.grouped(tokenMiddleware) let users = secured.grouped("users") users.get("me", handler: userController.me) } } - -extension GenealRoutes: EmptyInitializable { } From c68def668e796d87de967d14e2a4f22c78f0ee14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlio=20Cotta?= Date: Thu, 1 Jun 2017 20:30:58 -0300 Subject: [PATCH 2/2] included expiration time claim --- Sources/App/Controllers/UserController.swift | 12 +++------ Sources/App/Droplet+JWT.swift | 8 ++++-- Sources/App/Models/User.swift | 27 +++++++++++++++----- 3 files changed, 29 insertions(+), 18 deletions(-) diff --git a/Sources/App/Controllers/UserController.swift b/Sources/App/Controllers/UserController.swift index ad117db..b399745 100644 --- a/Sources/App/Controllers/UserController.swift +++ b/Sources/App/Controllers/UserController.swift @@ -12,7 +12,7 @@ import AuthProvider import JWT final class UserController { - var droplet: Droplet + let droplet: Droplet init(_ droplet: Droplet) { self.droplet = droplet } @@ -38,7 +38,7 @@ final class UserController { } return try JSON(node: [ - "access_token": self.generateJWTToken(userId), + "access_token": try droplet.createJwtToken(String(userId)), "user": user ]) } @@ -56,7 +56,7 @@ final class UserController { } return try JSON(node: [ - "access_token": self.generateJWTToken(userId), + "access_token": try droplet.createJwtToken(String(userId)), "user": user ]) } @@ -71,10 +71,4 @@ final class UserController { return try request.user() } - // MARK: JWT Token Generation - - func generateJWTToken(_ userId: Int) throws -> String { - return try droplet.createJwtToken(String(userId)) - } - } diff --git a/Sources/App/Droplet+JWT.swift b/Sources/App/Droplet+JWT.swift index f68045e..1e34413 100644 --- a/Sources/App/Droplet+JWT.swift +++ b/Sources/App/Droplet+JWT.swift @@ -10,8 +10,12 @@ extension Droplet { throw Abort.unauthorized } - let timeToLive = Seconds(5 * 60) // 5 min - let claims:[Claim] = [ExpirationTimeClaim(date: Date(), leeway: timeToLive), SubjectClaim(string: userId)] + let timeToLive = 5 * 60.0 // 5 minutes + let claims:[Claim] = [ + ExpirationTimeClaim(date: Date().addingTimeInterval(timeToLive)), + SubjectClaim(string: userId) + ] + let payload = JSON(claims) let jwt = try JWT(payload: payload, signer: sig) diff --git a/Sources/App/Models/User.swift b/Sources/App/Models/User.swift index ea04c6e..abfb897 100644 --- a/Sources/App/Models/User.swift +++ b/Sources/App/Models/User.swift @@ -139,18 +139,31 @@ extension User: TokenAuthenticatable { } -extension SubjectClaim: JSONInitializable { - public init(json: JSON) throws { - guard let value = try json.get(SubjectClaim.name) as String? else { +class Claims: JSONInitializable { + var subjectClaimValue : String + var expirationTimeClaimValue : Double + public required init(json: JSON) throws { + guard let subjectClaimValue = try json.get(SubjectClaim.name) as String? else { throw AuthenticationError.invalidCredentials } - self.value = value + self.subjectClaimValue = subjectClaimValue + + guard let expirationTimeClaimValue = try json.get(ExpirationTimeClaim.name) as String? else { + throw AuthenticationError.invalidCredentials + } + self.expirationTimeClaimValue = Double(expirationTimeClaimValue)! + } } + extension User: PayloadAuthenticatable { - typealias PayloadType = SubjectClaim - static func authenticate(_ payload: SubjectClaim) throws -> User { - let userId = payload.value + typealias PayloadType = Claims + static func authenticate(_ payload: Claims) throws -> User { + if payload.expirationTimeClaimValue < Date().timeIntervalSince1970 { + throw AuthenticationError.invalidCredentials + } + + let userId = payload.subjectClaimValue guard let user = try User.makeQuery() .filter(idKey, userId) .first()