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 oauth2_test_server::{AuthorizeParams, OAuthTestServer};

#[tokio::test]
async fn quick_start() {
    let server = OAuthTestServer::start().await;
    println!("server: {}", server.base_url());

    let client = server
        .register_client(serde_json::json!({
            "scope": "openid",
            "redirect_uris": ["http://localhost:8080/callback"],
            "client_name": "rust-mcp-sdk"
        }))
        .await;

    let token = server
        .generate_token(&client, server.jwt_options().user_id("rustmcp").build())
        .await;

    assert_eq!(token.access_token.split('.').count(), 3);
}

#[tokio::test]
async fn auth_code_flow_with_pkce() {
    let server = OAuthTestServer::start().await;

    let client = server
        .register_client(serde_json::json!({
            "scope": "openid profile email",
            "redirect_uris": ["http://localhost:8080/callback"],
            "client_name": "test-client"
        }))
        .await;

    let pkce = server.pkce_pair();

    let auth_url = server.authorize_url(
        &client,
        AuthorizeParams::new()
            .redirect_uri("http://localhost:8080/callback")
            .scope("openid profile")
            .nonce("test-nonce-123")
            .pkce(pkce.clone()),
    );

    let code = server.approve_consent(&auth_url, "test-user").await;

    let token_response = server.exchange_code(&client, &code, Some(&pkce)).await;

    assert!(token_response.get("access_token").is_some());
    assert!(token_response.get("id_token").is_some());
    assert!(token_response.get("refresh_token").is_some());
}

#[tokio::test]
async fn complete_auth_flow() {
    let server = OAuthTestServer::start().await;

    let client = server
        .register_client(serde_json::json!({
            "scope": "openid profile email",
            "redirect_uris": ["http://localhost:8080/callback"],
            "client_name": "test-client"
        }))
        .await;

    let token = server
        .complete_auth_flow(
            &client,
            AuthorizeParams::new()
                .redirect_uri("http://localhost:8080/callback")
                .scope("openid profile"),
            "test-user",
        )
        .await;

    assert!(token.get("access_token").is_some());
    assert!(token.get("id_token").is_some());
}

#[tokio::test]
async fn device_code_flow() {
    let server = OAuthTestServer::start().await;

    let client = server
        .register_client(serde_json::json!({
            "scope": "openid profile",
            "grant_types": ["urn:ietf:params:oauth:grant-type:device_code"],
            "client_name": "device-client"
        }))
        .await;

    let token = server
        .complete_device_flow(&client, "openid profile", "device-user")
        .await;

    assert!(token.get("access_token").is_some());
    assert!(token.get("refresh_token").is_some());
}

#[tokio::test]
async fn token_introspection_and_revoke() {
    let server = OAuthTestServer::start().await;

    let client = server
        .register_client(serde_json::json!({
            "scope": "openid",
            "redirect_uris": ["http://localhost:8080/callback"],
        }))
        .await;

    let pkce = server.pkce_pair();
    let auth_url = server.authorize_url(
        &client,
        AuthorizeParams::new()
            .redirect_uri("http://localhost:8080/callback")
            .scope("openid")
            .pkce(pkce.clone()),
    );
    let code = server.approve_consent(&auth_url, "test-user").await;
    let token = server.exchange_code(&client, &code, Some(&pkce)).await;

    let access_token = token["access_token"].as_str().unwrap();

    let introspection = server.introspect_token(&client, access_token).await;
    assert!(introspection["active"].as_bool().unwrap());

    server.revoke_token(&client, access_token).await;

    let introspection_after = server.introspect_token(&client, access_token).await;
    assert!(!introspection_after["active"].as_bool().unwrap());
}

#[tokio::test]
async fn custom_token() {
    let server = OAuthTestServer::start().await;

    let client = server
        .register_client(serde_json::json!({
            "scope": "openid profile email",
            "redirect_uris": ["http://localhost:8080/callback"]
        }))
        .await;

    let jwt = server.generate_jwt(
        &client,
        server
            .jwt_options()
            .user_id("custom-user")
            .scope("openid profile")
            .expires_in(7200)
            .build(),
    );

    assert!(jwt.split('.').count() == 3);
}

#[tokio::test]
async fn with_config() {
    use oauth2_test_server::IssuerConfig;

    let config = IssuerConfig {
        require_state: false,
        port: 0,
        ..Default::default()
    };

    let server = OAuthTestServer::start_with_config(config).await;

    let client = server
        .register_client(serde_json::json!({
            "scope": "openid",
            "redirect_uris": ["http://localhost:8080/callback"],
        }))
        .await;

    let token = server
        .generate_token(&client, server.jwt_options().user_id("testuser").build())
        .await;

    assert!(!token.access_token.is_empty());
}

#[tokio::test]
async fn test_accessor_methods() {
    let server = OAuthTestServer::start().await;

    assert_eq!(server.clients().await.len(), 0);
    assert_eq!(server.codes().await.len(), 0);
    assert_eq!(server.tokens().await.len(), 0);
    assert_eq!(server.refresh_tokens().await.len(), 0);

    let client = server
        .register_client(serde_json::json!({
            "scope": "openid",
            "redirect_uris": ["http://localhost:8080/callback"],
            "client_name": "test-client"
        }))
        .await;

    assert_eq!(server.clients().await.len(), 1);
    assert_eq!(server.clients().await[0].client_id, client.client_id);

    let _token = server
        .generate_token(&client, server.jwt_options().user_id("testuser").build())
        .await;

    assert_eq!(server.tokens().await.len(), 1);
    assert_eq!(server.refresh_tokens().await.len(), 1);
}

#[tokio::test]
async fn test_clear_methods() {
    let server = OAuthTestServer::start().await;

    let client = server
        .register_client(serde_json::json!({
            "scope": "openid",
            "redirect_uris": ["http://localhost:8080/callback"],
        }))
        .await;

    server
        .generate_token(&client, server.jwt_options().user_id("testuser").build())
        .await;

    assert_eq!(server.clients().await.len(), 1);
    assert_eq!(server.tokens().await.len(), 1);

    server.clear_clients().await;
    assert_eq!(server.clients().await.len(), 0);
    assert_eq!(server.tokens().await.len(), 1);

    server.clear_tokens().await;
    assert_eq!(server.tokens().await.len(), 0);
    assert_eq!(server.refresh_tokens().await.len(), 1);

    server.clear_refresh_tokens().await;
    assert_eq!(server.refresh_tokens().await.len(), 0);

    server.clear_all().await;
    assert_eq!(server.clients().await.len(), 0);
    assert_eq!(server.codes().await.len(), 0);
    assert_eq!(server.tokens().await.len(), 0);
    assert_eq!(server.refresh_tokens().await.len(), 0);
}