Initial commit: axum OIDC hello application
This commit is contained in:
27
.env.example
Normal file
27
.env.example
Normal 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
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
/target/
|
||||
sessions.db
|
||||
.env
|
||||
3403
Cargo.lock
generated
Normal file
3403
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
13
Cargo.toml
Normal file
13
Cargo.toml
Normal 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
100
src/main.rs
Normal 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(())
|
||||
}
|
||||
Reference in New Issue
Block a user