diff --git a/crates/tranquil-api/src/identity/provision.rs b/crates/tranquil-api/src/identity/provision.rs index a78d87f..1f96d75 100644 --- a/crates/tranquil-api/src/identity/provision.rs +++ b/crates/tranquil-api/src/identity/provision.rs @@ -264,19 +264,18 @@ pub async fn create_and_store_session( scope: &str, controller_did: Option<&Did>, ) -> Result { - let access_meta = - tranquil_pds::auth::create_access_token_with_metadata(did_str, signing_key_bytes).map_err( + let access_meta = tranquil_pds::auth::create_access_token_with_metadata(did, signing_key_bytes) + .map_err(|e| { + tracing::error!("Error creating access token: {:?}", e); + ApiError::InternalError(None) + })?; + let refresh_meta = + tranquil_pds::auth::create_refresh_token_with_metadata(did, signing_key_bytes).map_err( |e| { tracing::error!("Error creating access token: {:?}", e); ApiError::InternalError(None) }, )?; - let refresh_meta = - tranquil_pds::auth::create_refresh_token_with_metadata(did_str, signing_key_bytes) - .map_err(|e| { - tracing::error!("Error creating refresh token: {:?}", e); - ApiError::InternalError(None) - })?; let session_data = tranquil_db_traits::SessionTokenCreate { did: did.clone(), access_jti: access_meta.jti.clone(), diff --git a/crates/tranquil-api/src/server/session.rs b/crates/tranquil-api/src/server/session.rs index 38dc556..caad7c5 100644 --- a/crates/tranquil-api/src/server/session.rs +++ b/crates/tranquil-api/src/server/session.rs @@ -1130,13 +1130,13 @@ pub async fn list_sessions( Ok(Json(ListSessionsOutput { sessions })) } -fn extract_client_name(client_id: &str) -> String { +fn extract_client_name(client_id: &tranquil_types::ClientId) -> String { if client_id.starts_with("http://localhost") || client_id.starts_with("http://127.0.0.1") { "Localhost App".to_string() - } else if let Ok(parsed) = reqwest::Url::parse(client_id) { + } else if let Ok(parsed) = reqwest::Url::parse(client_id.as_str()) { parsed.host_str().unwrap_or("Unknown App").to_string() } else { - client_id.to_string() + client_id.as_str().to_string() } } @@ -1210,7 +1210,7 @@ pub async fn revoke_all_sessions( .delete_sessions_by_did(&auth.did) .await .log_db_err("revoking JWT sessions")?; - let jti_typed = TokenId::from(jti.clone()); + let token_id = TokenId::from(jti.clone().into_inner()); state .repos .oauth diff --git a/crates/tranquil-api/src/server/trusted_devices.rs b/crates/tranquil-api/src/server/trusted_devices.rs index d12c5f1..9e76792 100644 --- a/crates/tranquil-api/src/server/trusted_devices.rs +++ b/crates/tranquil-api/src/server/trusted_devices.rs @@ -52,7 +52,7 @@ impl DeviceTrustState { #[derive(Serialize)] #[serde(rename_all = "camelCase")] pub struct TrustedDevice { - pub id: String, + pub id: DeviceId, pub user_agent: Option, pub friendly_name: Option, pub trusted_at: Option>, diff --git a/crates/tranquil-db-traits/src/oauth.rs b/crates/tranquil-db-traits/src/oauth.rs index 23c0bd2..2d904e0 100644 --- a/crates/tranquil-db-traits/src/oauth.rs +++ b/crates/tranquil-db-traits/src/oauth.rs @@ -59,7 +59,7 @@ pub struct DeviceAccountRow { pub struct TwoFactorChallenge { pub id: Uuid, pub did: Did, - pub request_uri: String, + pub request_uri: RequestId, pub code: String, pub attempts: i32, pub created_at: DateTime, @@ -68,7 +68,7 @@ pub struct TwoFactorChallenge { #[derive(Debug, Clone)] pub struct TrustedDeviceRow { - pub id: String, + pub id: DeviceId, pub user_agent: Option, pub friendly_name: Option, pub trusted_at: Option>, diff --git a/crates/tranquil-db-traits/src/user.rs b/crates/tranquil-db-traits/src/user.rs index b6bfa86..d93161d 100644 --- a/crates/tranquil-db-traits/src/user.rs +++ b/crates/tranquil-db-traits/src/user.rs @@ -124,7 +124,7 @@ pub trait UserRepository: Send + Sync { async fn get_oauth_token_with_user( &self, - token_id: &str, + token_id: &TokenId, ) -> Result, DbError>; async fn get_user_info_by_did(&self, did: &Did) -> Result, DbError>; diff --git a/crates/tranquil-db/src/postgres/oauth.rs b/crates/tranquil-db/src/postgres/oauth.rs index c6a8d8e..dfcc9d9 100644 --- a/crates/tranquil-db/src/postgres/oauth.rs +++ b/crates/tranquil-db/src/postgres/oauth.rs @@ -65,13 +65,13 @@ impl OAuthRepository for PostgresOAuthRepository { data.created_at, data.updated_at, data.expires_at, - data.client_id, + data.client_id.as_str(), client_auth_json, - data.device_id.as_ref().map(|d| d.0.as_str()), + data.device_id.as_deref(), parameters_json, data.details, - data.code.as_ref().map(|c| c.0.as_str()), - data.current_refresh_token.as_ref().map(|r| r.0.as_str()), + data.code.as_deref(), + data.current_refresh_token.as_deref(), data.scope, data.controller_did.as_ref().map(|d| d.as_str()), ) @@ -100,17 +100,17 @@ impl OAuthRepository for PostgresOAuthRepository { .did .parse() .map_err(|_| DbError::Other("Invalid DID in token".into()))?, - token_id: OAuthTokenId(r.token_id), + token_id: TokenId::from(r.token_id), created_at: r.created_at, updated_at: r.updated_at, expires_at: r.expires_at, - client_id: r.client_id, + client_id: ClientId::from(r.client_id), client_auth: from_json(r.client_auth)?, - device_id: r.device_id.map(OAuthDeviceId), + device_id: r.device_id.map(DeviceId::from), parameters: from_json(r.parameters)?, details: r.details, - code: r.code.map(OAuthCode), - current_refresh_token: r.current_refresh_token.map(OAuthRefreshToken), + code: r.code.map(AuthorizationCode::from), + current_refresh_token: r.current_refresh_token.map(RefreshToken::from), scope: r.scope, controller_did: r .controller_did @@ -146,17 +146,17 @@ impl OAuthRepository for PostgresOAuthRepository { .did .parse() .map_err(|_| DbError::Other("Invalid DID in token".into()))?, - token_id: OAuthTokenId(r.token_id), + token_id: TokenId::from(r.token_id), created_at: r.created_at, updated_at: r.updated_at, expires_at: r.expires_at, - client_id: r.client_id, + client_id: ClientId::from(r.client_id), client_auth: from_json(r.client_auth)?, - device_id: r.device_id.map(OAuthDeviceId), + device_id: r.device_id.map(DeviceId::from), parameters: from_json(r.parameters)?, details: r.details, - code: r.code.map(OAuthCode), - current_refresh_token: r.current_refresh_token.map(OAuthRefreshToken), + code: r.code.map(AuthorizationCode::from), + current_refresh_token: r.current_refresh_token.map(RefreshToken::from), scope: r.scope, controller_did: r .controller_did @@ -195,17 +195,17 @@ impl OAuthRepository for PostgresOAuthRepository { .did .parse() .map_err(|_| DbError::Other("Invalid DID in token".into()))?, - token_id: OAuthTokenId(r.token_id), + token_id: TokenId::from(r.token_id), created_at: r.created_at, updated_at: r.updated_at, expires_at: r.expires_at, - client_id: r.client_id, + client_id: ClientId::from(r.client_id), client_auth: from_json(r.client_auth)?, - device_id: r.device_id.map(OAuthDeviceId), + device_id: r.device_id.map(DeviceId::from), parameters: from_json(r.parameters)?, details: r.details, - code: r.code.map(OAuthCode), - current_refresh_token: r.current_refresh_token.map(OAuthRefreshToken), + code: r.code.map(AuthorizationCode::from), + current_refresh_token: r.current_refresh_token.map(RefreshToken::from), scope: r.scope, controller_did: r .controller_did @@ -328,17 +328,17 @@ impl OAuthRepository for PostgresOAuthRepository { .did .parse() .map_err(|_| DbError::Other("Invalid DID in token".into()))?, - token_id: OAuthTokenId(r.token_id), + token_id: TokenId::from(r.token_id), created_at: r.created_at, updated_at: r.updated_at, expires_at: r.expires_at, - client_id: r.client_id, + client_id: ClientId::from(r.client_id), client_auth: from_json(r.client_auth)?, - device_id: r.device_id.map(OAuthDeviceId), + device_id: r.device_id.map(DeviceId::from), parameters: from_json(r.parameters)?, details: r.details, - code: r.code.map(OAuthCode), - current_refresh_token: r.current_refresh_token.map(OAuthRefreshToken), + code: r.code.map(AuthorizationCode::from), + current_refresh_token: r.current_refresh_token.map(RefreshToken::from), scope: r.scope, controller_did: r .controller_did @@ -437,8 +437,8 @@ impl OAuthRepository for PostgresOAuthRepository { "#, request_id.as_str(), data.did.as_ref().map(|d| d.as_str()), - data.device_id.as_ref().map(|d| d.0.as_str()), - data.client_id, + data.device_id.as_deref(), + data.client_id.as_str(), client_auth_json, parameters_json, data.expires_at, @@ -473,7 +473,7 @@ impl OAuthRepository for PostgresOAuthRepository { }; let parameters: AuthorizationRequestParameters = from_json(r.parameters)?; Ok(Some(RequestData { - client_id: r.client_id, + client_id: ClientId::from(r.client_id), client_auth, parameters, expires_at: r.expires_at, @@ -482,8 +482,8 @@ impl OAuthRepository for PostgresOAuthRepository { .map(|s| s.parse()) .transpose() .map_err(|_| DbError::Other("Invalid DID in DB".into()))?, - device_id: r.device_id.map(OAuthDeviceId), - code: r.code.map(OAuthCode), + device_id: r.device_id.map(DeviceId::from), + code: r.code.map(AuthorizationCode::from), controller_did: r .controller_did .map(|s| s.parse()) @@ -567,7 +567,7 @@ impl OAuthRepository for PostgresOAuthRepository { }; let parameters: AuthorizationRequestParameters = from_json(r.parameters)?; Ok(Some(RequestData { - client_id: r.client_id, + client_id: ClientId::from(r.client_id), client_auth, parameters, expires_at: r.expires_at, @@ -576,8 +576,8 @@ impl OAuthRepository for PostgresOAuthRepository { .map(|s| s.parse()) .transpose() .map_err(|_| DbError::Other("Invalid DID in DB".into()))?, - device_id: r.device_id.map(OAuthDeviceId), - code: r.code.map(OAuthCode), + device_id: r.device_id.map(DeviceId::from), + code: r.code.map(AuthorizationCode::from), controller_did: r .controller_did .map(|s| s.parse()) @@ -906,7 +906,7 @@ impl OAuthRepository for PostgresOAuthRepository { Ok(TwoFactorChallenge { id: row.id, did: Did::from(row.did), - request_uri: row.request_uri, + request_uri: RequestId::from(row.request_uri), code: row.code, attempts: row.attempts, created_at: row.created_at, @@ -932,7 +932,7 @@ impl OAuthRepository for PostgresOAuthRepository { Ok(row.map(|r| TwoFactorChallenge { id: r.id, did: Did::from(r.did), - request_uri: r.request_uri, + request_uri: RequestId::from(r.request_uri), code: r.code, attempts: r.attempts, created_at: r.created_at, @@ -1143,7 +1143,7 @@ impl OAuthRepository for PostgresOAuthRepository { Ok(rows .into_iter() .map(|r| TrustedDeviceRow { - id: r.id, + id: DeviceId::from(r.id), user_agent: r.user_agent, friendly_name: r.friendly_name, trusted_at: r.trusted_at, diff --git a/crates/tranquil-db/src/postgres/user.rs b/crates/tranquil-db/src/postgres/user.rs index 4a574bb..1841201 100644 --- a/crates/tranquil-db/src/postgres/user.rs +++ b/crates/tranquil-db/src/postgres/user.rs @@ -192,7 +192,7 @@ impl UserRepository for PostgresUserRepository { async fn get_oauth_token_with_user( &self, - token_id: &str, + token_id: &TokenId, ) -> Result, DbError> { let row = sqlx::query!( r#"SELECT t.did, t.expires_at, u.deactivated_at, u.takedown_ref, u.is_admin, diff --git a/crates/tranquil-oauth-server/src/endpoints/authorize/consent.rs b/crates/tranquil-oauth-server/src/endpoints/authorize/consent.rs index 85d3d97..60851a6 100644 --- a/crates/tranquil-oauth-server/src/endpoints/authorize/consent.rs +++ b/crates/tranquil-oauth-server/src/endpoints/authorize/consent.rs @@ -13,7 +13,7 @@ pub struct ScopeInfo { #[derive(Debug, Serialize)] pub struct ConsentResponse { pub request_uri: String, - pub client_id: String, + pub client_id: ClientId, pub client_name: Option, pub client_uri: Option, pub logo_uri: Option, @@ -136,11 +136,10 @@ pub async fn consent_get( } }; let requested_scopes: Vec<&str> = expanded_scope_str.split_whitespace().collect(); - let consent_client_id = ClientId::from(request_data.parameters.client_id.clone()); let preferences = state .repos .oauth - .get_scope_preferences(&did, &consent_client_id) + .get_scope_preferences(&did, &request_data.parameters.client_id) .await .unwrap_or_default(); let pref_map: std::collections::HashMap<_, _> = preferences @@ -152,7 +151,7 @@ pub async fn consent_get( let show_consent = should_show_consent( state.repos.oauth.as_ref(), &did, - &consent_client_id, + &request_data.parameters.client_id, &requested_scope_strings, ) .await @@ -385,11 +384,10 @@ pub async fn consent_post( granted: form.approved_scopes.contains(&s.to_string()), }) .collect(); - let consent_post_client_id = ClientId::from(request_data.parameters.client_id.clone()); let _ = state .repos .oauth - .upsert_scope_preferences(&did, &consent_post_client_id, &preferences) + .upsert_scope_preferences(&did, &request_data.parameters.client_id, &preferences) .await; } if let Err(e) = state @@ -400,20 +398,15 @@ pub async fn consent_post( { tracing::warn!("Failed to update request scope: {:?}", e); } - let code = Code::generate(); - let consent_post_device_id = request_data - .device_id - .as_ref() - .map(|d| DeviceIdType::new(d.0.clone())); - let consent_post_code = AuthorizationCode::from(code.0.clone()); + let code = AuthorizationCode::generate(); if state .repos .oauth .update_authorization_request( &consent_post_request_id, &did, - consent_post_device_id.as_ref(), - &consent_post_code, + request_data.device_id.as_ref(), + &code, ) .await .is_err() diff --git a/crates/tranquil-oauth-server/src/endpoints/authorize/login.rs b/crates/tranquil-oauth-server/src/endpoints/authorize/login.rs index eaa6070..02335e6 100644 --- a/crates/tranquil-oauth-server/src/endpoints/authorize/login.rs +++ b/crates/tranquil-oauth-server/src/endpoints/authorize/login.rs @@ -606,14 +606,13 @@ pub async fn authorize_post( } } } - let mut device_id: Option = extract_device_cookie(&headers); + let mut device_id: Option = extract_device_cookie(&headers); let mut new_cookie: Option = None; if form.remember_device { let final_device_id = if let Some(existing_id) = &device_id { existing_id.clone() } else { let new_id = DeviceId::generate(); - let new_device_id_typed = DeviceIdType::new(new_id.0.clone()); let device_data = DeviceData { session_id: SessionId::generate(), user_agent: extract_user_agent(&headers), @@ -623,14 +622,14 @@ pub async fn authorize_post( if state .repos .oauth - .create_device(&new_device_id_typed, &device_data) + .create_device(&new_id, &device_data) .await .is_ok() { - new_cookie = Some(make_device_cookie(&new_device_id_typed)); - device_id = Some(new_device_id_typed.clone()); + new_cookie = Some(make_device_cookie(&new_id)); + device_id = Some(new_id.clone()); } - new_device_id_typed + new_id }; let _ = state .repos @@ -657,11 +656,10 @@ pub async fn authorize_post( .split_whitespace() .map(|s| s.to_string()) .collect(); - let client_id_typed = ClientId::from(request_data.parameters.client_id.clone()); let needs_consent = should_show_consent( state.repos.oauth.as_ref(), &user.did, - &client_id_typed, + &request_data.parameters.client_id, &requested_scopes, ) .await @@ -691,7 +689,7 @@ pub async fn authorize_post( } return redirect_see_other(&consent_url); } - let code = Code::generate(); + let code = AuthorizationCode::generate(); let auth_post_device_id = device_id.clone(); let auth_post_code = AuthorizationCode::from(code.0.clone()); if state @@ -869,7 +867,6 @@ pub async fn authorize_select( .into_response(); } let has_totp = tranquil_api::server::has_totp_enabled(&state, &did).await; - let select_early_device_typed = device_id.clone(); if has_totp { let device_is_trusted = tranquil_api::server::is_device_trusted(state.repos.oauth.as_ref(), &device_id, &did) @@ -878,7 +875,7 @@ pub async fn authorize_select( if state .repos .oauth - .set_authorization_did(&select_request_id, &did, Some(&select_early_device_typed)) + .set_authorization_did(&select_request_id, &did, Some(&device_id)) .await .is_err() { @@ -942,17 +939,16 @@ pub async fn authorize_select( } } } - let select_device_typed = device_id.clone(); let _ = state .repos .oauth - .upsert_account_device(&did, &select_device_typed) + .upsert_account_device(&did, &device_id) .await; if state .repos .oauth - .set_authorization_did(&select_request_id, &did, Some(&select_device_typed)) + .set_authorization_did(&select_request_id, &did, Some(&device_id)) .await .is_err() { diff --git a/crates/tranquil-oauth-server/src/endpoints/authorize/mod.rs b/crates/tranquil-oauth-server/src/endpoints/authorize/mod.rs index 92febc4..6eb1c3c 100644 --- a/crates/tranquil-oauth-server/src/endpoints/authorize/mod.rs +++ b/crates/tranquil-oauth-server/src/endpoints/authorize/mod.rs @@ -14,7 +14,7 @@ use tranquil_db_traits::{ScopePreference, WebauthnChallengeType}; use tranquil_pds::auth::{BareLoginIdentifier, NormalizedLoginIdentifier}; use tranquil_pds::comms::comms_repo::enqueue_2fa_code; use tranquil_pds::oauth::{ - AuthFlow, ClientMetadataCache, Code, DeviceData, DeviceId, OAuthError, Prompt, SessionId, + AuthFlow, ClientMetadataCache, DeviceData, DeviceId, OAuthError, Prompt, SessionId, db::should_show_consent, scopes::expand_include_scopes, }; use tranquil_pds::rate_limit::{ @@ -24,7 +24,7 @@ use tranquil_pds::rate_limit::{ use tranquil_pds::state::AppState; use tranquil_pds::types::{Did, Handle, PlainPassword}; use tranquil_pds::util::ClientIp; -use tranquil_types::{AuthorizationCode, ClientId, DeviceId as DeviceIdType, RequestId}; +use tranquil_types::{AuthorizationCode, ClientId, RequestId}; use urlencoding::encode as url_encode; const DEVICE_COOKIE_NAME: &str = "oauth_device_id"; @@ -111,8 +111,7 @@ fn extract_user_agent(headers: &HeaderMap) -> Option { } fn make_device_cookie(device_id: &tranquil_types::DeviceId) -> String { - let signed_value = - tranquil_pds::config::AuthConfig::get().sign_device_cookie(device_id.as_str()); + let signed_value = tranquil_pds::config::AuthConfig::get().sign_device_cookie(device_id); format!( "{}={}; Path=/oauth; HttpOnly; Secure; SameSite=Lax; Max-Age=31536000", DEVICE_COOKIE_NAME, signed_value @@ -128,7 +127,7 @@ pub struct AuthorizeQuery { #[derive(Debug, Serialize)] pub struct AuthorizeResponse { - pub client_id: String, + pub client_id: ClientId, pub client_name: Option, pub scope: Option, pub redirect_uri: String, diff --git a/crates/tranquil-oauth-server/src/endpoints/authorize/passkey.rs b/crates/tranquil-oauth-server/src/endpoints/authorize/passkey.rs index 6771b5f..8521a6a 100644 --- a/crates/tranquil-oauth-server/src/endpoints/authorize/passkey.rs +++ b/crates/tranquil-oauth-server/src/endpoints/authorize/passkey.rs @@ -627,11 +627,10 @@ pub async fn passkey_finish( .map(|s| s.to_string()) .collect(); - let passkey_finish_client_id = ClientId::from(request_data.parameters.client_id.clone()); let needs_consent = should_show_consent( state.repos.oauth.as_ref(), &did, - &passkey_finish_client_id, + &request_data.parameters.client_id, &requested_scopes, ) .await @@ -645,7 +644,7 @@ pub async fn passkey_finish( return Json(serde_json::json!({"redirect_uri": consent_url})).into_response(); } - let code = Code::generate(); + let code = AuthorizationCode::generate(); let passkey_final_device_id = device_id.clone(); let passkey_final_code = AuthorizationCode::from(code.0.clone()); if state diff --git a/crates/tranquil-oauth-server/src/endpoints/authorize/registration.rs b/crates/tranquil-oauth-server/src/endpoints/authorize/registration.rs index 445944e..b1a8a88 100644 --- a/crates/tranquil-oauth-server/src/endpoints/authorize/registration.rs +++ b/crates/tranquil-oauth-server/src/endpoints/authorize/registration.rs @@ -237,11 +237,10 @@ pub async fn register_complete( .split_whitespace() .map(|s| s.to_string()) .collect(); - let client_id_typed = ClientId::from(request_data.parameters.client_id.clone()); let needs_consent = should_show_consent( state.repos.oauth.as_ref(), &did, - &client_id_typed, + &request_data.parameters.client_id, &requested_scopes, ) .await @@ -260,12 +259,11 @@ pub async fn register_complete( return Json(serde_json::json!({"redirect_uri": consent_url})).into_response(); } - let code = Code::generate(); - let auth_code = AuthorizationCode::from(code.0.clone()); + let code = AuthorizationCode::generate(); if let Err(e) = state .repos .oauth - .update_authorization_request(&request_id, &did, None, &auth_code) + .update_authorization_request(&request_id, &did, None, &code) .await { tracing::error!( @@ -315,8 +313,7 @@ pub async fn establish_session( (id, None) } None => { - let new_id = DeviceId::generate(); - let device_typed = DeviceIdType::new(new_id.0.clone()); + let device_id = DeviceId::generate(); let device_data = DeviceData { session_id: SessionId::generate(), user_agent: extract_user_agent(&headers), @@ -327,7 +324,7 @@ pub async fn establish_session( if let Err(e) = state .repos .oauth - .create_device(&device_typed, &device_data) + .create_device(&device_id, &device_data) .await { tracing::error!(error = ?e, "Failed to create device"); @@ -344,7 +341,7 @@ pub async fn establish_session( if let Err(e) = state .repos .oauth - .upsert_account_device(did, &device_typed) + .upsert_account_device(did, &device_id) .await { tracing::error!(error = ?e, "Failed to link device to account"); @@ -358,8 +355,8 @@ pub async fn establish_session( .into_response(); } - let cookie = make_device_cookie(&device_typed); - (device_typed, Some(cookie)) + let cookie = make_device_cookie(&device_id); + (device_id, Some(cookie)) } }; diff --git a/crates/tranquil-oauth-server/src/endpoints/authorize/two_factor.rs b/crates/tranquil-oauth-server/src/endpoints/authorize/two_factor.rs index 2a19aae..0d2bb96 100644 --- a/crates/tranquil-oauth-server/src/endpoints/authorize/two_factor.rs +++ b/crates/tranquil-oauth-server/src/endpoints/authorize/two_factor.rs @@ -162,7 +162,7 @@ pub async fn authorize_2fa_post( ); } let _ = state.repos.oauth.delete_2fa_challenge(challenge.id).await; - let code = Code::generate(); + let code = AuthorizationCode::generate(); let device_id = extract_device_cookie(&headers); let twofa_totp_device_id = device_id.clone(); let twofa_totp_code = AuthorizationCode::from(code.0.clone()); @@ -247,8 +247,7 @@ pub async fn authorize_2fa_post( let trust_device_id = match &device_id { Some(existing_id) => existing_id.clone(), None => { - let new_id = DeviceId::generate(); - let new_device_id_typed = DeviceIdType::new(new_id.0.clone()); + let new_device_id = DeviceId::generate(); let device_data = DeviceData { session_id: SessionId::generate(), user_agent: extract_user_agent(&headers), @@ -258,14 +257,14 @@ pub async fn authorize_2fa_post( if state .repos .oauth - .create_device(&new_device_id_typed, &device_data) + .create_device(&new_device_id, &device_data) .await .is_ok() { - new_cookie = Some(make_device_cookie(&new_device_id_typed)); - device_id = Some(new_device_id_typed.clone()); + new_cookie = Some(make_device_cookie(&new_device_id)); + device_id = Some(new_device_id.clone()); } - new_device_id_typed + new_device_id } }; let _ = state @@ -286,11 +285,10 @@ pub async fn authorize_2fa_post( .split_whitespace() .map(|s| s.to_string()) .collect(); - let twofa_post_client_id = ClientId::from(request_data.parameters.client_id.clone()); let needs_consent = should_show_consent( state.repos.oauth.as_ref(), &did, - &twofa_post_client_id, + &request_data.parameters.client_id, &requested_scopes, ) .await @@ -310,7 +308,7 @@ pub async fn authorize_2fa_post( } return Json(serde_json::json!({"redirect_uri": consent_url})).into_response(); } - let code = Code::generate(); + let code = AuthorizationCode::generate(); let twofa_final_device_id = device_id.clone(); let twofa_final_code = AuthorizationCode::from(code.0.clone()); if state diff --git a/crates/tranquil-oauth-server/src/endpoints/delegation.rs b/crates/tranquil-oauth-server/src/endpoints/delegation.rs index 164fd9d..b52b90b 100644 --- a/crates/tranquil-oauth-server/src/endpoints/delegation.rs +++ b/crates/tranquil-oauth-server/src/endpoints/delegation.rs @@ -450,7 +450,7 @@ pub async fn delegation_auth_token( #[derive(Debug, Deserialize)] pub struct CrossPdsCallbackParams { - pub code: String, + pub code: tranquil_types::AuthorizationCode, pub state: String, pub iss: Option, } diff --git a/crates/tranquil-oauth-server/src/endpoints/par.rs b/crates/tranquil-oauth-server/src/endpoints/par.rs index 516f82e..2c1e18c 100644 --- a/crates/tranquil-oauth-server/src/endpoints/par.rs +++ b/crates/tranquil-oauth-server/src/endpoints/par.rs @@ -9,14 +9,14 @@ use tranquil_pds::oauth::{ }; use tranquil_pds::rate_limit::{OAuthParLimit, OAuthRateLimited}; use tranquil_pds::state::AppState; -use tranquil_types::RequestId as RequestIdType; +use tranquil_types::{ClientId, JwkThumbprint}; const PAR_EXPIRY_SECONDS: i64 = 600; #[derive(Debug, Deserialize)] pub struct ParRequest { pub response_type: String, - pub client_id: String, + pub client_id: ClientId, pub redirect_uri: String, #[serde(default)] pub scope: Option, @@ -113,11 +113,10 @@ pub async fn pushed_authorization_request( code: None, controller_did: None, }; - let request_id_typed = RequestIdType::from(request_id.0.clone()); state .repos .oauth - .create_authorization_request(&request_id_typed, &request_data) + .create_authorization_request(&request_id, &request_data) .await .map_err(tranquil_pds::oauth::db_err_to_oauth)?; tokio::spawn({ @@ -131,7 +130,7 @@ pub async fn pushed_authorization_request( Ok(( axum::http::StatusCode::CREATED, Json(ParResponse { - request_uri: request_id.0, + request_uri: request_id.into_inner(), expires_in: u64::try_from(PAR_EXPIRY_SECONDS).unwrap_or(600), }), )) diff --git a/crates/tranquil-oauth-server/src/endpoints/token/grants.rs b/crates/tranquil-oauth-server/src/endpoints/token/grants.rs index 698eb40..b8a508d 100644 --- a/crates/tranquil-oauth-server/src/endpoints/token/grants.rs +++ b/crates/tranquil-oauth-server/src/endpoints/token/grants.rs @@ -33,7 +33,7 @@ pub async fn handle_authorization_code_grant( client_id = ?request.client_auth.client_id(), "Authorization code grant requested" ); - let (code, code_verifier, redirect_uri) = match request.grant { + let (auth_code, code_verifier, redirect_uri) = match request.grant { TokenGrant::AuthorizationCode { code, code_verifier, @@ -45,7 +45,6 @@ pub async fn handle_authorization_code_grant( )); } }; - let auth_code = AuthorizationCode::from(code); let auth_request = state .repos .oauth @@ -249,7 +248,7 @@ pub async fn handle_authorization_code_grant( None => TokenType::Bearer, }, expires_in: ACCESS_TOKEN_EXPIRY_SECONDS, - refresh_token: Some(refresh_token.0), + refresh_token: Some(refresh_token), scope: final_scope, sub: Some(did), }), @@ -262,7 +261,7 @@ pub async fn handle_refresh_token_grant( request: ValidatedTokenRequest, dpop_proof: Option, ) -> Result<(HeaderMap, Json), OAuthError> { - let refresh_token_str = match request.grant { + let refresh_token = match request.grant { TokenGrant::RefreshToken { refresh_token } => refresh_token, _ => { return Err(OAuthError::InvalidRequest( @@ -270,6 +269,7 @@ pub async fn handle_refresh_token_grant( )); } }; + let refresh_token_str = refresh_token.as_str(); let token_prefix = &refresh_token_str[..std::cmp::min(16, refresh_token_str.len())]; tracing::info!( refresh_token_prefix = %token_prefix, @@ -277,8 +277,7 @@ pub async fn handle_refresh_token_grant( "Refresh token grant requested" ); - let refresh_token_typed = RefreshTokenType::from(refresh_token_str.clone()); - let lookup = lookup_refresh_token(state.repos.oauth.as_ref(), &refresh_token_typed).await?; + let lookup = lookup_refresh_token(state.repos.oauth.as_ref(), &refresh_token).await?; let token_state = lookup.state(); tracing::debug!(state = %token_state, "Refresh token state"); @@ -319,7 +318,7 @@ pub async fn handle_refresh_token_grant( None => TokenType::Bearer, }, expires_in: ACCESS_TOKEN_EXPIRY_SECONDS, - refresh_token: token_data.current_refresh_token.map(|r| r.0), + refresh_token: token_data.current_refresh_token, scope: token_data.scope, sub: Some(token_data.did.to_string()), }), @@ -398,11 +397,10 @@ pub async fn handle_refresh_token_grant( REFRESH_TOKEN_EXPIRY_DAYS_CONFIDENTIAL }; let new_expires_at = Utc::now() + Duration::days(refresh_expiry_days); - let new_refresh_typed = RefreshTokenType::from(new_refresh_token.0.clone()); state .repos .oauth - .rotate_token(db_id, &new_refresh_typed, new_expires_at) + .rotate_token(db_id, &new_refresh_token, new_expires_at) .await .map_err(tranquil_pds::oauth::db_err_to_oauth)?; tracing::info!( @@ -434,7 +432,7 @@ pub async fn handle_refresh_token_grant( None => TokenType::Bearer, }, expires_in: ACCESS_TOKEN_EXPIRY_SECONDS, - refresh_token: Some(new_refresh_token.0), + refresh_token: Some(new_refresh_token), scope: token_data.scope, sub: Some(token_data.did.to_string()), }), diff --git a/crates/tranquil-oauth-server/src/endpoints/token/helpers.rs b/crates/tranquil-oauth-server/src/endpoints/token/helpers.rs index 1ee718c..8af930a 100644 --- a/crates/tranquil-oauth-server/src/endpoints/token/helpers.rs +++ b/crates/tranquil-oauth-server/src/endpoints/token/helpers.rs @@ -30,10 +30,10 @@ pub fn verify_pkce(code_challenge: &str, code_verifier: &str) -> Result<(), OAut Ok(()) } -pub fn create_access_token( - session_id: &str, - sub: &str, - dpop_jkt: Option<&str>, +pub fn create_access_token_with_delegation( + session_id: &tranquil_types::TokenId, + sub: &tranquil_types::Did, + dpop_jkt: Option<&tranquil_types::JwkThumbprint>, scope: Option<&str>, ) -> Result { create_access_token_with_delegation(session_id, sub, dpop_jkt, scope, None) diff --git a/crates/tranquil-oauth-server/src/endpoints/token/introspect.rs b/crates/tranquil-oauth-server/src/endpoints/token/introspect.rs index cda163b..35204b5 100644 --- a/crates/tranquil-oauth-server/src/endpoints/token/introspect.rs +++ b/crates/tranquil-oauth-server/src/endpoints/token/introspect.rs @@ -62,7 +62,7 @@ pub struct IntrospectResponse { #[serde(skip_serializing_if = "Option::is_none")] pub scope: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub client_id: Option, + pub client_id: Option, #[serde(skip_serializing_if = "Option::is_none")] pub username: Option, #[serde(skip_serializing_if = "Option::is_none")] diff --git a/crates/tranquil-oauth-server/src/endpoints/token/types.rs b/crates/tranquil-oauth-server/src/endpoints/token/types.rs index 84d3b16..7bff327 100644 --- a/crates/tranquil-oauth-server/src/endpoints/token/types.rs +++ b/crates/tranquil-oauth-server/src/endpoints/token/types.rs @@ -1,5 +1,6 @@ use serde::{Deserialize, Serialize}; use tranquil_pds::oauth::OAuthError; +use tranquil_types::{AuthorizationCode, RefreshToken}; #[derive(Debug, Clone, PartialEq, Eq)] pub enum GrantType { @@ -63,12 +64,12 @@ pub struct TokenRequest { #[derive(Debug, Clone)] pub enum TokenGrant { AuthorizationCode { - code: String, + code: AuthorizationCode, code_verifier: String, redirect_uri: Option, }, RefreshToken { - refresh_token: String, + refresh_token: RefreshToken, }, } @@ -123,7 +124,7 @@ impl TokenRequest { ) })?; TokenGrant::AuthorizationCode { - code, + code: AuthorizationCode::from(code), code_verifier, redirect_uri: self.redirect_uri, } @@ -134,7 +135,9 @@ impl TokenRequest { "refresh_token is required for refresh_token grant".to_string(), ) })?; - TokenGrant::RefreshToken { refresh_token } + TokenGrant::RefreshToken { + refresh_token: RefreshToken::from(refresh_token), + } } }; @@ -177,7 +180,7 @@ pub struct TokenResponse { pub token_type: TokenType, pub expires_in: u64, #[serde(skip_serializing_if = "Option::is_none")] - pub refresh_token: Option, + pub refresh_token: Option, #[serde(skip_serializing_if = "Option::is_none")] pub scope: Option, #[serde(skip_serializing_if = "Option::is_none")] diff --git a/crates/tranquil-oauth-server/src/sso_endpoints.rs b/crates/tranquil-oauth-server/src/sso_endpoints.rs index bfa83e5..de96bda 100644 --- a/crates/tranquil-oauth-server/src/sso_endpoints.rs +++ b/crates/tranquil-oauth-server/src/sso_endpoints.rs @@ -1256,7 +1256,7 @@ pub async fn complete_registration( if let Err(e) = state .repos .oauth - .set_authorization_did(&request_id, &did_typed, None) + .set_authorization_did(&request_id, &did, None) .await { tracing::error!("Failed to set authorization DID: {:?}", e); diff --git a/crates/tranquil-oauth/src/client.rs b/crates/tranquil-oauth/src/client.rs index 1e84e73..cb4b588 100644 --- a/crates/tranquil-oauth/src/client.rs +++ b/crates/tranquil-oauth/src/client.rs @@ -6,10 +6,11 @@ use tokio::sync::RwLock; use crate::OAuthError; use crate::types::ClientAuth; +use tranquil_types::ClientId; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ClientMetadata { - pub client_id: String, + pub client_id: ClientId, #[serde(skip_serializing_if = "Option::is_none")] pub client_name: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -38,7 +39,7 @@ pub struct ClientMetadata { impl Default for ClientMetadata { fn default() -> Self { Self { - client_id: String::new(), + client_id: ClientId::new(""), client_name: None, client_uri: None, logo_uri: None, @@ -97,7 +98,7 @@ impl ClientMetadataCache { } } - fn is_loopback_client(client_id: &str) -> bool { + fn is_loopback_client(client_id: &ClientId) -> bool { if let Ok(url) = reqwest::Url::parse(client_id) { url.scheme() == "http" && url.host_str() == Some("localhost") @@ -108,7 +109,7 @@ impl ClientMetadataCache { } } - fn build_loopback_metadata(client_id: &str) -> Result { + fn build_loopback_metadata(client_id: &ClientId) -> Result { let url = reqwest::Url::parse(client_id) .map_err(|_| OAuthError::InvalidClient("Invalid loopback client_id URL".into()))?; let mut redirect_uris = Vec::::new(); @@ -129,7 +130,7 @@ impl ClientMetadataCache { scope = Some("atproto".into()); } Ok(ClientMetadata { - client_id: client_id.into(), + client_id: client_id.clone(), client_name: Some("Loopback Client".into()), client_uri: None, logo_uri: None, @@ -145,13 +146,13 @@ impl ClientMetadataCache { }) } - pub async fn get(&self, client_id: &str) -> Result { + pub async fn get(&self, client_id: &ClientId) -> Result { if Self::is_loopback_client(client_id) { return Self::build_loopback_metadata(client_id); } { let cache = self.cache.read().await; - if let Some(cached) = cache.get(client_id) + if let Some(cached) = cache.get(client_id.as_str()) && cached.cached_at.elapsed().as_secs() < self.cache_ttl_secs { return Ok(cached.metadata.clone()); @@ -241,7 +242,7 @@ impl ClientMetadataCache { Ok(jwks) } - async fn fetch_metadata(&self, client_id: &str) -> Result { + async fn fetch_metadata(&self, client_id: &ClientId) -> Result { if !client_id.starts_with("http://") && !client_id.starts_with("https://") { return Err(OAuthError::InvalidClient( "client_id must be a URL".to_string(), @@ -257,7 +258,7 @@ impl ClientMetadataCache { } let response = self .http_client - .get(client_id) + .get(client_id.as_str()) .header("Accept", "application/json") .send() .await @@ -276,8 +277,8 @@ impl ClientMetadataCache { OAuthError::InvalidClient(format!("Invalid client metadata JSON: {}", e)) })?; if metadata.client_id.is_empty() { - metadata.client_id = client_id.to_string(); - } else if metadata.client_id != client_id { + metadata.client_id = client_id.clone(); + } else if metadata.client_id != *client_id { return Err(OAuthError::InvalidClient( "client_id in metadata does not match request".to_string(), )); diff --git a/crates/tranquil-oauth/src/lib.rs b/crates/tranquil-oauth/src/lib.rs index d90d7b3..052aae0 100644 --- a/crates/tranquil-oauth/src/lib.rs +++ b/crates/tranquil-oauth/src/lib.rs @@ -11,10 +11,10 @@ pub use dpop::{ }; pub use error::OAuthError; pub use types::{ - AuthFlow, AuthFlowWithUser, AuthorizationRequestParameters, AuthorizationServerMetadata, - AuthorizedClientData, ClientAuth, Code, CodeChallengeMethod, DPoPClaims, DeviceData, DeviceId, - FlowAuthenticated, FlowAuthorized, FlowExpired, FlowNotAuthenticated, FlowNotAuthorized, - FlowPending, JwkPublicKey, Jwks, OAuthClientMetadata, ParResponse, Prompt, + AuthFlow, AuthFlowWithUser, AuthorizationCode, AuthorizationRequestParameters, + AuthorizationServerMetadata, AuthorizedClientData, ClientAuth, CodeChallengeMethod, DeviceData, + DeviceId, FlowAuthenticated, FlowAuthorized, FlowExpired, FlowNotAuthenticated, + FlowNotAuthorized, FlowPending, JwkPublicKey, Jwks, OAuthClientMetadata, ParResponse, Prompt, ProtectedResourceMetadata, RefreshToken, RefreshTokenState, RequestData, RequestId, ResponseMode, ResponseType, SessionId, TokenData, TokenId, TokenRequest, TokenResponse, }; diff --git a/crates/tranquil-oauth/src/types.rs b/crates/tranquil-oauth/src/types.rs index 792e451..f8c1f3e 100644 --- a/crates/tranquil-oauth/src/types.rs +++ b/crates/tranquil-oauth/src/types.rs @@ -1,7 +1,9 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use serde_json::Value as JsonValue; -use tranquil_types::Did; +use tranquil_types::{ClientId, Did}; + +pub use tranquil_types::{AuthorizationCode, DeviceId, RefreshToken, RequestId, TokenId}; #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)] #[serde(transparent)] @@ -18,27 +20,8 @@ pub struct TokenId(pub String); #[sqlx(transparent)] pub struct DeviceId(pub String); -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)] -#[serde(transparent)] -#[sqlx(transparent)] -pub struct SessionId(pub String); - -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)] -#[serde(transparent)] -#[sqlx(transparent)] -pub struct Code(pub String); - -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)] -#[serde(transparent)] -#[sqlx(transparent)] -pub struct RefreshToken(pub String); - -impl RequestId { - pub fn generate() -> Self { - Self(format!( - "urn:ietf:params:oauth:request_uri:{}", - uuid::Uuid::new_v4() - )) + pub fn as_str(&self) -> &str { + &self.0 } } @@ -155,7 +138,7 @@ impl Prompt { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AuthorizationRequestParameters { pub response_type: ResponseType, - pub client_id: String, + pub client_id: ClientId, pub redirect_uri: String, pub scope: Option, pub state: Option, @@ -171,13 +154,13 @@ pub struct AuthorizationRequestParameters { #[derive(Debug, Clone)] pub struct RequestData { - pub client_id: String, + pub client_id: ClientId, pub client_auth: Option, pub parameters: AuthorizationRequestParameters, pub expires_at: DateTime, pub did: Option, pub device_id: Option, - pub code: Option, + pub code: Option, pub controller_did: Option, } @@ -196,12 +179,12 @@ pub struct TokenData { pub created_at: DateTime, pub updated_at: DateTime, pub expires_at: DateTime, - pub client_id: String, + pub client_id: ClientId, pub client_auth: ClientAuth, pub device_id: Option, pub parameters: AuthorizationRequestParameters, pub details: Option, - pub code: Option, + pub code: Option, pub current_refresh_token: Option, pub scope: Option, pub controller_did: Option, @@ -215,7 +198,7 @@ pub struct AuthorizedClientData { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct OAuthClientMetadata { - pub client_id: String, + pub client_id: ClientId, pub client_name: Option, pub client_uri: Option, pub logo_uri: Option, @@ -270,7 +253,7 @@ pub struct TokenResponse { pub token_type: String, pub expires_in: u64, #[serde(skip_serializing_if = "Option::is_none")] - pub refresh_token: Option, + pub refresh_token: Option, #[serde(skip_serializing_if = "Option::is_none")] pub scope: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -280,11 +263,11 @@ pub struct TokenResponse { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TokenRequest { pub grant_type: String, - pub code: Option, + pub code: Option, pub redirect_uri: Option, pub code_verifier: Option, - pub refresh_token: Option, - pub client_id: Option, + pub refresh_token: Option, + pub client_id: Option, pub client_secret: Option, } @@ -320,7 +303,7 @@ pub struct Jwks { #[derive(Debug, Clone)] pub struct FlowPending { pub parameters: AuthorizationRequestParameters, - pub client_id: String, + pub client_id: ClientId, pub client_auth: Option, pub expires_at: DateTime, pub controller_did: Option, @@ -329,7 +312,7 @@ pub struct FlowPending { #[derive(Debug, Clone)] pub struct FlowAuthenticated { pub parameters: AuthorizationRequestParameters, - pub client_id: String, + pub client_id: ClientId, pub client_auth: Option, pub expires_at: DateTime, pub did: Did, @@ -340,12 +323,12 @@ pub struct FlowAuthenticated { #[derive(Debug, Clone)] pub struct FlowAuthorized { pub parameters: AuthorizationRequestParameters, - pub client_id: String, + pub client_id: ClientId, pub client_auth: Option, pub expires_at: DateTime, pub did: Did, pub device_id: Option, - pub code: Code, + pub code: AuthorizationCode, pub controller_did: Option, } @@ -444,7 +427,7 @@ impl AuthFlowWithUser { } } - pub fn client_id(&self) -> &str { + pub fn client_id(&self) -> &ClientId { match self { AuthFlowWithUser::Authenticated(a) => &a.client_id, AuthFlowWithUser::Authorized(a) => &a.client_id, @@ -522,16 +505,16 @@ mod tests { fn make_request_data( did: Option, - code: Option, + code: Option, expires_in: Duration, ) -> RequestData { RequestData { - client_id: "test-client".into(), + client_id: ClientId::new("test-client"), client_auth: None, parameters: AuthorizationRequestParameters { response_type: ResponseType::Code, - client_id: "test-client".into(), - redirect_uri: "https://example.com/callback".into(), + client_id: ClientId::new("test-client"), + redirect_uri: "https://oyster.cafe/callback".into(), scope: Some("atproto".into()), state: None, code_challenge: "test".into(), @@ -554,8 +537,8 @@ mod tests { s.parse().expect("valid test DID") } - fn test_code(s: &str) -> Code { - Code(s.to_string()) + fn test_code(s: &str) -> AuthorizationCode { + AuthorizationCode::new(s) } #[test] diff --git a/crates/tranquil-pds/src/config.rs b/crates/tranquil-pds/src/config.rs index b7c00bf..9f950d4 100644 --- a/crates/tranquil-pds/src/config.rs +++ b/crates/tranquil-pds/src/config.rs @@ -126,7 +126,7 @@ impl AuthConfig { &self.dpop_secret } - pub fn sign_device_cookie(&self, device_id: &str) -> String { + pub fn sign_device_cookie(&self, device_id: &crate::types::DeviceId) -> String { use hmac::Mac; type HmacSha256 = hmac::Hmac; @@ -144,7 +144,7 @@ impl AuthConfig { format!("{}.{}.{}", device_id, timestamp, signature) } - pub fn verify_device_cookie(&self, cookie_value: &str) -> Option { + pub fn verify_device_cookie(&self, cookie_value: &str) -> Option { use hmac::Mac; type HmacSha256 = hmac::Hmac; @@ -181,7 +181,7 @@ impl AuthConfig { .ct_eq(expected_signature.as_bytes()) .into() { - Some(device_id.to_string()) + Some(crate::types::DeviceId::new(device_id)) } else { None } diff --git a/crates/tranquil-pds/src/oauth/client.rs b/crates/tranquil-pds/src/oauth/client.rs index 1ef482e..9717b15 100644 --- a/crates/tranquil-pds/src/oauth/client.rs +++ b/crates/tranquil-pds/src/oauth/client.rs @@ -10,7 +10,7 @@ use tranquil_oauth::{ AuthorizationServerMetadata, ClientMetadata, compute_es256_jkt, compute_pkce_challenge, create_dpop_proof, }; -use tranquil_types::Did; +use tranquil_types::{AuthorizationCode, ClientId, Did}; use crate::cache::Cache; @@ -46,13 +46,16 @@ pub struct ParResult { } pub struct DelegationOAuthUrls { - pub client_id: String, + pub client_id: ClientId, pub redirect_uri: String, } pub fn delegation_oauth_urls(hostname: &str) -> DelegationOAuthUrls { DelegationOAuthUrls { - client_id: format!("https://{}/oauth/delegation/client-metadata", hostname), + client_id: ClientId::new(format!( + "https://{}/oauth/delegation/client-metadata", + hostname + )), redirect_uri: format!("https://{}/oauth/delegation/callback", hostname), } } @@ -278,7 +281,7 @@ impl CrossPdsOAuthClient { let mut params = vec![ ("response_type", "code".to_string()), - ("client_id", urls.client_id.clone()), + ("client_id", urls.client_id.to_string()), ("redirect_uri", urls.redirect_uri.clone()), ("scope", "atproto".to_string()), ("state", state.clone()), @@ -340,8 +343,8 @@ impl CrossPdsOAuthClient { pub async fn exchange_code( &self, auth_state: &CrossPdsAuthState, - code: &str, - client_id: &str, + code: &AuthorizationCode, + client_id: &ClientId, redirect_uri: &str, ) -> Result { let meta = self diff --git a/crates/tranquil-pds/src/oauth/mod.rs b/crates/tranquil-pds/src/oauth/mod.rs index a919b85..53707c3 100644 --- a/crates/tranquil-pds/src/oauth/mod.rs +++ b/crates/tranquil-pds/src/oauth/mod.rs @@ -9,14 +9,14 @@ pub fn db_err_to_oauth(err: tranquil_db_traits::DbError) -> OAuthError { } pub use tranquil_oauth::{ - AuthFlow, AuthFlowWithUser, AuthorizationRequestParameters, AuthorizationServerMetadata, - AuthorizedClientData, ClientAuth, ClientMetadata, ClientMetadataCache, Code, - CodeChallengeMethod, DPoPClaims, DPoPJwk, DPoPProofHeader, DPoPProofPayload, DPoPVerifier, - DPoPVerifyResult, DeviceData, DeviceId, FlowAuthenticated, FlowAuthorized, FlowExpired, - FlowNotAuthenticated, FlowNotAuthorized, FlowPending, JwkPublicKey, Jwks, OAuthClientMetadata, - OAuthError, ParResponse, Prompt, ProtectedResourceMetadata, RefreshToken, RefreshTokenState, - RequestData, RequestId, ResponseMode, ResponseType, SessionId, TokenData, TokenId, - TokenRequest, TokenResponse, compute_access_token_hash, compute_jwk_thumbprint, + AuthFlow, AuthFlowWithUser, AuthorizationCode, AuthorizationRequestParameters, + AuthorizationServerMetadata, AuthorizedClientData, ClientAuth, ClientMetadata, + ClientMetadataCache, CodeChallengeMethod, DPoPJwk, DPoPProofHeader, DPoPProofPayload, + DPoPVerifier, DPoPVerifyResult, DeviceData, DeviceId, FlowAuthenticated, FlowAuthorized, + FlowExpired, FlowNotAuthenticated, FlowNotAuthorized, FlowPending, JwkPublicKey, Jwks, + OAuthClientMetadata, OAuthError, ParResponse, Prompt, ProtectedResourceMetadata, RefreshToken, + RefreshTokenState, RequestData, RequestId, ResponseMode, ResponseType, SessionId, TokenData, + TokenId, TokenRequest, TokenResponse, compute_access_token_hash, compute_jwk_thumbprint, compute_pkce_challenge, verify_client_auth, }; diff --git a/crates/tranquil-pds/src/oauth/verify.rs b/crates/tranquil-pds/src/oauth/verify.rs index 6e803da..9d7e66f 100644 --- a/crates/tranquil-pds/src/oauth/verify.rs +++ b/crates/tranquil-pds/src/oauth/verify.rs @@ -23,7 +23,6 @@ use crate::state::AppState; pub struct OAuthTokenInfo { pub did: Did, pub token_id: TokenId, - pub client_id: ClientId, pub scope: Option, pub dpop_jkt: Option, pub controller_did: Option, @@ -101,7 +100,7 @@ pub async fn verify_oauth_access_token( Ok(VerifyResult { did, token_id, - client_id: ClientId::from(token_data.client_id), + client_id: token_data.client_id, scope: token_data.scope, }) } @@ -171,16 +170,6 @@ pub fn extract_oauth_token_info(token: &str) -> Result Result) -> TokenData { - let client_id = "https://squid.nel.pet/client".to_string(); + let client_id = tranquil_types::ClientId::from("https://squid.nel.pet/client".to_string()); TokenData { did: did.clone(), - token_id: TokenId(token_id.to_string()), + token_id: TokenId::from(token_id.to_string()), created_at, updated_at: created_at, expires_at: created_at + Duration::hours(1), diff --git a/crates/tranquil-pds/tests/sso.rs b/crates/tranquil-pds/tests/sso.rs index 49b606f..011fc94 100644 --- a/crates/tranquil-pds/tests/sso.rs +++ b/crates/tranquil-pds/tests/sso.rs @@ -619,12 +619,12 @@ async fn test_sso_get_pending_registration_token_too_long() { fn test_request_data() -> RequestData { RequestData { - client_id: "https://test.example.com".to_string(), + client_id: tranquil_types::ClientId::from("https://squid.oyster.cafe".to_string()), client_auth: None, parameters: AuthorizationRequestParameters { response_type: ResponseType::Code, - client_id: "https://test.example.com".to_string(), - redirect_uri: "https://test.example.com/callback".to_string(), + client_id: tranquil_types::ClientId::from("https://squid.oyster.cafe".to_string()), + redirect_uri: "https://squid.oyster.cafe/callback".to_string(), scope: Some("atproto".to_string()), state: Some("teststate".to_string()), code_challenge: "testchallenge".to_string(), diff --git a/crates/tranquil-store/src/metastore/client.rs b/crates/tranquil-store/src/metastore/client.rs index 4136489..6c31730 100644 --- a/crates/tranquil-store/src/metastore/client.rs +++ b/crates/tranquil-store/src/metastore/client.rs @@ -3343,7 +3343,7 @@ impl tranquil_db_traits::UserRepository for MetastoreCli async fn get_oauth_token_with_user( &self, - token_id: &str, + token_id: &TokenId, ) -> Result, DbError> { let (tx, rx) = oneshot::channel(); self.pool diff --git a/crates/tranquil-store/src/metastore/oauth_ops.rs b/crates/tranquil-store/src/metastore/oauth_ops.rs index 95f8756..842f575 100644 --- a/crates/tranquil-store/src/metastore/oauth_ops.rs +++ b/crates/tranquil-store/src/metastore/oauth_ops.rs @@ -103,11 +103,11 @@ impl OAuthOps { fn token_value_to_data(&self, v: &OAuthTokenValue) -> Result { let did = Did::new(v.did.clone()) .map_err(|_| MetastoreError::CorruptData("invalid did in oauth token"))?; - let token_id = tranquil_oauth::TokenId(v.token_id.clone()); + let token_id = tranquil_oauth::TokenId::from(v.token_id.clone()); let refresh_token = if v.refresh_token.is_empty() { None } else { - Some(tranquil_oauth::RefreshToken(v.refresh_token.clone())) + Some(tranquil_oauth::RefreshToken::from(v.refresh_token.clone())) }; Ok(TokenData { @@ -116,7 +116,7 @@ impl OAuthOps { created_at: DateTime::from_timestamp_millis(v.created_at_ms).unwrap_or_default(), updated_at: DateTime::from_timestamp_millis(v.updated_at_ms).unwrap_or_default(), expires_at: DateTime::from_timestamp_millis(v.expires_at_ms).unwrap_or_default(), - client_id: v.client_id.clone(), + client_id: tranquil_types::ClientId::from(v.client_id.clone()), client_auth: tranquil_oauth::ClientAuth::None, device_id: None, parameters: serde_json::from_str(&v.parameters_json) @@ -189,7 +189,7 @@ impl OAuthOps { .map_err(|_| MetastoreError::CorruptData("corrupt oauth client_auth"))?; Ok(RequestData { - client_id: v.client_id.clone(), + client_id: tranquil_types::ClientId::from(v.client_id.clone()), client_auth, parameters, expires_at: DateTime::from_timestamp_millis(v.expires_at_ms).unwrap_or_default(), @@ -202,8 +202,11 @@ impl OAuthOps { device_id: v .device_id .as_ref() - .map(|d| tranquil_oauth::DeviceId(d.clone())), - code: v.code.as_ref().map(|c| tranquil_oauth::Code(c.clone())), + .map(|d| tranquil_oauth::DeviceId::from(d.clone())), + code: v + .code + .as_ref() + .map(|c| tranquil_oauth::AuthorizationCode::from(c.clone())), controller_did: v .controller_did .as_ref() @@ -217,7 +220,7 @@ impl OAuthOps { fn data_to_request_value(&self, data: &RequestData) -> OAuthRequestValue { OAuthRequestValue { - client_id: data.client_id.clone(), + client_id: data.client_id.to_string(), client_auth_json: data .client_auth .as_ref() @@ -225,8 +228,8 @@ impl OAuthOps { parameters_json: serde_json::to_string(&data.parameters).unwrap_or_default(), expires_at_ms: data.expires_at.timestamp_millis(), did: data.did.as_ref().map(|d| d.to_string()), - device_id: data.device_id.as_ref().map(|d| d.0.clone()), - code: data.code.as_ref().map(|c| c.0.clone()), + device_id: data.device_id.as_ref().map(|d| d.to_string()), + code: data.code.as_ref().map(|c| c.to_string()), controller_did: data.controller_did.as_ref().map(|d| d.to_string()), } } @@ -239,8 +242,8 @@ impl OAuthOps { let value = OAuthTokenValue { family_id, did: data.did.to_string(), - client_id: data.client_id.clone(), - token_id: data.token_id.0.clone(), + client_id: data.client_id.to_string(), + token_id: data.token_id.to_string(), refresh_token: data .current_refresh_token .as_ref() @@ -1100,7 +1103,7 @@ impl OAuthOps { Ok(TwoFactorChallenge { id, did: did.clone(), - request_uri: request_uri.as_str().to_owned(), + request_uri: request_uri.clone(), code, attempts: 0, created_at: now, @@ -1140,7 +1143,7 @@ impl OAuthOps { id: Uuid::from_bytes(v.id), did: Did::new(v.did) .map_err(|_| MetastoreError::CorruptData("invalid did in 2fa challenge"))?, - request_uri: v.request_uri, + request_uri: RequestId::from(v.request_uri), code: v.code, attempts: v.attempts, created_at: DateTime::from_timestamp_millis(v.created_at_ms).unwrap_or_default(), @@ -1358,7 +1361,7 @@ impl OAuthOps { match DeviceTrustValue::deserialize(&val_bytes) { Some(v) => { acc.push(TrustedDeviceRow { - id: v.device_id, + id: DeviceId::from(v.device_id), user_agent: v.user_agent, friendly_name: v.friendly_name, trusted_at: v.trusted_at_ms.and_then(DateTime::from_timestamp_millis), @@ -1604,7 +1607,7 @@ impl OAuthOps { fn default_parameters(client_id: &str) -> tranquil_oauth::AuthorizationRequestParameters { tranquil_oauth::AuthorizationRequestParameters { response_type: tranquil_oauth::ResponseType::Code, - client_id: client_id.to_owned(), + client_id: tranquil_types::ClientId::new(client_id), redirect_uri: String::new(), scope: None, state: None, diff --git a/crates/tranquil-types/src/lib.rs b/crates/tranquil-types/src/lib.rs index a50b928..320ed63 100644 --- a/crates/tranquil-types/src/lib.rs +++ b/crates/tranquil-types/src/lib.rs @@ -727,6 +727,12 @@ simple_string_newtype! { pub struct TokenId; } +impl TokenId { + pub fn generate() -> Self { + Self(uuid::Uuid::new_v4().to_string()) + } +} + simple_string_newtype! { pub struct ClientId; } @@ -735,10 +741,25 @@ simple_string_newtype! { pub struct DeviceId; } +impl DeviceId { + pub fn generate() -> Self { + Self(uuid::Uuid::new_v4().to_string()) + } +} + simple_string_newtype! { pub struct RequestId; } +impl RequestId { + pub fn generate() -> Self { + Self(format!( + "urn:ietf:params:oauth:request_uri:{}", + uuid::Uuid::new_v4() + )) + } +} + simple_string_newtype! { pub struct Jti; } @@ -747,10 +768,28 @@ simple_string_newtype! { pub struct AuthorizationCode; } +impl AuthorizationCode { + pub fn generate() -> Self { + Self(generate_url_safe_secret()) + } +} + simple_string_newtype! { pub struct RefreshToken; } +impl RefreshToken { + pub fn generate() -> Self { + Self(generate_url_safe_secret()) + } +} + +fn generate_url_safe_secret() -> String { + use rand::Rng; + let bytes: [u8; 32] = rand::thread_rng().r#gen(); + base64::Engine::encode(&base64::engine::general_purpose::URL_SAFE_NO_PAD, bytes) +} + simple_string_newtype! { pub struct InviteCode; }