npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@rustmcp/oauth2-test-server

v0.2.2

Published

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

Downloads

203

Readme

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.

This server was developed with the purpose of supporting testing and development of the rust-mcp-sdk, but it works perfectly as a general-purpose auth mocking in any Rust (or non-Rust) project , unit tests, integration suites, local dev, or quick prototypes.
Refer to Key Features to find out more.

⚠️ For testing/development only

  • In-memory storage (not persistent)
  • No rate limiting or attack protection
  • Use in test suites, CI/CD, local development

Purpose

This server implements all major OAuth 2.0 flows and OpenID Connect core features in-memory, making it ideal for:

  • Testing OAuth clients (web, mobile, SPA, backend)
  • Specifically tailored for testing authentication flow MCP Servers and Clients, with DCR support
  • End-to-end flow validation
  • Local development
  • Integration testing of authorization flows
  • Local development against a real OAuth provider
  • Demonstrating OAuth concepts
  • CI/CD pipeline validation

Supported Standards

| Standard | Implemented | |--------|-------------| | RFC 6749 – OAuth 2.0 | Full | | RFC 6750 – Bearer Token | Yes | | RFC 7636 – PKCE | Yes (plain, S256) | | RFC 7591 – Dynamic Client Registration | Yes | | RFC 7662 – Token Introspection | Yes | | RFC 7009 – Token Revocation | Yes | | RFC 7519 – JWT Access Tokens (RS256) | Yes | | RFC 8628 – Device Code Flow | Yes | | OpenID Connect Discovery 1.0 | Yes | | OpenID Connect Core 1.0 | Yes (ID Tokens, UserInfo, Claims) |

Key Features

  • Dynamic Client Registration (DCR) (POST /register) with full metadata support
  • Authorization Code Flow with PKCE (/authorize, /token)
  • Refresh Token Flow with rotation and revocation
  • Client Credentials Grant
  • Device Code Flow (RFC 8628) (/device/code, /device/token)
  • JWT Access Tokens signed with RS256 (auto-generated RSA key pair)
  • ID Tokens with at_hash, c_hash, nonce, standard claims
  • Token Introspection (POST /introspect) with expiration checking
  • Token Revocation (POST /revoke)
  • OpenID Connect Discovery (.well-known/openid-configuration)
  • JWKS Endpoint (.well-known/jwks.json)
  • UserInfo Endpoint (GET /userinfo)
  • In-memory stores (clients, codes, tokens, device codes) - no external DB required
  • Background TTL cleanup for expired tokens/codes
  • Full error handling with redirect errors and JSON error responses
  • State parameter (required by default, configurable)
  • Scope, redirect_uri validation
  • Authorization parameters: prompt, max_age, claims, ui_locales, response_mode
  • Configurable via YAML/TOML files or environment variables

Endpoints

| Method | Path | Description | |-------|------|-------------| | GET | /.well-known/openid-configuration | OIDC Discovery | | GET | /.well-known/jwks.json | Public keys for JWT validation | | POST | /register | Dynamic client registration | | GET | /register/:client_id | Retrieve registered client | | GET | /authorize | Authorization endpoint (code flow) | | POST | /token | Token endpoint (all grants) | | POST | /device/code | Device code flow - initiate | | POST | /device/token | Device code flow - poll token | | POST | /introspect | RFC 7662 introspection | | POST | /revoke | RFC 7009 revocation | | GET | /userinfo | OIDC user info (requires Bearer token) | | GET | /error | Human-readable error page |

Note: /token endpoint supports all OAuth2 grant types: Authorization Code, Refresh Token, Client Credentials. The Device Code flow uses separate endpoints (/device/code and /device/token) since it's a polling mechanism, but the main /token endpoint handles the three most common grants.

In-Memory Stores

  • clients: HashMap<String, Client> - registered clients
  • codes: HashMap<String, AuthorizationCode> - short-lived auth codes
  • tokens: HashMap<String, Token> - access tokens (JWTs)
  • refresh_tokens: HashMap<String, Token> - refresh token mapping
  • device_codes: HashMap<String, DeviceAuthorization> - device code flow state

Security & Testing

  • No persistence - perfect for isolated tests
  • Auto-generated RSA key pair on startup
  • PKCE verification (S256 and plain)
  • Token revocation propagation
  • Expiration enforcement (configurable TTL)
  • Background cleanup of expired tokens/codes
  • Scope and redirect_uri validation
  • State parameter (required by default, configurable)
  • ID Token security (at_hash, c_hash validation)
  • Configurable token expiration times

Run as a Standalone Binary (Great for Manual Testing & Debugging)

You can run the server directly from your terminal - no code required.

1. Install it globally using one of the following methods:

  • Cargo

    cargo install oauth2-test-server
  • Shell script

    curl --proto '=https' --tlsv1.2 -LsSf https://github.com/rust-mcp-stack/oauth2-test-server/releases/download/v0.2.2/oauth2-test-server-installer.sh | sh
  • PowerShell script

    powershell -ExecutionPolicy Bypass -c "irm https://github.com/rust-mcp-stack/oauth2-test-server/releases/download/v0.2.2/oauth2-test-server-installer.ps1 | iex"
  • Homebrew

    brew install rust-mcp-stack/tap/oauth2-test-server
  • NPM

    npm i -g @rustmcp/oauth2-test-server@latest

    The npm package is provided for convenience. It runs the same underlying Rust binary but can be installed and used as a standard npm package.

  • Download Binaries

2. Start the server

oauth2-test-server

You’ll see:

OAuth Test Server running on http://127.0.0.1:8090/
 • Discovery: http://127.0.0.1:8090/.well-known/openid-configuration
 • Jwks: http://127.0.0.1:8090/.well-known/jwks.json
 • Authorize: http://127.0.0.1:8090/authorize
 • Token: http://127.0.0.1:8090/token
 • Device Code: http://127.0.0.1:8090/device/code
 • Device Token: http://127.0.0.1:8090/device/token
 • Register: http://127.0.0.1:8090/register
 • Introspection: http://127.0.0.1:8090/introspect
 • UserInfo: http://127.0.0.1:8090/userinfo
 • Revoke: http://127.0.0.1:8090/revoke

Quick test with curl:

# Register a client
curl -X POST http://localhost:8090/register -H "Content-Type: application/json" -d '{
  "redirect_uris": ["http://localhost:8090/callback"],
  "grant_types": ["authorization_code"],
  "response_types": ["code"],
  "scope": "openid profile email"
}'

How to Use in Tests

Quick Start

#[tokio::test]
async fn quick_start() {
    let server = oauth2_test_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;
    
    // Generate a token directly
    let token = server.generate_token(&client, 
        server.jwt_options().user_id("rustmcp").build()
    ).await;
    
    assert_eq!(token.access_token.split('.').count(), 3);
}

Authorization Code Flow with PKCE

#[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()); // ID token with openid scope
    assert!(token_response.get("refresh_token").is_some());
}

Complete Auth Flow (One-Liner)

#[tokio::test]
async fn complete_auth_flow() {
    let server = oauth2_test_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;
    
    // Complete flow in one call
    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());
}

Device Code Flow

#[tokio::test]
async fn device_code_flow() {
    let server = oauth2_test_server::OAuthTestServer::start().await;
    
    // Register client with device_code grant
    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;
    
    // Complete device flow in one call
    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());
}

Token Introspection & Revocation

#[tokio::test]
async fn token_introspection_and_revoke() {
    let server = oauth2_test_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();
    
    // Introspect token
    let introspection = server.introspect_token(&client, access_token).await;
    assert!(introspection["active"].as_bool().unwrap());
    
    // Revoke token
    server.revoke_token(&client, access_token).await;
    
    // Verify revoked
    let introspection_after = server.introspect_token(&client, access_token).await;
    assert!(!introspection_after["active"].as_bool().unwrap());
}

Custom Token Generation (for unit tests)

#[tokio::test]
async fn custom_token() {
    let server = oauth2_test_server::OAuthTestServer::start().await;
    
    let client = server.register_client(serde_json::json!({
        "scope": "openid profile email",
        "redirect_uris": ["http://localhost:8080/callback"]
    })).await;
    
    // Generate JWT directly with custom claims
    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);
}

Loading from Config File

#[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());
}

Migration Guide

Upgrading from 0.1.x to 0.2.x

1. Methods are now async - Add .await:

// Before (0.1.x)
let client = server.register_client(json!({...}));
let token = server.generate_token(&client, options);

// After (0.2.x)
let client = server.register_client(json!({...})).await;
let token = server.generate_token(&client, options).await;

2. Accessor methods return Vec instead of Arc<RwLock<HashMap>>:

// Before (0.1.x)
assert_eq!(server.clients().read().iter().len(), 1);

// After (0.2.x)
assert_eq!(server.clients().await.len(), 1);

// Or for mutation (reset state between tests):
server.clear_all().await;