mirror of
https://tangled.org/tranquil.farm/tranquil-pds
synced 2026-09-20 09:14:15 +00:00
App password scopes
This commit is contained in:
@@ -94,6 +94,16 @@ pub async fn proxy_handler(
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Token validation failed: {:?}", e);
|
||||
if matches!(e, crate::auth::TokenValidationError::TokenExpired) {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(json!({
|
||||
"error": "ExpiredToken",
|
||||
"message": "Token has expired"
|
||||
})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
use tracing::error;
|
||||
use tracing::{error, info};
|
||||
|
||||
const MAX_BATCH_WRITES: usize = 200;
|
||||
|
||||
@@ -79,6 +79,11 @@ pub async fn apply_writes(
|
||||
headers: axum::http::HeaderMap,
|
||||
Json(input): Json<ApplyWritesInput>,
|
||||
) -> Response {
|
||||
info!(
|
||||
"apply_writes called: repo={}, writes={}",
|
||||
input.repo,
|
||||
input.writes.len()
|
||||
);
|
||||
let token = match crate::auth::extract_bearer_token_from_header(
|
||||
headers.get("Authorization").and_then(|h| h.to_str().ok()),
|
||||
) {
|
||||
@@ -147,7 +152,11 @@ pub async fn apply_writes(
|
||||
.into_response();
|
||||
}
|
||||
|
||||
if is_oauth {
|
||||
let has_custom_scope = scope
|
||||
.as_ref()
|
||||
.map(|s| s != "com.atproto.access")
|
||||
.unwrap_or(false);
|
||||
if is_oauth || has_custom_scope {
|
||||
use std::collections::HashSet;
|
||||
let create_collections: HashSet<&str> = input
|
||||
.writes
|
||||
|
||||
@@ -16,10 +16,10 @@ pub fn create_signed_commit(
|
||||
prev: Option<Cid>,
|
||||
signing_key: &SigningKey,
|
||||
) -> Result<(Vec<u8>, Bytes), String> {
|
||||
let did = jacquard::types::string::Did::new(did)
|
||||
.map_err(|e| format!("Invalid DID: {:?}", e))?;
|
||||
let rev = jacquard::types::string::Tid::from_str(rev)
|
||||
.map_err(|e| format!("Invalid TID: {:?}", e))?;
|
||||
let did =
|
||||
jacquard::types::string::Did::new(did).map_err(|e| format!("Invalid DID: {:?}", e))?;
|
||||
let rev =
|
||||
jacquard::types::string::Tid::from_str(rev).map_err(|e| format!("Invalid TID: {:?}", e))?;
|
||||
let unsigned = Commit::new_unsigned(did, data, rev, prev);
|
||||
let signed = unsigned
|
||||
.sign(signing_key)
|
||||
|
||||
@@ -18,6 +18,8 @@ pub struct AppPassword {
|
||||
pub name: String,
|
||||
pub created_at: String,
|
||||
pub privileged: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub scopes: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
@@ -34,7 +36,7 @@ pub async fn list_app_passwords(
|
||||
Err(e) => return ApiError::from(e).into_response(),
|
||||
};
|
||||
match sqlx::query!(
|
||||
"SELECT name, created_at, privileged FROM app_passwords WHERE user_id = $1 ORDER BY created_at DESC",
|
||||
"SELECT name, created_at, privileged, scopes FROM app_passwords WHERE user_id = $1 ORDER BY created_at DESC",
|
||||
user_id
|
||||
)
|
||||
.fetch_all(&state.db)
|
||||
@@ -47,6 +49,7 @@ pub async fn list_app_passwords(
|
||||
name: row.name.clone(),
|
||||
created_at: row.created_at.to_rfc3339(),
|
||||
privileged: row.privileged,
|
||||
scopes: row.scopes.clone(),
|
||||
})
|
||||
.collect();
|
||||
Json(ListAppPasswordsOutput { passwords }).into_response()
|
||||
@@ -62,6 +65,7 @@ pub async fn list_app_passwords(
|
||||
pub struct CreateAppPasswordInput {
|
||||
pub name: String,
|
||||
pub privileged: Option<bool>,
|
||||
pub scopes: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
@@ -71,6 +75,8 @@ pub struct CreateAppPasswordOutput {
|
||||
pub password: String,
|
||||
pub created_at: String,
|
||||
pub privileged: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub scopes: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn create_app_password(
|
||||
@@ -131,14 +137,16 @@ pub async fn create_app_password(
|
||||
}
|
||||
};
|
||||
let privileged = input.privileged.unwrap_or(false);
|
||||
let scopes = input.scopes.clone();
|
||||
let created_at = chrono::Utc::now();
|
||||
match sqlx::query!(
|
||||
"INSERT INTO app_passwords (user_id, name, password_hash, created_at, privileged) VALUES ($1, $2, $3, $4, $5)",
|
||||
"INSERT INTO app_passwords (user_id, name, password_hash, created_at, privileged, scopes) VALUES ($1, $2, $3, $4, $5, $6)",
|
||||
user_id,
|
||||
name,
|
||||
password_hash,
|
||||
created_at,
|
||||
privileged
|
||||
privileged,
|
||||
scopes
|
||||
)
|
||||
.execute(&state.db)
|
||||
.await
|
||||
@@ -148,6 +156,7 @@ pub async fn create_app_password(
|
||||
password,
|
||||
created_at: created_at.to_rfc3339(),
|
||||
privileged,
|
||||
scopes,
|
||||
})
|
||||
.into_response(),
|
||||
Err(e) => {
|
||||
|
||||
+33
-19
@@ -125,24 +125,28 @@ pub async fn create_session(
|
||||
return ApiError::InternalError.into_response();
|
||||
}
|
||||
};
|
||||
let password_valid = if row
|
||||
let (password_valid, app_password_scopes) = if row
|
||||
.password_hash
|
||||
.as_ref()
|
||||
.map(|h| verify(&input.password, h).unwrap_or(false))
|
||||
.unwrap_or(false)
|
||||
{
|
||||
true
|
||||
(true, None)
|
||||
} else {
|
||||
let app_passwords = sqlx::query!(
|
||||
"SELECT password_hash FROM app_passwords WHERE user_id = $1 ORDER BY created_at DESC LIMIT 20",
|
||||
"SELECT password_hash, scopes FROM app_passwords WHERE user_id = $1 ORDER BY created_at DESC LIMIT 20",
|
||||
row.id
|
||||
)
|
||||
.fetch_all(&state.db)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
app_passwords
|
||||
let matched = app_passwords
|
||||
.iter()
|
||||
.any(|app| verify(&input.password, &app.password_hash).unwrap_or(false))
|
||||
.find(|app| verify(&input.password, &app.password_hash).unwrap_or(false));
|
||||
match matched {
|
||||
Some(app) => (true, app.scopes.clone()),
|
||||
None => (false, None),
|
||||
}
|
||||
};
|
||||
if !password_valid {
|
||||
warn!("Password verification failed for login attempt");
|
||||
@@ -177,7 +181,11 @@ pub async fn create_session(
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
let access_meta = match crate::auth::create_access_token_with_metadata(&row.did, &key_bytes) {
|
||||
let access_meta = match crate::auth::create_access_token_with_scope_metadata(
|
||||
&row.did,
|
||||
&key_bytes,
|
||||
app_password_scopes.as_deref(),
|
||||
) {
|
||||
Ok(m) => m,
|
||||
Err(e) => {
|
||||
error!("Failed to create access token: {:?}", e);
|
||||
@@ -192,14 +200,15 @@ pub async fn create_session(
|
||||
}
|
||||
};
|
||||
if let Err(e) = sqlx::query!(
|
||||
"INSERT INTO session_tokens (did, access_jti, refresh_jti, access_expires_at, refresh_expires_at, legacy_login, mfa_verified) VALUES ($1, $2, $3, $4, $5, $6, $7)",
|
||||
"INSERT INTO session_tokens (did, access_jti, refresh_jti, access_expires_at, refresh_expires_at, legacy_login, mfa_verified, scope) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)",
|
||||
row.did,
|
||||
access_meta.jti,
|
||||
refresh_meta.jti,
|
||||
access_meta.expires_at,
|
||||
refresh_meta.expires_at,
|
||||
is_legacy_login,
|
||||
false
|
||||
false,
|
||||
app_password_scopes
|
||||
)
|
||||
.execute(&state.db)
|
||||
.await
|
||||
@@ -388,7 +397,7 @@ pub async fn refresh_session(
|
||||
.into_response();
|
||||
}
|
||||
let session_row = match sqlx::query!(
|
||||
r#"SELECT st.id, st.did, k.key_bytes, k.encryption_version
|
||||
r#"SELECT st.id, st.did, st.scope, k.key_bytes, k.encryption_version
|
||||
FROM session_tokens st
|
||||
JOIN users u ON st.did = u.did
|
||||
JOIN user_keys k ON u.id = k.user_id
|
||||
@@ -420,14 +429,17 @@ pub async fn refresh_session(
|
||||
if crate::auth::verify_refresh_token(&refresh_token, &key_bytes).is_err() {
|
||||
return ApiError::AuthenticationFailedMsg("Invalid refresh token".into()).into_response();
|
||||
}
|
||||
let new_access_meta =
|
||||
match crate::auth::create_access_token_with_metadata(&session_row.did, &key_bytes) {
|
||||
Ok(m) => m,
|
||||
Err(e) => {
|
||||
error!("Failed to create access token: {:?}", e);
|
||||
return ApiError::InternalError.into_response();
|
||||
}
|
||||
};
|
||||
let new_access_meta = match crate::auth::create_access_token_with_scope_metadata(
|
||||
&session_row.did,
|
||||
&key_bytes,
|
||||
session_row.scope.as_deref(),
|
||||
) {
|
||||
Ok(m) => m,
|
||||
Err(e) => {
|
||||
error!("Failed to create access token: {:?}", e);
|
||||
return ApiError::InternalError.into_response();
|
||||
}
|
||||
};
|
||||
let new_refresh_meta =
|
||||
match crate::auth::create_refresh_token_with_metadata(&session_row.did, &key_bytes) {
|
||||
Ok(m) => m,
|
||||
@@ -653,15 +665,17 @@ pub async fn confirm_signup(
|
||||
return ApiError::InternalError.into_response();
|
||||
}
|
||||
};
|
||||
let no_scope: Option<String> = None;
|
||||
if let Err(e) = sqlx::query!(
|
||||
"INSERT INTO session_tokens (did, access_jti, refresh_jti, access_expires_at, refresh_expires_at, legacy_login, mfa_verified) VALUES ($1, $2, $3, $4, $5, $6, $7)",
|
||||
"INSERT INTO session_tokens (did, access_jti, refresh_jti, access_expires_at, refresh_expires_at, legacy_login, mfa_verified, scope) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)",
|
||||
row.did,
|
||||
access_meta.jti,
|
||||
refresh_meta.jti,
|
||||
access_meta.expires_at,
|
||||
refresh_meta.expires_at,
|
||||
false,
|
||||
false
|
||||
false,
|
||||
no_scope
|
||||
)
|
||||
.execute(&state.db)
|
||||
.await
|
||||
|
||||
+28
-15
@@ -24,8 +24,8 @@ pub use service::{ServiceTokenClaims, ServiceTokenVerifier, is_service_token};
|
||||
pub use token::{
|
||||
SCOPE_ACCESS, SCOPE_APP_PASS, SCOPE_APP_PASS_PRIVILEGED, SCOPE_REFRESH, TOKEN_TYPE_ACCESS,
|
||||
TOKEN_TYPE_REFRESH, TOKEN_TYPE_SERVICE, TokenWithMetadata, create_access_token,
|
||||
create_access_token_with_metadata, create_refresh_token, create_refresh_token_with_metadata,
|
||||
create_service_token,
|
||||
create_access_token_with_metadata, create_access_token_with_scope_metadata,
|
||||
create_refresh_token, create_refresh_token_with_metadata, create_service_token,
|
||||
};
|
||||
pub use verify::{
|
||||
TokenVerifyError, get_did_from_token, get_jti_from_token, verify_access_token,
|
||||
@@ -66,6 +66,11 @@ pub struct AuthenticatedUser {
|
||||
|
||||
impl AuthenticatedUser {
|
||||
pub fn permissions(&self) -> ScopePermissions {
|
||||
if let Some(ref scope) = self.scope
|
||||
&& scope != SCOPE_ACCESS
|
||||
{
|
||||
return ScopePermissions::from_scope_string(Some(scope));
|
||||
}
|
||||
if !self.is_oauth {
|
||||
return ScopePermissions::from_scope_string(Some("atproto"));
|
||||
}
|
||||
@@ -212,8 +217,8 @@ async fn validate_bearer_token_with_options_internal(
|
||||
}
|
||||
|
||||
if !session_valid {
|
||||
let session_exists = sqlx::query_scalar!(
|
||||
"SELECT 1 as one FROM session_tokens WHERE did = $1 AND access_jti = $2 AND access_expires_at > NOW()",
|
||||
let session_row = sqlx::query!(
|
||||
"SELECT access_expires_at FROM session_tokens WHERE did = $1 AND access_jti = $2",
|
||||
did,
|
||||
jti
|
||||
)
|
||||
@@ -222,16 +227,24 @@ async fn validate_bearer_token_with_options_internal(
|
||||
.ok()
|
||||
.flatten();
|
||||
|
||||
session_valid = session_exists.is_some();
|
||||
|
||||
if session_valid && let Some(c) = cache {
|
||||
let _ = c
|
||||
.set(
|
||||
&session_cache_key,
|
||||
"1",
|
||||
Duration::from_secs(SESSION_CACHE_TTL_SECS),
|
||||
)
|
||||
.await;
|
||||
match session_row {
|
||||
Some(row) => {
|
||||
if row.access_expires_at > chrono::Utc::now() {
|
||||
session_valid = true;
|
||||
if let Some(c) = cache {
|
||||
let _ = c
|
||||
.set(
|
||||
&session_cache_key,
|
||||
"1",
|
||||
Duration::from_secs(SESSION_CACHE_TTL_SECS),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
} else {
|
||||
return Err(TokenValidationError::TokenExpired);
|
||||
}
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -241,7 +254,7 @@ async fn validate_bearer_token_with_options_internal(
|
||||
key_bytes: Some(decrypted_key),
|
||||
is_oauth: false,
|
||||
is_admin,
|
||||
scope: None,
|
||||
scope: token_data.claims.scope.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+14
-5
@@ -8,13 +8,22 @@ use crate::oauth::scopes::{
|
||||
AccountAction, AccountAttr, IdentityAttr, RepoAction, ScopePermissions,
|
||||
};
|
||||
|
||||
use super::token::SCOPE_ACCESS;
|
||||
|
||||
fn has_custom_scope(scope: Option<&str>) -> bool {
|
||||
match scope {
|
||||
None => false,
|
||||
Some(s) => s != SCOPE_ACCESS,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn check_repo_scope(
|
||||
is_oauth: bool,
|
||||
scope: Option<&str>,
|
||||
action: RepoAction,
|
||||
collection: &str,
|
||||
) -> Result<(), Response> {
|
||||
if !is_oauth {
|
||||
if !is_oauth && !has_custom_scope(scope) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -32,7 +41,7 @@ pub fn check_repo_scope(
|
||||
}
|
||||
|
||||
pub fn check_blob_scope(is_oauth: bool, scope: Option<&str>, mime: &str) -> Result<(), Response> {
|
||||
if !is_oauth {
|
||||
if !is_oauth && !has_custom_scope(scope) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -55,7 +64,7 @@ pub fn check_rpc_scope(
|
||||
aud: &str,
|
||||
lxm: &str,
|
||||
) -> Result<(), Response> {
|
||||
if !is_oauth {
|
||||
if !is_oauth && !has_custom_scope(scope) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -78,7 +87,7 @@ pub fn check_account_scope(
|
||||
attr: AccountAttr,
|
||||
action: AccountAction,
|
||||
) -> Result<(), Response> {
|
||||
if !is_oauth {
|
||||
if !is_oauth && !has_custom_scope(scope) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -100,7 +109,7 @@ pub fn check_identity_scope(
|
||||
scope: Option<&str>,
|
||||
attr: IdentityAttr,
|
||||
) -> Result<(), Response> {
|
||||
if !is_oauth {
|
||||
if !is_oauth && !has_custom_scope(scope) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
|
||||
+10
-1
@@ -33,9 +33,18 @@ pub fn create_refresh_token(did: &str, key_bytes: &[u8]) -> Result<String> {
|
||||
}
|
||||
|
||||
pub fn create_access_token_with_metadata(did: &str, key_bytes: &[u8]) -> Result<TokenWithMetadata> {
|
||||
create_access_token_with_scope_metadata(did, key_bytes, None)
|
||||
}
|
||||
|
||||
pub fn create_access_token_with_scope_metadata(
|
||||
did: &str,
|
||||
key_bytes: &[u8],
|
||||
scopes: Option<&str>,
|
||||
) -> Result<TokenWithMetadata> {
|
||||
let scope = scopes.unwrap_or(SCOPE_ACCESS);
|
||||
create_signed_token_with_metadata(
|
||||
did,
|
||||
SCOPE_ACCESS,
|
||||
scope,
|
||||
TOKEN_TYPE_ACCESS,
|
||||
key_bytes,
|
||||
Duration::minutes(15),
|
||||
|
||||
+5
-7
@@ -256,12 +256,7 @@ pub fn verify_access_token_typed(
|
||||
token: &str,
|
||||
key_bytes: &[u8],
|
||||
) -> Result<TokenData<Claims>, TokenVerifyError> {
|
||||
verify_token_typed_internal(
|
||||
token,
|
||||
key_bytes,
|
||||
Some(TOKEN_TYPE_ACCESS),
|
||||
Some(&[SCOPE_ACCESS, SCOPE_APP_PASS, SCOPE_APP_PASS_PRIVILEGED]),
|
||||
)
|
||||
verify_token_typed_internal(token, key_bytes, Some(TOKEN_TYPE_ACCESS), None)
|
||||
}
|
||||
|
||||
fn verify_token_typed_internal(
|
||||
@@ -307,7 +302,10 @@ fn verify_token_typed_internal(
|
||||
let verifying_key = VerifyingKey::from(&signing_key);
|
||||
|
||||
let message = format!("{}.{}", header_b64, claims_b64);
|
||||
if verifying_key.verify(message.as_bytes(), &signature).is_err() {
|
||||
if verifying_key
|
||||
.verify(message.as_bytes(), &signature)
|
||||
.is_err()
|
||||
{
|
||||
return Err(TokenVerifyError::Invalid);
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -26,7 +26,8 @@ pub use scope_preference::{
|
||||
pub use token::{
|
||||
check_refresh_token_used, count_tokens_for_user, create_token, delete_oldest_tokens_for_user,
|
||||
delete_token, delete_token_family, enforce_token_limit_for_user, get_token_by_id,
|
||||
get_token_by_refresh_token, list_tokens_for_user, revoke_tokens_for_client, rotate_token,
|
||||
get_token_by_previous_refresh_token, get_token_by_refresh_token, list_tokens_for_user,
|
||||
revoke_tokens_for_client, rotate_token,
|
||||
};
|
||||
pub use two_factor::{
|
||||
TwoFactorChallenge, check_user_2fa_enabled, cleanup_expired_2fa_challenges,
|
||||
|
||||
+47
-3
@@ -122,7 +122,7 @@ pub async fn rotate_token(
|
||||
)
|
||||
.fetch_one(&mut *tx)
|
||||
.await?;
|
||||
if let Some(old_rt) = old_refresh {
|
||||
if let Some(ref old_rt) = old_refresh {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO oauth_used_refresh_token (refresh_token, token_id)
|
||||
@@ -137,13 +137,15 @@ pub async fn rotate_token(
|
||||
sqlx::query!(
|
||||
r#"
|
||||
UPDATE oauth_token
|
||||
SET token_id = $2, current_refresh_token = $3, expires_at = $4, updated_at = NOW()
|
||||
SET token_id = $2, current_refresh_token = $3, expires_at = $4, updated_at = NOW(),
|
||||
previous_refresh_token = $5, rotated_at = NOW()
|
||||
WHERE id = $1
|
||||
"#,
|
||||
old_db_id,
|
||||
new_token_id,
|
||||
new_refresh_token,
|
||||
new_expires_at
|
||||
new_expires_at,
|
||||
old_refresh
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
@@ -166,6 +168,48 @@ pub async fn check_refresh_token_used(
|
||||
Ok(row)
|
||||
}
|
||||
|
||||
const REFRESH_GRACE_PERIOD_SECS: i64 = 60;
|
||||
|
||||
pub async fn get_token_by_previous_refresh_token(
|
||||
pool: &PgPool,
|
||||
refresh_token: &str,
|
||||
) -> Result<Option<(i32, TokenData)>, OAuthError> {
|
||||
let grace_cutoff = Utc::now() - chrono::Duration::seconds(REFRESH_GRACE_PERIOD_SECS);
|
||||
let row = sqlx::query!(
|
||||
r#"
|
||||
SELECT id, did, token_id, created_at, updated_at, expires_at, client_id, client_auth,
|
||||
device_id, parameters, details, code, current_refresh_token, scope
|
||||
FROM oauth_token
|
||||
WHERE previous_refresh_token = $1 AND rotated_at > $2
|
||||
"#,
|
||||
refresh_token,
|
||||
grace_cutoff
|
||||
)
|
||||
.fetch_optional(pool)
|
||||
.await?;
|
||||
match row {
|
||||
Some(r) => Ok(Some((
|
||||
r.id,
|
||||
TokenData {
|
||||
did: r.did,
|
||||
token_id: r.token_id,
|
||||
created_at: r.created_at,
|
||||
updated_at: r.updated_at,
|
||||
expires_at: r.expires_at,
|
||||
client_id: r.client_id,
|
||||
client_auth: from_json(r.client_auth)?,
|
||||
device_id: r.device_id,
|
||||
parameters: from_json(r.parameters)?,
|
||||
details: r.details,
|
||||
code: r.code,
|
||||
current_refresh_token: r.current_refresh_token,
|
||||
scope: r.scope,
|
||||
},
|
||||
))),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn delete_token(pool: &PgPool, token_id: &str) -> Result<(), OAuthError> {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
|
||||
@@ -175,6 +175,36 @@ pub async fn handle_refresh_token_grant(
|
||||
"Refresh token grant requested"
|
||||
);
|
||||
if let Some(token_id) = db::check_refresh_token_used(&state.db, &refresh_token_str).await? {
|
||||
if let Some((_db_id, token_data)) =
|
||||
db::get_token_by_previous_refresh_token(&state.db, &refresh_token_str).await?
|
||||
{
|
||||
tracing::info!(
|
||||
refresh_token_prefix = %&refresh_token_str[..std::cmp::min(16, refresh_token_str.len())],
|
||||
"Refresh token reuse within grace period, returning existing tokens"
|
||||
);
|
||||
let dpop_jkt = token_data.parameters.dpop_jkt.as_deref();
|
||||
let access_token = create_access_token(
|
||||
&token_data.token_id,
|
||||
&token_data.did,
|
||||
dpop_jkt,
|
||||
token_data.scope.as_deref(),
|
||||
)?;
|
||||
let mut response_headers = HeaderMap::new();
|
||||
let config = AuthConfig::get();
|
||||
let verifier = DPoPVerifier::new(config.dpop_secret().as_bytes());
|
||||
response_headers.insert("DPoP-Nonce", verifier.generate_nonce().parse().unwrap());
|
||||
return Ok((
|
||||
response_headers,
|
||||
Json(TokenResponse {
|
||||
access_token,
|
||||
token_type: if dpop_jkt.is_some() { "DPoP" } else { "Bearer" }.to_string(),
|
||||
expires_in: ACCESS_TOKEN_EXPIRY_SECONDS as u64,
|
||||
refresh_token: token_data.current_refresh_token,
|
||||
scope: token_data.scope,
|
||||
sub: Some(token_data.did),
|
||||
}),
|
||||
));
|
||||
}
|
||||
tracing::warn!(
|
||||
refresh_token_prefix = %&refresh_token_str[..std::cmp::min(16, refresh_token_str.len())],
|
||||
"Refresh token reuse detected, revoking token family"
|
||||
|
||||
Reference in New Issue
Block a user