use std::sync::Arc;
use oauth_as::client::{Client, ClientAuth, ClientId};
use oauth_as::grant::GrantType;
use oauth_as::http::{ApprovalDecision, ServiceBuilder};
use oauth_as::jwt::{AccessTokenFormat, EcdsaP256Key, JwtConfig};
use oauth_as::scope::ScopeSet;
use oauth_as::server::{AuthorizationServer, ServerConfig};
use oauth_as::store::MemoryStorage;
const PUBLIC_CLIENT_ID: &str = "conformance-public";
const PUBLIC_REDIRECT_URI: &str = "http://127.0.0.1:8917/cb";
const CONFIDENTIAL_CLIENT_ID: &str = "conformance-confidential";
const CONFIDENTIAL_CLIENT_SECRET: &str = "conformance-secret-0123456789abcdef";
const SEEDED_SUBJECT: &str = "conformance-user";
const SEEDED_AUDIENCE: &str = "https://rs.conformance.example";
const SEEDED_SIGNING_SCALAR: [u8; 32] = [
0x8e, 0x9b, 0x10, 0x9e, 0x71, 0x90, 0x98, 0xbf, 0x98, 0x04, 0x87, 0xdf, 0x1f, 0x5d, 0x77, 0xe9,
0xcb, 0x29, 0x60, 0x6e, 0xbe, 0xd2, 0x26, 0x3b, 0x5f, 0x57, 0xc2, 0x13, 0xdf, 0x84, 0xf4, 0xb2,
];
const SEEDED_KID: &str = "conformance-es256-1";
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let addr = std::env::var("OAUTH_AS_ADDR").unwrap_or_else(|_| "127.0.0.1:8914".to_string());
let issuer = std::env::var("OAUTH_AS_ISSUER").unwrap_or_else(|_| format!("http://{addr}"));
let seed = match (
std::env::var("OAUTH_AS_CONFORMANCE_SEED").as_deref() == Ok("1"),
issuer.starts_with("http://127.0.0.1")
|| issuer.starts_with("http://[::1]")
|| issuer.starts_with("http://localhost"),
) {
(true, true) => true,
(true, false) => {
return Err(
"OAUTH_AS_CONFORMANCE_SEED refuses to arm for a non-loopback issuer: it \
turns on a published signing key, a hard-coded client secret, \
auto-approval and the device form's dangerous verification override"
.into(),
)
}
(false, _) => false,
};
let verification_uri = format!("{}/device", issuer.trim_end_matches('/'));
let mut config = ServerConfig::new(issuer.clone(), verification_uri);
config.scopes_supported = Some(vec!["read".to_string(), "write".to_string()]);
if seed {
let key = EcdsaP256Key::from_scalar_bytes(SEEDED_KID, &SEEDED_SIGNING_SCALAR)?;
let jwks_uri = format!("{}/jwks", issuer.trim_end_matches('/'));
config.access_token_format = AccessTokenFormat::Jwt(Box::new(
JwtConfig::new(key, SEEDED_AUDIENCE).with_jwks_uri(jwks_uri),
));
}
let server = Arc::new(AuthorizationServer::new(config, MemoryStorage::new()));
if seed {
seed_fixtures(&server).await?;
}
let mut builder = ServiceBuilder::new(Arc::clone(&server));
if seed {
builder = builder
.with_subject_resolver(|_headers| Some(SEEDED_SUBJECT.to_string()))
.with_approval_resolver(|_request| ApprovalDecision::Approve)
.dangerously_disable_verification_protections();
}
let router = axum::Router::from(builder.build()?);
let listener = tokio::net::TcpListener::bind(&addr).await?;
println!("oauth-as example listening on {addr} (issuer {issuer}, seeded: {seed})");
axum::serve(listener, router).await?;
Ok(())
}
async fn seed_fixtures<S>(server: &AuthorizationServer<S>) -> Result<(), Box<dyn std::error::Error>>
where
S: oauth_as::store::Storage,
{
let scopes = ScopeSet::from_tokens(["read", "write"])?;
server
.register_client(Client {
client_id: ClientId::new(PUBLIC_CLIENT_ID),
auth: ClientAuth::Public,
grant_types: vec![
GrantType::AuthorizationCode,
GrantType::DeviceCode,
GrantType::RefreshToken,
],
redirect_uris: vec![PUBLIC_REDIRECT_URI.to_string()],
allowed_scopes: scopes.clone(),
default_scopes: scopes.clone(),
name: Some("Conformance public client".to_string()),
registration: None,
})
.await?;
server
.register_client(Client {
client_id: ClientId::new(CONFIDENTIAL_CLIENT_ID),
auth: ClientAuth::ConfidentialSecret {
secret: CONFIDENTIAL_CLIENT_SECRET.to_string(),
},
grant_types: vec![
GrantType::AuthorizationCode,
GrantType::ClientCredentials,
GrantType::DeviceCode,
GrantType::RefreshToken,
],
redirect_uris: vec![PUBLIC_REDIRECT_URI.to_string()],
allowed_scopes: scopes.clone(),
default_scopes: scopes,
name: Some("Conformance confidential client".to_string()),
registration: None,
})
.await?;
Ok(())
}