Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Client Library Guide

The auth_client crate provides an HTTP client and shared types for interacting with the Authentication Verifier. It is used by:

  • API servers that need to validate incoming session cookies and manage sessions.
  • Admin tools that manage realms, users, and credentials.
  • The Authentication Verifier itself (internal use).

Table of Contents


Installation

Add to your Cargo.toml:

[dependencies]
auth_client = { path = "../authentication_client" }

No Cargo features are required for session validation. The _server feature enables additional actix-web integrations and is only needed inside the Authentication Verifier crate itself.


Building a Client

All operations go through AuthClient. Create one at startup and share it as application state (it is Clone-able).

#![allow(unused)]
fn main() {
use auth_client::{AuthClient, AuthClientScheme};
use std::fs;

// Client for session validation — no authentication needed for this role
let client = AuthClient::new(
    "https://auth.example.com",          // base URL
    &fs::read_to_string("ca.cert.pem")?, // PEM of the server CA certificate
    AuthClientScheme::None,
)?;

println!("Base URL: {}", client.base_url());
}

AuthClient::new returns AuthResult<AuthClient>. It fails if the CA certificate is malformed or the authentication scheme configuration is invalid (e.g., unparsable PKCS#12 archive).


Authentication Schemes

AuthClientScheme controls how the client authenticates to the Authentication Verifier crate itself. For simple session validation no authentication is needed; admin operations require a logged-in session.

#![allow(unused)]
fn main() {
use auth_client::AuthClientScheme;

// No authentication — use for session validation and public endpoints
AuthClientScheme::None

// Username/password — use to log in as admin before calling management APIs
AuthClientScheme::UsernamePassword {
    username: "admin".to_string(),
    password: "s3cret".to_string(),
}

// JWT Bearer token — use for machine-to-machine admin access
AuthClientScheme::Jwt {
    token: jwt_string,
}

// Client Certificate (mTLS) — PKCS#12 DER archive + password
AuthClientScheme::ClientCertificate {
    pkcs12_der: fs::read("client.p12")?,
    password: "pkcs12_password".to_string(),
}
}

Session cookie management: After a successful login() call the session cookie is stored automatically in the client's built-in cookie jar and sent with all subsequent requests. You do not need to manage cookies manually.


Session Validation

The most common operation: your API server receives a request with an _ea_ cookie and needs to check whether the session is valid.

#![allow(unused)]
fn main() {
use auth_client::{AuthClient, AuthClientScheme, SessionData};

// Build a shared client at application startup
let auth = AuthClient::new(
    "https://auth.example.com",
    &fs::read_to_string("ca.cert.pem")?,
    AuthClientScheme::None,
)?;

// Later, in a request handler — extract session_id from the _ea_ cookie
let session_id = extract_session_id_from_cookie(&request);

let session: Option<SessionData> = auth.get_session(&session_id).await?;

match session {
    None => {
        // Session not found or expired — return 401
    }
    Some(data) => {
        println!("Authenticated: {} in realm {}", data.username, data.realm_id);
        // data.auth_scheme — "up" | "jwt" | "cc" | "f2" | "dc"
        // data.created_at  — Unix timestamp
    }
}
}

get_session maps to GET /sessions/{session_id}. It returns None — not an error — when the session is not found.

SessionData fields

FieldTypeDescription
session_idStringUnique session identifier (UUID)
realm_idStringRealm the session belongs to
usernameStringAuthenticated client identity
auth_schemeString"up" / "jwt" / "cc" / "f2" / "dc"
cookie_stringStringOpaque value from the _ea_ cookie
max_age_secondsi64Maximum absolute session lifetime
max_stale_age_secondsi64Idle timeout; any access resets this timer
created_ati64Unix timestamp of session creation

Session Actions — Log Out Everywhere

You can validate a session and perform a bulk logout in a single round-trip. This implements "keep this session, revoke all others" and "log out from every device" patterns.

#![allow(unused)]
fn main() {
use auth_client::{
    AuthClient, AuthClientScheme, AuthenticatedClientScheme, AuthScheme, SessionsAction,
};

let auth = AuthClient::new("https://auth.example.com", &ca_pem, AuthClientScheme::None)?;

let current_session_id = "550e8400-e29b-41d4-a716-446655440000";

// Validate the session AND revoke all other sessions for alice in one call
let session = auth.get_session_with_action(
    current_session_id,
    vec![AuthenticatedClientScheme {
        username: "alice".to_string(),
        auth_scheme: AuthScheme::UsernamePassword,
    }],
    SessionsAction::LogoutOtherSessions,
).await?;
}
SessionsActionEffect
LogoutOtherSessionsDeletes all sessions for the given clients except the current one.
LogoutAllSessionsDeletes all sessions for the given clients including the current one. Session data is returned before deletion.

Look up session IDs across authentication schemes

#![allow(unused)]
fn main() {
use auth_client::{AuthenticatedClientScheme, AuthScheme};

let session_ids = auth.get_sessions_for_clients(
    "my-service",
    &[
        AuthenticatedClientScheme { username: "alice".to_string(), auth_scheme: AuthScheme::UsernamePassword },
        AuthenticatedClientScheme { username: "alice".to_string(), auth_scheme: AuthScheme::Jwt },
        AuthenticatedClientScheme { username: "alice".to_string(), auth_scheme: AuthScheme::ClientCertificate },
    ],
).await?;

// Revoke all found sessions
auth.delete_sessions(&session_ids).await?;
}

Explicit logout

#![allow(unused)]
fn main() {
// Delete specific sessions
auth.delete_sessions(&[session_id.to_string()]).await?;

// Delete all sessions in a realm
auth.delete_sessions_for_realm("my-service").await?;

// Purge all globally expired sessions (maintenance)
auth.delete_expired_sessions().await?;
}

Logging In

Admin operations require a logged-in session. The login() method stores the session cookie automatically.

#![allow(unused)]
fn main() {
use auth_client::{AuthClient, AuthClientScheme, AuthenticationNextStep};

let admin = AuthClient::new(
    "https://auth.example.com",
    &fs::read_to_string("ca.cert.pem")?,
    AuthClientScheme::UsernamePassword {
        username: "admin".to_string(),
        password: "s3cret".to_string(),
    },
)?;

let (result, _cookie) = admin.login("_", None, None).await?;

match result.next_step {
    AuthenticationNextStep::Authenticated => {
        println!("Logged in. Session: {}", result.session_id.unwrap());
    }
    AuthenticationNextStep::TotpRequired => {
        // Prompt for TOTP code then re-login:
        let code = prompt_for_totp_code();
        let (result2, _) = admin.login("_", None, Some(code)).await?;
        assert!(matches!(result2.next_step, AuthenticationNextStep::Authenticated));
    }
    AuthenticationNextStep::ChangePassword => {
        // Handle forced password change
    }
}
}

Realm Management

Realm management requires super admin authentication.

#![allow(unused)]
fn main() {
use auth_client::{Realm, RealmAuthParams};

// List all realms
let realms: Vec<Realm> = admin.list_realms_as_super_admin().await?;

// Create a new realm with username/password authentication
let realm = Realm {
    id: "my-service".to_string(),
    auth_params: serde_json::from_str(r#"{
        "username_password_params": { "allow_expired_passwords": false }
    }"#)?,
    session_max_age_seconds: 3600,
    session_max_stale_age_seconds: 1800,
};
admin.create_realm_as_super_admin(&realm).await?;

// Create a realm with JWT/OIDC authentication (e.g., Google Sign-In)
let jwt_realm = Realm {
    id: "google-login".to_string(),
    auth_params: serde_json::from_str(r#"{
        "jwt_params": {
            "idp_params": [{
                "jwt_issuer_uri": "https://accounts.google.com",
                "jwks_uri": "https://www.googleapis.com/oauth2/v3/certs",
                "jwt_audience": "my-client-id.apps.googleusercontent.com"
            }]
        }
    }"#)?,
    session_max_age_seconds: 28800,
    session_max_stale_age_seconds: 3600,
};
admin.create_realm_as_super_admin(&jwt_realm).await?;

// Retrieve a realm
let r: Realm = admin.get_realm_as_super_admin("my-service").await?;

// Update a realm
admin.update_realm_as_super_admin("my-service", &r).await?;

// Delete a realm
admin.delete_realm_as_super_admin("my-service").await?;
}

Admin Management

#![allow(unused)]
fn main() {
use auth_client::Admin;

// Create a realm admin (can manage "my-service" and "other-service")
let user = Admin {
    id: "bob".to_string(),
    realms: vec!["my-service".to_string(), "other-service".to_string()],
    userpass: Some("bob".to_string()),
    jwt: None,
    fido2: None,
    digital_credentials: None,
    client_certificate: None,
    totp_enabled: Some(false),
    totp_secret: None,
    totp_auth_url: None,
};
let created: Admin = admin.create_admin_as_super_admin(&user).await?;

// Check admin roles
assert!(created.can_administer_realm("my-service"));
assert!(!created.is_super_admin());

// List all users
let all: Vec<Admin> = admin.list_admins_as_super_admin().await?;

// Add / remove realm from user
admin.add_admin_to_realm("bob", "another-realm").await?;
admin.remove_admin_from_realm("bob", "another-realm").await?;

// Delete
admin.delete_admin_as_super_admin("bob").await?;
}

Credential Management

Passwords are hashed with Argon2id server-side. Pass the plaintext password as a UTF-8 byte array. The server stores only the hash. When reading back a credential, the password field is always empty.

#![allow(unused)]
fn main() {
use auth_client::UserPass;

// Create credentials
admin.create_admin_credentials_in_realm(
    "my-service",
    &UserPass {
        realm: "my-service".to_string(),
        username: "alice".to_string(),
        password: b"hunter2".to_vec(),  // plaintext — server hashes with Argon2id
        change_password: false,
        roles: vec!["CryptoOfficer".to_string()],
    },
).await?;

// Read back — password is always empty
let stored: UserPass = admin.get_admin_credentials_in_realm("my-service", "alice").await?;
assert!(stored.password.is_empty());

// Force password change on next login
admin.update_admin_credentials_in_realm(
    "my-service",
    "alice",
    &UserPass {
        realm: "my-service".to_string(),
        username: "alice".to_string(),
        password: b"new-password".to_vec(),
        change_password: true,
        roles: vec!["CryptoOfficer".to_string()],
    },
).await?;

// List all credentials in a realm
let all: Vec<UserPass> = admin.list_admin_credentials_in_realm("my-service").await?;

// Delete
admin.delete_admin_credentials_in_realm("my-service", "alice").await?;
}

TOTP Management

TOTP enrollment is a two-step process: generate a secret, show the QR code to the user, then verify one code to confirm enrollment.

#![allow(unused)]
fn main() {
use auth_client::TotpGenerateResponse;

// Step 1 — generate a TOTP secret (not stored yet)
let resp: TotpGenerateResponse = admin
    .generate_totp("my-service", "alice", Some("My App".to_string()))
    .await?;

println!("Secret: {}", resp.secret_base32);
println!("QR code URL: {}", resp.otpauth_url);
// Render resp.otpauth_url as a QR code and show it to the user

// Step 2 — user scans the QR code and provides the current code
let totp_code = prompt_user_for_code(); // e.g., "482913"

admin.verify_and_enable_totp(
    "my-service",
    "alice",
    &resp.secret_base32,
    &totp_code,
    Some("My App".to_string()),
).await?;
// TOTP is now active. Alice's next login will return `TotpRequired`.

// Disable TOTP
admin.disable_totp("my-service", "alice").await?;
}

Machine Authentication (AppRole / Kubernetes / Token)

These methods are used by machine clients (SPIRE, CI pipelines, Kubernetes workloads). They do not use session cookies — they produce opaque app tokens (X-Vault-Token).

AppRole provisioning (admin operations)

#![allow(unused)]
fn main() {
use auth_client::{AppRoleRequest, AppRoleSecretIdRequest};

// Create or update a role (requires admin session)
client.approle_create_role("spire-server", &AppRoleRequest {
    token_ttl: 3600,
    secret_id_ttl: 86400,
    token_policies: vec!["default".to_string()],
    bind_secret_id: true,
}).await?;

// Read the stable role_id to hand to the service
let role_id_resp = client.approle_get_role_id("spire-server").await?;
let role_id = role_id_resp.data.role_id;
println!("role_id: {role_id}");

// Generate a secret_id (single-use by default)
let secret_resp = client.approle_generate_secret_id("spire-server", &AppRoleSecretIdRequest {
    ttl: 0,
    num_uses: 1,
}).await?;
let secret_id = secret_resp.data.secret_id;
let accessor   = secret_resp.data.secret_id_accessor;
println!("secret_id: {secret_id}  accessor: {accessor}");

// Destroy a secret_id by accessor (without knowing its value)
client.approle_destroy_secret_id("spire-server", &accessor).await?;

// List all role names
let roles = client.approle_list_roles().await?;
println!("roles: {:?}", roles.data.keys);

// Delete a role (cascades to all its secret IDs)
client.approle_delete_role("spire-server").await?;
}

AppRole login (client operation)

#![allow(unused)]
fn main() {
use auth_client::AppAuthResponse;

// Exchange role_id + secret_id for an app token
let auth: AppAuthResponse = client
    .approle_login(&role_id, Some(&secret_id))
    .await?;

let token = auth.auth.client_token;
println!("app token: {token}");
println!("renewable: {}, ttl: {}s", auth.auth.renewable, auth.auth.lease_duration);
}

Kubernetes role provisioning (admin operations)

#![allow(unused)]
fn main() {
use auth_client::K8sRoleRequest;

// Create or update a Kubernetes role (requires admin session)
client.k8s_create_role("my-k8s-role", &K8sRoleRequest {
    jwks_url: "https://kubernetes.default.svc/.well-known/jwks.json".to_string(),
    bound_service_account_names: vec!["my-app".to_string()],
    bound_service_account_namespaces: vec!["production".to_string()],
    token_ttl: 3600,
    expected_issuer: None,
    bound_audiences: None,
}).await?;

// Delete a Kubernetes role
client.k8s_delete_role("my-k8s-role").await?;
}

Kubernetes login (client operation)

#![allow(unused)]
fn main() {
// Read the projected service-account JWT from the pod filesystem
let jwt = std::fs::read_to_string(
    "/var/run/secrets/kubernetes.io/serviceaccount/token"
)?;

let auth = client.k8s_login("my-k8s-role", &jwt).await?;
let token = auth.auth.client_token;
}

Token self-service

#![allow(unused)]
fn main() {
use auth_client::APP_TOKEN_HEADER;

// Validate a token and read its metadata
let lookup = client.token_lookup_self(&token).await?;
println!("entity: {}, ttl: {}s", lookup.data.entity_id, lookup.data.ttl);

// Renew a token (resets TTL to lease_duration)
let renewed = client.token_renew_self(&token).await?;
println!("new lease_duration: {}s", renewed.auth.lease_duration);

// Revoke a token immediately
client.token_revoke_self(&token).await?;

// The header constant, if you need to set it manually:
println!("Header name: {APP_TOKEN_HEADER}");  // "X-Vault-Token"
}

Public Endpoints

#![allow(unused)]
fn main() {
// Server version — no authentication required
let version: String = client.get_version().await?;
println!("Auth server version: {}", version);

// Current session claims — requires a valid session cookie in the jar
use auth_client::ClientClaims;
let claims: ClientClaims = client.whoami("_").await?;
println!("Logged in as: {}", claims.registered.sub.as_deref().unwrap_or("unknown"));
}

Error Handling

All methods return AuthResult<T>, which is Result<T, AuthError>.

#![allow(unused)]
fn main() {
use auth_client::AuthError;

match client.get_session("bad-id").await {
    Ok(None)    => { /* session not found — normal path */ }
    Ok(Some(s)) => { /* valid session */ }
    Err(AuthError::FailedHttpStatus(msg)) => eprintln!("Auth server error: {}", msg),
    Err(AuthError::SessionNotFound)       => eprintln!("Session not found"),
    Err(AuthError::Config(msg))           => eprintln!("Client config error: {}", msg),
    Err(AuthError::JWT(msg))              => eprintln!("JWT error: {}", msg),
    Err(e)                                => eprintln!("Unexpected error: {}", e),
}
}
VariantMeaning
AuthError::FailedHttpStatus(msg)Non-2xx response; msg contains status code and body
AuthError::SessionNotFoundGET /sessions/{id} returned 404
AuthError::Config(msg)Client misconfiguration (bad URL, certificate parse error, etc.)
AuthError::JWT(msg)JWT signing or validation failure
AuthError::Generic(msg)Network or serialization error

Types Reference

AuthClientScheme

#![allow(unused)]
fn main() {
pub enum AuthClientScheme {
    None,
    Jwt         { token: String },
    ClientCertificate { pkcs12_der: Vec<u8>, password: String },
    UsernamePassword  { username: String, password: String },
}
}

AuthScheme (wire format in AuthenticatedClientScheme)

Rust VariantJSON value
UsernamePassword"up"
Jwt"jwt"
ClientCertificate"cc"
Fido2"f2"
DigitalCredentials"dc"

AuthenticationNextStep

VariantMeaning
AuthenticatedLogin complete; session cookie issued
TotpRequiredPrimary credentials accepted; re-submit with totp_code
ChangePasswordPassword has expired; must be changed

ClientClaims

Returned by whoami(). The struct has three flattened sub-structs — the JWT is a flat JSON object on the wire; the sub-struct boundaries exist only in Rust:

#![allow(unused)]
fn main() {
pub struct ClientClaims {
    pub registered:    RegisteredClaims,    // RFC 7519 §4.1
    pub authorization: AuthorizationClaims, // RFC 9068 §2.2.3.1
    pub private:       AuthPrivateClaims,   // Auth-specific
    pub extra:         HashMap<String, Value>,
}

pub struct RegisteredClaims {
    pub iss: Option<String>,      // Issuer
    pub sub: Option<String>,      // Subject (username)
    pub aud: Option<Vec<String>>, // Audience
    pub exp: Option<i64>,         // Expiry (Unix secs)
    pub nbf: Option<i64>,         // Not-before (Unix secs)
    pub iat: Option<i64>,         // Issued-at (Unix secs)
    pub jti: Option<String>,      // JWT ID
}

/// Authorization claims (RFC 9068 §2.2.3.1 / RFC 7643 §4.1.2).
pub struct AuthorizationClaims {
    // "roles" — RBAC roles assigned to the user at credential level.
    // Omitted when empty (fail-closed in OPA and downstream consumers).
    pub roles: Option<Vec<String>>,
}

pub struct AuthPrivateClaims {
    // "as_as" — auth scheme: "up" | "jwt" | "cc" | "f2" | "dc"
    pub auth_scheme: Option<AuthScheme>,
    // "as_pk" — client public key PEM (when using mTLS)
    pub public_key: Option<String>,
    // "as_rid" — realm ID (also used as the OPA domain scope)
    pub realm_id: Option<String>,
}
}

Reading roles from a validated session

#![allow(unused)]
fn main() {
let claims: ClientClaims = client.whoami("my-service").await?;

let roles = claims.authorization.roles.as_deref().unwrap_or(&[]);
if roles.contains(&"CryptoOfficer".to_string()) {
    // grant access to crypto operations
}
}

Wire format example

A token issued for a user with the CryptoOfficer role in realm acme looks like:

{
  "sub": "alice",
  "exp": 1893456000,
  "iat": 1893452400,
  "roles": ["CryptoOfficer"],
  "as_as": "up",
  "as_rid": "acme"
}

The roles claim is absent (not []) when the credential has no roles assigned, so downstream OPA policies can treat absence as fail-closed without an explicit empty-array check.


Actix-web Integration Example

A complete pattern for an Actix-web API server that validates sessions via the Authentication Verifier.

use std::sync::Arc;
use actix_web::{web, HttpRequest, HttpResponse, middleware};
use auth_client::{AuthClient, AuthClientScheme, AuthError, SessionData};

/// Shared application state containing the auth client.
pub struct AppState {
    pub auth: Arc<AuthClient>,
}

/// Extract and validate the _ea_ session cookie.
async fn validate_session(
    state: &AppState,
    req: &HttpRequest,
) -> Result<SessionData, HttpResponse> {
    let cookie = req
        .cookie("_ea_")
        .ok_or_else(|| HttpResponse::Unauthorized().body("Missing _ea_ cookie"))?;

    state
        .auth
        .get_session(cookie.value())
        .await
        .map_err(|e| HttpResponse::InternalServerError().body(e.to_string()))?
        .ok_or_else(|| HttpResponse::Unauthorized().body("Session not found or expired"))
}

/// A protected endpoint.
async fn protected_handler(
    state: web::Data<AppState>,
    req: HttpRequest,
) -> HttpResponse {
    let session = match validate_session(&state, &req).await {
        Ok(s) => s,
        Err(resp) => return resp,
    };

    HttpResponse::Ok().json(serde_json::json!({
        "message": format!("Hello, {}!", session.username),
        "realm": session.realm_id,
    }))
}

/// Build the shared auth client at application startup.
pub fn build_auth_client(
    auth_verifier_url: &str,
    ca_cert_pem: &str,
) -> Arc<AuthClient> {
    Arc::new(
        AuthClient::new(auth_verifier_url, ca_cert_pem, AuthClientScheme::None)
            .expect("Failed to build auth client"),
    )
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    use actix_web::{App, HttpServer};

    let ca_pem = std::fs::read_to_string("ca.cert.pem").unwrap();
    let auth = build_auth_client("https://auth.example.com", &ca_pem);

    HttpServer::new(move || {
        App::new()
            .app_data(web::Data::new(AppState { auth: auth.clone() }))
            .route("/api/resource", web::get().to(protected_handler))
    })
    .bind("0.0.0.0:8080")?
    .run()
    .await
}