Initial commit: axum OIDC hello application

This commit is contained in:
Opencode Agent
2026-05-01 09:20:22 +00:00
commit 8dae38712f
5 changed files with 3546 additions and 0 deletions

27
.env.example Normal file
View File

@@ -0,0 +1,27 @@
# Base URL of the OIDC provider (e.g. Keycloak realm URL)
OIDC_PROVIDER_URL=http://localhost:8080
# OAuth2 client credentials (required)
OIDC_CLIENT_ID=your-client-id
OIDC_CLIENT_SECRET=your-client-secret
# Full callback URL — must match the redirect URI configured at the provider
OIDC_REDIRECT_URI=http://localhost:3000/auth/callback
# Secret key for encrypting session cookies (at least 32 bytes)
OIDC_COOKIE_KEY=change-me-to-a-random-64-char-string
# Maximum session age in minutes
OIDC_SESSION_MAX_AGE=3600
# Space-separated OAuth2 scopes to request
OIDC_SCOPES=openid profile
# URL to redirect to after logout
OIDC_POST_LOGOUT_REDIRECT_URI=/
# Path to the SQLite database file for session storage
OIDC_SQLITE_PATH=sessions.db
# Base path for auth routes (default: /auth)
OIDC_AUTH_BASE_PATH=/auth

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
/target/
sessions.db
.env

3403
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

13
Cargo.toml Normal file
View File

@@ -0,0 +1,13 @@
[package]
name = "axum-oidc-hello"
version = "0.1.0"
edition = "2021"
[dependencies]
axum = "0.8"
axum-oidc-client = { version = "0.5", features = ["sql-cache-sqlite"] }
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
base64 = "0.22"
anyhow = "1"

100
src/main.rs Normal file
View File

@@ -0,0 +1,100 @@
use std::env;
use std::sync::Arc;
use axum::{http::StatusCode, routing::get, Json, Router};
use axum_oidc_client::authentication::AuthenticationLayer;
use axum_oidc_client::authentication::builder::OAuthConfigurationBuilder;
use axum_oidc_client::authentication::logout::handle_default_logout::DefaultLogoutHandler;
use axum_oidc_client::auth_cache::AuthCache;
use axum_oidc_client::auth_session::AuthSession;
use axum_oidc_client::cache::{TwoTierAuthCache, config::TwoTierCacheConfig};
use axum_oidc_client::sql_cache::{SqlAuthCache, SqlCacheConfig};
use axum_oidc_client::authentication::CodeChallengeMethod;
use base64::Engine;
use serde::Serialize;
#[derive(Serialize)]
struct HelloResponse {
preferred_username: String,
}
fn env_or(key: &str, default: &str) -> String {
env::var(key).unwrap_or_else(|_| default.to_string())
}
fn env_required(key: &str) -> String {
env::var(key).unwrap_or_else(|_| panic!("Environment variable {key} is required"))
}
async fn hello(session: AuthSession) -> Result<Json<HelloResponse>, StatusCode> {
let id_token = &session.id_token;
let payload = id_token.split('.').nth(1).ok_or(StatusCode::UNAUTHORIZED)?;
let payload_json = base64::engine::general_purpose::URL_SAFE_NO_PAD
.decode(payload)
.map_err(|_| StatusCode::UNAUTHORIZED)?;
let claims: serde_json::Value =
serde_json::from_slice(&payload_json).map_err(|_| StatusCode::UNAUTHORIZED)?;
let preferred_username = claims["preferred_username"]
.as_str()
.map(String::from)
.ok_or(StatusCode::UNAUTHORIZED)?;
Ok(Json(HelloResponse { preferred_username }))
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let provider_url = env_required("OIDC_PROVIDER_URL");
let client_id = env_required("OIDC_CLIENT_ID");
let client_secret = env_required("OIDC_CLIENT_SECRET");
let redirect_uri = env_or("OIDC_REDIRECT_URI", "http://localhost:3000/auth/callback");
let cookie_key = env_required("OIDC_COOKIE_KEY");
let session_max_age: i64 = env_or("OIDC_SESSION_MAX_AGE", "3600").parse().unwrap_or(3600);
let scopes_default = env_or("OIDC_SCOPES", "openid profile");
let scopes: Vec<&str> = scopes_default.split_whitespace().collect();
let post_logout_redirect = env_or("OIDC_POST_LOGOUT_REDIRECT_URI", "/");
let sqlite_path = env_or("OIDC_SQLITE_PATH", "sessions.db");
let auth_base_path = env_or("OIDC_AUTH_BASE_PATH", "/auth");
let config = OAuthConfigurationBuilder::default()
.with_issuer(&provider_url)
.await?
.with_client_id(&client_id)
.with_client_secret(&client_secret)
.with_redirect_uri(&redirect_uri)
.with_private_cookie_key(&cookie_key)
.with_scopes(scopes)
.with_code_challenge_method(CodeChallengeMethod::S256)
.with_session_max_age(session_max_age)
.with_post_logout_redirect_uri(&post_logout_redirect)
.with_base_path(&auth_base_path)
.build()?;
let sql_cache = SqlAuthCache::new(SqlCacheConfig {
connection_string: format!("sqlite://{sqlite_path}"),
..Default::default()
})
.await?;
sql_cache.init_schema().await?;
let cache: Arc<dyn AuthCache + Send + Sync> = Arc::new(TwoTierAuthCache::new(
Some(Arc::new(sql_cache)),
TwoTierCacheConfig::default(),
)?);
let logout_handler = Arc::new(DefaultLogoutHandler);
let app = Router::new()
.route("/hello", get(hello))
.layer(AuthenticationLayer::new(
Arc::new(config),
cache,
logout_handler,
));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
println!("Listening on http://0.0.0.0:3000");
axum::serve(listener, app).await?;
Ok(())
}