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):

EndpointDefault pathPurpose
Discovery/.well-known/openid-configurationOIDC discovery document
JWKS/.well-known/jwks.jsonPublic signing key(s)
Token/tokenIssues tokens for all supported grants
Authorize/authorizeAuthorization-code grant (issues a code, supports PKCE + nonce)
Userinfo/userinfoReturns the subject and additional claims
Introspection/introspectRFC 7662 token introspection
Revocation/revokeRFC 7009 token revocation
End-session (logout)/logoutRP-initiated logout (302 to post_logout_redirect_uri or 200)
Device authorization/device_authorizationRFC 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_tokeniss, sub, aud = clientId, exp, iat, nbf, nonce (when supplied), at_hash, profile/email claims for the requested scopes, plus any additionalClaims. Issued only when the openid scope is requested.
  • access_tokeniss, sub, aud = audience, exp, iat, nbf, scope, client_id, plus any additionalClaims.

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_basicAuthorization: Basic base64(clientId:clientSecret)
  • client_secret_postclient_id and client_secret form 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

FieldDefaultDescription
issuerhttp://localhost:1080Issuer URL; used to build every endpoint URL in the discovery document
jwksPath / tokenPath / authorizePath / userinfoPath / introspectPath / revokePath / deviceAuthorizationPathstandard pathsOverride individual endpoint paths
subjectmock-usersub claim in tokens and userinfo
clientIdmock-clientAudience of the id_token and client_id in the access_token
clientSecretmock-client-secretExpected client secret when enforceClientAuthentication is on
audiencemock-audienceAudience of the access_token
scopes["openid","profile","email"]Default scopes; an id_token is issued only when openid is present
tokenExpirySeconds3600Token lifetime (expires_in / exp)
additionalClaims{}Extra claims merged into the issued tokens and userinfo
signingAlgorithmRS256RS256, RS384, RS512, ES256, ES384, ES512
privateKeyPem + certificatePemgeneratedSupply your own signing key (PEM). The public key is published at the JWKS endpoint
jwkJsongeneratedAlternatively, supply the signing key as a private JWK
keyIdrandomStable kid so JWKS-caching clients keep working across restarts
deviceCodePendingPolls0Device flow: number of /token polls that return authorization_pending before approval (0 = approve immediately)
enforceClientAuthenticationfalseRequire client_secret_basic / client_secret_post at the token endpoint
opaqueAccessTokenfalseIssue an opaque (non-JWT) access_token validated via introspection; id_token stays a signed JWT
issueExpiredTokenfalseNegative test: issue tokens whose exp is in the past
wrongIssuerfalseNegative test: sign tokens with an iss that does not match the discovery document
tamperedSignaturefalseNegative 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.