Mock an OIDC / OAuth2 Provider
Mock an OIDC / OAuth2 provider in one call
A single request to PUT /mockserver/oidc generates a complete mock OpenID Connect / OAuth2
identity provider. MockServer creates expectations for every standard endpoint and signs all tokens with a
key pair whose public key is published at the JWKS endpoint, so the tokens your application receives
verify end-to-end against the discovery document — no real identity provider required.
This is ideal for:
- Integration tests for OAuth2/OIDC-secured applications and resource servers
- Offline development against a realistic IdP with no network dependency
- Edge-case testing — expired tokens, wrong issuer, tampered signatures
Generated endpoints
A single call generates the following expectations (paths are configurable):
| Endpoint | Default path | Purpose |
|---|---|---|
| Discovery | /.well-known/openid-configuration | OIDC discovery document |
| JWKS | /.well-known/jwks.json | Public signing key(s) |
| Token | /token | Issues tokens for all supported grants |
| Authorize | /authorize | Authorization-code grant (issues a code, supports PKCE + nonce) |
| Userinfo | /userinfo | Returns the subject and additional claims |
| Introspection | /introspect | RFC 7662 token introspection |
| Revocation | /revoke | RFC 7009 token revocation |
| End-session (logout) | /logout | RP-initiated logout (302 to post_logout_redirect_uri or 200) |
| Device authorization | /device_authorization | RFC 8628 device-code grant (issues a device_code + user_code) |
Create it with the REST API
Use defaults (issuer http://localhost:1080, RS256 signing) with an empty body:
curl -v -X PUT "http://localhost:1080/mockserver/oidc" -d '{}'
Or supply configuration:
curl -v -X PUT "http://localhost:1080/mockserver/oidc" -d '{
"issuer": "http://localhost:1080",
"clientId": "my-app",
"audience": "my-api",
"scopes": [ "openid", "profile", "email" ],
"signingAlgorithm": "RS256",
"tokenExpirySeconds": 3600,
"additionalClaims": {
"name": "Test User",
"email": "user@example.com",
"email_verified": true
}
}'
Create it with the Java client
The Java client exposes a typed one-call method:
// defaults
new MockServerClient("localhost", 1080).mockOpenIdProvider();
// or with configuration
new MockServerClient("localhost", 1080).mockOpenIdProvider(
new OidcProviderConfiguration()
.setIssuer("http://localhost:1080")
.setClientId("my-app")
.setAudience("my-api")
.setSigningAlgorithm("RS256")
);
Token shapes
Tokens are minted at request time, so the nonce from an /authorize request is
echoed into the id_token. The two tokens are kept distinct, per the OIDC core spec:
id_token—iss,sub,aud = clientId,exp,iat,nbf,nonce(when supplied),at_hash, profile/email claims for the requested scopes, plus anyadditionalClaims. Issued only when theopenidscope is requested.access_token—iss,sub,aud = audience,exp,iat,nbf,scope,client_id, plus anyadditionalClaims.
The token endpoint returns a refresh_token for the authorization_code,
refresh_token, and device-code grants. The discovery document advertises the implemented grants
(authorization_code, client_credentials, refresh_token,
urn:ietf:params:oauth:grant-type:device_code), the PKCE methods (S256,
plain), the token-endpoint auth methods, the end_session_endpoint, and the
device_authorization_endpoint.
Device authorization grant (RFC 8628)
The device flow lets input-constrained devices (TVs, CLIs) obtain tokens. Start it at the
/device_authorization endpoint, then poll /token:
# 1. start the device flow
curl -s -X POST "http://localhost:1080/device_authorization" -d 'scope=openid profile'
# => { "device_code": "...", "user_code": "BCDF-GHJK",
# "verification_uri": "http://localhost:1080/device",
# "verification_uri_complete": "http://localhost:1080/device?user_code=BCDF-GHJK",
# "expires_in": 300, "interval": 5 }
# 2. poll the token endpoint with the device_code
curl -s -X POST "http://localhost:1080/token" \
-d 'grant_type=urn:ietf:params:oauth:grant-type:device_code&device_code=...'
# => {"error":"authorization_pending"} (HTTP 400) for the first deviceCodePendingPolls polls
# => { "access_token": "...", "id_token": "...", "refresh_token": "..." } once approved
The mock has no device login screen, so approval is simulated by the deviceCodePendingPolls
setting: the first N polls return RFC 8628 authorization_pending (HTTP 400), then tokens are
minted. The default 0 approves immediately on the first poll. A device code is single-use once
approved and expires after expires_in seconds; an unknown, already-redeemed, or expired
device_code returns expired_token.
Token-endpoint client authentication
By default the mock /token endpoint accepts any caller (so tests stay simple). Set
enforceClientAuthentication to true to require the client to authenticate with the
configured clientId / clientSecret, using either standard method:
client_secret_basic—Authorization: Basic base64(clientId:clientSecret)client_secret_post—client_idandclient_secretform parameters
Missing or wrong credentials return RFC 6749 §5.2 {"error":"invalid_client"} with HTTP
401 and a WWW-Authenticate: Basic header. With the flag off (the default), no client
authentication is performed.
Opaque access tokens
Many real IdPs issue opaque access tokens — random reference strings that are not JWTs — whose only
validation path is the introspection endpoint. Set opaqueAccessToken to true to
mock this: the access_token becomes a random opaque string while the id_token
stays a signed JWT that still verifies against the JWKS.
Validate an opaque token by introspecting it (RFC 7662): a known, unexpired token returns
{"active":true, ...claims} and an unknown or expired token returns {"active":false}.
curl -s -X POST "http://localhost:1080/introspect" -d 'token=<opaque-access-token>'
# => { "active": true, "sub": "mock-user", "scope": "openid profile email", ... }
Configuration
| Field | Default | Description |
|---|---|---|
issuer | http://localhost:1080 | Issuer URL; used to build every endpoint URL in the discovery document |
jwksPath / tokenPath / authorizePath / userinfoPath / introspectPath / revokePath / deviceAuthorizationPath | standard paths | Override individual endpoint paths |
subject | mock-user | sub claim in tokens and userinfo |
clientId | mock-client | Audience of the id_token and client_id in the access_token |
clientSecret | mock-client-secret | Expected client secret when enforceClientAuthentication is on |
audience | mock-audience | Audience of the access_token |
scopes | ["openid","profile","email"] | Default scopes; an id_token is issued only when openid is present |
tokenExpirySeconds | 3600 | Token lifetime (expires_in / exp) |
additionalClaims | {} | Extra claims merged into the issued tokens and userinfo |
signingAlgorithm | RS256 | RS256, RS384, RS512, ES256, ES384, ES512 |
privateKeyPem + certificatePem | generated | Supply your own signing key (PEM). The public key is published at the JWKS endpoint |
jwkJson | generated | Alternatively, supply the signing key as a private JWK |
keyId | random | Stable kid so JWKS-caching clients keep working across restarts |
deviceCodePendingPolls | 0 | Device flow: number of /token polls that return authorization_pending before approval (0 = approve immediately) |
enforceClientAuthentication | false | Require client_secret_basic / client_secret_post at the token endpoint |
opaqueAccessToken | false | Issue an opaque (non-JWT) access_token validated via introspection; id_token stays a signed JWT |
issueExpiredToken | false | Negative test: issue tokens whose exp is in the past |
wrongIssuer | false | Negative test: sign tokens with an iss that does not match the discovery document |
tamperedSignature | false | Negative test: corrupt the signature so verification fails |
When the default (generated) key is used, the key pair is fresh per call, so a client that has
cached the JWKS before re-generating the provider will see a new kid. Supply a fixed
keyId (and key material) for a stable JWKS across restarts.
Spring Security example
Point a Spring Security resource server (or OAuth2 login client) at the mock provider's issuer; Spring discovers the endpoints and JWKS automatically:
# application.yml
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: http://localhost:1080
Because the issued tokens verify against the published JWKS, Spring Security validates them with no further
configuration. For the OAuth2 login (authorization-code) flow, the mock /authorize endpoint
issues a code (echoing state and recording any PKCE challenge and nonce), and the
/token endpoint exchanges it for tokens carrying the nonce.