openstack-sdk-auth-core 0.22.7

OpenStack SDK auth core
Documentation

Core trait for implementing OpenStack authentication plugins to [openstack_sdk]

This crate provides the foundational types and traits required to authenticate against the OpenStack Identity service (Keystone). It defines:

  • [OpenStackAuthType] — the primary trait that all authentication plugins must implement to enable login flows (password, token, JWT, WebSSO, etc.).
  • [OpenStackMultifactorAuthMethod] — the trait for multifactor-capable methods that can be composed into multipass/multifactor authentication requests.
  • [AuthToken] — the structure that represents a successful Keystone authentication result, including the bearer token and parsed server response.
  • [AuthTokenScope] — represents the authorization scope attached to a token (project, domain, system, or unscoped).
  • [AuthError] — the unified error type covering all authentication-related failures, including receipts for multipass flows.
  • [Auth] — the enum that wraps the current authentication state (token or none).
  • [AuthState] — describes token validity (valid, expired, about-to-expire, or unset).

Plugin Registration

Authentication plugins are registered at compile time using Rust's inventory crate. Implement [OpenStackAuthType] and submit an [AuthPluginRegistration] via inventory::submit!{}. For multipass support, additionally implement [OpenStackMultifactorAuthMethod] and submit an [AuthMethodPluginRegistration].

The [execute_auth_request] function provides a common, instrumented pathway for sending authentication HTTP requests with timing and request-id logging.

Examples

Basic Authentication

Authenticate by creating an AuthToken from an existing token string (e.g., obtained from an environment variable or a previous login):

use secrecy::{ExposeSecret, SecretString};
use openstack_sdk_auth_core::{AuthToken, AuthTokenScope};

# async fn example() -> Result<(), Box<dyn std::error::Error>> {
let token = AuthToken::new(SecretString::from("my-token"), None);
let scope = AuthTokenScope::Unscoped;

println!("Scope: {:?}", token.get_scope());
println!("State: {:?}", token.get_state(None));
# Ok(())
# }

Checking Token State

Tokens can be checked for validity with an optional expiration threshold:

use chrono::TimeDelta;
use openstack_sdk_auth_core::AuthState;
# async fn example() -> Result<(), Box<dyn std::error::Error>> {
# let auth = openstack_sdk_auth_core::AuthToken::default();
let offset = TimeDelta::minutes(5);
match auth.get_state(Some(offset)) {
    AuthState::Valid => println!("Token is valid"),
    AuthState::AboutToExpire => println!("Token will expire within 5 minutes"),
    AuthState::Expired => println!("Token has expired"),
    AuthState::Unset => println!("No token data available"),
}
# Ok(())
# }

Setting Request Headers

The Auth type can inject the X-Auth-Token header into outgoing HTTP requests:

use http::HeaderMap;
# async fn example() -> Result<(), Box<dyn std::error::Error>> {
# let auth = openstack_sdk_auth_core::Auth::None;
let mut headers = HeaderMap::new();
auth.set_header(&mut headers)?;
// headers now contains the `X-Auth-Token` header if the auth type is a token.
# Ok(())
# }

Implementing a Custom Authenticator

Creating a new authentication method requires implementing [OpenStackAuthType]:

use std::collections::HashMap;
use async_trait::async_trait;
use secrecy::SecretString;
use serde_json::{Value, json};
use openstack_sdk_auth_core::{Auth, AuthError, AuthToken, AuthTokenScope, OpenStackAuthType};

pub struct MyAuthenticator;

static PLUGIN: MyAuthenticator = MyAuthenticator;
inventory::submit! {
    openstack_sdk_auth_core::AuthPluginRegistration { method: &PLUGIN }
}

#[async_trait]
impl OpenStackAuthType for MyAuthenticator {
    fn get_supported_auth_methods(&self) -> Vec<&'static str> {
        vec!["v3myauth", "myauth"]
    }

    fn requirements(&self, _hints: Option<&Value>) -> Result<Value, AuthError> {
        Ok(json!({
            "type": "object",
            "required": ["token_id"],
            "properties": {
                "token_id": {
                    "type": "string",
                    "description": "The token identifier"
                }
            }
        }))
    }

    fn api_version(&self) -> (u8, u8) {
        (3, 0)
    }

    async fn auth(
        &self,
        _http_client: &reqwest::Client,
        _identity_url: &url::Url,
        _values: &HashMap<String, SecretString>,
        _scope: Option<&AuthTokenScope>,
        _hints: Option<&Value>,
    ) -> Result<Auth, AuthError> {
        // Perform your authentication logic here and return an AuthToken
        let auth_token = AuthToken::new("example-token", None);
        Ok(Auth::AuthToken(Box::new(auth_token)))
    }
}

Error Handling Patterns

The [AuthError] enum provides different error variants that you can match on to handle specific failure scenarios:

use openstack_sdk_auth_core::{AuthError, Auth, AuthToken, AuthTokenScope};
# async fn example() -> Result<(), Box<dyn std::error::Error>> {
# fn simulate_auth_result() -> Result<Auth, AuthError> { Ok(Auth::None) }

match simulate_auth_result() {
    Ok(Auth::AuthToken(token)) => {
        println!("Authentication successful");
        println!("Token expires at: {:?}", token.get_state(None));
    }
    Ok(Auth::None) => {
        println!("No authentication available");
    }
    Ok(auth) => {
        println!("Unknown auth type: {:?}", auth);
    }
    Err(AuthError::AuthReceipt(receipt)) => {
        println!("Multifactor authentication required");
        let methods: Vec<_> = receipt.required_auth_methods.iter()
            .flatten()
            .cloned()
            .collect();
        println!("Required methods: {:?}", methods);
    }
    Err(AuthError::Serde { .. }) => {
        println!("Failed to parse response (malformed JSON)");
    }
    Err(AuthError::UnknownAuth { code, message }) => {
        println!("Unknown authentication error (code: {})", code);
        if let Some(msg) = &message {
            println!("Message: {}", msg);
        }
    }
    Err(e) => {
        println!("Authentication failed: {}", e);
    }
}
# Ok(())
# }

Implementing a Multifactor-Authenticator

To support multifactor authentication (e.g., TOTP or password + token), implement both [OpenStackAuthType] and [OpenStackMultifactorAuthMethod]:

use std::collections::HashMap;
use async_trait::async_trait;
use secrecy::{SecretString, ExposeSecret};
use serde_json::{Value, json};
use openstack_sdk_auth_core::{
    Auth, AuthError, AuthToken, AuthTokenScope,
    OpenStackAuthType, OpenStackMultifactorAuthMethod,
};

pub struct MyMultifactorAuthenticator;

static PLUGIN: MyMultifactorAuthenticator = MyMultifactorAuthenticator;
inventory::submit! {
    openstack_sdk_auth_core::AuthPluginRegistration { method: &PLUGIN }
}
inventory::submit! {
    openstack_sdk_auth_core::AuthMethodPluginRegistration { method: &PLUGIN }
}

#[async_trait]
impl OpenStackMultifactorAuthMethod for MyMultifactorAuthenticator {
    fn get_supported_auth_methods(&self) -> Vec<&'static str> {
        vec!["v3myauth", "myauth"]
    }

    fn requirements(&self, _hints: Option<&Value>) -> Result<Value, AuthError> {
        Ok(json!({
            "type": "object",
            "required": ["auth_code"],  // Additional auth method requirement
            "properties": {
                "auth_code": {
                    "type": "string",
                    "format": "password",
                    "description": "One-time authentication code"
                }
            }
        }))
    }

    /// Extracts authentication data from the values map.
    fn get_auth_data(
        &self,
        values: &HashMap<String, SecretString>,
    ) -> Result<(&'static str, Value), AuthError> {
        let auth_code = values
            .get("auth_code")
            .ok_or_else(|| AuthError::AuthValueNotSupplied("auth_code".to_string()))?;
        Ok(("myauth", json!({
            "auth_code": auth_code.expose_secret()
        })))
    }
}

#[async_trait]
impl OpenStackAuthType for MyMultifactorAuthenticator {
    fn get_supported_auth_methods(&self) -> Vec<&'static str> {
        vec!["v3myauth", "myauth"]
    }

    fn requirements(&self, _hints: Option<&Value>) -> Result<Value, AuthError> {
        Ok(json!({
            "type": "object",
            "required": ["auth_code"],  
            "properties": {
                "auth_code": {
                    "type": "string",
                    "format": "password",
                    "description": "One-time authentication code"
                }
            }
        }))
    }

    fn api_version(&self) -> (u8, u8) {
        (3, 0)
    }

    async fn auth(
        &self,
        _http_client: &reqwest::Client,
        _identity_url: &url::Url,
        _values: &HashMap<String, SecretString>,
        _scope: Option<&AuthTokenScope>,
        _hints: Option<&Value>,
    ) -> Result<Auth, AuthError> {
        // Perform multifactor authentication and return an AuthToken:
        let auth_token = AuthToken::new("example-token", None);
        Ok(Auth::AuthToken(Box::new(auth_token)))
    }
}

Token Scoping

Tokens can be scoped to specific projects or domains using [AuthTokenScope]:

use openstack_sdk_auth_core::{AuthTokenScope};
use openstack_sdk_auth_core::types::{Project, Domain};

// Project-scope by ID
let project_scope = AuthTokenScope::Project(Project {
    id: Some("project-id-123".to_string()),
    name: None,
    domain: None,
});

// Domain-scope with domain name
let domain_scope = AuthTokenScope::Domain(Domain {
    id: None,
    name: Some("Default".to_string()),
});

// Unscoped (default)
let unscoped: AuthTokenScope = AuthTokenScope::default();

Handling Authentication Receipts

When multifactor authentication is enabled, Keystone returns an authentication receipt instead of a new token. The receipt contains information about what additional authentication methods are required:

use openstack_sdk_auth_core::{AuthTokenScope, AuthError, Auth, AuthReceiptResponse, AuthReceipt};
use chrono::Local;

# async fn example() -> Result<(), Box<dyn std::error::Error>> {
# fn simulate_auth_result() -> Result<Auth, AuthError> {
#     Ok(Auth::None)
# }

let scope = Some(&AuthTokenScope::Unscoped);
match simulate_auth_result() {
    Ok(auth) => {
        match auth {
            Auth::AuthToken(token) => {
                println!("Authenticated with token");
                println!("Token: {:?}", token.get_scope());
            }
            Auth::None => {
                println!("Not authenticated");
            }
            _ => {
                println!("Unknown auth type");
            }
        }
    }
    Err(AuthError::AuthReceipt(receipt)) => {
        println!("Additional authentication methods required");
        println!("Required methods: {:?}", receipt.required_auth_methods);
        let methods: Vec<_> = receipt.receipt.methods.iter().cloned().collect();
        println!("Already completed methods: {:?}", methods);

        // Use the receipt token for subsequent authentication requests
        if let Some(receipt_token) = &receipt.token {
            println!("Receipt token: {}...", &receipt_token.chars().take(8).collect::<String>());
        }
    }
    Err(e) => {
        println!("Authentication error: {}", e);
    }
}
# Ok(())
# }

Token State Management

The [AuthToken::get_state] method can be used to determine if a token is valid and needs to be refreshed. Using an expiration offset allows you to proactively refresh tokens before they expire:

use chrono::TimeDelta;
use openstack_sdk_auth_core::{AuthToken, AuthState, types::{AuthResponse, TokenInfo, User, Project}};
use secrecy::SecretString;

// Create a token with expiration info
let auth_info = AuthResponse {
    token: TokenInfo {
        expires_at: chrono::Utc::now() + TimeDelta::hours(24),
        user: User::default(),
        ..Default::default()
    },
};
let token = AuthToken::new("my-token", Some(auth_info));

// Check with 5-minute buffer for proactive refresh
match token.get_state(Some(TimeDelta::minutes(5))) {
    AuthState::Valid => {
        println!("Token is valid");
    }
    AuthState::AboutToExpire => {
        println!("Token will expire soon, refreshing...");
        // Refresh the token before it expires
    }
    AuthState::Expired => {
        println!("Token has expired, re-authenticating...");
    }
    AuthState::Unset => {
        println!("No token data available");
    }
}