Rust SDK
Official OAuth42 SDK for Rust. Works with Actix Web, Axum, Rocket, and any Rust web framework.
Installation
Add to your Cargo.toml:
[dependencies]
o42sdk = "0.1"With framework support:
[dependencies]
o42sdk = { version = "0.1", features = ["actix", "axum"] }Quick Start
use o42sdk::{OAuth42Client, Config};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create client with builder pattern
let mut client = OAuth42Client::builder()
.issuer("https://api.oauth42.com")?
.client_id("your_client_id")
.client_secret("your_client_secret")
.redirect_uri("https://yourapp.com/callback")?
.scopes(vec!["openid", "profile", "email"])
.build()?;
// Discover endpoints automatically
client.discover().await?;
// Generate authorization URL with PKCE
let (auth_url, code_verifier) = client
.authorize_url().await?
.with_pkce()
.build()?;
println!("Visit: {}", auth_url);
// After user authorizes, exchange code for tokens
let tokens = client
.exchange_code(&code, Some(&code_verifier))
.await?;
println!("Access token: {}", tokens.access_token);
Ok(())
}Actix Web Integration
use actix_web::{web, App, HttpResponse, HttpServer};
use actix_session::{Session, SessionMiddleware, storage::CookieSessionStore};
use actix_web::cookie::Key;
use o42sdk::{OAuth42Client, Config};
async fn login(
client: web::Data<OAuth42Client>,
session: Session,
) -> Result<HttpResponse, actix_web::Error> {
let mut client = client.as_ref().clone();
let (auth_url, code_verifier) = client
.authorize_url().await
.map_err(actix_web::error::ErrorInternalServerError)?
.with_pkce()
.build()
.map_err(actix_web::error::ErrorInternalServerError)?;
session.insert("code_verifier", code_verifier)?;
Ok(HttpResponse::Found()
.append_header(("Location", auth_url.to_string()))
.finish())
}
async fn callback(
client: web::Data<OAuth42Client>,
session: Session,
query: web::Query<CallbackQuery>,
) -> Result<HttpResponse, actix_web::Error> {
let code_verifier = session
.get::<String>("code_verifier")?
.ok_or_else(|| actix_web::error::ErrorUnauthorized("No code verifier"))?;
let mut client = client.as_ref().clone();
let tokens = client
.exchange_code(&query.code, Some(&code_verifier))
.await
.map_err(actix_web::error::ErrorInternalServerError)?;
session.insert("access_token", tokens.access_token)?;
Ok(HttpResponse::Found()
.append_header(("Location", "/dashboard"))
.finish())
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let client = OAuth42Client::builder()
.issuer("https://api.oauth42.com").unwrap()
.client_id(std::env::var("OAUTH42_CLIENT_ID").unwrap())
.client_secret(std::env::var("OAUTH42_CLIENT_SECRET").unwrap())
.redirect_uri("https://yourapp.com/callback").unwrap()
.scopes(vec!["openid", "profile", "email"])
.build()
.unwrap();
let client_data = web::Data::new(client);
HttpServer::new(move || {
App::new()
.app_data(client_data.clone())
.wrap(SessionMiddleware::new(
CookieSessionStore::default(),
Key::generate(),
))
.route("/login", web::get().to(login))
.route("/callback", web::get().to(callback))
})
.bind(("127.0.0.1", 8080))?
.run()
.await
}
#[derive(serde::Deserialize)]
struct CallbackQuery {
code: String,
}Axum Integration
use axum::{
extract::{Query, State},
http::StatusCode,
response::{IntoResponse, Redirect},
routing::get,
Router,
};
use axum_extra::extract::cookie::{Cookie, CookieJar};
use o42sdk::OAuth42Client;
use std::sync::Arc;
use tokio::sync::Mutex;
type AppState = Arc<Mutex<OAuth42Client>>;
async fn login(
State(client): State<AppState>,
jar: CookieJar,
) -> Result<(CookieJar, Redirect), StatusCode> {
let mut client = client.lock().await;
let (auth_url, code_verifier) = client
.authorize_url().await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
.with_pkce()
.build()
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let jar = jar.add(Cookie::new("code_verifier", code_verifier));
Ok((jar, Redirect::to(auth_url.as_str())))
}
#[derive(serde::Deserialize)]
struct CallbackParams {
code: String,
}
async fn callback(
State(client): State<AppState>,
jar: CookieJar,
Query(params): Query<CallbackParams>,
) -> Result<(CookieJar, Redirect), StatusCode> {
let code_verifier = jar
.get("code_verifier")
.ok_or(StatusCode::UNAUTHORIZED)?
.value()
.to_string();
let mut client = client.lock().await;
let tokens = client
.exchange_code(¶ms.code, Some(&code_verifier))
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let jar = jar.add(Cookie::new("access_token", tokens.access_token));
Ok((jar, Redirect::to("/dashboard")))
}
#[tokio::main]
async fn main() {
let client = OAuth42Client::builder()
.issuer("https://api.oauth42.com").unwrap()
.client_id(std::env::var("OAUTH42_CLIENT_ID").unwrap())
.client_secret(std::env::var("OAUTH42_CLIENT_SECRET").unwrap())
.redirect_uri("https://yourapp.com/callback").unwrap()
.scopes(vec!["openid", "profile", "email"])
.build()
.unwrap();
let state = Arc::new(Mutex::new(client));
let app = Router::new()
.route("/login", get(login))
.route("/callback", get(callback))
.with_state(state);
let listener = tokio::net::TcpListener::bind("127.0.0.1:8080")
.await
.unwrap();
axum::serve(listener, app).await.unwrap();
}Client Credentials Flow (App-to-App)
The client credentials flow is used for app-to-app authentication where there is no user involved. This is ideal for backend services, APIs, microservices, and automated tasks that need to authenticate directly with OAuth42.
Creating a Service Account
For machine-to-machine authentication (backend services, APIs, microservices), create a Service Account in the OAuth42 Portal:
- Log in to the OAuth42 Portal
- Navigate to Service Accounts and click "Create Service Account"
- Enter a name and description for your service
- Select the scopes you need (e.g.,
adminfor managing groups/users) - Optionally set an expiration period for security
- Click Create and immediately save the Client ID and Client Secret - the secret is only shown once!
💡 Tip: Service accounts are simpler than OAuth applications for machine-to-machine auth and include built-in audit logging.
Basic Usage
use o42sdk::OAuth42Client;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create client with client credentials
let mut client = OAuth42Client::builder()
.issuer("https://api.oauth42.com")?
.client_id("your_client_id")
.client_secret("your_client_secret")
.build()?;
// Discover endpoints
client.discover().await?;
// Get access token using client credentials flow
let tokens = client
.client_credentials_flow(vec!["api:read", "api:write"])
.await?;
println!("Access token: {}", tokens.access_token);
// Use the token to make authenticated API calls
// The token represents the application itself, not a user
Ok(())
}Microservice Example
Here's a complete example of a microservice that authenticates using client credentials:
use o42sdk::OAuth42Client;
use std::sync::Arc;
use tokio::sync::RwLock;
struct ApiClient {
oauth_client: Arc<RwLock<OAuth42Client>>,
access_token: Arc<RwLock<Option<String>>>,
}
impl ApiClient {
async fn new(
client_id: &str,
client_secret: &str,
) -> Result<Self, Box<dyn std::error::Error>> {
let mut client = OAuth42Client::builder()
.issuer("https://api.oauth42.com")?
.client_id(client_id)
.client_secret(client_secret)
.build()?;
client.discover().await?;
Ok(Self {
oauth_client: Arc::new(RwLock::new(client)),
access_token: Arc::new(RwLock::new(None)),
})
}
async fn ensure_authenticated(&self) -> Result<(), Box<dyn std::error::Error>> {
let mut client = self.oauth_client.write().await;
// Get new token using client credentials
let tokens = client
.client_credentials_flow(vec!["api:read", "api:write"])
.await?;
// Store token for subsequent requests
let mut token = self.access_token.write().await;
*token = Some(tokens.access_token);
Ok(())
}
async fn make_api_call(&self, endpoint: &str) -> Result<String, Box<dyn std::error::Error>> {
// Ensure we have a valid token
if self.access_token.read().await.is_none() {
self.ensure_authenticated().await?;
}
let token = self.access_token.read().await;
let token = token.as_ref().unwrap();
// Make authenticated API call
let client = reqwest::Client::new();
let response = client
.get(endpoint)
.bearer_auth(token)
.send()
.await?;
Ok(response.text().await?)
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = ApiClient::new(
&std::env::var("OAUTH42_CLIENT_ID")?,
&std::env::var("OAUTH42_CLIENT_SECRET")?,
).await?;
// Make authenticated requests
let data = client.make_api_call("https://api.example.com/data").await?;
println!("Received: {}", data);
Ok(())
}JWT Validation (Protecting Your API)
When building APIs that accept OAuth42 tokens, you need to validate incoming JWTs from users. This section shows how to verify JWT signatures, extract claims, and protect your endpoints.
📋 Prerequisites
Add these dependencies to your Cargo.toml:
[dependencies]
jsonwebtoken = "9.2"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
reqwest = { version = "0.11", features = ["json"] }
tokio = { version = "1", features = ["full"] }
actix-web = "4" # For Actix Web examples
once_cell = "1.19" # For caching JWKSStep 1: Define JWT Claims
First, define the claims structure matching OAuth42's JWT format:
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
pub struct Claims {
pub sub: String, // User ID
pub email: Option<String>, // User email
pub iss: String, // Issuer (e.g., "https://api.oauth42.com")
pub aud: String, // Audience (your client_id)
pub exp: usize, // Expiration timestamp
pub iat: usize, // Issued at timestamp
pub azp: Option<String>, // Authorized party
pub scope: Option<String>, // OAuth2 scopes (space-separated)
}
impl Claims {
/// Check if token is expired
pub fn is_expired(&self) -> bool {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs() as usize;
self.exp < now
}
/// Check if token has required scope
pub fn has_scope(&self, required_scope: &str) -> bool {
self.scope
.as_ref()
.map(|s| s.split_whitespace().any(|scope| scope == required_scope))
.unwrap_or(false)
}
}Step 2: Fetch and Cache JWKS (Public Keys)
OAuth42 uses RS256 (RSA signatures). Fetch the public keys from the JWKS endpoint:
use jsonwebtoken::{decode, decode_header, Algorithm, DecodingKey, Validation};
use once_cell::sync::Lazy;
use std::collections::HashMap;
use std::sync::RwLock;
#[derive(Debug, Deserialize)]
struct Jwks {
keys: Vec<Jwk>,
}
#[derive(Debug, Deserialize, Clone)]
struct Jwk {
kid: String, // Key ID
n: String, // RSA modulus
e: String, // RSA exponent
alg: String, // Algorithm
kty: String, // Key type
}
// Global cache for JWKS keys (refresh every 24 hours in production)
static JWKS_CACHE: Lazy<RwLock<HashMap<String, DecodingKey>>> =
Lazy::new(|| RwLock::new(HashMap::new()));
async fn fetch_jwks(issuer: &str) -> Result<Jwks, Box<dyn std::error::Error>> {
let jwks_url = format!("{}/.well-known/jwks.json", issuer);
let response = reqwest::get(&jwks_url).await?;
let jwks: Jwks = response.json().await?;
Ok(jwks)
}
async fn get_decoding_key(
kid: &str,
issuer: &str,
) -> Result<DecodingKey, Box<dyn std::error::Error>> {
// Check cache first
{
let cache = JWKS_CACHE.read().unwrap();
if let Some(key) = cache.get(kid) {
return Ok(key.clone());
}
}
// Fetch fresh JWKS
let jwks = fetch_jwks(issuer).await?;
// Update cache with all keys
{
let mut cache = JWKS_CACHE.write().unwrap();
for key in jwks.keys {
let decoding_key = DecodingKey::from_rsa_components(&key.n, &key.e)?;
cache.insert(key.kid.clone(), decoding_key);
}
}
// Try again from cache
let cache = JWKS_CACHE.read().unwrap();
cache
.get(kid)
.cloned()
.ok_or_else(|| "Key ID not found in JWKS".into())
}Step 3: Validate JWT Tokens
Create a function to validate incoming JWTs:
pub async fn validate_jwt(
token: &str,
expected_audience: &str,
) -> Result<Claims, Box<dyn std::error::Error>> {
// Decode header to get key ID (kid)
let header = decode_header(token)?;
let kid = header.kid.ok_or("Missing kid in token header")?;
// Fetch decoding key (from cache or JWKS endpoint)
let issuer = "https://api.oauth42.com";
let decoding_key = get_decoding_key(&kid, issuer).await?;
// Set up validation rules
let mut validation = Validation::new(Algorithm::RS256);
validation.set_audience(&[expected_audience]);
validation.set_issuer(&[issuer]);
validation.validate_exp = true;
// Decode and validate token
let token_data = decode::<Claims>(token, &decoding_key, &validation)?;
Ok(token_data.claims)
}
// Extract bearer token from Authorization header
pub fn extract_bearer_token(auth_header: &str) -> Result<&str, Box<dyn std::error::Error>> {
if let Some(token) = auth_header.strip_prefix("Bearer ") {
Ok(token)
} else {
Err("Invalid Authorization header format".into())
}
}Step 4: Actix Web Middleware
Protect your Actix Web endpoints with JWT authentication middleware:
use actix_web::{
dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform},
Error, HttpMessage, HttpResponse,
};
use futures_util::future::LocalBoxFuture;
use std::future::{ready, Ready};
pub struct JwtAuth {
pub expected_audience: String,
}
impl<S, B> Transform<S, ServiceRequest> for JwtAuth
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
B: 'static,
{
type Response = ServiceResponse<B>;
type Error = Error;
type InitError = ();
type Transform = JwtAuthMiddleware<S>;
type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
ready(Ok(JwtAuthMiddleware {
service,
expected_audience: self.expected_audience.clone(),
}))
}
}
pub struct JwtAuthMiddleware<S> {
service: S,
expected_audience: String,
}
impl<S, B> Service<ServiceRequest> for JwtAuthMiddleware<S>
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
B: 'static,
{
type Response = ServiceResponse<B>;
type Error = Error;
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
forward_ready!(service);
fn call(&self, req: ServiceRequest) -> Self::Future {
let expected_audience = self.expected_audience.clone();
// Extract Authorization header
let auth_header = req
.headers()
.get("Authorization")
.and_then(|h| h.to_str().ok())
.map(|s| s.to_string());
let fut = self.service.call(req);
Box::pin(async move {
if let Some(auth) = auth_header {
match extract_bearer_token(&auth) {
Ok(token) => {
match validate_jwt(token, &expected_audience).await {
Ok(claims) => {
// Attach claims to request extensions
let (req, res) = fut.await?.into_parts();
req.extensions_mut().insert(claims);
return Ok(ServiceResponse::new(req.into_parts().0, res));
}
Err(e) => {
return Err(actix_web::error::ErrorUnauthorized(
format!("Invalid token: {}", e)
));
}
}
}
Err(_) => {
return Err(actix_web::error::ErrorUnauthorized("Invalid Authorization header"));
}
}
} else {
return Err(actix_web::error::ErrorUnauthorized("Missing Authorization header"));
}
})
}
}
// Usage in your Actix app:
use actix_web::{web, App, HttpRequest, HttpResponse, HttpServer};
async fn protected_handler(req: HttpRequest) -> Result<HttpResponse, Error> {
// Extract claims from request extensions
let claims = req
.extensions()
.get::<Claims>()
.ok_or_else(|| actix_web::error::ErrorInternalServerError("Claims not found"))?
.clone();
// Use claims to authorize the request
if !claims.has_scope("read") {
return Err(actix_web::error::ErrorForbidden("Insufficient permissions"));
}
Ok(HttpResponse::Ok().json(serde_json::json!({
"message": "Hello authenticated user!",
"user_id": claims.sub,
"email": claims.email,
})))
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
App::new()
.wrap(JwtAuth {
expected_audience: std::env::var("OAUTH42_CLIENT_ID").unwrap(),
})
.route("/api/protected", web::get().to(protected_handler))
})
.bind(("127.0.0.1", 8080))?
.run()
.await
}Step 5: Axum Middleware
For Axum users, here's how to implement JWT authentication:
use axum::{
extract::{Request, State},
http::{HeaderMap, StatusCode},
middleware::Next,
response::Response,
Json, Router,
};
use std::sync::Arc;
#[derive(Clone)]
pub struct AppState {
pub expected_audience: String,
}
// Middleware function
pub async fn jwt_auth_middleware(
State(state): State<Arc<AppState>>,
headers: HeaderMap,
mut request: Request,
next: Next,
) -> Result<Response, StatusCode> {
// Extract Authorization header
let auth_header = headers
.get("Authorization")
.and_then(|h| h.to_str().ok())
.ok_or(StatusCode::UNAUTHORIZED)?;
// Extract bearer token
let token = extract_bearer_token(auth_header)
.map_err(|_| StatusCode::UNAUTHORIZED)?;
// Validate JWT
let claims = validate_jwt(token, &state.expected_audience)
.await
.map_err(|_| StatusCode::UNAUTHORIZED)?;
// Insert claims into request extensions
request.extensions_mut().insert(claims);
Ok(next.run(request).await)
}
// Handler that uses claims
async fn protected_handler(
req: Request,
) -> Result<Json<serde_json::Value>, StatusCode> {
// Extract claims from extensions
let claims = req
.extensions()
.get::<Claims>()
.ok_or(StatusCode::INTERNAL_SERVER_ERROR)?;
// Check scope
if !claims.has_scope("read") {
return Err(StatusCode::FORBIDDEN);
}
Ok(Json(serde_json::json!({
"message": "Hello authenticated user!",
"user_id": claims.sub,
"email": claims.email,
})))
}
#[tokio::main]
async fn main() {
let state = Arc::new(AppState {
expected_audience: std::env::var("OAUTH42_CLIENT_ID").unwrap(),
});
let app = Router::new()
.route("/api/protected", axum::routing::get(protected_handler))
.layer(axum::middleware::from_fn_with_state(
state.clone(),
jwt_auth_middleware,
))
.with_state(state);
let listener = tokio::net::TcpListener::bind("127.0.0.1:8080")
.await
.unwrap();
axum::serve(listener, app).await.unwrap();
}Step 6: Token Introspection (Alternative)
For opaque tokens or additional validation, use OAuth42's introspection endpoint:
use serde::Deserialize;
#[derive(Debug, Deserialize)]
pub struct IntrospectionResponse {
pub active: bool,
pub scope: Option<String>,
pub client_id: Option<String>,
pub username: Option<String>,
pub exp: Option<usize>,
pub sub: Option<String>,
}
pub async fn introspect_token(
token: &str,
client_id: &str,
client_secret: &str,
) -> Result<IntrospectionResponse, Box<dyn std::error::Error>> {
let client = reqwest::Client::new();
let response = client
.post("https://api.oauth42.com/oauth2/introspect")
.basic_auth(client_id, Some(client_secret))
.form(&[("token", token)])
.send()
.await?;
let introspection: IntrospectionResponse = response.json().await?;
if !introspection.active {
return Err("Token is not active".into());
}
Ok(introspection)
}
// With caching to reduce API calls
use std::time::{Duration, Instant};
struct CachedIntrospection {
response: IntrospectionResponse,
cached_at: Instant,
}
static INTROSPECTION_CACHE: Lazy<RwLock<HashMap<String, CachedIntrospection>>> =
Lazy::new(|| RwLock::new(HashMap::new()));
pub async fn introspect_token_cached(
token: &str,
client_id: &str,
client_secret: &str,
) -> Result<IntrospectionResponse, Box<dyn std::error::Error>> {
// Check cache (5 minute TTL)
{
let cache = INTROSPECTION_CACHE.read().unwrap();
if let Some(cached) = cache.get(token) {
if cached.cached_at.elapsed() < Duration::from_secs(300) {
return Ok(cached.response.clone());
}
}
}
// Introspect and cache
let response = introspect_token(token, client_id, client_secret).await?;
{
let mut cache = INTROSPECTION_CACHE.write().unwrap();
cache.insert(
token.to_string(),
CachedIntrospection {
response: response.clone(),
cached_at: Instant::now(),
},
);
}
Ok(response)
}âš¡ Performance Best Practices
- Cache JWKS keys: Fetch public keys once and cache for 24 hours (refresh on 404/key rotation)
- Cache introspection results: If using introspection, cache for 5 minutes to reduce API calls
- Prefer JWT validation: Local validation is faster than introspection (no network call)
- Use background refresh: Refresh JWKS cache in background before expiration
- Connection pooling: Reuse HTTP clients with connection pools for introspection
🔒 Security Checklist
- ✅ Always validate token signature using JWKS public keys
- ✅ Check token expiration (exp claim)
- ✅ Validate issuer matches OAuth42 (iss claim)
- ✅ Validate audience matches your client_id (aud claim)
- ✅ Verify scopes for each protected endpoint
- ✅ Use HTTPS in production (prevent token interception)
- ✅ Never log full JWT tokens (contains sensitive data)
- ✅ Handle token refresh gracefully (401 → client refreshes)
Features
Type Safety
Full type safety with Result types and compile-time guarantees.
Async/Await
Built on Tokio for high-performance async operations.
Framework Support
Optional integrations for Actix Web, Axum, and Rocket.
Zero-Cost Abstractions
Minimal runtime overhead with compile-time optimizations.
Best Practices
Use Environment Variables
Store credentials securely using std::env or dotenvy crate.
Error Handling
Use Result types and the ? operator for idiomatic error handling.
Enable PKCE
Always use .with_pkce() for enhanced security.
TLS/SSL
Uses rustls for secure TLS connections by default.