use std::collections::HashMap;
use std::sync::Arc;
use axum::Router;
use axum::body::Body;
use axum::http::{Request, header};
use axum::routing::get;
use oauth_resource_server::axum::{AuthLayer, metadata_router};
use oauth_resource_server::testing;
use oauth_resource_server::{
Credential, KeyNaming, OAuthConfig, OAuthValidator, static_token_policy,
};
use tower::ServiceExt;
use tracing::Level;
use tracing_subscriber::filter::Targets;
use tracing_subscriber::prelude::*;
const ISSUER: &str = "https://auth.example.com/";
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
tracing_subscriber::registry()
.with(tracing_subscriber::fmt::layer())
.with(
Targets::new()
.with_target("oauth_resource_server", Level::DEBUG)
.with_default(Level::INFO),
)
.init();
let fake_as = testing::spawn_http_server(
HashMap::from([("/jwks".to_string(), ("200 OK", testing::jwks_body()))]),
None,
)
.await;
let yaml = format!(
r#"
enabled: true
issuer: "{ISSUER}"
jwks_uri: "{jwks}"
audience: "example-api"
resource: "https://api.example.com/v1"
required_scope: "api:read"
scopes_supported: ["api:read", "api:write"]
"#,
jwks = fake_as.url,
);
let config: OAuthConfig = serde_yaml_ng::from_str(&yaml)?;
let resolved = config
.resolve(KeyNaming::Dotted("oauth"))?
.expect("enabled: true");
let oauth = Arc::new(OAuthValidator::new(&resolved)?);
oauth.spawn_background_refresh();
oauth.refresh_now().await?;
let decision = static_token_policy(None, Some(&resolved), false)?;
let auth = AuthLayer::from_decision(decision, Some(Arc::clone(&oauth)))?;
let app = Router::new()
.route("/v1/whoami", get(whoami))
.route_layer(auth)
.merge(metadata_router(Some(oauth)));
let reader = mint(&["api:read"]);
let no_scope = mint(&["profile"]);
for (what, uri, token) in [
("no token", "/v1/whoami", None),
("token without api:read", "/v1/whoami", Some(&no_scope)),
("token with api:read", "/v1/whoami", Some(&reader)),
(
"garbage token",
"/v1/whoami",
Some(&"not-a-jwt".to_string()),
),
(
"metadata (no token needed)",
"/.well-known/oauth-protected-resource/v1",
None,
),
] {
let mut request = Request::builder().uri(uri);
if let Some(token) = token {
request = request.header(header::AUTHORIZATION, format!("Bearer {token}"));
}
let response = app.clone().oneshot(request.body(Body::empty())?).await?;
println!("\n== {what}: GET {uri}");
println!(" status: {}", response.status());
if let Some(challenge) = response.headers().get(header::WWW_AUTHENTICATE) {
println!(" www-authenticate: {}", challenge.to_str()?);
}
let body = axum::body::to_bytes(response.into_body(), 64 * 1024).await?;
if !body.is_empty() {
println!(" body: {}", String::from_utf8_lossy(&body));
}
}
Ok(())
}
async fn whoami(credential: Credential) -> String {
match credential {
Credential::OAuth(token) => format!(
"hello {} (scopes: {})",
token.principal.as_deref().unwrap_or("(unnamed)"),
token.scopes.join(" ")
),
Credential::StaticToken => "hello, static-token caller".to_string(),
_ => "hello".to_string(),
}
}
fn mint(scopes: &[&str]) -> String {
testing::mint(
testing::KEY_A_PEM,
testing::KID_A,
&serde_json::json!({
"iss": ISSUER,
"aud": "example-api",
"sub": "0b9c7a52-0000-4000-8000-000000000003",
"preferred_username": "alice",
"exp": testing::now() + 3600,
"scope": scopes.join(" "),
}),
)
}