Files
tranquil-pds/crates/tranquil-auth/src/token.rs
T

375 lines
9.9 KiB
Rust

use 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, DidRef, 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: &DidRef,
lxm: Option<&Nsid>,
exp: Option<i64>,
key_bytes: &[u8],
) -> Result<String> {
let signing_key = SigningKey::from_slice(key_bytes)?;
let expiration = match exp {
Some(exp) => exp,
None => 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: 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))
}