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 ;
use ;
// Project-scope by ID
let project_scope = Project;
// Domain-scope with domain name
let domain_scope = Domain;
// Unscoped (default)
let unscoped: 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 TimeDelta;
use ;
use SecretString;
// Create a token with expiration info
let auth_info = AuthResponse ;
let token = new;
// Check with 5-minute buffer for proactive refresh
match token.get_state