-
Notifications
You must be signed in to change notification settings - Fork 5
/
BasicJWTAuthenticatable.swift
138 lines (110 loc) · 5.41 KB
/
BasicJWTAuthenticatable.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
import VaporRequestStorage
import Authentication
import JWTVapor
import Fluent
import Crypto
import Vapor
extension String {
/// The key for the JWT payload when it is stored in a Vapor `Request` object.
public static let payloadKey: String = "skelpo-payload"
}
/// Used to decode a request body in
/// `BasicJWTAuthenticatable.authBody(from:)`.
///
/// This type is generic so we can access the property
/// name of the `usernameKey` as the `username` deocding string value.
struct UsernamePassword<Model: BasicJWTAuthenticatable>: Codable {
/// The `username` value for creating
/// a `BasicAuthorization` instance.
let username: String?
/// The `password` value for creating
/// a `BasicAuthorization` instance.
let password: String?
/// The keys used to decode a request
/// body to this struct type.
enum CodingKeys: CodingKey {
/// The decoding key for the `password` property.
case password
/// The decoding key for the `username` property.
case username
/// See `CodingKey.stringValue`.
var stringValue: String {
switch self {
case .password: return "password"
case .username: return (try? Model.reflectProperty(forKey: Model.usernameKey)?.path[0] ?? "email") ?? "email"
}
}
}
}
/// Represents a type that can be authenticated with a basic
/// username/email and password and be authorized with
/// a JWT payload.
///
/// The `AuthBody` type is constrained to `BasicAuthorization` and the `Database` type
/// must conform to the `QuerySupporting` protocol.
public protocol BasicJWTAuthenticatable: JWTAuthenticatable where AuthBody == BasicAuthorization {
/// The keypath for the property
/// that is used to authenticate the
/// model. This is probably `username`
/// or `email`.
static var usernameKey: WritableKeyPath<Self, String> { get }
/// A string that is used with the
/// property of the `usernameKey`
/// to authenticate the model.
/// This model is expected to be
/// a hash created with BCrypt.
var password: String { get }
}
/// Default implementations of some methods
/// required by the `JWTAuthenticatable` protocol.
extension BasicJWTAuthenticatable {
public static func authBody(from request: Request)throws -> Future<BasicAuthorization?> {
// We can't decode the body if there isn't one, so let's return `nil` before we try and fail.
if request.http.method.hasRequestBody == .no || request.http.method.hasRequestBody == .unlikely {
return request.eventLoop.newSucceededFuture(result: nil)
}
// Get the request body as a `UsernamePassword` instance and convert it to a `BasicAuthorization` instance.
return try request.content.decode(UsernamePassword<Self>.self).map(to: AuthBody?.self) { authData in
guard let password = authData.password, let username = authData.username else {
return nil
}
return AuthBody(username: username, password: password)
}
}
public static func authenticate(from payload: Payload, on request: Request)throws -> Future<Self> {
// Fetch the model from the database that has an ID
// matching the one in the JWT payload.
return Self.find(payload.id, on: request)
// No user was found. Throw a 404 (Not Found) error
.unwrap(or: Abort(.notFound, reason: "No user found with the ID from the access token"))
.map(to: Self.self, { (model) in
// Store the model and payload in the request
// using the request's `privateContainer`.
try request.authenticate(model)
try request.set(.payloadKey, to: payload)
return model
})
}
public static func authenticate(from body: AuthBody, on request: Request)throws -> Future<Self> {
// We use the same error when the user is not found and the password doesn't match
// as an anti-attack technique. We don't want someone to knwo they guessed a valid
// username.
// Get the user where the property referanced by `usernameKey` matches the `body.username` value.
// If no model is found, throw a 401 (Unauothorized) error.
let futureUser = Self.query(on: request).filter(Self.usernameKey == body.username).first().unwrap(or: Abort(.unauthorized, reason: "Username or password is incorrect"))
return futureUser.flatMap(to: (Payload, Self).self) { (found) in
// Verify the stored password hash against the password in the `body` object.
guard try BCrypt.verify(body.password, created: found.password) else {
throw Abort(.unauthorized, reason: "Username or password is incorrect")
}
// Get the access token from the model that was found.
return try found.accessToken(on: request).map(to: (Payload, Self).self) { ($0, found) }
}.map(to: Self.self) { (authenticated) in
// Store the payload and the model in the request
// for later access.
try request.set(.payloadKey, to: authenticated.0)
try request.authenticate(authenticated.1)
return authenticated.1
}
}
}