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
-
Shell script
| -
PowerShell script
-
Homebrew
-
NPM
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
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
Configuration
The server is configured via IssuerConfig.
Default User
The server uses a hardcoded user identity for all authorization requests. By default this is "test-user-123".
Set it programmatically:
use IssuerConfig;
let config = IssuerConfig ;
let server = start_with_config.await;
Or, when using the library in your own tests, load from environment variables (OAUTH_*) or a YAML/TOML file:
// From environment variables (requires "config" feature)
let config = from_env?;
// From a YAML or TOML file (requires "config" feature)
let config = from_file?;
Note: The standalone binary (
oauth2-test-server) does not currently accept CLI flags or config files. Use the library API for custom configuration.
A complete sample config file with all options and their defaults can be found at config.sample.yaml.
All Configuration Options
| Field | Env Var | Default | Description |
|---|---|---|---|
scheme |
OAUTH_SCHEME |
http |
URL scheme |
host |
OAUTH_HOST |
localhost |
Bind host |
port |
OAUTH_PORT |
8090 |
Listen port (0 = random) |
default_user_id |
OAUTH_DEFAULT_USER_ID |
test-user-123 |
Default sub claim when no user is logged in |
require_state |
OAUTH_REQUIRE_STATE |
true |
Require state param in auth requests |
generate_client_secret_for_dcr |
OAUTH_GENERATE_CLIENT_SECRET_FOR_DCR |
true |
Auto-generate client secret on DCR |
access_token_expires_in |
OAUTH_ACCESS_TOKEN_EXPIRES_IN |
3600 |
Access token TTL (seconds) |
refresh_token_expires_in |
OAUTH_REFRESH_TOKEN_EXPIRES_IN |
2592000 |
Refresh token TTL (seconds, 30 days) |
authorization_code_expires_in |
OAUTH_AUTHORIZATION_CODE_EXPIRES_IN |
600 |
Auth code TTL (seconds, 10 min) |
cleanup_interval_secs |
OAUTH_CLEANUP_INTERVAL_SECS |
300 |
Expired entry cleanup interval (0 = disable) |
allowed_origins |
OAUTH_ALLOWED_ORIGINS |
[] |
CORS origins (empty = allow all) |
Loading Order
- Programmatic
IssuerConfig(highest priority) - Environment variables (
OAUTH_*) - YAML/TOML config file (detected by extension:
.yaml,.yml,.toml) - Built-in defaults (lowest priority)
The from_env and from_file methods are available when the config feature is enabled (included by default).
How to Use in Tests
Quick Start
async
Authorization Code Flow with PKCE
async
Complete Auth Flow (One-Liner)
async
Device Code Flow
async
Token Introspection & Revocation
async
Custom Token Generation (for unit tests)
async
Loading from Config File
async
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;
let token = server.generate_token;
// After (0.2.x)
let client = server.register_client.await;
let token = server.generate_token.await;
2. Accessor methods return Vec instead of Arc<RwLock<HashMap>>:
// Before (0.1.x)
assert_eq!;
// After (0.2.x)
assert_eq!;
// Or for mutation (reset state between tests):
server.clear_all.await;