use chrono::Utc;
use serde::{Deserialize, Serialize};
use serde_json::json;
use foundation_auth::shared::jwt::{JwtSigningKey, JwtVerifier};
use crate::core::error::{AppError, AppResult};
use crate::core::store::users::UserRow;
pub const ISSUER: &str = "urn:foundation_keychain";
pub const ACCESS_TOKEN_TTL_SECS: i64 = 3600;
pub const REFRESH_TOKEN_TTL_SECS: i64 = 30 * 24 * 3600;
pub const TYPE_ACCESS: &str = "access_token";
pub const TYPE_REFRESH: &str = "refresh_token";
pub fn mint_access_token(
signing_key: &JwtSigningKey,
user: &UserRow,
device_identifier: &str,
) -> AppResult<String> {
let now = Utc::now().timestamp();
let claims = json!({
"sub": user.uuid,
"iss": ISSUER,
"type": TYPE_ACCESS,
"nbf": now,
"exp": now + ACCESS_TOKEN_TTL_SECS,
"premium": true,
"name": user.name,
"email": user.email,
"email_verified": user.email_verified,
"sstamp": user.security_stamp,
"device": device_identifier,
"scope": ["api", "offline_access"],
});
signing_key
.sign_claims(&claims)
.map_err(|e| AppError::Internal(format!("sign access token: {e}")))
}
pub fn mint_refresh_token(
signing_key: &JwtSigningKey,
user_uuid: &str,
device_identifier: &str,
) -> AppResult<String> {
let now = Utc::now().timestamp();
let claims = json!({
"sub": user_uuid,
"iss": ISSUER,
"type": TYPE_REFRESH,
"nbf": now,
"exp": now + REFRESH_TOKEN_TTL_SECS,
"device": device_identifier,
});
signing_key
.sign_claims(&claims)
.map_err(|e| AppError::Internal(format!("sign refresh token: {e}")))
}
#[derive(Debug, Clone)]
pub struct AuthedToken {
pub user_uuid: String,
pub security_stamp: Option<String>,
pub device: Option<String>,
}
fn custom_str(
claims: &foundation_auth::shared::jwt::VerifiedClaims,
key: &str,
) -> Option<String> {
claims.custom.get(key).and_then(|v| v.as_str()).map(String::from)
}
pub fn verify_access_token(verifier: &JwtVerifier, token: &str) -> AppResult<AuthedToken> {
let claims = verifier.verify(token).map_err(|_| AppError::Unauthorized)?;
if custom_str(&claims, "type").as_deref() != Some(TYPE_ACCESS) {
return Err(AppError::Unauthorized);
}
let security_stamp = custom_str(&claims, "sstamp");
let device = custom_str(&claims, "device");
Ok(AuthedToken {
user_uuid: claims.sub,
security_stamp,
device,
})
}
pub fn verify_refresh_token(
verifier: &JwtVerifier,
token: &str,
) -> AppResult<(String, Option<String>)> {
let claims = verifier.verify(token).map_err(|_| AppError::Unauthorized)?;
if custom_str(&claims, "type").as_deref() != Some(TYPE_REFRESH) {
return Err(AppError::Unauthorized);
}
let device = custom_str(&claims, "device");
Ok((claims.sub, device))
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BitwardenClaims {
#[serde(skip_serializing_if = "Option::is_none")]
pub sstamp: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub orgowner: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub orgadmin: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub orguser: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub orgmanager: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub premium: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub device: Option<String>,
}
#[must_use]
pub fn verify_security_stamp(token_stamp: Option<&str>, db_stamp: &str) -> bool {
match token_stamp {
Some(ts) => ts == db_stamp,
None => true, }
}