use std::collections::HashMap;
use oauth_resource_server::testing;
use oauth_resource_server::{
Credential, KeyNaming, OAuthConfig, OAuthValidator, TokenRejection, authenticate,
};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
tracing_subscriber::fmt().init();
let fake_as = testing::spawn_http_server(HashMap::new(), None).await;
let issuer = fake_as.base.clone();
fake_as.routes.lock().unwrap().extend([
(
"/.well-known/openid-configuration".to_string(),
(
"200 OK",
serde_json::json!({ "issuer": issuer, "jwks_uri": format!("{issuer}/jwks") })
.to_string(),
),
),
("/jwks".to_string(), ("200 OK", testing::jwks_body())),
]);
let resolved = OAuthConfig {
enabled: true,
issuer: issuer.clone(),
audience: "example-api".into(),
resource: "https://api.example.com".into(),
required_scopes: vec!["api:read".into()],
scopes_supported: Some(vec!["api:read".into(), "api:write".into()]),
..OAuthConfig::default()
}
.resolve(KeyNaming::Dotted("oauth"))?
.expect("enabled: true");
let validator = OAuthValidator::new(&resolved)?;
let keys = validator.refresh_now().await?;
println!("loaded {keys} signing key(s)");
println!("metadata path: {}", validator.metadata_path());
println!("metadata document: {}", validator.metadata());
println!("401 challenge: {}", validator.invalid_token_challenge());
println!(
"403 challenge: {}",
validator.insufficient_scope_challenge()
);
let claims = |scope: &str, exp_offset: i64, aud: &str| {
serde_json::json!({
"iss": issuer,
"aud": aud,
"sub": "5c1d8e4a-0000-4000-8000-000000000002",
"exp": testing::now().saturating_add_signed(exp_offset),
"scope": scope,
})
};
let good = testing::mint(
testing::KEY_A_PEM,
testing::KID_A,
&claims("api:read api:write", 3600, "example-api"),
);
let cases = [
("valid token", good.clone()),
(
"missing the required scope",
testing::mint(
testing::KEY_A_PEM,
testing::KID_A,
&claims("profile", 3600, "example-api"),
),
),
(
"expired an hour ago",
testing::mint(
testing::KEY_A_PEM,
testing::KID_A,
&claims("api:read", -3600, "example-api"),
),
),
(
"for another audience",
testing::mint(
testing::KEY_A_PEM,
testing::KID_A,
&claims("api:read", 3600, "some-other-api"),
),
),
(
"signed by a key the server never published",
testing::mint(
testing::KEY_B_PEM,
testing::KID_A,
&claims("api:read", 3600, "example-api"),
),
),
("not a JWT at all", "an-opaque-token".to_string()),
];
println!();
for (what, token) in &cases {
match validator.validate(token).await {
Ok(accepted) => println!(
"{what}: accepted, subject {:?}, scopes {:?}",
accepted.subject, accepted.scopes
),
Err(TokenRejection::InsufficientScope) => {
println!("{what}: 403 insufficient_scope")
}
Err(TokenRejection::Invalid(reason)) => {
println!("{what}: 401, kind {}, reason: {reason}", reason.kind())
}
Err(other) => println!("{what}: 401 ({other:?})"),
}
}
println!();
let static_token = Some("example-static-key-change-me");
for (what, candidates) in [
("no candidates", vec![]),
(
"junk plus the static key",
vec!["junk", "example-static-key-change-me"],
),
(
"junk plus a valid access token",
vec!["junk", good.as_str()],
),
("junk only", vec!["junk"]),
] {
let outcome = authenticate(candidates, static_token, Some(&validator)).await;
let described = match outcome {
Ok(Credential::StaticToken) => "accepted: static token".to_string(),
Ok(Credential::OAuth(token)) => format!("accepted: OAuth, subject {:?}", token.subject),
Ok(other) => format!("accepted: {other:?}"),
Err(rejection) => format!("refused: {rejection:?}"),
};
println!("authenticate, {what}: {described}");
}
Ok(())
}