oauth2-test-server 0.2.2

A fast, fully configurable, in-memory OAuth 2.0 + OpenID Connect authorization server for testing, zero-HTTP mode and DCR support for testing auth flow in MCP Servers and MCP Clients
Documentation
use axum::{
    http::{self, header, HeaderValue},
    routing::{get, post},
    Router,
};
use tower_http::{
    cors::{AllowHeaders, AllowMethods, AllowOrigin, CorsLayer},
    trace::TraceLayer,
};

use crate::{
    config::IssuerConfig,
    handlers::{
        authorize::authorize,
        device::{device_code, device_token},
        discovery::{jwks, well_known_openid_configuration},
        error::error_page,
        introspect::introspect,
        register::{get_client, register_client},
        revoke::revoke,
        token::token_endpoint,
        userinfo::userinfo,
    },
    store::AppState,
};

pub fn build_router(state: AppState) -> Router {
    let cors = build_cors_layer(&state.config);
    Router::new()
        .route(
            "/.well-known/openid-configuration",
            get(well_known_openid_configuration),
        )
        .route("/.well-known/jwks.json", get(jwks))
        .route("/register", post(register_client))
        .route("/register/{client_id}", get(get_client))
        .route("/authorize", get(authorize))
        .route("/token", post(token_endpoint))
        .route("/device/code", post(device_code))
        .route("/device/token", post(device_token))
        .route("/introspect", post(introspect))
        .route("/revoke", post(revoke))
        .route("/userinfo", get(userinfo))
        .route("/error", get(error_page))
        .layer(TraceLayer::new_for_http())
        .with_state(state)
        .layer(cors)
}

fn build_cors_layer(config: &IssuerConfig) -> CorsLayer {
    let allowed_origins: Vec<HeaderValue> = config
        .allowed_origins
        .iter()
        .filter_map(|s| s.parse().ok())
        .collect();

    let allowed_methods =
        AllowMethods::list([http::Method::GET, http::Method::POST, http::Method::OPTIONS]);

    let allowed_headers = AllowHeaders::list([
        header::AUTHORIZATION,
        header::CONTENT_TYPE,
        header::ACCEPT,
        "x-requested-with"
            .parse()
            .expect("Static header name should be valid"),
    ]);

    let mut cors = CorsLayer::new()
        .allow_methods(allowed_methods)
        .allow_headers(allowed_headers)
        .max_age(std::time::Duration::from_secs(86400));

    if allowed_origins.is_empty() {
        cors = cors.allow_origin(AllowOrigin::any());
    } else {
        cors = cors
            .allow_origin(AllowOrigin::list(allowed_origins))
            .allow_credentials(true);
    }

    cors
}