The auth_client crate provides an HTTP client and shared types for interacting with the Auth authentication server. 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 server itself (internal use).
- Installation
- Building a Client
- Authentication Schemes
- Session Validation
- Session Actions — Log Out Everywhere
- Logging In
- Realm Management
- Admin Management
- Credential Management
- TOTP Management
- Public Endpoints
- Error Handling
- Types Reference
- Actix-web Integration Example
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 auth server crate itself.
All operations go through AuthClient. Create one at startup and share it as application state (it is Clone-able).
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., unparseable PKCS#12 archive).
AuthClientScheme controls how the client authenticates to the auth server itself. For simple session validation no authentication is needed; admin operations require a logged-in session.
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.
The most common operation: your API server receives a request with an _ea_ cookie and needs to check whether the session is valid.
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.
| Field | Type | Description |
|---|---|---|
session_id |
String |
Unique session identifier (UUID) |
realm_id |
String |
Realm the session belongs to |
username |
String |
Authenticated client identity |
auth_scheme |
String |
"up" / "jwt" / "cc" / "f2" / "dc" |
cookie_string |
String |
Opaque value from the _ea_ cookie |
max_age_seconds |
i64 |
Maximum absolute session lifetime |
max_stale_age_seconds |
i64 |
Idle timeout; any access resets this timer |
created_at |
i64 |
Unix timestamp of session creation |
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.
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?;SessionsAction |
Effect |
|---|---|
LogoutOtherSessions |
Deletes all sessions for the given clients except the current one. |
LogoutAllSessions |
Deletes all sessions for the given clients including the current one. Session data is returned before deletion. |
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?;// 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?;Admin operations require a logged-in session. The login() method stores the session cookie automatically.
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 requires super admin authentication.
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?;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?;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.
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,
},
).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,
},
).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 enrollment is a two-step process: generate a secret, show the QR code to the user, then verify one code to confirm enrollment.
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?;// 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"));All methods return AuthResult<T>, which is Result<T, AuthError>.
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),
}| Variant | Meaning |
|---|---|
AuthError::FailedHttpStatus(msg) |
Non-2xx response; msg contains status code and body |
AuthError::SessionNotFound |
GET /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 |
pub enum AuthClientScheme {
None,
Jwt { token: String },
ClientCertificate { pkcs12_der: Vec<u8>, password: String },
UsernamePassword { username: String, password: String },
}| Rust Variant | JSON value |
|---|---|
UsernamePassword |
"up" |
Jwt |
"jwt" |
ClientCertificate |
"cc" |
Fido2 |
"f2" |
DigitalCredentials |
"dc" |
| Variant | Meaning |
|---|---|
Authenticated |
Login complete; session cookie issued |
TotpRequired |
Primary credentials accepted; re-submit with totp_code |
ChangePassword |
Password has expired; must be changed |
Returned by whoami(). The struct has two flattened sub-structs:
pub struct ClientClaims {
pub registered: RegisteredClaims, // RFC 7519 §4.1
pub private: AuthPrivateClaims, // Auth-specific
pub extra: HashMap<String, Value>,
}
pub struct RegisteredClaims {
pub iss: Option<String>, // Issuer
pub sub: Option<String>, // Subject
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
}
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
pub realm_id: Option<String>,
}A complete pattern for an Actix-web API server that validates sessions via the auth server.
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_server_url: &str,
ca_cert_pem: &str,
) -> Arc<AuthClient> {
Arc::new(
AuthClient::new(auth_server_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
}