-
Notifications
You must be signed in to change notification settings - Fork 84
/
Copy pathauth.rs
289 lines (259 loc) · 12.9 KB
/
auth.rs
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
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
use crate::error::WebauthnError;
use crate::startup::AppState;
use axum::{
extract::{Extension, Json, Path},
http::StatusCode,
response::IntoResponse,
};
use tower_sessions::Session;
/*
* Webauthn RS auth handlers.
* These files use webauthn to process the data received from each route, and are closely tied to axum
*/
// 1. Import the prelude - this contains everything needed for the server to function.
use webauthn_rs::prelude::*;
// 2. The first step a client (user) will carry out is requesting a credential to be
// registered. We need to provide a challenge for this. The work flow will be:
//
// ┌───────────────┐ ┌───────────────┐ ┌───────────────┐
// │ Authenticator │ │ Browser │ │ Site │
// └───────────────┘ └───────────────┘ └───────────────┘
// │ │ │
// │ │ 1. Start Reg │
// │ │─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─▶│
// │ │ │
// │ │ 2. Challenge │
// │ │◀ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┤
// │ │ │
// │ 3. Select Token │ │
// ─ ─ ─│◀ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─│ │
// 4. Verify │ │ │ │
// │ 4. Yield PubKey │ │
// └ ─ ─▶│─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─▶ │
// │ │ │
// │ │ 5. Send Reg Opts │
// │ │─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─▶│─ ─ ─
// │ │ │ │ 5. Verify
// │ │ │ PubKey
// │ │ │◀─ ─ ┘
// │ │ │─ ─ ─
// │ │ │ │ 6. Persist
// │ │ │ Credential
// │ │ │◀─ ─ ┘
// │ │ │
// │ │ │
//
// In this step, we are responding to the start reg(istration) request, and providing
// the challenge to the browser.
pub async fn start_register(
Extension(app_state): Extension<AppState>,
session: Session,
Path(username): Path<String>,
) -> Result<impl IntoResponse, WebauthnError> {
info!("Start register");
// We get the username from the URL, but you could get this via form submission or
// some other process. In some parts of Webauthn, you could also use this as a "display name"
// instead of a username. Generally you should consider that the user *can* and *will* change
// their username at any time.
// Since a user's username could change at anytime, we need to bind to a unique id.
// We use uuid's for this purpose, and you should generate these randomly. If the
// username does exist and is found, we can match back to our unique id. This is
// important in authentication, where presented credentials may *only* provide
// the unique id, and not the username!
let user_unique_id = {
let users_guard = app_state.users.lock().await;
users_guard
.name_to_id
.get(&username)
.copied()
.unwrap_or_else(Uuid::new_v4)
};
// Remove any previous registrations that may have occured from the session.
let _ = session.remove_value("reg_state").await;
// If the user has any other credentials, we exclude these here so they can't be duplicate registered.
// It also hints to the browser that only new credentials should be "blinked" for interaction.
let exclude_credentials = {
let users_guard = app_state.users.lock().await;
users_guard
.keys
.get(&user_unique_id)
.map(|keys| keys.iter().map(|sk| sk.cred_id().clone()).collect())
};
let res = match app_state.webauthn.start_passkey_registration(
user_unique_id,
&username,
&username,
exclude_credentials,
) {
Ok((ccr, reg_state)) => {
// Note that due to the session store in use being a server side memory store, this is
// safe to store the reg_state into the session since it is not client controlled and
// not open to replay attacks. If this was a cookie store, this would be UNSAFE.
session
.insert("reg_state", (username, user_unique_id, reg_state))
.await
.expect("Failed to insert");
info!("Registration Successful!");
Json(ccr)
}
Err(e) => {
info!("challenge_register -> {:?}", e);
return Err(WebauthnError::Unknown);
}
};
Ok(res)
}
// 3. The browser has completed it's steps and the user has created a public key
// on their device. Now we have the registration options sent to us, and we need
// to verify these and persist them.
pub async fn finish_register(
Extension(app_state): Extension<AppState>,
session: Session,
Json(reg): Json<RegisterPublicKeyCredential>,
) -> Result<impl IntoResponse, WebauthnError> {
let (username, user_unique_id, reg_state) = match session.get("reg_state").await? {
Some((username, user_unique_id, reg_state)) => (username, user_unique_id, reg_state),
None => {
error!("Failed to get session");
return Err(WebauthnError::CorruptSession);
}
};
let _ = session.remove_value("reg_state").await;
let res = match app_state
.webauthn
.finish_passkey_registration(®, ®_state)
{
Ok(sk) => {
let mut users_guard = app_state.users.lock().await;
//TODO: This is where we would store the credential in a db, or persist them in some other way.
users_guard
.keys
.entry(user_unique_id)
.and_modify(|keys| keys.push(sk.clone()))
.or_insert_with(|| vec![sk.clone()]);
users_guard.name_to_id.insert(username, user_unique_id);
StatusCode::OK
}
Err(e) => {
error!("challenge_register -> {:?}", e);
StatusCode::BAD_REQUEST
}
};
Ok(res)
}
// 4. Now that our public key has been registered, we can authenticate a user and verify
// that they are the holder of that security token. The work flow is similar to registration.
//
// ┌───────────────┐ ┌───────────────┐ ┌───────────────┐
// │ Authenticator │ │ Browser │ │ Site │
// └───────────────┘ └───────────────┘ └───────────────┘
// │ │ │
// │ │ 1. Start Auth │
// │ │─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─▶│
// │ │ │
// │ │ 2. Challenge │
// │ │◀ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┤
// │ │ │
// │ 3. Select Token │ │
// ─ ─ ─│◀ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─│ │
// 4. Verify │ │ │ │
// │ 4. Yield Sig │ │
// └ ─ ─▶│─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─▶ │
// │ │ 5. Send Auth │
// │ │ Opts │
// │ │─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─▶│─ ─ ─
// │ │ │ │ 5. Verify
// │ │ │ Sig
// │ │ │◀─ ─ ┘
// │ │ │
// │ │ │
//
// The user indicates the wish to start authentication and we need to provide a challenge.
pub async fn start_authentication(
Extension(app_state): Extension<AppState>,
session: Session,
Path(username): Path<String>,
) -> Result<impl IntoResponse, WebauthnError> {
info!("Start Authentication");
// We get the username from the URL, but you could get this via form submission or
// some other process.
// Remove any previous authentication that may have occured from the session.
let _ = session.remove_value("auth_state").await;
// Get the set of keys that the user possesses
let users_guard = app_state.users.lock().await;
// Look up their unique id from the username
let user_unique_id = users_guard
.name_to_id
.get(&username)
.copied()
.ok_or(WebauthnError::UserNotFound)?;
let allow_credentials = users_guard
.keys
.get(&user_unique_id)
.ok_or(WebauthnError::UserHasNoCredentials)?;
let res = match app_state
.webauthn
.start_passkey_authentication(allow_credentials)
{
Ok((rcr, auth_state)) => {
// Drop the mutex to allow the mut borrows below to proceed
drop(users_guard);
// Note that due to the session store in use being a server side memory store, this is
// safe to store the auth_state into the session since it is not client controlled and
// not open to replay attacks. If this was a cookie store, this would be UNSAFE.
session
.insert("auth_state", (user_unique_id, auth_state))
.await
.expect("Failed to insert");
Json(rcr)
}
Err(e) => {
info!("challenge_authenticate -> {:?}", e);
return Err(WebauthnError::Unknown);
}
};
Ok(res)
}
// 5. The browser and user have completed their part of the processing. Only in the
// case that the webauthn authenticate call returns Ok, is authentication considered
// a success. If the browser does not complete this call, or *any* error occurs,
// this is an authentication failure.
pub async fn finish_authentication(
Extension(app_state): Extension<AppState>,
session: Session,
Json(auth): Json<PublicKeyCredential>,
) -> Result<impl IntoResponse, WebauthnError> {
let (user_unique_id, auth_state): (Uuid, PasskeyAuthentication) = session
.get("auth_state")
.await?
.ok_or(WebauthnError::CorruptSession)?;
let _ = session.remove_value("auth_state").await;
let res = match app_state
.webauthn
.finish_passkey_authentication(&auth, &auth_state)
{
Ok(auth_result) => {
let mut users_guard = app_state.users.lock().await;
// Update the credential counter, if possible.
users_guard
.keys
.get_mut(&user_unique_id)
.map(|keys| {
keys.iter_mut().for_each(|sk| {
// This will update the credential if it's the matching
// one. Otherwise it's ignored. That is why it is safe to
// iterate this over the full list.
sk.update_credential(&auth_result);
})
})
.ok_or(WebauthnError::UserHasNoCredentials)?;
StatusCode::OK
}
Err(e) => {
info!("challenge_register -> {:?}", e);
StatusCode::BAD_REQUEST
}
};
info!("Authentication Successful!");
Ok(res)
}