Something went wrong. Try again.
A downstream fork of Tranquil PDS for use in margin.cafe
Something went wrong. Try again.
10 kB · 370 lines
Rust
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371use crate::compress::encode_scope;
use super::types::{ ActClaim, Claims, Header, SigningAlgorithm, TokenScope, TokenType, TokenWithMetadata,};use anyhow::{Context, Result};use base64::Engine as _;use base64::engine::general_purpose::URL_SAFE_NO_PAD;use chrono::{DateTime, Duration, Utc};use hmac::{Hmac, Mac};use k256::ecdsa::{Signature, SigningKey, signature::Signer};use sha2::Sha256;use tranquil_types::{Did, Jti, Nsid};
type HmacSha256 = Hmac<Sha256>;
pub fn create_access_token(did: &Did, key_bytes: &[u8]) -> Result<String> { Ok(create_access_token_with_metadata(did, key_bytes)?.token)}
pub fn create_refresh_token(did: &Did, key_bytes: &[u8]) -> Result<String> { Ok(create_refresh_token_with_metadata(did, key_bytes)?.token)}
pub fn create_access_token_with_metadata(did: &Did, key_bytes: &[u8]) -> Result<TokenWithMetadata> { create_access_token_with_scope_metadata(did, key_bytes, None, None)}
pub fn create_access_token_with_scope_metadata( did: &Did, key_bytes: &[u8], scopes: Option<&str>, hostname: Option<&str>,) -> Result<TokenWithMetadata> { let scope = scopes.unwrap_or(TokenScope::Access.as_str()); create_signed_token_with_metadata( did, scope, TokenType::Access, key_bytes, Duration::minutes(120), hostname, )}
pub fn create_access_token_with_delegation( did: &Did, key_bytes: &[u8], scopes: Option<&str>, controller_did: Option<&Did>, hostname: Option<&str>,) -> Result<TokenWithMetadata> { let scope = scopes.unwrap_or(TokenScope::Access.as_str()); let act = controller_did.map(|c| ActClaim { sub: c.clone() }); create_signed_token_with_act( did, scope, TokenType::Access, key_bytes, Duration::minutes(120), act, hostname, )}
pub fn create_refresh_token_with_metadata( did: &Did, key_bytes: &[u8],) -> Result<TokenWithMetadata> { create_signed_token_with_metadata( did, TokenScope::Refresh.as_str(), TokenType::Refresh, key_bytes, Duration::days(90), None, )}
/// Re-mint an access token carrying a specific `jti` and expiry. Used by the/// refresh grace window to reproduce a session's current access token without/// persisting the signed JWT itself.pub fn create_access_token_with_jti( did: &Did, key_bytes: &[u8], scopes: Option<&str>, controller_did: Option<&Did>, hostname: Option<&str>, jti: &Jti, expires_at: DateTime<Utc>,) -> Result<String> { let scope = scopes.unwrap_or(TokenScope::Access.as_str()); let act = controller_did.map(|c| ActClaim { sub: c.clone() }); Ok(create_signed_token_pinned( did, scope, TokenType::Access, key_bytes, expires_at, jti.clone(), act, hostname, )? .token)}
/// Re-mint a refresh token carrying a specific `jti` and expiry. Counterpart to/// [`create_access_token_with_jti`] for the refresh grace window.pub fn create_refresh_token_with_jti( did: &Did, key_bytes: &[u8], jti: &Jti, expires_at: DateTime<Utc>,) -> Result<String> { Ok(create_signed_token_pinned( did, TokenScope::Refresh.as_str(), TokenType::Refresh, key_bytes, expires_at, jti.clone(), None, None, )? .token)}
pub fn create_service_token( did: &Did, aud: &str, lxm: Option<&Nsid>, key_bytes: &[u8],) -> Result<String> { let signing_key = SigningKey::from_slice(key_bytes)?;
let expiration = Utc::now() .checked_add_signed(Duration::seconds(60)) .expect("valid timestamp") .timestamp();
let claims = Claims { iss: did.clone(), sub: did.clone(), aud: aud.to_owned(), exp: expiration, iat: Utc::now().timestamp(), scope: None, lxm: lxm.cloned(), jti: Jti::new(uuid::Uuid::new_v4().to_string()), act: None, };
sign_claims(claims, &signing_key)}
fn create_signed_token_with_metadata( did: &Did, scope: &str, typ: TokenType, key_bytes: &[u8], duration: Duration, hostname: Option<&str>,) -> Result<TokenWithMetadata> { create_signed_token_with_act(did, scope, typ, key_bytes, duration, None, hostname)}
fn create_signed_token_with_act( did: &Did, scope: &str, typ: TokenType, key_bytes: &[u8], duration: Duration, act: Option<ActClaim>, hostname: Option<&str>,) -> Result<TokenWithMetadata> { let expires_at = Utc::now() .checked_add_signed(duration) .expect("valid timestamp"); let jti = Jti::new(uuid::Uuid::new_v4().to_string()); create_signed_token_pinned(did, scope, typ, key_bytes, expires_at, jti, act, hostname)}
#[allow(clippy::too_many_arguments)]fn create_signed_token_pinned( did: &Did, scope: &str, typ: TokenType, key_bytes: &[u8], expires_at: DateTime<Utc>, jti: Jti, act: Option<ActClaim>, hostname: Option<&str>,) -> Result<TokenWithMetadata> { let signing_key = SigningKey::from_slice(key_bytes)?;
let expiration = expires_at.timestamp();
let aud_hostname = hostname.map(|h| h.to_string()).unwrap_or_else(|| { tranquil_config::try_get() .map(|c| c.server.hostname.clone()) .unwrap_or_else(|| "localhost".to_string()) });
let claims = Claims { iss: did.clone(), sub: did.clone(), aud: format!("did:web:{}", aud_hostname), exp: expiration, iat: Utc::now().timestamp(), scope: Some(encode_scope(scope).context("Scope too large to encode")?), lxm: None, jti: jti.clone(), act, };
let token = sign_claims_with_type(claims, &signing_key, typ)?;
Ok(TokenWithMetadata { token, jti, expires_at, })}
fn sign_claims(claims: Claims, key: &SigningKey) -> Result<String> { sign_claims_with_type(claims, key, TokenType::Service)}
fn sign_claims_with_type(claims: Claims, key: &SigningKey, typ: TokenType) -> Result<String> { let header = Header { alg: SigningAlgorithm::ES256K, typ, };
let header_json = serde_json::to_string(&header)?; let claims_json = serde_json::to_string(&claims)?;
let header_b64 = URL_SAFE_NO_PAD.encode(header_json); let claims_b64 = URL_SAFE_NO_PAD.encode(claims_json);
let message = format!("{}.{}", header_b64, claims_b64); let signature: Signature = key.sign(message.as_bytes()); let signature_b64 = URL_SAFE_NO_PAD.encode(signature.to_bytes());
Ok(format!("{}.{}", message, signature_b64))}
pub fn create_access_token_hs256(did: &Did, secret: &[u8]) -> Result<String> { Ok(create_access_token_hs256_with_metadata(did, secret)?.token)}
pub fn create_refresh_token_hs256(did: &Did, secret: &[u8]) -> Result<String> { Ok(create_refresh_token_hs256_with_metadata(did, secret)?.token)}
pub fn create_access_token_hs256_with_metadata( did: &Did, secret: &[u8],) -> Result<TokenWithMetadata> { create_hs256_token_with_metadata( did, TokenScope::Access.as_str(), TokenType::Access, secret, Duration::minutes(120), )}
pub fn create_refresh_token_hs256_with_metadata( did: &Did, secret: &[u8],) -> Result<TokenWithMetadata> { create_hs256_token_with_metadata( did, TokenScope::Refresh.as_str(), TokenType::Refresh, secret, Duration::days(90), )}
pub fn create_service_token_hs256( did: &Did, aud: &Did, lxm: &Nsid, secret: &[u8],) -> Result<String> { let expiration = Utc::now() .checked_add_signed(Duration::seconds(60)) .expect("valid timestamp") .timestamp();
let claims = Claims { iss: did.clone(), sub: did.clone(), aud: aud.to_string(), exp: expiration, iat: Utc::now().timestamp(), scope: None, lxm: Some(lxm.clone()), jti: Jti::new(uuid::Uuid::new_v4().to_string()), act: None, };
sign_claims_hs256(claims, TokenType::Service, secret)}
fn create_hs256_token_with_metadata( did: &Did, scope: &str, typ: TokenType, secret: &[u8], duration: Duration,) -> Result<TokenWithMetadata> { let expires_at = Utc::now() .checked_add_signed(duration) .expect("valid timestamp");
let expiration = expires_at.timestamp(); let jti = Jti::new(uuid::Uuid::new_v4().to_string());
let claims = Claims { iss: did.clone(), sub: did.clone(), aud: format!( "did:web:{}", tranquil_config::try_get() .map(|c| c.server.hostname.clone()) .unwrap_or_else(|| "localhost".to_string()) ), exp: expiration, iat: Utc::now().timestamp(), scope: Some(encode_scope(scope).context("Scope too large to encode")?), lxm: None, jti: jti.clone(), act: None, };
let token = sign_claims_hs256(claims, typ, secret)?;
Ok(TokenWithMetadata { token, jti, expires_at, })}
fn sign_claims_hs256(claims: Claims, typ: TokenType, secret: &[u8]) -> Result<String> { let header = Header { alg: SigningAlgorithm::HS256, typ, };
let header_json = serde_json::to_string(&header)?; let claims_json = serde_json::to_string(&claims)?;
let header_b64 = URL_SAFE_NO_PAD.encode(header_json); let claims_b64 = URL_SAFE_NO_PAD.encode(claims_json);
let message = format!("{}.{}", header_b64, claims_b64);
let mut mac = HmacSha256::new_from_slice(secret) .map_err(|e| anyhow::anyhow!("Invalid secret length: {}", e))?; mac.update(message.as_bytes());
let signature = mac.finalize().into_bytes(); let signature_b64 = URL_SAFE_NO_PAD.encode(signature);
Ok(format!("{}.{}", message, signature_b64))}