-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathmain.rs
361 lines (328 loc) · 12.3 KB
/
main.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
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
use passage_flex::PassageFlex;
use poem::{listener::TcpListener, Route};
use poem_openapi::{payload::Json, Object, OpenApi, OpenApiService};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tokio::sync::Mutex;
use uuid::Uuid;
// Represents a user in the system
#[derive(Debug, Serialize, Deserialize, Object)]
struct User {
/// A unique immutable string to identify the user
id: String,
/// The email address of the user
email: String,
/// The authentication token for this user (if authenticated)
auth_token: Option<String>,
}
// Request payload for user registration
#[derive(Debug, Deserialize, Object)]
struct RegisterRequest {
/// The email address of the user to register
email: String,
}
// Request payload for user authentication
#[derive(Debug, Deserialize, Object)]
struct AuthenticateRequest {
/// The email address of the user to authenticate
email: String,
}
// Response payload containing the transaction ID
#[derive(Debug, Serialize, Object)]
struct TransactionResponse {
/// The transaction ID returned by Passage
transaction_id: String,
}
// Request payload for nonce verification
#[derive(Debug, Deserialize, Object)]
struct VerifyRequest {
/// The nonce to verify
nonce: String,
}
// Response payload containing the auth token
#[derive(Debug, Serialize, Object)]
struct VerifyResponse {
/// The authentication token for the user
auth_token: String,
}
// Request query parameters simulating an authenticated user
#[derive(Debug, Deserialize, Object)]
struct AuthenticatedRequest {
/// The authentication token of the user
auth_token: String,
}
// Response payload containing the list of passkeys for a user
#[derive(Debug, Serialize, Object)]
struct UserPasskeysResponse {
/// The list of passkeys registered for the user
passkeys: Vec<Passkey>,
}
// Request payload for revoking a passkey
#[derive(Debug, Deserialize, Object)]
struct RevokePasskeyRequest {
/// The ID of the passkey to revoke
passkey_id: String,
}
// Represents a passkey registered by a user
#[derive(Debug, Serialize, Deserialize, Object)]
struct Passkey {
/// The display name for the passkey
pub friendly_name: String,
/// The ID of the passkey
pub id: String,
}
// Shared state for managing users (in-memory for simplicity)
struct AppState {
users: Mutex<Vec<User>>,
passage_client: PassageFlex, // Passage client instance
}
// The API struct containing all the endpoints
struct Api {
state: Arc<AppState>,
}
#[OpenApi]
impl Api {
/// Register a new user
#[oai(path = "/auth/register", method = "post")]
async fn register_user(
&self,
req: Json<RegisterRequest>,
) -> poem::Result<Json<TransactionResponse>> {
// Generate a unique identifier for the user
let user_id = Uuid::new_v4().to_string();
// Create and store the user
let user = User {
email: req.0.email.clone(),
id: user_id.clone(),
auth_token: None,
};
// Store the user in shared state (simulates saving to a database)
let mut users = self.state.users.lock().await;
users.push(user);
// Create a register transaction using the Passage SDK
let transaction_id = self
.state
.passage_client
.auth
.create_register_transaction(user_id.clone(), req.0.email.clone())
.await
.map_err(|e| {
poem::Error::from_string(
format!("Passage SDK error: {}", e),
poem::http::StatusCode::INTERNAL_SERVER_ERROR,
)
})?; // Convert SDK error into poem error
// Return the transaction ID in the response
Ok(Json(TransactionResponse { transaction_id }))
}
/// Authenticate an existing user
#[oai(path = "/auth/login", method = "post")]
async fn authenticate_user(
&self,
req: Json<AuthenticateRequest>,
) -> poem::Result<Json<TransactionResponse>> {
// Simulate finding the user in the database
let user_email = req.0.email.clone();
// In a real implementation, you'd query your database here
// For simplicity, we'll just search the in-memory list
let user_id = {
let users = self.state.users.lock().await;
users
.iter()
.find(|user| user.email == user_email)
.map(|user| user.id.clone())
};
if let Some(user_id) = user_id {
// Create an authentication transaction using the Passage SDK
let transaction_id = self
.state
.passage_client
.auth
.create_authenticate_transaction(user_id)
.await
.map_err(|e| match e {
passage_flex::Error::UserHasNoPasskeys => poem::Error::from_string(
"User has no passkeys".to_owned(),
poem::http::StatusCode::BAD_REQUEST,
),
_ => poem::Error::from_string(
"Internal server error".to_owned(),
poem::http::StatusCode::INTERNAL_SERVER_ERROR,
),
})?;
Ok(Json(TransactionResponse { transaction_id }))
} else {
Err(poem::Error::from_string(
"User not found".to_owned(),
poem::http::StatusCode::NOT_FOUND,
))
}
}
/// Verify the nonce received from Passage and generate an auth token
#[oai(path = "/auth/verify", method = "post")]
async fn verify_nonce(&self, req: Json<VerifyRequest>) -> poem::Result<Json<VerifyResponse>> {
// Verify the nonce using the Passage SDK
match self.state.passage_client.auth.verify_nonce(req.0.nonce).await {
Ok(user_id) => {
// Find the user in the database and set the auth token
let mut users = self.state.users.lock().await;
if let Some(user) = users.iter_mut().find(|user| user.id == user_id) {
// In a real implementation, you'd generate a secure token here
// For simplicity, we'll just use a plaintext UUID
let auth_token = Uuid::new_v4().to_string();
// Set the user auth token
user.auth_token = Some(auth_token.clone());
// Return the auth token
Ok(Json(VerifyResponse { auth_token }))
} else {
Err(poem::Error::from_string(
"User not found".to_owned(),
poem::http::StatusCode::NOT_FOUND,
))
}
}
Err(e) => Err(poem::Error::from_string(
format!("Verification error: {}", e),
poem::http::StatusCode::UNAUTHORIZED,
)),
}
}
/// Add a new passkey for an authenticated user
#[oai(path = "/user/passkeys/add", method = "post")]
async fn add_passkey(
&self,
query: poem::web::Query<AuthenticatedRequest>,
) -> poem::Result<Json<TransactionResponse>> {
// Verify user identity by finding the user based on auth_token
let users = self.state.users.lock().await;
if let Some(user) = users.iter().find(|user| {
user.auth_token
.as_ref()
.is_some_and(|t| *t == query.auth_token)
}) {
// Create a register transaction using the Passage SDK
let transaction_id = self
.state
.passage_client
.auth
.create_register_transaction(user.id.clone(), user.email.clone())
.await
.map_err(|e| {
poem::Error::from_string(
format!("Failed to create register transaction: {}", e),
poem::http::StatusCode::INTERNAL_SERVER_ERROR,
)
})?;
// Return the transaction ID to the client
Ok(Json(TransactionResponse { transaction_id }))
} else {
Err(poem::Error::from_string(
"Unauthorized".to_owned(),
poem::http::StatusCode::UNAUTHORIZED,
))
}
}
/// Get the list of passkeys for an authenticated user
#[oai(path = "/user/passkeys", method = "get")]
async fn get_user_passkeys(
&self,
query: poem::web::Query<AuthenticatedRequest>,
) -> poem::Result<Json<UserPasskeysResponse>> {
// Verify user identity by finding the user based on auth_token
let users = self.state.users.lock().await;
if let Some(user) = users.iter().find(|user| {
user.auth_token
.as_ref()
.is_some_and(|t| *t == query.auth_token)
}) {
// Retrieve a list of all devices used to register a passkey
let passkeys = self
.state
.passage_client
.user
.list_devices(user.id.clone())
.await
.map(|devices| {
devices
.into_iter()
.map(|device| Passkey {
friendly_name: device.friendly_name,
id: device.id,
})
.collect()
})
.map_err(|e| {
poem::Error::from_string(
format!("Failed to retrieve passkeys: {}", e),
poem::http::StatusCode::INTERNAL_SERVER_ERROR,
)
})?;
// Return the list of passkeys to the client
Ok(Json(UserPasskeysResponse { passkeys }))
} else {
Err(poem::Error::from_string(
"Unauthorized".to_owned(),
poem::http::StatusCode::UNAUTHORIZED,
))
}
}
/// Revoke a passkey for an authenticated user
#[oai(path = "/user/passkeys/revoke", method = "post")]
async fn revoke_passkey(
&self,
query: poem::web::Query<AuthenticatedRequest>,
req: Json<RevokePasskeyRequest>,
) -> poem::Result<poem_openapi::payload::PlainText<String>> {
// Verify user identity by finding the user based on auth_token
let users = self.state.users.lock().await;
if let Some(user) = users.iter().find(|user| {
user.auth_token
.as_ref()
.is_some_and(|t| *t == query.auth_token)
}) {
// Revoke the passkey device using the Passage SDK
self.state
.passage_client
.user
.revoke_device(user.id.clone(), req.0.passkey_id.clone())
.await
.map_err(|e| {
poem::Error::from_string(
format!("Failed to revoke passkey: {}", e),
poem::http::StatusCode::INTERNAL_SERVER_ERROR,
)
})?;
// Return a success response
Ok(poem_openapi::payload::PlainText("OK".to_string()))
} else {
// If the user is not found or unauthorized, return a 401 error
Err(poem::Error::from_string(
"Unauthorized".to_owned(),
poem::http::StatusCode::UNAUTHORIZED,
))
}
}
}
// Main function to start the server
#[tokio::main]
async fn main() -> Result<(), std::io::Error> {
// Initialize the PassageFlex client
let passage_client = PassageFlex::new(
std::env::var("PASSAGE_APP_ID").expect("PASSAGE_APP_ID required"),
std::env::var("PASSAGE_API_KEY").expect("PASSAGE_API_KEY required"),
);
// Initialize shared application state
let state = Arc::new(AppState {
users: Mutex::new(Vec::new()),
passage_client,
});
// Create API service
let api_service =
OpenApiService::new(Api { state }, "Passkey API", "1.0").server("http://localhost:3000");
// Create the app route at the root
let app = Route::new().nest("/", api_service);
// Run the server
poem::Server::new(TcpListener::bind("0.0.0.0:3000"))
.run(app)
.await
}