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(&params.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:

  1. Log in to the OAuth42 Portal
  2. Navigate to Service Accounts and click "Create Service Account"
  3. Enter a name and description for your service
  4. Select the scopes you need (e.g., admin for managing groups/users)
  5. Optionally set an expiration period for security
  6. 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 JWKS

Step 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.

Next Steps