securitydept-token-set-context 0.2.0

Token Set Context of SecurityDept, a layered authentication and authorization toolkit built as reusable Rust crates.
Documentation
use std::collections::HashMap;

use chrono::{DateTime, Utc};
use http::header::{AUTHORIZATION, HeaderMap, HeaderValue};
use securitydept_utils::principal::AuthenticatedPrincipal as SharedAuthenticatedPrincipal;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use typed_builder::TypedBuilder;

use crate::backend_oidc_mode::SealedRefreshMaterial;

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "snake_case")]
pub enum AuthenticationSourceKind {
    OidcAuthorizationCode,
    RefreshToken,
    ForwardedBearer,
    StaticToken,
    #[default]
    Unknown,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TypedBuilder, Default)]
pub struct AuthenticationSource {
    #[builder(default = AuthenticationSourceKind::Unknown)]
    #[serde(default)]
    pub kind: AuthenticationSourceKind,
    #[serde(skip_serializing_if = "Option::is_none")]
    #[builder(default, setter(strip_option, into))]
    pub provider_id: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    #[builder(default, setter(strip_option, into))]
    pub issuer: Option<String>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    #[builder(default)]
    pub kind_history: Vec<AuthenticationSourceKind>,
    #[serde(flatten)]
    #[builder(default)]
    pub attributes: HashMap<String, Value>,
}

pub type AuthenticatedPrincipal = SharedAuthenticatedPrincipal;

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TypedBuilder)]
pub struct AuthTokenSnapshot {
    #[builder(setter(into))]
    pub access_token: String,
    #[builder(default, setter(into))]
    pub id_token: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    #[builder(default, setter(strip_option))]
    pub refresh_material: Option<SealedRefreshMaterial>,
    #[serde(skip_serializing_if = "Option::is_none")]
    #[builder(default, setter(strip_option))]
    pub access_token_expires_at: Option<DateTime<Utc>>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TypedBuilder)]
pub struct AuthTokenDelta {
    #[builder(setter(into))]
    pub access_token: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    #[builder(default, setter(strip_option, into))]
    pub id_token: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    #[builder(default, setter(strip_option))]
    pub refresh_material: Option<SealedRefreshMaterial>,
    #[serde(skip_serializing_if = "Option::is_none")]
    #[builder(default, setter(strip_option))]
    pub access_token_expires_at: Option<DateTime<Utc>>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TypedBuilder, Default)]
pub struct AuthStateMetadataSnapshot {
    #[serde(skip_serializing_if = "Option::is_none")]
    #[builder(default, setter(strip_option))]
    pub principal: Option<AuthenticatedPrincipal>,
    #[builder(default)]
    #[serde(default)]
    pub source: AuthenticationSource,
    #[serde(flatten, skip_serializing_if = "HashMap::is_empty")]
    #[builder(default)]
    pub attributes: HashMap<String, Value>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TypedBuilder, Default)]
pub struct CurrentAuthenticationSourcePartial {
    #[serde(skip_serializing_if = "Option::is_none")]
    #[builder(default, setter(strip_option))]
    pub kind: Option<AuthenticationSourceKind>,
    #[serde(skip_serializing_if = "Option::is_none")]
    #[builder(default, setter(strip_option))]
    pub provider_id: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    #[builder(default, setter(strip_option))]
    pub issuer: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    #[builder(default, setter(strip_option))]
    pub kind_history: Option<Vec<AuthenticationSourceKind>>,
    #[serde(default, flatten, skip_serializing_if = "HashMap::is_empty")]
    #[builder(default)]
    pub attributes: HashMap<String, Value>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TypedBuilder, Default)]
pub struct CurrentAuthStateMetadataSnapshotPartial {
    #[serde(skip_serializing_if = "Option::is_none")]
    #[builder(default, setter(strip_option))]
    pub principal: Option<AuthenticatedPrincipal>,
    #[serde(skip_serializing_if = "Option::is_none")]
    #[builder(default, setter(strip_option))]
    pub source: Option<CurrentAuthenticationSourcePartial>,
    #[serde(default, flatten, skip_serializing_if = "HashMap::is_empty")]
    #[builder(default)]
    pub attributes: HashMap<String, Value>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TypedBuilder, Default)]
pub struct AuthStateMetadataDelta {
    #[serde(skip_serializing_if = "Option::is_none")]
    #[builder(default, setter(strip_option))]
    pub principal: Option<AuthenticatedPrincipal>,
    #[serde(skip_serializing_if = "Option::is_none")]
    #[builder(default, setter(strip_option))]
    pub source: Option<AuthenticationSource>,
    #[serde(default, flatten, skip_serializing_if = "HashMap::is_empty")]
    #[builder(default)]
    pub attributes: HashMap<String, Value>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TypedBuilder)]
pub struct AuthStateSnapshot {
    pub tokens: AuthTokenSnapshot,
    pub metadata: AuthStateMetadataSnapshot,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TypedBuilder)]
pub struct AuthStateDelta {
    pub tokens: AuthTokenDelta,
    #[builder(default)]
    #[serde(
        default,
        flatten,
        skip_serializing_if = "AuthStateMetadataDelta::is_empty"
    )]
    pub metadata: AuthStateMetadataDelta,
}

impl AuthTokenSnapshot {
    pub fn access_token_is_expired_at(&self, now: DateTime<Utc>) -> bool {
        self.access_token_expires_at
            .is_some_and(|expires_at| expires_at <= now)
    }

    pub fn should_refresh_at(&self, now: DateTime<Utc>) -> bool {
        self.access_token_is_expired_at(now)
            || self
                .access_token_expires_at
                .is_some_and(|expires_at| expires_at <= now + chrono::TimeDelta::minutes(1))
    }

    pub fn authorization_value(&self) -> String {
        format!("Bearer {}", self.access_token)
    }

    pub fn authorization_header_value(
        &self,
    ) -> Result<HeaderValue, http::header::InvalidHeaderValue> {
        HeaderValue::from_str(&self.authorization_value())
    }

    pub fn apply_authorization_header(
        &self,
        headers: &mut HeaderMap,
    ) -> Result<(), http::header::InvalidHeaderValue> {
        headers.insert(AUTHORIZATION, self.authorization_header_value()?);
        Ok(())
    }
}

impl AuthStateMetadataDelta {
    pub fn is_empty(&self) -> bool {
        self.principal.is_none() && self.source.is_none() && self.attributes.is_empty()
    }
}