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