mirror of
https://github.com/CloudNebulaProject/barycenter.git
synced 2026-04-10 13:10:42 +00:00
Implement Passkey classification features
Signed-off-by: Till Wegmueller <toasterson@gmail.com>
This commit is contained in:
parent
d39c757be5
commit
ecd6b00a1e
3 changed files with 160 additions and 14 deletions
|
|
@ -32,7 +32,11 @@
|
||||||
"Bash(find:*)",
|
"Bash(find:*)",
|
||||||
"Bash(wc:*)",
|
"Bash(wc:*)",
|
||||||
"Bash(cargo fix:*)",
|
"Bash(cargo fix:*)",
|
||||||
"Bash(tee:*)"
|
"Bash(tee:*)",
|
||||||
|
"mcp__context7__query-docs",
|
||||||
|
"Bash(cargo expand:*)",
|
||||||
|
"Bash(cargo tree:*)",
|
||||||
|
"Bash(cargo metadata:*)"
|
||||||
],
|
],
|
||||||
"deny": [],
|
"deny": [],
|
||||||
"ask": []
|
"ask": []
|
||||||
|
|
|
||||||
|
|
@ -1042,6 +1042,22 @@ pub async fn append_session_amr(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Revoke an access token by marking it as revoked (RFC 7009)
|
||||||
|
pub async fn revoke_access_token(db: &DatabaseConnection, token: &str) -> Result<(), CrabError> {
|
||||||
|
use entities::access_token::{Column, Entity};
|
||||||
|
|
||||||
|
if let Some(at) = Entity::find()
|
||||||
|
.filter(Column::Token.eq(token))
|
||||||
|
.one(db)
|
||||||
|
.await?
|
||||||
|
{
|
||||||
|
let mut active: entities::access_token::ActiveModel = at.into();
|
||||||
|
active.revoked = Set(1);
|
||||||
|
active.update(db).await?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
|
||||||
152
src/web.rs
152
src/web.rs
|
|
@ -119,6 +119,7 @@ pub async fn serve(
|
||||||
.route("/logout", get(logout))
|
.route("/logout", get(logout))
|
||||||
.route("/authorize", get(authorize))
|
.route("/authorize", get(authorize))
|
||||||
.route("/token", post(token))
|
.route("/token", post(token))
|
||||||
|
.route("/revoke", post(token_revoke))
|
||||||
.route("/userinfo", get(userinfo))
|
.route("/userinfo", get(userinfo))
|
||||||
// WebAuthn / Passkey endpoints
|
// WebAuthn / Passkey endpoints
|
||||||
.route("/webauthn/register/start", post(passkey_register_start))
|
.route("/webauthn/register/start", post(passkey_register_start))
|
||||||
|
|
@ -1013,6 +1014,94 @@ async fn handle_authorization_code_grant(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// POST /revoke - Token revocation endpoint (RFC 7009)
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct TokenRevokeRequest {
|
||||||
|
token: String,
|
||||||
|
token_type_hint: Option<String>,
|
||||||
|
client_id: Option<String>,
|
||||||
|
client_secret: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn token_revoke(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
headers: HeaderMap,
|
||||||
|
Form(req): Form<TokenRevokeRequest>,
|
||||||
|
) -> Response {
|
||||||
|
// Client authentication: try Basic auth first, then form body
|
||||||
|
let mut basic_client: Option<(String, String)> = None;
|
||||||
|
if let Some(auth_val) = headers
|
||||||
|
.get(axum::http::header::AUTHORIZATION)
|
||||||
|
.and_then(|h| h.to_str().ok())
|
||||||
|
{
|
||||||
|
if let Some(b64) = auth_val.strip_prefix("Basic ") {
|
||||||
|
if let Ok(mut decoded) = Base64::decode_vec(b64) {
|
||||||
|
if let Ok(s) = String::from_utf8(std::mem::take(&mut decoded)) {
|
||||||
|
if let Some((id, sec)) = s.split_once(':') {
|
||||||
|
basic_client = Some((id.to_string(), sec.to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let (client_id, client_secret) = if let Some(pair) = basic_client {
|
||||||
|
pair
|
||||||
|
} else {
|
||||||
|
// Try client_secret_post (form body)
|
||||||
|
match (req.client_id.clone(), req.client_secret.clone()) {
|
||||||
|
(Some(id), Some(sec)) => (id, sec),
|
||||||
|
_ => {
|
||||||
|
return json_with_headers(
|
||||||
|
StatusCode::UNAUTHORIZED,
|
||||||
|
json!({"error":"invalid_client"}),
|
||||||
|
&[(
|
||||||
|
"www-authenticate",
|
||||||
|
"Basic realm=\"token\", error=\"invalid_client\"".to_string(),
|
||||||
|
)],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Verify client exists and credentials match
|
||||||
|
let client = match storage::get_client(&state.db, &client_id).await {
|
||||||
|
Ok(Some(c)) => c,
|
||||||
|
_ => {
|
||||||
|
return json_with_headers(
|
||||||
|
StatusCode::UNAUTHORIZED,
|
||||||
|
json!({"error":"invalid_client"}),
|
||||||
|
&[(
|
||||||
|
"www-authenticate",
|
||||||
|
"Basic realm=\"token\", error=\"invalid_client\"".to_string(),
|
||||||
|
)],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if client.client_secret != client_secret {
|
||||||
|
return json_with_headers(
|
||||||
|
StatusCode::UNAUTHORIZED,
|
||||||
|
json!({"error":"invalid_client"}),
|
||||||
|
&[(
|
||||||
|
"www-authenticate",
|
||||||
|
"Basic realm=\"token\", error=\"invalid_client\"".to_string(),
|
||||||
|
)],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Per RFC 7009 section 2.2: The authorization server responds with HTTP 200
|
||||||
|
// whether the token was revoked or not (to prevent token scanning)
|
||||||
|
let _ = storage::revoke_access_token(&state.db, &req.token).await;
|
||||||
|
let _ = storage::revoke_refresh_token(&state.db, &req.token).await;
|
||||||
|
|
||||||
|
// Return 200 OK with empty response
|
||||||
|
Response::builder()
|
||||||
|
.status(StatusCode::OK)
|
||||||
|
.body(Body::empty())
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
async fn handle_refresh_token_grant(
|
async fn handle_refresh_token_grant(
|
||||||
state: AppState,
|
state: AppState,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
|
|
@ -2001,6 +2090,34 @@ async fn passkey_register_finish(
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
// Parse serialized passkey to extract fields (cred is private, use JSON introspection)
|
||||||
|
let passkey_data: serde_json::Value = serde_json::from_str(&passkey_json).map_err(|e| {
|
||||||
|
(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
format!("Failed to parse passkey JSON: {}", e),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Extract counter from cred.counter
|
||||||
|
let counter = passkey_data
|
||||||
|
.get("cred")
|
||||||
|
.and_then(|c| c.get("counter"))
|
||||||
|
.and_then(|c| c.as_u64())
|
||||||
|
.unwrap_or(0) as i64;
|
||||||
|
|
||||||
|
// Extract backup flags from cred
|
||||||
|
let backup_eligible = passkey_data
|
||||||
|
.get("cred")
|
||||||
|
.and_then(|c| c.get("backup_eligible"))
|
||||||
|
.and_then(|b| b.as_bool())
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
let backup_state = passkey_data
|
||||||
|
.get("cred")
|
||||||
|
.and_then(|c| c.get("backup_state"))
|
||||||
|
.and_then(|b| b.as_bool())
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
// Use credential ID from the request
|
// Use credential ID from the request
|
||||||
let cred_id_b64 = Base64UrlUnpadded::encode_string(req.credential.id.as_bytes());
|
let cred_id_b64 = Base64UrlUnpadded::encode_string(req.credential.id.as_bytes());
|
||||||
|
|
||||||
|
|
@ -2009,12 +2126,12 @@ async fn passkey_register_finish(
|
||||||
&cred_id_b64,
|
&cred_id_b64,
|
||||||
&session.subject,
|
&session.subject,
|
||||||
&passkey_json,
|
&passkey_json,
|
||||||
0, // counter - TODO: extract from passkey when we understand the API
|
counter, // Extracted from passkey
|
||||||
None, // aaguid
|
None, // aaguid
|
||||||
false, // backup_eligible - TODO: extract from passkey
|
backup_eligible, // Extracted from passkey
|
||||||
false, // backup_state - TODO: extract from passkey
|
backup_state, // Extracted from passkey
|
||||||
None, // transports
|
None, // transports
|
||||||
req.name.as_deref(), // Name from request
|
req.name.as_deref(), // Name from request
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
||||||
|
|
@ -2138,7 +2255,7 @@ async fn passkey_auth_finish(
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
// Finish authentication
|
// Finish authentication
|
||||||
let _auth_result = state
|
let auth_result = state
|
||||||
.webauthn
|
.webauthn
|
||||||
.finish_passkey_authentication(&req.credential, &auth_state)
|
.finish_passkey_authentication(&req.credential, &auth_state)
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
|
|
@ -2148,9 +2265,13 @@ async fn passkey_auth_finish(
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
// Update counter (TODO: extract counter from auth_result when we understand the API)
|
// Extract counter using public API (AuthenticationResult has accessor methods)
|
||||||
// For now, just update last_used_at
|
let new_counter = auth_result.counter() as i64;
|
||||||
// storage::update_passkey_counter(&state.db, &cred_id_b64, new_counter).await?;
|
|
||||||
|
// Update counter for clone detection
|
||||||
|
storage::update_passkey_counter(&state.db, &cred_id_b64, new_counter)
|
||||||
|
.await
|
||||||
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
||||||
|
|
||||||
// Determine AMR based on backup state
|
// Determine AMR based on backup state
|
||||||
let amr = if passkey.backup_eligible == 1 && passkey.backup_state == 1 {
|
let amr = if passkey.backup_eligible == 1 && passkey.backup_state == 1 {
|
||||||
|
|
@ -2306,7 +2427,7 @@ async fn passkey_2fa_finish(
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
// Finish authentication
|
// Finish authentication
|
||||||
let _auth_result = state
|
let auth_result = state
|
||||||
.webauthn
|
.webauthn
|
||||||
.finish_passkey_authentication(&req.credential, &auth_state)
|
.finish_passkey_authentication(&req.credential, &auth_state)
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
|
|
@ -2323,8 +2444,13 @@ async fn passkey_2fa_finish(
|
||||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?
|
||||||
.ok_or((StatusCode::NOT_FOUND, "Passkey not found".to_string()))?;
|
.ok_or((StatusCode::NOT_FOUND, "Passkey not found".to_string()))?;
|
||||||
|
|
||||||
// Update counter (TODO: extract counter from auth_result when we understand the API)
|
// Extract counter using public API (AuthenticationResult has accessor methods)
|
||||||
// storage::update_passkey_counter(&state.db, &cred_id_b64, new_counter).await?;
|
let new_counter = auth_result.counter() as i64;
|
||||||
|
|
||||||
|
// Update counter for clone detection
|
||||||
|
storage::update_passkey_counter(&state.db, &cred_id_b64, new_counter)
|
||||||
|
.await
|
||||||
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
||||||
|
|
||||||
// Append passkey method to AMR
|
// Append passkey method to AMR
|
||||||
let amr_method = if passkey.backup_eligible == 1 && passkey.backup_state == 1 {
|
let amr_method = if passkey.backup_eligible == 1 && passkey.backup_state == 1 {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue