alibi-plugins 0.8.0

Built-in authentication plugins and API metadata for Alibi
Documentation
//! Client credentials grant application authority without a delegated user.
use super::OAuthProviderPlugin;
use alibi_core::endpoint::EndpointResponse;
use alibi_core::session::SessionRequest;
use alibi_core::store::{CreateOAuthToken, OAuthTokenKind};
use alibi_core::{AuthContext, AuthResult, AuthSchema};
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
use serde_json::{Value, json};
use sha2::{Digest as _, Sha256};
impl OAuthProviderPlugin {
    pub(super) async fn machine_grant<S: AuthSchema>(
        &self,
        body: &Value,
        req: &impl SessionRequest,
        ctx: &AuthContext<S>,
    ) -> AuthResult<EndpointResponse> {
        let credentials = super::credentials::extract(body, req);
        if credentials.client_id.is_empty() {
            return error("invalid_request", "Missing required client_id");
        }
        let client = match credentials.authenticate(ctx).await? {
            Ok(client) => client,
            Err(response) => return Ok(no_store(response)),
        };
        if client
            .metadata
            .get("grant_types")
            .and_then(Value::as_array)
            .is_some_and(|grants| !grants.iter().any(|grant| grant == "client_credentials"))
        {
            return error(
                "unauthorized_client",
                "client is not authorized to use grant type client_credentials",
            );
        }
        if client
            .metadata
            .get("token_endpoint_auth_method")
            .and_then(Value::as_str)
            == Some("none")
        {
            return error(
                "unauthorized_client",
                "public clients cannot use the client_credentials grant",
            );
        }
        let allowed = client
            .metadata
            .get("client_credentials_scopes")
            .and_then(Value::as_array)
            .map(|values| values.iter().filter_map(Value::as_str).collect::<Vec<_>>())
            .unwrap_or_default();
        if allowed.is_empty() {
            return error(
                "unauthorized_client",
                "client has no authorized client_credentials scopes",
            );
        }
        let scopes = body
            .get("scope")
            .and_then(Value::as_str)
            .map_or_else(|| allowed.clone(), |value| value.split(' ').collect());
        let invalid = scopes
            .iter()
            .filter(|scope| {
                !allowed.contains(scope)
                    || ["openid", "profile", "email", "offline_access"].contains(scope)
            })
            .copied()
            .collect::<Vec<_>>();
        if !invalid.is_empty() {
            return error(
                "invalid_scope",
                &format!("The following scopes are invalid: {}", invalid.join(", ")),
            );
        }
        let token = rand::RngExt::sample_iter(rand::rng(), rand::distr::Alphanumeric)
            .map(char::from)
            .filter(char::is_ascii_alphabetic)
            .take(32)
            .collect::<String>();
        let issued = chrono::DateTime::from_timestamp(chrono::Utc::now().timestamp(), 0)
            .ok_or_else(|| alibi_core::AuthError::internal("Invalid token issue time"))?;
        let expires = issued + chrono::Duration::seconds(3600);
        _ = ctx
            .database
            .create_oauth_token(
                OAuthTokenKind::Access,
                CreateOAuthToken {
                    token: URL_SAFE_NO_PAD.encode(Sha256::digest(&token)),
                    client_id: client.client_id,
                    user_id: None,
                    session_id: None,
                    authorization_code_id: None,
                    metadata: json!({"scopes":scopes,"resources":Value::Null}),
                    created_at: issued,
                    expires_at: expires,
                },
            )
            .await?;
        Ok(no_store(EndpointResponse::json(
            &json!({"access_token":token,"expires_in":3600,"expires_at":expires.timestamp(),"token_type":"Bearer","scope":scopes.join(" ")}),
        )?))
    }
}
fn no_store(response: EndpointResponse) -> EndpointResponse {
    response
        .with_header("cache-control", "no-store")
        .with_header("pragma", "no-cache")
}
fn error(code: &str, description: &str) -> AuthResult<EndpointResponse> {
    super::clients::status_protocol_error(400, code, description).map(no_store)
}