)] { get }
26 | }
27 |
28 | public protocol Randomizable {
29 | func randomize()
30 | }
31 |
32 | public struct TableSource {
33 | public init() {
34 | }
35 |
36 | public enum Availability {
37 | case onlyAuthenthicated
38 | case openAccess
39 | }
40 |
41 | public var description : String = ""
42 | public var availability : Availability = .openAccess
43 | public var viewController : () -> (UIViewController) = {
44 | return UIViewController()
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/SublimateUI/SublimateUI/Classes/UIStoryboard+Instantiate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIStoryboard+Instantiate.swift
3 | // SublimateUI
4 | //
5 | // ___ ____
6 | // / __)( _ \
7 | // ( (_ \ ) __/
8 | // \___/(__) gabrielepalma.name
9 | //
10 |
11 | import UIKit
12 |
13 | extension UIStoryboard {
14 | static public func instantiate(storyboard: String, identifier : String = String(describing: T.self)) -> T {
15 | guard let result = UIStoryboard(name: storyboard, bundle: Bundle(for: T.self)).instantiateViewController(withIdentifier: identifier) as? T else {
16 | fatalError("Fatal error when instantiating \(identifier) from storyboard \(storyboard).")
17 | }
18 | return result
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/SublimateUI/SublimateUI/Resources/ActionPanel.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
31 |
38 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
--------------------------------------------------------------------------------
/SublimateUI/SublimateUI/Resources/FieldCell.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
28 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/SublimateUI/SublimateUI/Resources/ObjectCell.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
28 |
37 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
--------------------------------------------------------------------------------
/SublimateUI/SublimateUI/Resources/SchemeCell.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
25 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/SublimateVapor/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 |
3 | jobs:
4 | linux:
5 | docker:
6 | - image: swift:4.1
7 | steps:
8 | - checkout
9 | - run:
10 | name: Compile code
11 | command: swift build
12 | - run:
13 | name: Run unit tests
14 | command: swift test
15 |
16 | linux-release:
17 | docker:
18 | - image: swift:4.1
19 | steps:
20 | - checkout
21 | - run:
22 | name: Compile code with optimizations
23 | command: swift build -c release
24 |
25 | workflows:
26 | version: 2
27 | tests:
28 | jobs:
29 | - linux
30 | - linux-release
31 |
32 | nightly:
33 | triggers:
34 | - schedule:
35 | cron: "0 0 * * *"
36 | filters:
37 | branches:
38 | only:
39 | - master
40 | jobs:
41 | - linux
42 | - linux-release
43 |
44 |
--------------------------------------------------------------------------------
/SublimateVapor/.dockerignore:
--------------------------------------------------------------------------------
1 | .git
2 | .build
3 | DerivedData
4 | Package.resolved
5 | *.xcodeproj
6 |
7 |
--------------------------------------------------------------------------------
/SublimateVapor/.gitignore:
--------------------------------------------------------------------------------
1 | #### Build generated
2 | build
3 | DerivedData
4 |
5 |
6 |
7 | #### Various settings
8 | *.pbxuser
9 | !default.pbxuser
10 | *.mode1v3
11 | !default.mode1v3
12 | *.mode2v3
13 | !default.mode2v3
14 | *.perspectivev3
15 | !default.perspectivev3
16 | xcuserdata/
17 |
18 |
19 |
20 | #### Other
21 | *.moved-aside
22 | *.xccheckout
23 | *.xcscmblueprint
24 |
25 |
26 |
27 | #### Obj-C/Swift specific
28 | *.hmap
29 | *.ipa
30 | *.dSYM.zip
31 | *.dSYM
32 |
33 |
34 |
35 | #### Playgrounds
36 |
37 | timeline.xctimeline
38 | playground.xcworkspace
39 |
40 |
41 |
42 | #### SublimateVapor, Package Manager
43 |
44 | Packages
45 | Package.pins
46 | .build
47 | xcuserdata
48 | *.xcodeproj
49 | DerivedData
50 | .DS_Store
51 |
52 | # SublimateVapor/Package.resolved
53 |
--------------------------------------------------------------------------------
/SublimateVapor/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "object": {
3 | "pins": [
4 | {
5 | "package": "Auth",
6 | "repositoryURL": "https://github.com/vapor/auth.git",
7 | "state": {
8 | "branch": null,
9 | "revision": "90868627c7587ea207c0b6d4054265e68f6a33ef",
10 | "version": "2.0.1"
11 | }
12 | },
13 | {
14 | "package": "Console",
15 | "repositoryURL": "https://github.com/vapor/console.git",
16 | "state": {
17 | "branch": null,
18 | "revision": "d6cf07af59ae63cd95c4b5f98cf1f25627750fd1",
19 | "version": "3.1.0"
20 | }
21 | },
22 | {
23 | "package": "Core",
24 | "repositoryURL": "https://github.com/vapor/core.git",
25 | "state": {
26 | "branch": null,
27 | "revision": "96ce86ebf9198328795c4b9cb711489460be083c",
28 | "version": "3.4.4"
29 | }
30 | },
31 | {
32 | "package": "Crypto",
33 | "repositoryURL": "https://github.com/vapor/crypto.git",
34 | "state": {
35 | "branch": null,
36 | "revision": "5605334590affd4785a5839806b4504407e054ac",
37 | "version": "3.3.0"
38 | }
39 | },
40 | {
41 | "package": "DatabaseKit",
42 | "repositoryURL": "https://github.com/vapor/database-kit.git",
43 | "state": {
44 | "branch": null,
45 | "revision": "3a17dbbe9be5f8c37703e4b7982c1332ad6b00c4",
46 | "version": "1.3.1"
47 | }
48 | },
49 | {
50 | "package": "Fluent",
51 | "repositoryURL": "https://github.com/vapor/fluent.git",
52 | "state": {
53 | "branch": null,
54 | "revision": "00b81a9362549facb8e2ac93b17d2a78599fce3b",
55 | "version": "3.1.2"
56 | }
57 | },
58 | {
59 | "package": "FluentSQLite",
60 | "repositoryURL": "https://github.com/vapor/fluent-sqlite.git",
61 | "state": {
62 | "branch": null,
63 | "revision": "c32f5bda84bf4ea691d19afe183d40044f579e11",
64 | "version": "3.0.0"
65 | }
66 | },
67 | {
68 | "package": "HTTP",
69 | "repositoryURL": "https://github.com/vapor/http.git",
70 | "state": {
71 | "branch": null,
72 | "revision": "6973bf50dab8dd00e4daf8cb82ca72b33f5db016",
73 | "version": "3.1.6"
74 | }
75 | },
76 | {
77 | "package": "JWT",
78 | "repositoryURL": "https://github.com/vapor/jwt.git",
79 | "state": {
80 | "branch": null,
81 | "revision": "2e225c722bf26407c1c4bd11d341e48759f46095",
82 | "version": "3.0.0"
83 | }
84 | },
85 | {
86 | "package": "Multipart",
87 | "repositoryURL": "https://github.com/vapor/multipart.git",
88 | "state": {
89 | "branch": null,
90 | "revision": "e57007c23a52b68e44ebdfc70cbe882a7c4f1ec3",
91 | "version": "3.0.2"
92 | }
93 | },
94 | {
95 | "package": "Routing",
96 | "repositoryURL": "https://github.com/vapor/routing.git",
97 | "state": {
98 | "branch": null,
99 | "revision": "3219e328491b0853b8554c5a694add344d2c6cfb",
100 | "version": "3.0.1"
101 | }
102 | },
103 | {
104 | "package": "Service",
105 | "repositoryURL": "https://github.com/vapor/service.git",
106 | "state": {
107 | "branch": null,
108 | "revision": "281a70b69783891900be31a9e70051b6fe19e146",
109 | "version": "1.0.0"
110 | }
111 | },
112 | {
113 | "package": "SQL",
114 | "repositoryURL": "https://github.com/vapor/sql.git",
115 | "state": {
116 | "branch": null,
117 | "revision": "839cf96eba296d26151ff1d7a746e9fe35053584",
118 | "version": "2.2.0"
119 | }
120 | },
121 | {
122 | "package": "SQLite",
123 | "repositoryURL": "https://github.com/vapor/sqlite.git",
124 | "state": {
125 | "branch": null,
126 | "revision": "ad2e9bc9f0ed00ef2c6a05f89c1cec605467c90f",
127 | "version": "3.1.0"
128 | }
129 | },
130 | {
131 | "package": "swift-nio",
132 | "repositoryURL": "https://github.com/apple/swift-nio.git",
133 | "state": {
134 | "branch": null,
135 | "revision": "a20e129c22ad00a51c902dca54a5456f90664780",
136 | "version": "1.12.0"
137 | }
138 | },
139 | {
140 | "package": "swift-nio-ssl",
141 | "repositoryURL": "https://github.com/apple/swift-nio-ssl.git",
142 | "state": {
143 | "branch": null,
144 | "revision": "db16c3a90b101bb53b26a58867a344ad428072e0",
145 | "version": "1.3.2"
146 | }
147 | },
148 | {
149 | "package": "swift-nio-ssl-support",
150 | "repositoryURL": "https://github.com/apple/swift-nio-ssl-support.git",
151 | "state": {
152 | "branch": null,
153 | "revision": "c02eec4e0e6d351cd092938cf44195a8e669f555",
154 | "version": "1.0.0"
155 | }
156 | },
157 | {
158 | "package": "swift-nio-zlib-support",
159 | "repositoryURL": "https://github.com/apple/swift-nio-zlib-support.git",
160 | "state": {
161 | "branch": null,
162 | "revision": "37760e9a52030bb9011972c5213c3350fa9d41fd",
163 | "version": "1.0.0"
164 | }
165 | },
166 | {
167 | "package": "TemplateKit",
168 | "repositoryURL": "https://github.com/vapor/template-kit.git",
169 | "state": {
170 | "branch": null,
171 | "revision": "aff2d6fc65bfd04579b0201b31a8d6720239c1cf",
172 | "version": "1.1.1"
173 | }
174 | },
175 | {
176 | "package": "URLEncodedForm",
177 | "repositoryURL": "https://github.com/vapor/url-encoded-form.git",
178 | "state": {
179 | "branch": null,
180 | "revision": "932024f363ee5ff59059cf7d67194a1c271d3d0c",
181 | "version": "1.0.5"
182 | }
183 | },
184 | {
185 | "package": "Validation",
186 | "repositoryURL": "https://github.com/vapor/validation.git",
187 | "state": {
188 | "branch": null,
189 | "revision": "4de213cf319b694e4ce19e5339592601d4dd3ff6",
190 | "version": "2.1.1"
191 | }
192 | },
193 | {
194 | "package": "Vapor",
195 | "repositoryURL": "https://github.com/vapor/vapor.git",
196 | "state": {
197 | "branch": null,
198 | "revision": "157d3b15336caa882662cc75024dd04b2e225246",
199 | "version": "3.1.0"
200 | }
201 | },
202 | {
203 | "package": "WebSocket",
204 | "repositoryURL": "https://github.com/vapor/websocket.git",
205 | "state": {
206 | "branch": null,
207 | "revision": "eb4277f75f1d96a3d15c852cdd89af1799093dcd",
208 | "version": "1.1.0"
209 | }
210 | }
211 | ]
212 | },
213 | "version": 1
214 | }
215 |
--------------------------------------------------------------------------------
/SublimateVapor/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:4.0
2 | import PackageDescription
3 |
4 | let package = Package(
5 | name: "SublimateVapor",
6 | dependencies: [
7 | .package(url: "https://github.com/vapor/vapor.git", from: "3.0.0"),
8 | .package(url: "https://github.com/vapor/fluent-sqlite.git", from: "3.0.0"),
9 | .package(url: "https://github.com/vapor/jwt.git", from: "3.0.0"),
10 | .package(url: "https://github.com/vapor/multipart.git", from: "3.0.0"),
11 | .package(url: "https://github.com/vapor/auth.git", from: "2.0.0")
12 | ],
13 | targets: [
14 | .target(name: "App", dependencies: ["FluentSQLite", "Vapor", "JWT", "Authentication", "Multipart"]),
15 | .target(name: "Run", dependencies: ["App"]),
16 | .testTarget(name: "AppTests", dependencies: ["App"])
17 | ]
18 | )
19 |
--------------------------------------------------------------------------------
/SublimateVapor/Public/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gabrielepalma/sublimate/99b5059f6c58f7fe2cf1c78fe84702d7117c4c87/SublimateVapor/Public/.gitkeep
--------------------------------------------------------------------------------
/SublimateVapor/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/SublimateVapor/Sources/App/Controllers/UserController.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import Vapor
3 | import Authentication
4 | import JWT
5 |
6 | struct LoginBody : Codable {
7 | var refreshToken : String?
8 | enum CodingKeys: String, CodingKey {
9 | case refreshToken = "refresh_token"
10 | }
11 | }
12 |
13 | struct LogoutBody : Codable {
14 | var refreshToken : String
15 | enum CodingKeys: String, CodingKey {
16 | case refreshToken = "refresh_token"
17 | }
18 | }
19 |
20 | final class UserController {
21 |
22 | func createUser(_ request: Request) throws -> Future {
23 | return try request.content.decode(UserCreationBody.self).flatMap(to: PublicUser.self) { user in
24 | guard user.validate() else {
25 | throw Abort(HTTPResponseStatus.badRequest)
26 | }
27 | let passwordHashed = try request.make(BCryptDigest.self).hash(user.password)
28 | let newUser = User(username: user.username, password: passwordHashed)
29 | return newUser.save(on: request).map(to: PublicUser.self) { createdUser in
30 | guard let uuid = createdUser.id?.uuidString else {
31 | throw Abort(HTTPResponseStatus.internalServerError)
32 | }
33 | let publicUser = PublicUser(userId: uuid, isAdmin: newUser.isAdmin)
34 | return publicUser
35 | }
36 | }
37 | }
38 |
39 | func loginUser(_ request: Request) throws -> Future
40 | {
41 | let minimumTimeToLiveForRefreshToken : Double = 60 * 60 * 24 * 30 // One month
42 | let user = try request.requireAuthenticated(User.self)
43 | guard let userId = user.id else {
44 | throw Abort(HTTPResponseStatus.internalServerError)
45 | }
46 | let accessTokenString = try user.createJwt(usage: SublimateJwt.AccessToken.usage, expiration: SublimateJwt.AccessToken.expiration).0
47 | let promise = request.eventLoop.newPromise(of: AuthorizedUser.self)
48 | DispatchQueue.global().async {
49 | // Decode the body
50 | guard let decodedBody = try? request.content.decode(LoginBody.self).wait() else {
51 | promise.fail(error: Abort(HTTPResponseStatus.badRequest))
52 | return
53 | }
54 | let refreshTokenString : String
55 | // If a token was given, do token cleaning and reissuing operations, otherwise just create a new one
56 | if let givenTokenString = decodedBody.refreshToken, let data = givenTokenString.data(using: .utf8),
57 | let givenToken = try? JWT< SublimateJwt.Payload>(unverifiedFrom: data)
58 | {
59 | // Delete, if existing, the refresh token that was replaced by the one used in this requet
60 | // This is now safe as we have proof the new refresh token was successfully received by the client
61 | try? RefreshToken.query(on: request).filter(\RefreshToken.issuedToReplace == givenToken.payload.tokenId.value).delete().wait()
62 | let queried = try? RefreshToken.query(on: request).filter(\RefreshToken.refreshToken == givenToken.payload.tokenId.value).first().wait()
63 | guard let fetched = queried, fetched != nil else {
64 | promise.fail(error: Abort(HTTPResponseStatus.unauthorized))
65 | return
66 | }
67 |
68 | // Check if given token is about to expire and we want to replace it
69 | let timeToLive = givenToken.payload.exp.value.timeIntervalSince1970 - Date().timeIntervalSince1970
70 | if timeToLive < minimumTimeToLiveForRefreshToken {
71 | // Create and save the new token
72 | guard let result = newToken(for: user, on: request, toReplace: givenTokenString) else {
73 | promise.fail(error: Abort(HTTPResponseStatus.internalServerError))
74 | return
75 | }
76 | refreshTokenString = result
77 | }
78 | else {
79 | // We don't need to reissue the refresh token, we return back the old one, no changes in database
80 | refreshTokenString = givenTokenString
81 | }
82 | }
83 | else {
84 | // Create and save the new token
85 | guard let result = newToken(for: user, on: request, toReplace: nil) else {
86 | promise.fail(error: Abort(HTTPResponseStatus.internalServerError))
87 | return
88 | }
89 | refreshTokenString = result
90 | }
91 | promise.succeed(result: AuthorizedUser(userId: userId.uuidString, username: user.username, refreshToken: refreshTokenString, accessToken: accessTokenString))
92 | }
93 | return promise.futureResult
94 | }
95 |
96 | func logout(_ request: Request) throws -> Future {
97 | let user = try request.requireAuthenticated(User.self)
98 | guard let userId = user.id else {
99 | throw Abort(HTTPResponseStatus.internalServerError)
100 | }
101 | let promise = request.eventLoop.newPromise(of: PublicUser.self)
102 | DispatchQueue.global().async {
103 | guard let decodedBody = try? request.content.decode(LoginBody.self).wait() else {
104 | promise.fail(error: Abort(HTTPResponseStatus.badRequest))
105 | return
106 | }
107 | guard let givenTokenString = decodedBody.refreshToken, let data = givenTokenString.data(using: .utf8),
108 | let givenToken = try? JWT< SublimateJwt.Payload>(unverifiedFrom: data) else {
109 | promise.fail(error: Abort(HTTPResponseStatus.badRequest))
110 | return
111 | }
112 | try? RefreshToken.query(on: request).filter(\RefreshToken.refreshToken == givenToken.payload.tokenId.value).delete().wait()
113 | promise.succeed(result: PublicUser(userId: userId.uuidString, isAdmin: user.isAdmin))
114 | }
115 | return promise.futureResult
116 | }
117 | }
118 |
119 | func newToken(for user: User, on connection: DatabaseConnectable, toReplace: String?) -> String? {
120 | guard let userId = user.id else {
121 | return nil
122 | }
123 | // We create a new token
124 | guard let newToken = try? user.createJwt(usage: SublimateJwt.RefreshToken.usage, expiration: SublimateJwt.RefreshToken.expiration) else {
125 | return nil
126 | }
127 | // We save the new token in the database
128 | guard let _ = try? RefreshToken(refreshToken: newToken.1.tokenId.value, userId: userId, expiresAt: newToken.1.exp.value.timeIntervalSince1970, issuedToReplace: toReplace).save(on: connection).wait() else {
129 | return nil
130 | }
131 | return newToken.0
132 | }
133 |
--------------------------------------------------------------------------------
/SublimateVapor/Sources/App/Middlewares/BearerMiddleware.swift:
--------------------------------------------------------------------------------
1 | // This file implements Bearer access JWT token authentication middleware
2 | // I didn't use the protocols provided by Auth as Bearer authentication will not authorize a Fluent User but a different data structure, to keep Authentication layer fully separate from Resource Access layer. This hypothetically allows for a more distributed deployment.
3 |
4 | import Vapor
5 | import Authentication
6 | import JWT
7 |
8 | final class SublimateBearerAuthenticationMiddleware: Middleware where A: SublimateBearerAuthenticatable {
9 |
10 | public let signers: JWTSigners
11 | public init(authenticatable type: A.Type = A.self, signers: JWTSigners) {
12 | self.signers = signers
13 | }
14 |
15 | public func respond(to req: Request, chainingTo next: Responder) throws -> Future {
16 | if try req.isAuthenticated(A.self) {
17 | return try next.respond(to: req)
18 | }
19 |
20 | guard let token = req.http.headers.bearerAuthorization, let jwtData = token.token.data(using: .utf8) else {
21 | return try next.respond(to: req)
22 | }
23 |
24 | guard let jwt = try? JWT< SublimateJwt.Payload>(from: jwtData, verifiedUsing: signers) else {
25 | return try next.respond(to: req)
26 | }
27 | guard jwt.payload.usage == SublimateJwt.AccessToken.usage else {
28 | return try next.respond(to: req)
29 | }
30 | guard jwt.payload.iss.value == SublimateJwt.issuerClaim else {
31 | return try next.respond(to: req)
32 | }
33 | return A.authenticate(bearerJwt: jwt.payload, eventLoop: req.eventLoop).flatMap { a in
34 | if let a = a {
35 | try req.authenticate(a)
36 | }
37 | return try next.respond(to: req)
38 | }
39 | }
40 | }
41 |
42 | protocol SublimateBearerAuthenticatable : Authenticatable {
43 | static func authenticate(bearerJwt: SublimateJwt.Payload, eventLoop: EventLoop) -> Future
44 | }
45 |
46 | extension SublimateBearerAuthenticatable {
47 | public static func sublimateBearerAuthMiddleware(signers: JWTSigners) -> SublimateBearerAuthenticationMiddleware {
48 | return SublimateBearerAuthenticationMiddleware(signers: signers)
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/SublimateVapor/Sources/App/Middlewares/PasswordMiddleware.swift:
--------------------------------------------------------------------------------
1 | // This file implements Bearer access JWT token authentication middleware
2 | // I didn't use the protocols provided by Auth as Bearer authentication will not authorize a Fluent User but a different data structure, to keep Authentication layer fully separate from Resource Access layer. This hypothetically allows for a more distributed deployment.
3 |
4 | import Vapor
5 | import Authentication
6 |
7 | final class SublimatePasswordAuthenticationMiddleware: Middleware where A: PasswordAuthenticatable {
8 |
9 | public let verifier: PasswordVerifier
10 | public init(authenticatable type: A.Type = A.self, verifier: PasswordVerifier) {
11 | self.verifier = verifier
12 | }
13 |
14 | public func respond(to req: Request, chainingTo next: Responder) throws -> Future {
15 | if try req.isAuthenticated(A.self) {
16 | return try next.respond(to: req)
17 | }
18 |
19 | return try req.content.decode(SublimateLoginBody.self).flatMap({ [verifier] (object) -> Future in
20 | guard object.grantType == "password" else {
21 | return try next.respond(to: req)
22 | }
23 | guard let username = object.username, let password = object.password else {
24 | return try next.respond(to: req)
25 | }
26 |
27 | return A.authenticate(using: BasicAuthorization(username: username, password: password), verifier: verifier, on: req).flatMap { a in
28 | if let a = a {
29 | try req.authenticate(a)
30 | }
31 | return try next.respond(to: req)
32 | }
33 | })
34 | }
35 | }
36 |
37 | extension PasswordAuthenticatable {
38 | static func sublimatePasswordAuthMiddleware(using verifier: PasswordVerifier) -> SublimatePasswordAuthenticationMiddleware {
39 | return SublimatePasswordAuthenticationMiddleware(verifier: verifier)
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/SublimateVapor/Sources/App/Middlewares/RefreshMiddleware.swift:
--------------------------------------------------------------------------------
1 | // This file implements Bearer access JWT token authentication middleware
2 | // I didn't use the protocols provided by Auth as Bearer authentication will not authorize a Fluent User but a different data structure, to keep Authentication layer fully separate from Resource Access layer. This hypothetically allows for a more distributed deployment.
3 |
4 | import Vapor
5 | import Authentication
6 | import JWT
7 |
8 | struct SublimateLoginBody : Codable {
9 | var grantType : String
10 | var refreshToken : String?
11 | var username : String?
12 | var password : String?
13 |
14 | enum CodingKeys: String, CodingKey {
15 | case grantType = "grant_type"
16 | case refreshToken = "refresh_token"
17 | case username
18 | case password
19 | }
20 | }
21 |
22 | final class SublimateRefreshJwtTokenMiddleware: Middleware where A: SublimateRefreshJwtAuthenticatable {
23 |
24 | public let signers: JWTSigners
25 | public init(authenticatable type: A.Type = A.self, signers: JWTSigners) {
26 | self.signers = signers
27 | }
28 |
29 | public func respond(to req: Request, chainingTo next: Responder) throws -> Future {
30 | if try req.isAuthenticated(A.self) {
31 | return try next.respond(to: req)
32 | }
33 |
34 | return try req.content.decode(SublimateLoginBody.self).flatMap({ [signers] (object) -> Future in
35 | guard
36 | object.grantType == "refresh_token",
37 | let refreshToken = object.refreshToken,
38 | let jwtData = refreshToken.data(using: .utf8)
39 | else {
40 | return try next.respond(to: req)
41 | }
42 |
43 | guard let jwt = try? JWT< SublimateJwt.Payload>(from: jwtData, verifiedUsing: signers) else {
44 | return try next.respond(to: req)
45 | }
46 | guard jwt.payload.usage == SublimateJwt.RefreshToken.usage else {
47 | return try next.respond(to: req)
48 | }
49 | guard jwt.payload.iss.value == SublimateJwt.issuerClaim else {
50 | return try next.respond(to: req)
51 | }
52 |
53 | return A.authenticate(refreshJwt: jwt.payload, on: req).flatMap { a in
54 | if let a = a {
55 | try req.authenticate(a)
56 | }
57 | return try next.respond(to: req)
58 | }
59 | })
60 | }
61 | }
62 |
63 | protocol SublimateRefreshJwtAuthenticatable : Authenticatable {
64 | static func authenticate(refreshJwt: SublimateJwt.Payload, on connection: DatabaseConnectable) -> Future
65 | }
66 |
67 | extension SublimateRefreshJwtAuthenticatable {
68 | static func sublimateRefreshJwtAuthMiddleware(signers: JWTSigners) -> SublimateRefreshJwtTokenMiddleware {
69 | return SublimateRefreshJwtTokenMiddleware(signers: signers)
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/SublimateVapor/Sources/App/Models/RefreshToken.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import Vapor
3 | import FluentSQLite
4 | import Authentication
5 |
6 | final class RefreshToken: SQLiteUUIDModel {
7 | var id: UUID? // Only internal for database use
8 | var refreshToken: String // As appears in the IDClaim
9 | var expiresAt: Double
10 | var issuedToReplace: String?
11 | var userId: User.ID
12 |
13 | init(refreshToken: String, userId: User.ID, expiresAt : Double, issuedToReplace : String? = nil) {
14 | self.refreshToken = refreshToken
15 | self.expiresAt = expiresAt
16 | self.userId = userId
17 | self.issuedToReplace = issuedToReplace
18 | }
19 | }
20 |
21 | extension RefreshToken: Content { }
22 | extension RefreshToken: Parameter { }
23 | extension RefreshToken: Migration {
24 | static func prepare(on connection: SQLiteConnection) -> Future {
25 | return Database.create(self, on: connection) { builder in
26 | try addProperties(to: builder)
27 | builder.unique(on: \.refreshToken)
28 | }
29 | }
30 | }
31 |
32 |
33 |
--------------------------------------------------------------------------------
/SublimateVapor/Sources/App/Models/SublimateJwt.swift:
--------------------------------------------------------------------------------
1 | //
2 | // jwt.swift
3 | // App
4 | //
5 | // Created by i335287 on 03/01/2019.
6 | //
7 |
8 | import JWT
9 | import Vapor
10 |
11 | struct SublimateJwt {
12 | static let symmetricKey = "mySymmetricKey"
13 | static let issuerClaim = "sublimate"
14 | static var signers = SublimateJwt.signers(kids: kids)
15 | static let kids = [
16 | Kid(name: "symm", alg: "H256", signer: JWTSigner.hs256(key: Data(symmetricKey.utf8)))
17 | ]
18 |
19 | struct AccessToken {
20 | // static let expiration : Double = 60 * 20 // 20 minutes
21 | static let expiration : Double = 60 // 20 minutes
22 | static let usage = "access_token"
23 | }
24 |
25 | struct RefreshToken {
26 | static let expiration : Double = 60 * 60 * 24 * 256 // One year
27 | static let usage = "refresh_token"
28 | }
29 |
30 | static func headers(kid : Kid) -> JWTHeader {
31 | return JWTHeader(alg: kid.alg, typ: "JWT", cty: nil, crit: nil, kid: kid.name)
32 | }
33 |
34 | static func signers(kids : [Kid]) -> JWTSigners {
35 | let signers = JWTSigners()
36 | for i in kids {
37 | signers.use(i.signer, kid: i.name)
38 | }
39 | return signers
40 | }
41 |
42 | struct Payload: JWTPayload {
43 | var userId: String
44 | var isAdmin: Bool
45 | var tokenId: IDClaim
46 | var usage: String
47 | var iat: IssuedAtClaim
48 | var exp: ExpirationClaim
49 | var iss: IssuerClaim = IssuerClaim(value: SublimateJwt.issuerClaim)
50 |
51 | func verify(using signer: JWTSigner) throws {
52 | try exp.verifyNotExpired()
53 | }
54 |
55 | init(userId: String, isAdmin: Bool, usage: String, iat: Double, exp: Double, tokenId: String = UUID().uuidString) {
56 | self.userId = userId
57 | self.usage = usage
58 | self.iat = IssuedAtClaim(value: Date(timeIntervalSince1970: iat))
59 | self.exp = ExpirationClaim(value: Date(timeIntervalSince1970: exp))
60 | self.tokenId = IDClaim(value: tokenId)
61 | self.isAdmin = isAdmin
62 | }
63 | }
64 |
65 | struct Kid {
66 | let name : String
67 | let alg : String
68 | let signer : JWTSigner
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/SublimateVapor/Sources/App/Models/User+JWT.swift:
--------------------------------------------------------------------------------
1 | //
2 | // USerViewController.swift
3 | // App
4 | //
5 | // Created by i335287 on 12/01/2019.
6 | //
7 |
8 | import JWT
9 | import Vapor
10 |
11 | extension User {
12 | func createJwt(usage : String, expiration : Double) throws -> (String, SublimateJwt.Payload) {
13 | if let id = self.id?.uuidString {
14 | let now = Date().timeIntervalSince1970
15 | let exp = Date().timeIntervalSince1970 + expiration
16 | let payLoad = SublimateJwt.Payload(userId: id, isAdmin: self.isAdmin, usage: usage, iat: now, exp: exp)
17 | let jwt = JWT(header: SublimateJwt.headers(kid: SublimateJwt.kids[0]), payload: payLoad)
18 | let data = try jwt.sign(using: SublimateJwt.signers)
19 | if let token = String(data: data, encoding: .utf8) {
20 | return (token, payLoad)
21 | }
22 | else {
23 | throw JWTError(identifier: "JWT", reason: "Error while creating JWT: Serializing data")
24 | }
25 | }
26 | else {
27 | throw JWTError(identifier: "JWT", reason: "Error while creating JWT: User ID was nil")
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/SublimateVapor/Sources/App/Models/User.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UserAuthentication.swift
3 | // App
4 | //
5 | // Created by i335287 on 02/01/2019.
6 | //
7 |
8 | import Foundation
9 | import Vapor
10 | import FluentSQLite
11 | import Authentication
12 |
13 | extension User : PasswordAuthenticatable {
14 | static var usernameKey: WritableKeyPath { return \User.username }
15 | static var passwordKey: WritableKeyPath { return \User.password }
16 | }
17 |
18 | extension User : SublimateRefreshJwtAuthenticatable {
19 | static func authenticate(refreshJwt: SublimateJwt.Payload, on connection: DatabaseConnectable) -> EventLoopFuture {
20 |
21 | let promise : Promise = connection.eventLoop.newPromise()
22 | // Joins seems to have issues with Fluent + SQLite :(
23 | // https://github.com/vapor/fluent/issues/563
24 | DispatchQueue.global().async {
25 | let tokenFetched = try? RefreshToken.query(on: connection).filter(\RefreshToken.refreshToken == refreshJwt.tokenId.value).first().wait()
26 |
27 | // We successfully fetched a RefreshToken for that token id
28 | guard let tryToken = tokenFetched, let token = tryToken else {
29 | promise.succeed(result: nil)
30 | return
31 | }
32 | let userFetched = try? User.query(on: connection).filter(\User.id == token.userId).first().wait()
33 |
34 | // We successfully fetched a user for that RefreshToken token id
35 | guard let tryUser = userFetched, let user = tryUser, let userId = user.id?.uuidString else {
36 | promise.succeed(result: nil)
37 | return
38 | }
39 |
40 | // The User associated to that RefreshToken is the same specified in the payload
41 | guard userId == refreshJwt.userId else {
42 | promise.succeed(result: nil)
43 | return
44 | }
45 | promise.succeed(result: user)
46 | }
47 | return promise.futureResult
48 | }
49 | }
50 |
51 | extension PublicUser : SublimateBearerAuthenticatable {
52 | static func authenticate(bearerJwt: SublimateJwt.Payload, eventLoop: EventLoop) -> EventLoopFuture {
53 | // The Middleware already validated the JWT, nothing else to be done here
54 | return eventLoop.future(PublicUser(userId: bearerJwt.userId, isAdmin: bearerJwt.isAdmin))
55 | }
56 | }
57 |
58 | final class User: SQLiteUUIDModel {
59 |
60 | // Primary key
61 | var id: UUID?
62 |
63 | // User is admin
64 | // GPTODO: Admin status is not respected by mutation endpoints, needs to fix
65 | var isAdmin = false
66 | var username: String
67 |
68 | // Hashed password
69 | var password: String
70 |
71 | init(username: String, password: String) {
72 | self.username = username
73 | self.password = password
74 | }
75 | }
76 |
77 | // Response after new registration or logout; authorized object for resource routes
78 | struct PublicUser: Content {
79 | var userId: String
80 | var isAdmin: Bool
81 | }
82 |
83 | // Request for user creation
84 | struct UserCreationBody : Codable {
85 | var username: String
86 | var password: String
87 |
88 | func validate() -> Bool {
89 | return username.count > 0 && password.count > 0
90 | }
91 | }
92 |
93 | // Response for login routes
94 | struct AuthorizedUser: Content {
95 | var userId: String
96 | var username: String
97 | var refreshToken: String
98 | var accessToken: String
99 | }
100 |
101 | extension User: Content { }
102 | extension User: Parameter { }
103 | extension User: Migration {
104 | static func prepare(on connection: SQLiteConnection) -> Future {
105 | return Database
106 | .create(self, on: connection) { builder in
107 | try addProperties(to: builder)
108 | builder.unique(on: \.username)
109 | }
110 | .then({ _ -> Future in
111 | let user = User(username: "admin", password: try! BCrypt.hash("admin"))
112 | user.isAdmin = true
113 | return user.save(on: connection)
114 | })
115 | .transform(to: ())
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/SublimateVapor/Sources/App/app.swift:
--------------------------------------------------------------------------------
1 | import Vapor
2 |
3 | /// Creates an instance of Application. This is called from main.swift in the run target.
4 | public func app(_ env: Environment) throws -> Application {
5 | var config = Config.default()
6 | var env = env
7 | var services = Services.default()
8 | try configure(&config, &env, &services)
9 | let app = try Application(config: config, environment: env, services: services)
10 | try boot(app)
11 | return app
12 | }
13 |
--------------------------------------------------------------------------------
/SublimateVapor/Sources/App/boot.swift:
--------------------------------------------------------------------------------
1 | import Vapor
2 |
3 | /// Called after your application has initialized.
4 | public func boot(_ app: Application) throws {
5 | // your code here
6 |
7 | }
8 |
--------------------------------------------------------------------------------
/SublimateVapor/Sources/App/configure.swift:
--------------------------------------------------------------------------------
1 | import FluentSQLite
2 | import Vapor
3 | import Authentication
4 |
5 | /// Called before your application initializes.
6 | public func configure(_ config: inout Config, _ env: inout Environment, _ services: inout Services) throws {
7 | /// Register providers first
8 | try services.register(FluentSQLiteProvider())
9 | try services.register(AuthenticationProvider())
10 |
11 | /// Register routes to the router
12 | let router = EngineRouter.default()
13 | try routes(router)
14 | services.register(router, as: Router.self)
15 |
16 | /// Register middleware
17 | var middlewares = MiddlewareConfig() // Create _empty_ middleware config
18 | /// middlewares.use(FileMiddleware.self) // Serves files from `Public/` directory
19 | middlewares.use(ErrorMiddleware.self) // Catches errors and converts to HTTP response
20 | services.register(middlewares)
21 |
22 | // Configure a SQLite database
23 | let sqlite = try SQLiteDatabase(storage: .memory)
24 |
25 | /// Register the configured SQLite database to the database config.
26 | var databases = DatabasesConfig()
27 | databases.add(database: sqlite, as: .sqlite)
28 | services.register(databases)
29 |
30 |
31 | /// Configure migrations
32 | var migrations = MigrationConfig()
33 | migrations.add(model: User.self, database: .sqlite)
34 | migrations.add(model: RefreshToken.self, database: .sqlite)
35 | configureSublimateMigration(migrations: &migrations)
36 | services.register(migrations)
37 | }
38 |
--------------------------------------------------------------------------------
/SublimateVapor/Sources/App/routes.swift:
--------------------------------------------------------------------------------
1 | import Vapor
2 | import Crypto
3 |
4 | /// Register your application's routes here.
5 | public func routes(_ router: Router) throws {
6 | let password = User.sublimatePasswordAuthMiddleware(using: BCryptDigest())
7 | let refresh = User.sublimateRefreshJwtAuthMiddleware(signers: SublimateJwt.signers)
8 | let access = PublicUser.sublimateBearerAuthMiddleware(signers: SublimateJwt.signers)
9 |
10 | let authenticationGroup = router.grouped([password, refresh])
11 | let accessGroup = router.grouped(access)
12 |
13 | let userController = UserController()
14 | router.post("createUser", use: userController.createUser)
15 | authenticationGroup.post("token", use: userController.loginUser)
16 | authenticationGroup.post("logout", use: userController.logout)
17 |
18 | let debug = Debug()
19 | router.get("tokens", use: debug.listTokens)
20 | router.get("users", use: debug.listUsers)
21 |
22 | configureSublimateRoutes(plain: router, resourceGroup: accessGroup)
23 | }
24 |
25 | class Debug {
26 |
27 | /// Returns the list
28 | func listTokens(_ req: Request) throws -> Future<[RefreshToken]> {
29 | return RefreshToken.query(on: req).all()
30 | }
31 |
32 | /// Returns the list
33 | func listUsers(_ req: Request) throws -> Future<[User]> {
34 | return User.query(on: req).all()
35 | }
36 |
37 |
38 | }
39 |
--------------------------------------------------------------------------------
/SublimateVapor/Sources/Run/main.swift:
--------------------------------------------------------------------------------
1 | import App
2 |
3 | try app(.detect()).run()
4 |
--------------------------------------------------------------------------------
/SublimateVapor/Tests/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gabrielepalma/sublimate/99b5059f6c58f7fe2cf1c78fe84702d7117c4c87/SublimateVapor/Tests/.gitkeep
--------------------------------------------------------------------------------
/SublimateVapor/Tests/AppTests/AppTests.swift:
--------------------------------------------------------------------------------
1 | import App
2 | import XCTest
3 |
4 | final class AppTests: XCTestCase {
5 | func testNothing() throws {
6 | // add your tests here
7 | XCTAssert(true)
8 | }
9 |
10 | static let allTests = [
11 | ("testNothing", testNothing)
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------
/SublimateVapor/Tests/LinuxMain.swift:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gabrielepalma/sublimate/99b5059f6c58f7fe2cf1c78fe84702d7117c4c87/SublimateVapor/Tests/LinuxMain.swift
--------------------------------------------------------------------------------
/SublimateVapor/cloud.yml:
--------------------------------------------------------------------------------
1 | type: "vapor"
2 | swift_version: "4.1.0"
3 | run_parameters: "serve --port 8080 --hostname 0.0.0.0"
4 |
--------------------------------------------------------------------------------