@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
Maintainers
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:
/tokenendpoint supports all OAuth2 grant types:Authorization Code,Refresh Token,Client Credentials. The Device Code flow uses separate endpoints (/device/codeand/device/token) since it's a polling mechanism, but the main/tokenendpoint handles the three most common grants.
In-Memory Stores
clients:HashMap<String, Client>- registered clientscodes:HashMap<String, AuthorizationCode>- short-lived auth codestokens:HashMap<String, Token>- access tokens (JWTs)refresh_tokens:HashMap<String, Token>- refresh token mappingdevice_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 (
S256andplain) - 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_hashvalidation) - 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-serverShell 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 | shPowerShell 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-serverNPM
npm i -g @rustmcp/oauth2-test-server@latestThe 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-serverYou’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/revokeQuick 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;