web: add account passkey login UI
This commit is contained in:
@@ -38,8 +38,8 @@ use worker_runtime::worker_backend::{ProfileRuntimeWorkerFactory, WorkerRuntimeE
|
||||
|
||||
use crate::auth::{
|
||||
AuthPublicConfig, AuthenticatedUser, RequestActor, auth_error, is_expired, mint_secret, new_id,
|
||||
new_user_code, normalize_handle, resolve_request_actor, rfc3339_after, session_set_cookie,
|
||||
token_hash,
|
||||
new_user_code, normalize_handle, parse_cookie, resolve_request_actor, rfc3339_after,
|
||||
session_set_cookie, token_hash,
|
||||
};
|
||||
use crate::companion::{
|
||||
CompanionCancelRequest, CompanionConsole, CompanionMessageRequest, CompanionMessageResponse,
|
||||
@@ -337,6 +337,7 @@ pub fn build_router(api: WorkspaceApi) -> Router {
|
||||
"/api/auth/passkeys/login/complete",
|
||||
post(post_passkey_login_complete),
|
||||
)
|
||||
.route("/api/auth/logout", post(post_auth_logout))
|
||||
.route("/api/auth/device-login/start", post(post_device_login_start))
|
||||
.route("/api/auth/device-login/approve", post(post_device_login_approve))
|
||||
.route("/api/auth/device-login/poll", post(post_device_login_poll))
|
||||
@@ -2595,6 +2596,11 @@ struct WhoamiResponse {
|
||||
actor: Option<RequestActor>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct LogoutResponse {
|
||||
status: String,
|
||||
}
|
||||
|
||||
async fn get_auth_config(State(api): State<WorkspaceApi>) -> ApiResult<Json<AuthPublicConfig>> {
|
||||
Ok(Json(auth_public_config(&api.config)))
|
||||
}
|
||||
@@ -3023,6 +3029,37 @@ async fn get_auth_whoami(
|
||||
}))
|
||||
}
|
||||
|
||||
async fn post_auth_logout(
|
||||
State(api): State<WorkspaceApi>,
|
||||
headers: HeaderMap,
|
||||
) -> ApiResult<Response> {
|
||||
let auth = auth_public_config(&api.config);
|
||||
if let Some(session_token) = parse_cookie(&headers, &auth.cookie_name) {
|
||||
let _ = api
|
||||
.store
|
||||
.revoke_browser_session(&token_hash(&session_token), &crate::auth::now_rfc3339())?;
|
||||
}
|
||||
let mut response_headers = HeaderMap::new();
|
||||
response_headers.insert(
|
||||
SET_COOKIE,
|
||||
session_set_cookie(&auth.cookie_name, "", 0)
|
||||
.parse()
|
||||
.map_err(|error| {
|
||||
auth_error(
|
||||
"invalid_session_cookie",
|
||||
&format!("failed to build logout cookie: {error}"),
|
||||
)
|
||||
})?,
|
||||
);
|
||||
Ok((
|
||||
response_headers,
|
||||
Json(LogoutResponse {
|
||||
status: "logged_out".to_string(),
|
||||
}),
|
||||
)
|
||||
.into_response())
|
||||
}
|
||||
|
||||
fn webauthn(config: &ServerConfig) -> ApiResult<Webauthn> {
|
||||
let auth = auth_public_config(config);
|
||||
let origin = Url::parse(&auth.origin)
|
||||
|
||||
@@ -243,6 +243,7 @@ pub trait ControlPlaneStore: Send + Sync {
|
||||
) -> Result<Option<AuthChallengeRecord>>;
|
||||
fn create_browser_session(&self, record: &BrowserSessionRecord) -> Result<()>;
|
||||
fn resolve_browser_session(&self, token_hash: &str) -> Result<Option<BrowserSessionRecord>>;
|
||||
fn revoke_browser_session(&self, token_hash: &str, revoked_at: &str) -> Result<bool>;
|
||||
fn create_api_token(&self, record: &ApiTokenRecord) -> Result<()>;
|
||||
fn resolve_api_token(&self, token_hash: &str) -> Result<Option<ApiTokenRecord>>;
|
||||
fn mark_api_token_used(&self, token_hash: &str, used_at: &str) -> Result<()>;
|
||||
@@ -621,6 +622,16 @@ impl ControlPlaneStore for SqliteWorkspaceStore {
|
||||
})
|
||||
}
|
||||
|
||||
fn revoke_browser_session(&self, token_hash: &str, revoked_at: &str) -> Result<bool> {
|
||||
self.with_conn(|conn| {
|
||||
let changed = conn.execute(
|
||||
"UPDATE browser_sessions SET revoked_at = ?2 WHERE token_hash = ?1 AND revoked_at IS NULL",
|
||||
params![token_hash, revoked_at],
|
||||
)?;
|
||||
Ok(changed > 0)
|
||||
})
|
||||
}
|
||||
|
||||
fn create_api_token(&self, record: &ApiTokenRecord) -> Result<()> {
|
||||
self.with_conn(|conn| {
|
||||
conn.execute(
|
||||
@@ -2455,6 +2466,31 @@ mod tests {
|
||||
None
|
||||
);
|
||||
|
||||
let browser_session = BrowserSessionRecord {
|
||||
session_id: "session-id".to_string(),
|
||||
token_hash: "session-hash".to_string(),
|
||||
user_id: user.user_id.clone(),
|
||||
created_at: now.clone(),
|
||||
expires_at: "2026-07-22T12:00:00Z".to_string(),
|
||||
revoked_at: None,
|
||||
};
|
||||
store.create_browser_session(&browser_session).unwrap();
|
||||
assert_eq!(
|
||||
store.resolve_browser_session("session-hash").unwrap(),
|
||||
Some(browser_session)
|
||||
);
|
||||
assert!(
|
||||
store
|
||||
.revoke_browser_session("session-hash", "2026-07-22T00:02:00Z")
|
||||
.unwrap()
|
||||
);
|
||||
assert_eq!(store.resolve_browser_session("session-hash").unwrap(), None);
|
||||
assert!(
|
||||
!store
|
||||
.revoke_browser_session("session-hash", "2026-07-22T00:03:00Z")
|
||||
.unwrap()
|
||||
);
|
||||
|
||||
let api_token = ApiTokenRecord {
|
||||
token_id: "token-id".to_string(),
|
||||
token_hash: "hash".to_string(),
|
||||
|
||||
Reference in New Issue
Block a user