Authentication - Rust SDK Documentation
Overview
Authentication in BosBase is stateless and token-based. A client is considered authenticated as long as it sends a valid Authorization: YOUR_AUTH_TOKEN header with requests.
Key Points:
- No sessions: BosBase APIs are fully stateless (tokens are not stored in the database)
- No logout endpoint: To “logout”, simply clear the token from your local state (
pb.auth_store().clear()) - Token generation: Auth tokens are generated through auth collection Web APIs or programmatically
- Admin users:
_superuserscollection works like regular auth collections but with full access (API rules are ignored) - OAuth2 limitation: OAuth2 is not supported for
_superuserscollection
Authentication Methods
BosBase supports multiple authentication methods that can be configured individually for each auth collection:
- Password Authentication - Email/username + password
- OTP Authentication - One-time password via email
- OAuth2 Authentication - Google, GitHub, Microsoft, etc.
- Multi-factor Authentication (MFA) - Requires 2 different auth methods
Authentication Store
The SDK maintains an auth_store that automatically manages the authentication state:
use bosbase::BosBase;
use std::collections::HashMap;
let pb = BosBase::new("http://localhost:8090");
// Check authentication status
println!("Is valid: {}", pb.auth_store().is_valid());
println!("Token: {}", pb.auth_store().token());
println!("Record: {:?}", pb.auth_store().record());
// Clear authentication (logout)
pb.auth_store().clear();
Password Authentication
Authenticate using email/username and password. The identity field can be configured in the collection options (default is email).
Backend Endpoint: POST /api/collections/{collection}/auth-with-password
Basic Usage
use bosbase::BosBase;
use std::collections::HashMap;
let pb = BosBase::new("http://localhost:8090");
// Authenticate with email and password
let auth_data = pb.collection("users").auth_with_password(
"test@example.com",
"password123",
HashMap::new(),
HashMap::new(),
None
).await?;
// Auth data is automatically stored in pb.auth_store()
println!("Is valid: {}", pb.auth_store().is_valid()); // true
println!("Token: {}", pb.auth_store().token()); // JWT token
println!("User ID: {}", pb.auth_store().record()["id"]); // user record ID
Response Format
// Response structure:
// {
// "token": "eyJhbGciOiJIUzI1NiJ9...",
// "record": {
// "id": "record_id",
// "email": "test@example.com",
// // ... other user fields
// }
// }
Error Handling with MFA
use bosbase::errors::ClientResponseError;
match pb.collection("users").auth_with_password(
"test@example.com",
"pass123",
HashMap::new(),
HashMap::new(),
None
).await {
Ok(auth_data) => {
println!("Authentication successful");
}
Err(err) => {
// Check for MFA requirement
if let Some(data) = err.data().as_object() {
if let Some(mfa_id) = data.get("mfaId") {
let mfa_id_str = mfa_id.as_str().unwrap();
// Handle MFA flow (see Multi-factor Authentication section)
} else {
eprintln!("Authentication failed: {:?}", err);
}
}
}
}
OTP Authentication
One-time password authentication via email.
Backend Endpoints:
POST /api/collections/{collection}/request-otp- Request OTPPOST /api/collections/{collection}/auth-with-otp- Authenticate with OTP
Request OTP
// Send OTP to user's email
let result = pb.collection("users").request_otp(
"test@example.com",
HashMap::new(),
HashMap::new()
).await?;
println!("OTP ID: {}", result["otpId"]); // OTP ID to use in auth_with_otp
Authenticate with OTP
// Step 1: Request OTP
let result = pb.collection("users").request_otp(
"test@example.com",
HashMap::new(),
HashMap::new()
).await?;
// Step 2: User enters OTP from email
// Step 3: Authenticate with OTP
let auth_data = pb.collection("users").auth_with_otp(
result["otpId"].as_str().unwrap(),
"123456", // OTP code from email
None,
HashMap::new(),
HashMap::new()
).await?;
OAuth2 Authentication
Backend Endpoint: POST /api/collections/{collection}/auth-with-oauth2
Manual Code Exchange
// Get auth methods
let auth_methods = pb.collection("users").list_auth_methods(
HashMap::new(),
HashMap::new()
).await?;
// Find provider
let providers = auth_methods["oauth2"]["providers"].as_array().unwrap();
let provider = providers.iter().find(|p| p["name"] == "google");
if let Some(provider) = provider {
// Exchange code for token (after OAuth2 redirect)
let auth_data = pb.collection("users").auth_with_oauth2_code(
"google", // Provider name
"AUTHORIZATION_CODE", // From redirect URL
provider["codeVerifier"].as_str().unwrap(),
"https://yourapp.com/callback", // Redirect URL
serde_json::json!({}), // Optional data for new accounts
HashMap::new(),
HashMap::new()
).await?;
}
Multi-Factor Authentication (MFA)
Requires 2 different auth methods.
let mut mfa_id: Option<String> = None;
match pb.collection("users").auth_with_password(
"test@example.com",
"pass123",
HashMap::new(),
HashMap::new(),
None
).await {
Ok(_) => {
println!("Authentication successful");
}
Err(err) => {
if let Some(data) = err.data().as_object() {
if let Some(mfa_id_val) = data.get("mfaId") {
mfa_id = mfa_id_val.as_str().map(|s| s.to_string());
// Second auth method (OTP)
let otp_result = pb.collection("users").request_otp(
"test@example.com",
HashMap::new(),
HashMap::new()
).await?;
let auth_data = pb.collection("users").auth_with_otp(
otp_result["otpId"].as_str().unwrap(),
"123456",
mfa_id.clone(),
HashMap::new(),
HashMap::new()
).await?;
}
}
}
}
User Impersonation
Superusers can impersonate other users.
Backend Endpoint: POST /api/collections/{collection}/impersonate/{id}
// Authenticate as superuser
pb.admins().auth_with_password("admin@example.com", "adminpass").await?;
// Impersonate a user
let impersonate_client = pb.collection("users").impersonate(
"USER_RECORD_ID",
3600, // Optional: token duration in seconds
HashMap::new(),
HashMap::new()
).await?;
// Use impersonate client
let data = impersonate_client.collection("posts").get_full_list(
200,
HashMap::new(),
HashMap::new(),
None,
None,
None,
None
).await?;
Auth Token Verification
Verify token by calling auth_refresh().
Backend Endpoint: POST /api/collections/{collection}/auth-refresh
match pb.collection("users").auth_refresh(
HashMap::new(),
HashMap::new()
).await {
Ok(_) => {
println!("Token is valid");
}
Err(err) => {
eprintln!("Token verification failed: {:?}", err);
pb.auth_store().clear();
}
}
List Available Auth Methods
Backend Endpoint: GET /api/collections/{collection}/auth-methods
let auth_methods = pb.collection("users").list_auth_methods(
HashMap::new(),
HashMap::new()
).await?;
println!("Password enabled: {}", auth_methods["password"]["enabled"]);
println!("OAuth2 providers: {:?}", auth_methods["oauth2"]["providers"]);
println!("MFA enabled: {}", auth_methods["mfa"]["enabled"]);
Complete Examples
Example 1: Complete Authentication Flow with Error Handling
use bosbase::BosBase;
use bosbase::errors::ClientResponseError;
use std::collections::HashMap;
async fn authenticate_user(
pb: &BosBase,
email: &str,
password: &str,
) -> Result<serde_json::Value, Box<dyn std::error::Error>> {
match pb.collection("users").auth_with_password(
email,
password,
HashMap::new(),
HashMap::new(),
None
).await {
Ok(auth_data) => {
println!("Successfully authenticated: {}", auth_data["record"]["email"]);
Ok(auth_data)
}
Err(err) => {
// Check if MFA is required
if err.status() == 401 {
if let Some(data) = err.data().as_object() {
if let Some(mfa_id) = data.get("mfaId") {
println!("MFA required, proceeding with second factor...");
return handle_mfa(pb, email, mfa_id.as_str().unwrap()).await;
}
}
}
// Handle other errors
if err.status() == 400 {
return Err("Invalid credentials".into());
} else if err.status() == 403 {
return Err("Password authentication is not enabled for this collection".into());
} else {
return Err(err.into());
}
}
}
}
async fn handle_mfa(
pb: &BosBase,
email: &str,
mfa_id: &str,
) -> Result<serde_json::Value, Box<dyn std::error::Error>> {
// Request OTP for second factor
let otp_result = pb.collection("users").request_otp(
email,
HashMap::new(),
HashMap::new()
).await?;
// In a real app, get OTP from user input
let user_entered_otp = "123456"; // Get from user input
match pb.collection("users").auth_with_otp(
otp_result["otpId"].as_str().unwrap(),
user_entered_otp,
Some(mfa_id.to_string()),
HashMap::new(),
HashMap::new()
).await {
Ok(auth_data) => {
println!("MFA authentication successful");
Ok(auth_data)
}
Err(err) => {
if err.status() == 429 {
return Err("Too many OTP attempts, please request a new OTP".into());
}
Err("Invalid OTP code".into())
}
}
}
// Usage
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let pb = BosBase::new("http://localhost:8090");
authenticate_user(&pb, "user@example.com", "password123").await?;
println!("User is authenticated: {:?}", pb.auth_store().record());
Ok(())
}
Example 2: Token Management and Refresh
use bosbase::BosBase;
use std::collections::HashMap;
async fn check_auth(pb: &BosBase) -> Result<bool, Box<dyn std::error::Error>> {
if pb.auth_store().is_valid() {
println!("User is authenticated: {:?}", pb.auth_store().record()["email"]);
// Verify token is still valid and refresh if needed
match pb.collection("users").auth_refresh(
HashMap::new(),
HashMap::new()
).await {
Ok(_) => {
println!("Token refreshed successfully");
Ok(true)
}
Err(_) => {
println!("Token expired or invalid, clearing auth");
pb.auth_store().clear();
Ok(false)
}
}
} else {
Ok(false)
}
}
Example 3: Admin Impersonation for Support
use bosbase::BosBase;
use std::collections::HashMap;
async fn impersonate_user_for_support(
pb: &BosBase,
user_id: &str,
) -> Result<serde_json::Value, Box<dyn std::error::Error>> {
// Authenticate as admin
pb.admins().auth_with_password("admin@example.com", "adminpassword").await?;
// Impersonate the user (1 hour token)
let user_client = pb.collection("users").impersonate(
user_id,
3600,
HashMap::new(),
HashMap::new()
).await?;
println!("Impersonating user: {:?}", user_client.auth_store().record()["email"]);
// Use the impersonated client to test user experience
let user_records = user_client.collection("posts").get_full_list(
200,
HashMap::new(),
HashMap::new(),
None,
None,
None,
None
).await?;
println!("User can see {} posts", user_records.as_array().unwrap().len());
// Check what the user sees
let user_view = user_client.collection("posts").get_list(
1,
10,
false,
HashMap::new(),
HashMap::new(),
Some(r#"published = true"#.to_string()),
None,
None,
None
).await?;
Ok(json!({
"canAccess": user_view["items"].as_array().unwrap().len(),
"totalPosts": user_records.as_array().unwrap().len()
}))
}
Best Practices
- Secure Token Storage: Never expose tokens in client-side code or logs
- Token Refresh: Implement automatic token refresh before expiration
- Error Handling: Always handle MFA requirements and token expiration
- OAuth2 Security: Always validate the
stateparameter in OAuth2 callbacks - API Keys: Use impersonation tokens for server-to-server communication only
- Superuser Tokens: Never expose superuser impersonation tokens in client code
- OTP Security: Use OTP with MFA for security-critical applications
- Rate Limiting: Be aware of rate limits on authentication endpoints
Troubleshooting
Token Expired
If you get 401 errors, check if the token has expired:
match pb.collection("users").auth_refresh(
HashMap::new(),
HashMap::new()
).await {
Ok(_) => {
println!("Token is still valid");
}
Err(_) => {
// Token expired, require re-authentication
pb.auth_store().clear();
// Redirect to login
}
}
MFA Required
If authentication returns 401 with mfaId:
if err.status() == 401 {
if let Some(data) = err.data().as_object() {
if let Some(_mfa_id) = data.get("mfaId") {
// Proceed with second authentication factor
}
}
}