MockServer can simulate OAuth2 authorization servers and OpenID Connect providers, enabling you to test OAuth-secured applications without connecting to a real identity provider. This is useful for:

  • Integration testing — verify your application correctly handles token acquisition, refresh, and error scenarios
  • Offline development — develop and test locally without network access to an IdP
  • Fast CI pipelines — eliminate external dependencies and network latency from your test suite
  • Edge case testing — simulate expired tokens, invalid grants, and error responses that are difficult to trigger with a real IdP

The examples below show how to create MockServer expectations that simulate each major OAuth2 flow. All examples use the REST API to create expectations.

 

Client Credentials Flow

The client credentials flow is used for machine-to-machine communication where no user is involved. The client authenticates directly with the authorization server using its client_id and client_secret to obtain an access token.

This is the simplest OAuth2 flow and is commonly used for service-to-service API calls, background jobs, and microservice communication.

curl -v -X PUT "http://localhost:1080/mockserver/expectation" -d '{
    "httpRequest": {
        "method": "POST",
        "path": "/oauth/token",
        "headers": {
            "Content-Type": ["application/x-www-form-urlencoded"]
        },
        "body": {
            "type": "PARAMETERS",
            "parameters": {
                "grant_type": ["client_credentials"],
                "client_id": ["my-client-id"],
                "client_secret": ["my-client-secret"]
            }
        }
    },
    "httpResponse": {
        "statusCode": 200,
        "headers": {
            "Content-Type": ["application/json"]
        },
        "body": {
            "type": "JSON",
            "json": "{\"access_token\": \"mock-access-token-12345\", \"token_type\": \"Bearer\", \"expires_in\": 3600, \"scope\": \"read write\"}"
        }
    }
}'
curl -v -X PUT "http://localhost:1080/mockserver/expectation" -d '{
    "httpRequest": {
        "method": "POST",
        "path": "/oauth/token",
        "body": {
            "type": "PARAMETERS",
            "parameters": {
                "grant_type": ["client_credentials"],
                "scope": ["read"]
            }
        }
    },
    "httpResponse": {
        "statusCode": 200,
        "headers": {
            "Content-Type": ["application/json"]
        },
        "body": {
            "type": "JSON",
            "json": "{\"access_token\": \"mock-read-only-token\", \"token_type\": \"Bearer\", \"expires_in\": 3600, \"scope\": \"read\"}"
        }
    }
}'
curl -v -X PUT "http://localhost:1080/mockserver/expectation" -d '{
    "httpRequest": {
        "method": "POST",
        "path": "/oauth/token",
        "headers": {
            "Authorization": ["Basic bXktY2xpZW50LWlkOm15LWNsaWVudC1zZWNyZXQ="]
        },
        "body": {
            "type": "PARAMETERS",
            "parameters": {
                "grant_type": ["client_credentials"]
            }
        }
    },
    "httpResponse": {
        "statusCode": 200,
        "headers": {
            "Content-Type": ["application/json"]
        },
        "body": {
            "type": "JSON",
            "json": "{\"access_token\": \"mock-access-token-basic\", \"token_type\": \"Bearer\", \"expires_in\": 3600}"
        }
    }
}'

Some OAuth2 clients send credentials as an HTTP Basic Authorization header (base64(client_id:client_secret)) instead of form parameters.

 

Authorization Code Flow

The authorization code flow is used by web applications and native apps to obtain tokens on behalf of a user. It involves two steps:

  1. The client redirects the user to the authorization server's /authorize endpoint, which authenticates the user and redirects back with an authorization code
  2. The client exchanges the authorization code for tokens at the /token endpoint

To mock this flow, create expectations for both the authorization endpoint and the token endpoint.

curl -v -X PUT "http://localhost:1080/mockserver/expectation" -d '{
    "httpRequest": {
        "method": "GET",
        "path": "/authorize",
        "queryStringParameters": {
            "response_type": ["code"],
            "client_id": ["my-client-id"],
            "redirect_uri": ["https://myapp.example.com/callback"]
        }
    },
    "httpResponseTemplate": {
        "templateType": "JAVASCRIPT",
        "template": "return { statusCode: 302, headers: { Location: [\"https://myapp.example.com/callback?code=mock-auth-code-xyz&state=\" + (request.queryStringParameters[\"state\"] && request.queryStringParameters[\"state\"][0] ? request.queryStringParameters[\"state\"][0] : \"\")] } };"
    }
}'

The authorization endpoint redirects the user's browser back to the client application's redirect_uri with an authorization code. The state parameter is echoed back using a JavaScript response template that reads from the incoming request's query string parameters, preserving CSRF protection.

curl -v -X PUT "http://localhost:1080/mockserver/expectation" -d '{
    "httpRequest": {
        "method": "POST",
        "path": "/oauth/token",
        "body": {
            "type": "PARAMETERS",
            "parameters": {
                "grant_type": ["authorization_code"],
                "code": ["mock-auth-code-xyz"],
                "redirect_uri": ["https://myapp.example.com/callback"],
                "client_id": ["my-client-id"]
            }
        }
    },
    "httpResponse": {
        "statusCode": 200,
        "headers": {
            "Content-Type": ["application/json"]
        },
        "body": {
            "type": "JSON",
            "json": "{\"access_token\": \"mock-access-token-from-code\", \"token_type\": \"Bearer\", \"expires_in\": 3600, \"refresh_token\": \"mock-refresh-token-abc\", \"scope\": \"openid profile email\"}"
        }
    }
}'

This example omits client_secret from the token request. Confidential clients (server-side web apps) must also include client_secret as a form parameter or via HTTP Basic authentication. Public clients (SPAs, mobile apps) omit it.

 

Refresh Token Flow

The refresh token flow allows clients to obtain a new access token without requiring user interaction, using a previously issued refresh token. This is used when access tokens expire and the client needs to maintain a session.

curl -v -X PUT "http://localhost:1080/mockserver/expectation" -d '{
    "httpRequest": {
        "method": "POST",
        "path": "/oauth/token",
        "body": {
            "type": "PARAMETERS",
            "parameters": {
                "grant_type": ["refresh_token"],
                "refresh_token": ["mock-refresh-token-abc"],
                "client_id": ["my-client-id"]
            }
        }
    },
    "httpResponse": {
        "statusCode": 200,
        "headers": {
            "Content-Type": ["application/json"]
        },
        "body": {
            "type": "JSON",
            "json": "{\"access_token\": \"mock-new-access-token-67890\", \"token_type\": \"Bearer\", \"expires_in\": 3600, \"refresh_token\": \"mock-new-refresh-token-def\"}"
        }
    }
}'

Many authorization servers issue a new refresh token alongside the new access token (refresh token rotation). This example demonstrates that pattern.

curl -v -X PUT "http://localhost:1080/mockserver/expectation" -d '{
    "httpRequest": {
        "method": "POST",
        "path": "/oauth/token",
        "body": {
            "type": "PARAMETERS",
            "parameters": {
                "grant_type": ["refresh_token"],
                "refresh_token": ["expired-refresh-token"]
            }
        }
    },
    "httpResponse": {
        "statusCode": 400,
        "headers": {
            "Content-Type": ["application/json"]
        },
        "body": {
            "type": "JSON",
            "json": "{\"error\": \"invalid_grant\", \"error_description\": \"The refresh token has expired\"}"
        }
    }
}'

Use this pattern to test how your application handles refresh token expiry — typically the user should be redirected to re-authenticate.

 

PKCE Extension

PKCE (Proof Key for Code Exchange, RFC 7636) is an extension to the authorization code flow designed for public clients (single-page apps, mobile apps) that cannot securely store a client secret. The client generates a random code_verifier and sends a derived code_challenge in the authorization request, then proves possession of the verifier when exchanging the code for tokens.

curl -v -X PUT "http://localhost:1080/mockserver/expectation" -d '{
    "httpRequest": {
        "method": "GET",
        "path": "/authorize",
        "queryStringParameters": {
            "response_type": ["code"],
            "client_id": ["my-spa-client"],
            "redirect_uri": ["https://myapp.example.com/callback"],
            "code_challenge_method": ["S256"],
            "code_challenge": [".+"]
        }
    },
    "httpResponseTemplate": {
        "templateType": "JAVASCRIPT",
        "template": "return { statusCode: 302, headers: { Location: [\"https://myapp.example.com/callback?code=mock-pkce-auth-code&state=\" + (request.queryStringParameters[\"state\"] && request.queryStringParameters[\"state\"][0] ? request.queryStringParameters[\"state\"][0] : \"\")] } };"
    }
}'

The code_challenge parameter is matched with a regex (.+) since it will be a different Base64url-encoded hash each time. The state parameter is echoed back using a JavaScript response template. In a real flow, the authorization server validates the challenge against the verifier during token exchange.

curl -v -X PUT "http://localhost:1080/mockserver/expectation" -d '{
    "httpRequest": {
        "method": "POST",
        "path": "/oauth/token",
        "body": {
            "type": "PARAMETERS",
            "parameters": {
                "grant_type": ["authorization_code"],
                "code": ["mock-pkce-auth-code"],
                "redirect_uri": ["https://myapp.example.com/callback"],
                "client_id": ["my-spa-client"],
                "code_verifier": [".+"]
            }
        }
    },
    "httpResponse": {
        "statusCode": 200,
        "headers": {
            "Content-Type": ["application/json"]
        },
        "body": {
            "type": "JSON",
            "json": "{\"access_token\": \"mock-pkce-access-token\", \"token_type\": \"Bearer\", \"expires_in\": 3600, \"id_token\": \"mock-id-token-jwt\", \"scope\": \"openid profile\"}"
        }
    }
}'

The code_verifier is matched with a regex since MockServer does not need to verify the PKCE proof — it just needs to accept the token exchange request. Note that PKCE flows for public clients do not include a client_secret.

 

Dynamic Token Responses with Response Templates

The examples above return static token values. For more realistic testing — especially when running multiple tests in parallel — you can use response templates to generate unique tokens for each request.

curl -v -X PUT "http://localhost:1080/mockserver/expectation" -d '{
    "httpRequest": {
        "method": "POST",
        "path": "/oauth/token",
        "body": {
            "type": "PARAMETERS",
            "parameters": {
                "grant_type": ["client_credentials"]
            }
        }
    },
    "httpResponseTemplate": {
        "templateType": "MUSTACHE",
        "template": "{\"statusCode\": 200, \"headers\": {\"Content-Type\": [\"application/json\"]}, \"body\": \"{\\\"access_token\\\": \\\"\\\", \\\"token_type\\\": \\\"Bearer\\\", \\\"expires_in\\\": 3600, \\\"scope\\\": \\\"read write\\\"}\"}"
    }
}'

Each request receives a unique access_token generated by the built-in template variable.

curl -v -X PUT "http://localhost:1080/mockserver/expectation" -d '{
    "httpRequest": {
        "method": "POST",
        "path": "/oauth/token",
        "body": {
            "type": "PARAMETERS",
            "parameters": {
                "grant_type": ["client_credentials"]
            }
        }
    },
    "httpResponseTemplate": {
        "templateType": "JAVASCRIPT",
        "template": "return { statusCode: 200, headers: { \"Content-Type\": [\"application/json\"] }, body: JSON.stringify({ access_token: uuid, token_type: \"Bearer\", expires_in: 3600, issued_at: parseInt(now_epoch), scope: \"read write\" }) };"
    }
}'

JavaScript templates provide access to built-in template variables like uuid and now_epoch for generating dynamic values. The issued_at field contains the epoch time when the token was issued.

 

Matching Protected Resources by Bearer Token

After obtaining an access token, clients include it in the Authorization header when calling protected APIs. You can mock these protected endpoints to accept any bearer token, require a specific token, or reject missing/invalid tokens.

curl -v -X PUT "http://localhost:1080/mockserver/expectation" -d '{
    "httpRequest": {
        "method": "GET",
        "path": "/api/resource",
        "headers": {
            "Authorization": ["Bearer .+"]
        }
    },
    "httpResponse": {
        "statusCode": 200,
        "headers": {
            "Content-Type": ["application/json"]
        },
        "body": {
            "type": "JSON",
            "json": "{\"id\": 1, \"name\": \"Protected Resource\", \"description\": \"This resource requires authentication\"}"
        }
    }
}'

The regex Bearer .+ matches any non-empty bearer token value.

curl -v -X PUT "http://localhost:1080/mockserver/expectation" -d '{
    "httpRequest": {
        "method": "GET",
        "path": "/api/resource",
        "headers": {
            "Authorization": ["Bearer mock-access-token-12345"]
        }
    },
    "httpResponse": {
        "statusCode": 200,
        "headers": {
            "Content-Type": ["application/json"]
        },
        "body": {
            "type": "JSON",
            "json": "{\"id\": 1, \"name\": \"Protected Resource\"}"
        }
    }
}'

Match a specific token to ensure your application sends the correct token obtained from the mock token endpoint.

curl -v -X PUT "http://localhost:1080/mockserver/expectation" -d '{
    "httpRequest": {
        "method": "GET",
        "path": "/api/resource",
        "headers": {
            "!Authorization": ["Bearer .+"]
        }
    },
    "httpResponse": {
        "statusCode": 401,
        "headers": {
            "Content-Type": ["application/json"],
            "WWW-Authenticate": ["Bearer realm=\"mockserver\", error=\"invalid_token\""]
        },
        "body": {
            "type": "JSON",
            "json": "{\"error\": \"unauthorized\", \"error_description\": \"Access token is missing or invalid\"}"
        }
    }
}'

The ! prefix on the header name creates a negative matcher — this expectation matches requests that do not have a valid Bearer token. Use this with a lower priority to create a catch-all 401 response alongside a higher-priority expectation that matches valid tokens.

curl -v -X PUT "http://localhost:1080/mockserver/expectation" -d '{
    "httpRequest": {
        "method": "DELETE",
        "path": "/api/admin/resource",
        "headers": {
            "Authorization": ["Bearer mock-read-only-token"]
        }
    },
    "httpResponse": {
        "statusCode": 403,
        "headers": {
            "Content-Type": ["application/json"],
            "WWW-Authenticate": ["Bearer realm=\"mockserver\", error=\"insufficient_scope\", scope=\"admin\""]
        },
        "body": {
            "type": "JSON",
            "json": "{\"error\": \"forbidden\", \"error_description\": \"Insufficient scope for this resource\"}"
        }
    }
}'

Simulate a scenario where the access token is valid but does not have the required scope for the requested operation.

 

OpenID Connect Discovery

OpenID Connect (OIDC) clients typically start by fetching the provider's discovery document from /.well-known/openid-configuration to learn the authorization, token, and JWKS endpoint URLs. Many OAuth2 client libraries require this endpoint to be available during initialization.

curl -v -X PUT "http://localhost:1080/mockserver/expectation" -d '{
    "httpRequest": {
        "method": "GET",
        "path": "/.well-known/openid-configuration"
    },
    "httpResponse": {
        "statusCode": 200,
        "headers": {
            "Content-Type": ["application/json"]
        },
        "body": {
            "type": "JSON",
            "json": "{\"issuer\": \"http://localhost:1080\", \"authorization_endpoint\": \"http://localhost:1080/authorize\", \"token_endpoint\": \"http://localhost:1080/oauth/token\", \"userinfo_endpoint\": \"http://localhost:1080/userinfo\", \"jwks_uri\": \"http://localhost:1080/.well-known/jwks.json\", \"response_types_supported\": [\"code\", \"token\", \"id_token\"], \"subject_types_supported\": [\"public\"], \"id_token_signing_alg_values_supported\": [\"RS256\"], \"scopes_supported\": [\"openid\", \"profile\", \"email\"], \"token_endpoint_auth_methods_supported\": [\"client_secret_basic\", \"client_secret_post\"], \"grant_types_supported\": [\"authorization_code\", \"client_credentials\", \"refresh_token\"]}"
        }
    }
}'

The discovery document tells OIDC clients where to find all the other endpoints. All URLs point to MockServer (localhost:1080) so the client library interacts entirely with the mock.

curl -v -X PUT "http://localhost:1080/mockserver/expectation" -d '{
    "httpRequest": {
        "method": "GET",
        "path": "/.well-known/jwks.json"
    },
    "httpResponse": {
        "statusCode": 200,
        "headers": {
            "Content-Type": ["application/json"]
        },
        "body": {
            "type": "JSON",
            "json": "{\"keys\": [{\"kty\": \"RSA\", \"use\": \"sig\", \"kid\": \"mock-key-1\", \"alg\": \"RS256\", \"n\": \"0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw\", \"e\": \"AQAB\"}]}"
        }
    }
}'

The JWKS endpoint exposes public keys that OIDC clients use to verify the signature of ID tokens. For testing purposes you can use a placeholder RSA key — unless your application actually validates JWT signatures, in which case you should generate a real key pair and sign your mock ID tokens with the corresponding private key.

curl -v -X PUT "http://localhost:1080/mockserver/expectation" -d '{
    "httpRequest": {
        "method": "GET",
        "path": "/userinfo",
        "headers": {
            "Authorization": ["Bearer .+"]
        }
    },
    "httpResponse": {
        "statusCode": 200,
        "headers": {
            "Content-Type": ["application/json"]
        },
        "body": {
            "type": "JSON",
            "json": "{\"sub\": \"mock-user-123\", \"name\": \"Test User\", \"email\": \"test@example.com\", \"email_verified\": true, \"picture\": \"https://example.com/photo.jpg\"}"
        }
    }
}'

The UserInfo endpoint returns claims about the authenticated user. This is called by OIDC clients after obtaining an access token.

 

Error Responses

Testing error scenarios is critical for building resilient OAuth2 integrations. The OAuth2 specification defines standard error codes that authorization servers return when requests fail.

curl -v -X PUT "http://localhost:1080/mockserver/expectation" -d '{
    "httpRequest": {
        "method": "POST",
        "path": "/oauth/token",
        "body": {
            "type": "PARAMETERS",
            "parameters": {
                "grant_type": ["client_credentials"],
                "client_id": ["invalid-client"]
            }
        }
    },
    "httpResponse": {
        "statusCode": 401,
        "headers": {
            "Content-Type": ["application/json"]
        },
        "body": {
            "type": "JSON",
            "json": "{\"error\": \"invalid_client\", \"error_description\": \"Client authentication failed\"}"
        }
    }
}'
curl -v -X PUT "http://localhost:1080/mockserver/expectation" -d '{
    "httpRequest": {
        "method": "POST",
        "path": "/oauth/token",
        "body": {
            "type": "PARAMETERS",
            "parameters": {
                "grant_type": ["authorization_code"],
                "code": ["expired-or-used-code"]
            }
        }
    },
    "httpResponse": {
        "statusCode": 400,
        "headers": {
            "Content-Type": ["application/json"]
        },
        "body": {
            "type": "JSON",
            "json": "{\"error\": \"invalid_grant\", \"error_description\": \"The authorization code has expired or has already been used\"}"
        }
    }
}'
curl -v -X PUT "http://localhost:1080/mockserver/expectation" -d '{
    "httpRequest": {
        "method": "POST",
        "path": "/oauth/token",
        "body": {
            "type": "PARAMETERS",
            "parameters": {
                "grant_type": ["password"]
            }
        }
    },
    "httpResponse": {
        "statusCode": 400,
        "headers": {
            "Content-Type": ["application/json"]
        },
        "body": {
            "type": "JSON",
            "json": "{\"error\": \"unsupported_grant_type\", \"error_description\": \"The authorization grant type is not supported\"}"
        }
    }
}'