Skip to content

Commit a4f23e1

Browse files
authored
add poem example (#15)
* add poem example * rm unneeded Rust reference in README * update instructions to use inline env vars * rm mut borrows that are not needed
1 parent 9be3fcc commit a4f23e1

File tree

3 files changed

+391
-0
lines changed

3 files changed

+391
-0
lines changed

examples/poem-openapi/Cargo.toml

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
[package]
2+
name = "poem-openapi-with-passage-flex"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[dependencies]
7+
poem = "3"
8+
poem-openapi = { version = "5", features = ["swagger-ui"]}
9+
tokio = { version = "1", features = ["full"] }
10+
serde = "1.0"
11+
uuid = "1.10.0"
12+
thiserror = "1.0.63"
13+
passage_flex = { path = "../../" }
14+
http = "1.1.0"

examples/poem-openapi/README.md

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# poem-openapi with passage_flex
2+
3+
This is an example of how to use the `passage_flex` crate with an HTTP server implemented with [poem-openapi](https://crates.io/crates/poem-openapi).
4+
5+
6+
## How to run
7+
8+
```bash
9+
```
10+
11+
Start the server, setting `PASSAGE_APP_ID` and `PASSAGE_API_KEY` environment variables with valid values:
12+
```bash
13+
PASSAGE_APP_ID=your-app-id PASSAGE_API_KEY=your-api-key cargo run
14+
```
15+
16+
Test the server operations:
17+
```bash
18+
curl -X POST http://localhost:3000/auth/register \
19+
-H 'content-type: application/json' \
20+
-d '{"email":"[email protected]"}'
21+
```

examples/poem-openapi/src/main.rs

+356
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,356 @@
1+
use passage_flex::PassageFlex;
2+
use poem::{listener::TcpListener, Route};
3+
use poem_openapi::{payload::Json, Object, OpenApi, OpenApiService};
4+
use serde::{Deserialize, Serialize};
5+
use std::sync::Arc;
6+
use tokio::sync::Mutex;
7+
use uuid::Uuid;
8+
9+
// Represents a user in the system
10+
#[derive(Debug, Serialize, Deserialize, Object)]
11+
struct User {
12+
/// A unique immutable string to identify the user
13+
id: String,
14+
/// The email address of the user
15+
email: String,
16+
/// The authentication token for this user (if authenticated)
17+
auth_token: Option<String>,
18+
}
19+
20+
// Request payload for user registration
21+
#[derive(Debug, Deserialize, Object)]
22+
struct RegisterRequest {
23+
/// The email address of the user to register
24+
email: String,
25+
}
26+
27+
// Request payload for user authentication
28+
#[derive(Debug, Deserialize, Object)]
29+
struct AuthenticateRequest {
30+
/// The email address of the user to authenticate
31+
email: String,
32+
}
33+
34+
// Response payload containing the transaction ID
35+
#[derive(Debug, Serialize, Object)]
36+
struct TransactionResponse {
37+
/// The transaction ID returned by Passage
38+
transaction_id: String,
39+
}
40+
41+
// Request payload for nonce verification
42+
#[derive(Debug, Deserialize, Object)]
43+
struct VerifyRequest {
44+
/// The nonce to verify
45+
nonce: String,
46+
}
47+
48+
// Response payload containing the auth token
49+
#[derive(Debug, Serialize, Object)]
50+
struct VerifyResponse {
51+
/// The authentication token for the user
52+
auth_token: String,
53+
}
54+
55+
// Request query parameters simulating an authenticated user
56+
#[derive(Debug, Deserialize, Object)]
57+
struct AuthenticatedRequest {
58+
/// The authentication token of the user
59+
auth_token: String,
60+
}
61+
62+
// Response payload containing the list of passkeys for a user
63+
#[derive(Debug, Serialize, Object)]
64+
struct UserPasskeysResponse {
65+
/// The list of passkeys registered for the user
66+
passkeys: Vec<Passkey>,
67+
}
68+
69+
// Request payload for revoking a passkey
70+
#[derive(Debug, Deserialize, Object)]
71+
struct RevokePasskeyRequest {
72+
/// The ID of the passkey to revoke
73+
passkey_id: String,
74+
}
75+
76+
// Represents a passkey registered by a user
77+
#[derive(Debug, Serialize, Deserialize, Object)]
78+
struct Passkey {
79+
/// The display name for the passkey
80+
pub friendly_name: String,
81+
/// The ID of the passkey
82+
pub id: String,
83+
}
84+
85+
// Shared state for managing users (in-memory for simplicity)
86+
struct AppState {
87+
users: Mutex<Vec<User>>,
88+
passage_client: PassageFlex, // Passage client instance
89+
}
90+
91+
// The API struct containing all the endpoints
92+
struct Api {
93+
state: Arc<AppState>,
94+
}
95+
96+
#[OpenApi]
97+
impl Api {
98+
/// Register a new user
99+
#[oai(path = "/auth/register", method = "post")]
100+
async fn register_user(
101+
&self,
102+
req: Json<RegisterRequest>,
103+
) -> poem::Result<Json<TransactionResponse>> {
104+
// Generate a unique identifier for the user
105+
let user_id = Uuid::new_v4().to_string();
106+
107+
// Create and store the user
108+
let user = User {
109+
email: req.0.email.clone(),
110+
id: user_id.clone(),
111+
auth_token: None,
112+
};
113+
114+
// Store the user in shared state (simulates saving to a database)
115+
let mut users = self.state.users.lock().await;
116+
users.push(user);
117+
118+
// Create a register transaction using the Passage SDK
119+
let transaction_id = self
120+
.state
121+
.passage_client
122+
.create_register_transaction(user_id.clone(), req.0.email.clone())
123+
.await
124+
.map_err(|e| {
125+
poem::Error::from_string(
126+
format!("Passage SDK error: {}", e),
127+
poem::http::StatusCode::INTERNAL_SERVER_ERROR,
128+
)
129+
})?; // Convert SDK error into poem error
130+
131+
// Return the transaction ID in the response
132+
Ok(Json(TransactionResponse { transaction_id }))
133+
}
134+
135+
/// Authenticate an existing user
136+
#[oai(path = "/auth/login", method = "post")]
137+
async fn authenticate_user(
138+
&self,
139+
req: Json<AuthenticateRequest>,
140+
) -> poem::Result<Json<TransactionResponse>> {
141+
// Simulate finding the user in the database
142+
let user_email = req.0.email.clone();
143+
// In a real implementation, you'd query your database here
144+
// For simplicity, we'll just search the in-memory list
145+
let user_id = {
146+
let users = self.state.users.lock().await;
147+
users
148+
.iter()
149+
.find(|user| user.email == user_email)
150+
.map(|user| user.id.clone())
151+
};
152+
153+
if let Some(user_id) = user_id {
154+
// Create an authentication transaction using the Passage SDK
155+
let transaction_id = self
156+
.state
157+
.passage_client
158+
.create_authenticate_transaction(user_id)
159+
.await
160+
.map_err(|e| match e {
161+
passage_flex::Error::UserHasNoPasskeys => poem::Error::from_string(
162+
"User has no passkeys".to_owned(),
163+
poem::http::StatusCode::BAD_REQUEST,
164+
),
165+
_ => poem::Error::from_string(
166+
"Internal server error".to_owned(),
167+
poem::http::StatusCode::INTERNAL_SERVER_ERROR,
168+
),
169+
})?;
170+
Ok(Json(TransactionResponse { transaction_id }))
171+
} else {
172+
Err(poem::Error::from_string(
173+
"User not found".to_owned(),
174+
poem::http::StatusCode::NOT_FOUND,
175+
))
176+
}
177+
}
178+
179+
/// Verify the nonce received from Passage and generate an auth token
180+
#[oai(path = "/auth/verify", method = "post")]
181+
async fn verify_nonce(&self, req: Json<VerifyRequest>) -> poem::Result<Json<VerifyResponse>> {
182+
// Verify the nonce using the Passage SDK
183+
match self.state.passage_client.verify_nonce(req.0.nonce).await {
184+
Ok(user_id) => {
185+
// Find the user in the database and set the auth token
186+
let mut users = self.state.users.lock().await;
187+
if let Some(user) = users.iter_mut().find(|user| user.id == user_id) {
188+
// In a real implementation, you'd generate a secure token here
189+
// For simplicity, we'll just use a plaintext UUID
190+
let auth_token = Uuid::new_v4().to_string();
191+
192+
// Set the user auth token
193+
user.auth_token = Some(auth_token.clone());
194+
195+
// Return the auth token
196+
Ok(Json(VerifyResponse { auth_token }))
197+
} else {
198+
Err(poem::Error::from_string(
199+
"User not found".to_owned(),
200+
poem::http::StatusCode::NOT_FOUND,
201+
))
202+
}
203+
}
204+
Err(e) => Err(poem::Error::from_string(
205+
format!("Verification error: {}", e),
206+
poem::http::StatusCode::UNAUTHORIZED,
207+
)),
208+
}
209+
}
210+
211+
/// Add a new passkey for an authenticated user
212+
#[oai(path = "/user/passkeys/add", method = "post")]
213+
async fn add_passkey(
214+
&self,
215+
query: poem::web::Query<AuthenticatedRequest>,
216+
) -> poem::Result<Json<TransactionResponse>> {
217+
// Verify user identity by finding the user based on auth_token
218+
let users = self.state.users.lock().await;
219+
if let Some(user) = users.iter().find(|user| {
220+
user.auth_token
221+
.as_ref()
222+
.is_some_and(|t| *t == query.auth_token)
223+
}) {
224+
// Create a register transaction using the Passage SDK
225+
let transaction_id = self
226+
.state
227+
.passage_client
228+
.create_register_transaction(user.id.clone(), user.email.clone())
229+
.await
230+
.map_err(|e| {
231+
poem::Error::from_string(
232+
format!("Failed to create register transaction: {}", e),
233+
poem::http::StatusCode::INTERNAL_SERVER_ERROR,
234+
)
235+
})?;
236+
237+
// Return the transaction ID to the client
238+
Ok(Json(TransactionResponse { transaction_id }))
239+
} else {
240+
Err(poem::Error::from_string(
241+
"Unauthorized".to_owned(),
242+
poem::http::StatusCode::UNAUTHORIZED,
243+
))
244+
}
245+
}
246+
247+
/// Get the list of passkeys for an authenticated user
248+
#[oai(path = "/user/passkeys", method = "get")]
249+
async fn get_user_passkeys(
250+
&self,
251+
query: poem::web::Query<AuthenticatedRequest>,
252+
) -> poem::Result<Json<UserPasskeysResponse>> {
253+
// Verify user identity by finding the user based on auth_token
254+
let users = self.state.users.lock().await;
255+
if let Some(user) = users.iter().find(|user| {
256+
user.auth_token
257+
.as_ref()
258+
.is_some_and(|t| *t == query.auth_token)
259+
}) {
260+
// Retrieve a list of all devices used to register a passkey
261+
let passkeys = self
262+
.state
263+
.passage_client
264+
.get_devices(user.id.clone())
265+
.await
266+
.map(|devices| {
267+
devices
268+
.into_iter()
269+
.map(|device| Passkey {
270+
friendly_name: device.friendly_name,
271+
id: device.id,
272+
})
273+
.collect()
274+
})
275+
.map_err(|e| {
276+
poem::Error::from_string(
277+
format!("Failed to retrieve passkeys: {}", e),
278+
poem::http::StatusCode::INTERNAL_SERVER_ERROR,
279+
)
280+
})?;
281+
282+
// Return the list of passkeys to the client
283+
Ok(Json(UserPasskeysResponse { passkeys }))
284+
} else {
285+
Err(poem::Error::from_string(
286+
"Unauthorized".to_owned(),
287+
poem::http::StatusCode::UNAUTHORIZED,
288+
))
289+
}
290+
}
291+
292+
/// Revoke a passkey for an authenticated user
293+
#[oai(path = "/user/passkeys/revoke", method = "post")]
294+
async fn revoke_passkey(
295+
&self,
296+
query: poem::web::Query<AuthenticatedRequest>,
297+
req: Json<RevokePasskeyRequest>,
298+
) -> poem::Result<poem_openapi::payload::PlainText<String>> {
299+
// Verify user identity by finding the user based on auth_token
300+
let users = self.state.users.lock().await;
301+
if let Some(user) = users.iter().find(|user| {
302+
user.auth_token
303+
.as_ref()
304+
.is_some_and(|t| *t == query.auth_token)
305+
}) {
306+
// Revoke the passkey device using the Passage SDK
307+
self.state
308+
.passage_client
309+
.revoke_device(user.id.clone(), req.0.passkey_id.clone())
310+
.await
311+
.map_err(|e| {
312+
poem::Error::from_string(
313+
format!("Failed to revoke passkey: {}", e),
314+
poem::http::StatusCode::INTERNAL_SERVER_ERROR,
315+
)
316+
})?;
317+
318+
// Return a success response
319+
Ok(poem_openapi::payload::PlainText("OK".to_string()))
320+
} else {
321+
// If the user is not found or unauthorized, return a 401 error
322+
Err(poem::Error::from_string(
323+
"Unauthorized".to_owned(),
324+
poem::http::StatusCode::UNAUTHORIZED,
325+
))
326+
}
327+
}
328+
}
329+
330+
// Main function to start the server
331+
#[tokio::main]
332+
async fn main() -> Result<(), std::io::Error> {
333+
// Initialize the PassageFlex client
334+
let passage_client = PassageFlex::new(
335+
std::env::var("PASSAGE_APP_ID").expect("PASSAGE_APP_ID required"),
336+
std::env::var("PASSAGE_API_KEY").expect("PASSAGE_API_KEY required"),
337+
);
338+
339+
// Initialize shared application state
340+
let state = Arc::new(AppState {
341+
users: Mutex::new(Vec::new()),
342+
passage_client,
343+
});
344+
345+
// Create API service
346+
let api_service =
347+
OpenApiService::new(Api { state }, "Passkey API", "1.0").server("http://localhost:3000");
348+
349+
// Create the app route at the root
350+
let app = Route::new().nest("/", api_service);
351+
352+
// Run the server
353+
poem::Server::new(TcpListener::bind("0.0.0.0:3000"))
354+
.run(app)
355+
.await
356+
}

0 commit comments

Comments
 (0)