canic-core 0.29.6

Canic — a canister orchestration and management toolkit for the Internet Computer
Documentation
use super::{VerifiedAccessToken, dependency_unavailable};
use crate::{
    access::AccessError,
    cdk::{api::msg_arg_data, candid::de::IDLDeserialize, types::Principal},
    dto::auth::DelegatedToken,
    ops::{
        auth::{AuthOps, VerifyDelegatedTokenRuntimeInput},
        config::ConfigOps,
        ic::IcOps,
    },
};

const MAX_INGRESS_BYTES: usize = 64 * 1024; // 64 KiB
const DEFAULT_DELEGATED_AUTH_MAX_TTL_SECS: u64 = 24 * 60 * 60;

pub(super) fn delegated_token_verified(
    authenticated_subject: Principal,
    required_scope: Option<&str>,
) -> Result<VerifiedAccessToken, AccessError> {
    let token = delegated_token_from_args()?;

    let now_secs = IcOps::now_secs();

    verify_token(token, authenticated_subject, now_secs, required_scope)
}

// Verify a delegated token without local proof-cache lookup.
fn verify_token(
    token: DelegatedToken,
    caller: Principal,
    now_secs: u64,
    required_scope: Option<&str>,
) -> Result<VerifiedAccessToken, AccessError> {
    let max_ttl_secs = delegated_token_max_ttl_secs()?;
    let required_scopes = required_scope
        .map(|scope| vec![scope.to_string()])
        .unwrap_or_default();
    let verified = AuthOps::verify_token(VerifyDelegatedTokenRuntimeInput {
        token: &token,
        max_cert_ttl_secs: max_ttl_secs,
        max_token_ttl_secs: max_ttl_secs,
        required_scopes: &required_scopes,
        now_secs,
    })
    .map_err(|err| AccessError::Denied(err.to_string()))?;

    enforce_subject_binding(verified.subject, caller)?;
    enforce_required_scope(required_scope, &verified.scopes)?;

    Ok(VerifiedAccessToken {
        issuer_shard_pid: verified.issuer_shard_pid,
    })
}

pub(super) fn enforce_subject_binding(
    sub: Principal,
    caller: Principal,
) -> Result<(), AccessError> {
    if sub == caller {
        Ok(())
    } else {
        Err(AccessError::Denied(format!(
            "delegated token subject '{sub}' does not match caller '{caller}'"
        )))
    }
}

pub(super) fn enforce_required_scope(
    required_scope: Option<&str>,
    token_scopes: &[String],
) -> Result<(), AccessError> {
    let Some(required_scope) = required_scope else {
        return Ok(());
    };

    if token_scopes.iter().any(|scope| scope == required_scope) {
        Ok(())
    } else {
        Err(AccessError::Denied(format!(
            "delegated token missing required scope '{required_scope}'"
        )))
    }
}

fn delegated_token_from_args() -> Result<DelegatedToken, AccessError> {
    let bytes = msg_arg_data();

    if bytes.len() > MAX_INGRESS_BYTES {
        return Err(AccessError::Denied(
            "delegated token payload exceeds size limit".to_string(),
        ));
    }

    delegated_token_from_bytes(&bytes).map_err(|err| {
        AccessError::Denied(format!(
            "failed to decode DelegatedToken as first argument: {err}"
        ))
    })
}

// Decode the first ingress argument as a delegated token.
fn delegated_token_from_bytes(bytes: &[u8]) -> Result<DelegatedToken, String> {
    let mut decoder = IDLDeserialize::new(bytes)
        .map_err(|err| format!("failed to decode ingress arguments: {err}"))?;
    decoder
        .get_value::<DelegatedToken>()
        .map_err(|err| err.to_string())
}

// Resolve the verifier-side TTL policy from delegated-token config.
fn delegated_token_max_ttl_secs() -> Result<u64, AccessError> {
    let cfg = ConfigOps::delegated_tokens_config()
        .map_err(|_| dependency_unavailable("delegated token config unavailable"))?;
    if !cfg.enabled {
        return Err(AccessError::Denied(
            "delegated token auth disabled; set auth.delegated_tokens.enabled=true in canic.toml"
                .to_string(),
        ));
    }

    Ok(cfg
        .max_ttl_secs
        .unwrap_or(DEFAULT_DELEGATED_AUTH_MAX_TTL_SECS))
}