Mock OAuth2/OpenID Connect server for JVM tests and Docker Compose. No security workarounds needed.
Kotlin
424
1,311 commits
updated Sep 29, 2026
Scriptable OAuth2/OpenID Connect server for JVM tests and Docker Compose.
[!NOTE] The latest stable version is shown in the Maven Central badge above. See the release notes for what has changed.
Add the dependency:
Gradle Kotlin DSL
testImplementation("no.nav.security:mock-oauth2-server:$mockOAuth2ServerVersion")
Maven
<dependency>
<groupId>no.nav.security</groupId>
<artifactId>mock-oauth2-server</artifactId>
<version>${mock-oauth2-server.version}</version>
<scope>test</scope>
</dependency>
Start the server and issue a token in your test:
val server = MockOAuth2Server()
server.start()
val token = server.issueToken(
issuerId = "default",
subject = "user123",
audience = "my-api",
)
// Point your app at the discovery URL
val wellKnownUrl = server.wellKnownUrl("default").toString()
// Attach the token to a request
request.addHeader("Authorization", "Bearer ${token.serialize()}")
server.shutdown()
Or run it as a Docker container:
docker run -p 8080:8080 ghcr.io/navikt/mock-oauth2-server:$MOCK_OAUTH2_SERVER_VERSION
Token endpoint: http://localhost:8080/default/token
Discovery: http://localhost:8080/default/.well-known/openid-configuration
mock-oauth2-server lets you test applications that depend on a real OAuth2/OpenID Connect server without disabling security. It issues signed JWTs that are verifiable through standard JWKS and discovery endpoints, so your app does not need any special test configuration.
It supports multi-issuer setups, all major OAuth2 grant types, token customization, and runs both embedded in JVM tests and as a standalone process in Docker Compose.
[!WARNING] This server is for testing only. Do not use it in production.
Issued JWT tokens are verifiable through standard mechanisms via OpenID Connect Discovery and OAuth2 Authorization Server Metadata endpoints. Multi-issuer setups are supported with no configuration — the first path segment in any request URL determines the issuer.
val server = MockOAuth2Server()
server.start()
val wellKnownUrl = server.wellKnownUrl("default").toString()
// configure your app to use wellKnownUrl, run your test, then:
server.shutdown()
Use withMockOAuth2Server to avoid manual start/shutdown:
withMockOAuth2Server {
val wellKnownUrl = wellKnownUrl("default").toString()
// configure your app and run your test here
}
The simplest way to issue a token:
val token: SignedJWT = server.issueToken(
issuerId = "default",
subject = "user123",
audience = "my-api",
claims = mapOf("roles" to listOf("admin")),
expiry = 3600,
)
request.addHeader("Authorization", "Bearer ${token.serialize()}")
To issue a token with a custom callback object for full control:
val token: SignedJWT = server.issueToken(issuerId, "someclientid", DefaultOAuth2TokenCallback())
To issue a token for an external issuer URL that is still verifiable via this server's JWKS:
val token: SignedJWT = server.anyToken(
issuerUrl = "https://external-idp.example.com".toHttpUrl(),
claims = mapOf("sub" to "user123", "aud" to "my-api"),
)
Enqueue a callback to control what is returned when your app exchanges the code:
@Test
fun loginWithIdTokenForSubjectFoo() {
server.enqueueCallback(
DefaultOAuth2TokenCallback(
issuerId = issuerId,
subject = "foo"
)
)
// Invoke your app here and assert user foo is logged in
}
To set specific claims in the id_token:
@Test
fun loginWithIdTokenForAcrClaimEqualsLevel4() {
server.enqueueCallback(
DefaultOAuth2TokenCallback(
issuerId = issuerId,
claims = mapOf("acr" to "Level4")
)
)
// Invoke your app here and assert acr=Level4 is present in id_token
}
[!NOTE]
takeRequest()is only available when usingMockWebServerWrapper(the default). It throwsUnsupportedOperationExceptionwithNettyWrapper.
val request = server.takeRequest()
assertThat(request.path).contains("/default/token")
val server = MockOAuth2Server(
config = OAuth2Config(
tokenProvider = OAuth2TokenProvider(systemTime = Instant.parse("2020-01-21T00:00:00Z"))
)
)
val token = server.issueToken(issuerId = "issuer1")
// token has iat=2020-01-21T00:00:00Z
The first path segment in any request URL is the issuerId. No configuration needed:
http://localhost:8080/issuer-a/.well-known/openid-configuration → issuerId = issuer-a
http://localhost:8080/issuer-b/.well-known/openid-configuration → issuerId = issuer-b
Each issuer has its own discovery document, token endpoint, and JWKS.
The standalone server defaults to port 8080.
Run with Docker:
docker run -p 8080:8080 ghcr.io/navikt/mock-oauth2-server:$MOCK_OAUTH2_SERVER_VERSION
Images are also tagged by major and minor version, so you can pin to a release stream:
| Tag | Resolves to |
|---|---|
ghcr.io/navikt/mock-oauth2-server:3.1.4 | exact version |
ghcr.io/navikt/mock-oauth2-server:3.1 | latest 3.1.x |
ghcr.io/navikt/mock-oauth2-server:3 | latest 3.x.x |
Build locally:
./gradlew -Djib.from.platforms=linux/amd64 jibDockerBuild
docker run -p 8080:8080 $IMAGE_NAME
Health check: GET /isalive returns 200 when the server is ready.
On Windows, specify the host explicitly: docker run -p 8080:8080 -h localhost $IMAGE_NAME
When running mock-oauth2-server alongside your application in Docker Compose, there are two networking scenarios to consider.
Scenario 1: Container-to-container only (most common for integration tests)
Both services communicate over Docker's internal network. Your app references the mock server using the Docker Compose service name as the hostname:
services:
your_app:
build: .
ports:
- 8080:8080
environment:
- SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_JWK_SET_URI=http://mock-oauth2-server:8080/default/jwks
mock-oauth2-server:
image: ghcr.io/navikt/mock-oauth2-server:$MOCK_OAUTH2_SERVER_VERSION
ports:
- 8090:8080
Your app reaches the mock server at http://mock-oauth2-server:8080 (internal Docker network). From your host machine the mock server is at http://localhost:8090.
Scenario 2: Container-to-container + browser interaction (e.g. Authorization Code Flow)
If a browser also needs to reach the mock server, issuer URLs in tokens must be resolvable both from inside Docker and from your browser:
127.0.0.1 host.docker.internal to your /etc/hosts file (Linux only; macOS and Windows Docker Desktop add this automatically).hostname: host.docker.internal on the mock server service.services:
your_app:
build: .
ports:
- 8080:8080
environment:
- SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_JWK_SET_URI=http://host.docker.internal:8090/default/jwks
mock-oauth2-server:
image: ghcr.io/navikt/mock-oauth2-server:$MOCK_OAUTH2_SERVER_VERSION
ports:
- 8090:8080
hostname: host.docker.internal
[!NOTE] Each service must use a different host port. Mapping two services to the same host port causes a
port is already allocatederror.
When running standalone or in Docker you can configure token callbacks via the JSON_CONFIG environment variable (or JSON_CONFIG_PATH pointing to a file). When neither is set the server looks for config.json in the current working directory.
A token callback lets you define what claims are returned when a token request matches a given parameter:
{
"interactiveLogin": true,
"httpServer": "NettyWrapper",
"tokenCallbacks": [
{
"issuerId": "issuer1",
"tokenExpiry": 120,
"requestMappings": [
{
"requestParam": "code",
"match": "*",
"claims": {
"sub": "subByCode",
"aud": ["audByCode"]
}
}
]
},
{
"issuerId": "issuer2",
"requestMappings": [
{
"requestParam": "someparam",
"match": "somevalue",
"claims": {
"sub": "subBySomeParam",
"aud": ["audBySomeParam"]
}
}
]
}
]
}
A token request to http://localhost:8080/issuer1/token with any code parameter will match the first tokenCallback and return a token with:
{
"sub": "subByCode",
"aud": ["audByCode"],
"iss": "http://localhost:8080/issuer1",
...
}
The match field supports exact strings, "*" (matches any value), and full regular expressions. If the pattern is an invalid regular expression, it does not throw — regex evaluation is skipped, but exact-string matching still applies.
Use ${clientId} (or ${client_id}) in claim values to insert the requesting client ID dynamically. All form parameters from the token request are available as template variables:
{
"issuerId": "issuer1",
"requestMappings": [
{
"requestParam": "code",
"match": "*",
"claims": {
"sub": "${clientId}",
"aud": ["audByCode"]
}
}
]
}
In addition to form parameters, the following built-in variables are always available:
| Variable | Value |
|---|---|
${clientId} / ${client_id} | The client_id from the token request |
When interactiveLogin is enabled, requestMappings can match on the username submitted at the login page using "requestParam": "subject". The login username is also available as ${subject} in claim values.
This allows a single JSON_CONFIG to serve different claim sets per user without a custom login page:
{
"interactiveLogin": true,
"tokenCallbacks": [
{
"issuerId": "default",
"requestMappings": [
{
"requestParam": "subject",
"match": "alice",
"claims": {
"role": "admin",
"preferred_username": "${subject}"
}
},
{
"requestParam": "subject",
"match": ".*",
"claims": {
"role": "user",
"preferred_username": "${subject}"
}
}
]
}
]
}
Claim precedence when combining requestMappings with interactive login:
requestMapping take priority.Template variable precedence (highest wins):
client_id / clientId — always authoritative${subject} and other built-in variablesEvery token issued by DefaultOAuth2TokenCallback automatically includes the following claims regardless of what you configure:
| Claim | Value | When |
|---|---|---|
tid | the issuerId (e.g. "default") | always |
azp | the client_id from the token request | Authorization Code grant only |
You can override tid by including it in your claims map. azp cannot be overridden for Authorization Code grants as it is added after your claims are applied.
aud claim resolutionWhen using DefaultOAuth2TokenCallback the aud claim is resolved in the following order:
audience value passed explicitly to the constructoraudience parameter from the token request (e.g. in Token Exchange)"default" if none of the above are presentGenerate a keystore automatically:
val ssl = Ssl()
val server = MockOAuth2Server(
OAuth2Config(httpServer = MockWebServerWrapper(ssl))
)
[!TIP] Add
ssl.sslKeystore.keyStoreto your client's truststore to trust the generated certificate.
Bring your own keystore:
val ssl = Ssl(
SslKeystore(
keyPassword = "",
keystoreFile = File("src/test/resources/localhost.p12"),
keystorePassword = "",
keystoreType = SslKeystore.KeyStoreType.PKCS12
)
)
val server = MockOAuth2Server(
OAuth2Config(httpServer = MockWebServerWrapper(ssl))
)
Generate keystore:
{
"httpServer": {
"type": "NettyWrapper",
"ssl": {}
}
}
Bring your own:
{
"httpServer": {
"type": "NettyWrapper",
"ssl": {
"keyPassword": "",
"keystoreFile": "src/test/resources/localhost.p12",
"keystoreType": "PKCS12",
"keystorePassword": ""
}
}
}
A ready to use Docker Compose setup with SSL is available at docker-compose-ssl.yaml in the root of this repository.
The server automatically adds CORS headers to every response when an Origin header is present. No configuration is required.
For regular requests:
Access-Control-Allow-Origin: <origin>
Access-Control-Allow-Credentials: true
For OPTIONS preflight requests:
Access-Control-Allow-Headers: <requested-headers>
Access-Control-Allow-Methods: POST, GET, OPTIONS
Browser based OAuth2 clients and SPAs can call the token, JWKS and other endpoints directly without any proxy setup.
Point your browser to http://localhost:8080/default/debugger to open the OAuth2 client debugger. It implements the Authorization Code Flow and lets you inspect request parameters and token responses interactively.
| Variable | Description |
|---|---|
SERVER_HOSTNAME | Hostname to bind to. Defaults to the wildcard address (typically 0.0.0.0 or :: depending on the JVM and OS). |
SERVER_PORT or PORT | Port to listen on. Defaults to 8080. PORT is also accepted for Heroku compatibility. |
JSON_CONFIG | Full JSON content of OAuth2Config. Takes precedence over JSON_CONFIG_PATH. |
JSON_CONFIG_PATH | Absolute path to a JSON file containing OAuth2Config. |
LOG_LEVEL | Root log level. Defaults to INFO. |
LOGBACK_CONFIG | Path to a custom logback XML file. Overrides the default logback-standalone.xml. |
When neither JSON_CONFIG nor JSON_CONFIG_PATH is set, the server looks for a file named config.json in the current working directory before falling back to defaults.
| Property | Description |
|---|---|
interactiveLogin | true or false. Enables a login screen on the /authorize endpoint. Defaults to true in standalone mode, false in library mode. |
loginPagePath | Path to a custom HTML login page. The page must contain a form that posts username and optionally claims. See src/test/resources/login.example.html for an example. |
staticAssetsPath | Path to a directory of static assets served under /static. E.g. http://localhost:8080/static/myimage.svg. |
rotateRefreshToken | true or false. When true, a new refresh token is issued on each refresh grant, invalidating the previous one. |
httpServer | The HTTP server implementation to use: MockWebServerWrapper (default, supports takeRequest()) or NettyWrapper (required for HTTPS). Can also be a JSON object: {"type": "NettyWrapper", "ssl": {...}}. |
tokenCallbacks | A list of RequestMappingTokenCallback objects that define which claims to return based on request parameters. |
Additional token provider options:
{
"tokenProvider": {
"keyProvider": {
"algorithm": "ES256"
},
"systemTime": "2020-01-21T00:00:00Z"
}
}
The first path segment in any URL is the issuerId. Both OIDC and OAuth2 AS metadata endpoints are served:
GET /{issuerId}/.well-known/openid-configuration
GET /{issuerId}/.well-known/oauth-authorization-server
http://localhost:8080/default/.well-known/openid-configuration{
"issuer": "http://localhost:8080/default",
"authorization_endpoint": "http://localhost:8080/default/authorize",
"token_endpoint": "http://localhost:8080/default/token",
"jwks_uri": "http://localhost:8080/default/jwks",
"userinfo_endpoint": "http://localhost:8080/default/userinfo",
"introspection_endpoint": "http://localhost:8080/default/introspect",
"revocation_endpoint": "http://localhost:8080/default/revoke",
"end_session_endpoint": "http://localhost:8080/default/endsession"
}
Introspect (POST /{issuerId}/introspect) requires an Authorization header. Either Authorization: Bearer <token> or Authorization: Basic <credentials> is accepted. Requests without it will receive 400 invalid_client.
Revocation (POST /{issuerId}/revoke) only supports token_type_hint=refresh_token. Passing any other value returns 400 unsupported_token_type.
server.wellKnownUrl("default") // OIDC discovery URL
server.oauth2AuthorizationServerMetadataUrl("default") // OAuth2 AS metadata URL
server.tokenEndpointUrl("default")
server.jwksUrl("default")
server.userInfoUrl("default")
server.introspectUrl("default")
server.revocationEndpointUrl("default")
server.endSessionEndpointUrl("default")
server.baseUrl() // server root URL
navikt.github.io/mock-oauth2-server
This project is maintained by @navikt.
To raise an issue or question, open an issue in this repository.
For internal NAV contact, use the Slack channel #nais.
See the release notes for the full version history.
Fork the repo, check out a new branch, and build with:
./gradlew build
See CONTRIBUTING.md for more details.
This library is licensed under the MIT License.
See MIGRATION.md for upgrade instructions between versions.
46 followers · starred Feb 2023
13 followers · starred Aug 2024
24 followers · starred Aug 2024
22 followers · starred Jun 2023
Kotlin
94.4%
Java
2.9%
FreeMarker
1.9%
Mock OAuth2/OpenID Connect server for JVM tests and Docker Compose. No security workarounds needed.
Kotlin
424
1,311 commits
updated Sep 29, 2026
Scriptable OAuth2/OpenID Connect server for JVM tests and Docker Compose.
[!NOTE] The latest stable version is shown in the Maven Central badge above. See the release notes for what has changed.
Add the dependency:
Gradle Kotlin DSL
testImplementation("no.nav.security:mock-oauth2-server:$mockOAuth2ServerVersion")
Maven
<dependency>
<groupId>no.nav.security</groupId>
<artifactId>mock-oauth2-server</artifactId>
<version>${mock-oauth2-server.version}</version>
<scope>test</scope>
</dependency>
Start the server and issue a token in your test:
val server = MockOAuth2Server()
server.start()
val token = server.issueToken(
issuerId = "default",
subject = "user123",
audience = "my-api",
)
// Point your app at the discovery URL
val wellKnownUrl = server.wellKnownUrl("default").toString()
// Attach the token to a request
request.addHeader("Authorization", "Bearer ${token.serialize()}")
server.shutdown()
Or run it as a Docker container:
docker run -p 8080:8080 ghcr.io/navikt/mock-oauth2-server:$MOCK_OAUTH2_SERVER_VERSION
Token endpoint: http://localhost:8080/default/token
Discovery: http://localhost:8080/default/.well-known/openid-configuration
mock-oauth2-server lets you test applications that depend on a real OAuth2/OpenID Connect server without disabling security. It issues signed JWTs that are verifiable through standard JWKS and discovery endpoints, so your app does not need any special test configuration.
It supports multi-issuer setups, all major OAuth2 grant types, token customization, and runs both embedded in JVM tests and as a standalone process in Docker Compose.
[!WARNING] This server is for testing only. Do not use it in production.
Issued JWT tokens are verifiable through standard mechanisms via OpenID Connect Discovery and OAuth2 Authorization Server Metadata endpoints. Multi-issuer setups are supported with no configuration — the first path segment in any request URL determines the issuer.
val server = MockOAuth2Server()
server.start()
val wellKnownUrl = server.wellKnownUrl("default").toString()
// configure your app to use wellKnownUrl, run your test, then:
server.shutdown()
Use withMockOAuth2Server to avoid manual start/shutdown:
withMockOAuth2Server {
val wellKnownUrl = wellKnownUrl("default").toString()
// configure your app and run your test here
}
The simplest way to issue a token:
val token: SignedJWT = server.issueToken(
issuerId = "default",
subject = "user123",
audience = "my-api",
claims = mapOf("roles" to listOf("admin")),
expiry = 3600,
)
request.addHeader("Authorization", "Bearer ${token.serialize()}")
To issue a token with a custom callback object for full control:
val token: SignedJWT = server.issueToken(issuerId, "someclientid", DefaultOAuth2TokenCallback())
To issue a token for an external issuer URL that is still verifiable via this server's JWKS:
val token: SignedJWT = server.anyToken(
issuerUrl = "https://external-idp.example.com".toHttpUrl(),
claims = mapOf("sub" to "user123", "aud" to "my-api"),
)
Enqueue a callback to control what is returned when your app exchanges the code:
@Test
fun loginWithIdTokenForSubjectFoo() {
server.enqueueCallback(
DefaultOAuth2TokenCallback(
issuerId = issuerId,
subject = "foo"
)
)
// Invoke your app here and assert user foo is logged in
}
To set specific claims in the id_token:
@Test
fun loginWithIdTokenForAcrClaimEqualsLevel4() {
server.enqueueCallback(
DefaultOAuth2TokenCallback(
issuerId = issuerId,
claims = mapOf("acr" to "Level4")
)
)
// Invoke your app here and assert acr=Level4 is present in id_token
}
[!NOTE]
takeRequest()is only available when usingMockWebServerWrapper(the default). It throwsUnsupportedOperationExceptionwithNettyWrapper.
val request = server.takeRequest()
assertThat(request.path).contains("/default/token")
val server = MockOAuth2Server(
config = OAuth2Config(
tokenProvider = OAuth2TokenProvider(systemTime = Instant.parse("2020-01-21T00:00:00Z"))
)
)
val token = server.issueToken(issuerId = "issuer1")
// token has iat=2020-01-21T00:00:00Z
The first path segment in any request URL is the issuerId. No configuration needed:
http://localhost:8080/issuer-a/.well-known/openid-configuration → issuerId = issuer-a
http://localhost:8080/issuer-b/.well-known/openid-configuration → issuerId = issuer-b
Each issuer has its own discovery document, token endpoint, and JWKS.
The standalone server defaults to port 8080.
Run with Docker:
docker run -p 8080:8080 ghcr.io/navikt/mock-oauth2-server:$MOCK_OAUTH2_SERVER_VERSION
Images are also tagged by major and minor version, so you can pin to a release stream:
| Tag | Resolves to |
|---|---|
ghcr.io/navikt/mock-oauth2-server:3.1.4 | exact version |
ghcr.io/navikt/mock-oauth2-server:3.1 | latest 3.1.x |
ghcr.io/navikt/mock-oauth2-server:3 | latest 3.x.x |
Build locally:
./gradlew -Djib.from.platforms=linux/amd64 jibDockerBuild
docker run -p 8080:8080 $IMAGE_NAME
Health check: GET /isalive returns 200 when the server is ready.
On Windows, specify the host explicitly: docker run -p 8080:8080 -h localhost $IMAGE_NAME
When running mock-oauth2-server alongside your application in Docker Compose, there are two networking scenarios to consider.
Scenario 1: Container-to-container only (most common for integration tests)
Both services communicate over Docker's internal network. Your app references the mock server using the Docker Compose service name as the hostname:
services:
your_app:
build: .
ports:
- 8080:8080
environment:
- SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_JWK_SET_URI=http://mock-oauth2-server:8080/default/jwks
mock-oauth2-server:
image: ghcr.io/navikt/mock-oauth2-server:$MOCK_OAUTH2_SERVER_VERSION
ports:
- 8090:8080
Your app reaches the mock server at http://mock-oauth2-server:8080 (internal Docker network). From your host machine the mock server is at http://localhost:8090.
Scenario 2: Container-to-container + browser interaction (e.g. Authorization Code Flow)
If a browser also needs to reach the mock server, issuer URLs in tokens must be resolvable both from inside Docker and from your browser:
127.0.0.1 host.docker.internal to your /etc/hosts file (Linux only; macOS and Windows Docker Desktop add this automatically).hostname: host.docker.internal on the mock server service.services:
your_app:
build: .
ports:
- 8080:8080
environment:
- SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_JWK_SET_URI=http://host.docker.internal:8090/default/jwks
mock-oauth2-server:
image: ghcr.io/navikt/mock-oauth2-server:$MOCK_OAUTH2_SERVER_VERSION
ports:
- 8090:8080
hostname: host.docker.internal
[!NOTE] Each service must use a different host port. Mapping two services to the same host port causes a
port is already allocatederror.
When running standalone or in Docker you can configure token callbacks via the JSON_CONFIG environment variable (or JSON_CONFIG_PATH pointing to a file). When neither is set the server looks for config.json in the current working directory.
A token callback lets you define what claims are returned when a token request matches a given parameter:
{
"interactiveLogin": true,
"httpServer": "NettyWrapper",
"tokenCallbacks": [
{
"issuerId": "issuer1",
"tokenExpiry": 120,
"requestMappings": [
{
"requestParam": "code",
"match": "*",
"claims": {
"sub": "subByCode",
"aud": ["audByCode"]
}
}
]
},
{
"issuerId": "issuer2",
"requestMappings": [
{
"requestParam": "someparam",
"match": "somevalue",
"claims": {
"sub": "subBySomeParam",
"aud": ["audBySomeParam"]
}
}
]
}
]
}
A token request to http://localhost:8080/issuer1/token with any code parameter will match the first tokenCallback and return a token with:
{
"sub": "subByCode",
"aud": ["audByCode"],
"iss": "http://localhost:8080/issuer1",
...
}
The match field supports exact strings, "*" (matches any value), and full regular expressions. If the pattern is an invalid regular expression, it does not throw — regex evaluation is skipped, but exact-string matching still applies.
Use ${clientId} (or ${client_id}) in claim values to insert the requesting client ID dynamically. All form parameters from the token request are available as template variables:
{
"issuerId": "issuer1",
"requestMappings": [
{
"requestParam": "code",
"match": "*",
"claims": {
"sub": "${clientId}",
"aud": ["audByCode"]
}
}
]
}
In addition to form parameters, the following built-in variables are always available:
| Variable | Value |
|---|---|
${clientId} / ${client_id} | The client_id from the token request |
When interactiveLogin is enabled, requestMappings can match on the username submitted at the login page using "requestParam": "subject". The login username is also available as ${subject} in claim values.
This allows a single JSON_CONFIG to serve different claim sets per user without a custom login page:
{
"interactiveLogin": true,
"tokenCallbacks": [
{
"issuerId": "default",
"requestMappings": [
{
"requestParam": "subject",
"match": "alice",
"claims": {
"role": "admin",
"preferred_username": "${subject}"
}
},
{
"requestParam": "subject",
"match": ".*",
"claims": {
"role": "user",
"preferred_username": "${subject}"
}
}
]
}
]
}
Claim precedence when combining requestMappings with interactive login:
requestMapping take priority.Template variable precedence (highest wins):
client_id / clientId — always authoritative${subject} and other built-in variablesEvery token issued by DefaultOAuth2TokenCallback automatically includes the following claims regardless of what you configure:
| Claim | Value | When |
|---|---|---|
tid | the issuerId (e.g. "default") | always |
azp | the client_id from the token request | Authorization Code grant only |
You can override tid by including it in your claims map. azp cannot be overridden for Authorization Code grants as it is added after your claims are applied.
aud claim resolutionWhen using DefaultOAuth2TokenCallback the aud claim is resolved in the following order:
audience value passed explicitly to the constructoraudience parameter from the token request (e.g. in Token Exchange)"default" if none of the above are presentGenerate a keystore automatically:
val ssl = Ssl()
val server = MockOAuth2Server(
OAuth2Config(httpServer = MockWebServerWrapper(ssl))
)
[!TIP] Add
ssl.sslKeystore.keyStoreto your client's truststore to trust the generated certificate.
Bring your own keystore:
val ssl = Ssl(
SslKeystore(
keyPassword = "",
keystoreFile = File("src/test/resources/localhost.p12"),
keystorePassword = "",
keystoreType = SslKeystore.KeyStoreType.PKCS12
)
)
val server = MockOAuth2Server(
OAuth2Config(httpServer = MockWebServerWrapper(ssl))
)
Generate keystore:
{
"httpServer": {
"type": "NettyWrapper",
"ssl": {}
}
}
Bring your own:
{
"httpServer": {
"type": "NettyWrapper",
"ssl": {
"keyPassword": "",
"keystoreFile": "src/test/resources/localhost.p12",
"keystoreType": "PKCS12",
"keystorePassword": ""
}
}
}
A ready to use Docker Compose setup with SSL is available at docker-compose-ssl.yaml in the root of this repository.
The server automatically adds CORS headers to every response when an Origin header is present. No configuration is required.
For regular requests:
Access-Control-Allow-Origin: <origin>
Access-Control-Allow-Credentials: true
For OPTIONS preflight requests:
Access-Control-Allow-Headers: <requested-headers>
Access-Control-Allow-Methods: POST, GET, OPTIONS
Browser based OAuth2 clients and SPAs can call the token, JWKS and other endpoints directly without any proxy setup.
Point your browser to http://localhost:8080/default/debugger to open the OAuth2 client debugger. It implements the Authorization Code Flow and lets you inspect request parameters and token responses interactively.
| Variable | Description |
|---|---|
SERVER_HOSTNAME | Hostname to bind to. Defaults to the wildcard address (typically 0.0.0.0 or :: depending on the JVM and OS). |
SERVER_PORT or PORT | Port to listen on. Defaults to 8080. PORT is also accepted for Heroku compatibility. |
JSON_CONFIG | Full JSON content of OAuth2Config. Takes precedence over JSON_CONFIG_PATH. |
JSON_CONFIG_PATH | Absolute path to a JSON file containing OAuth2Config. |
LOG_LEVEL | Root log level. Defaults to INFO. |
LOGBACK_CONFIG | Path to a custom logback XML file. Overrides the default logback-standalone.xml. |
When neither JSON_CONFIG nor JSON_CONFIG_PATH is set, the server looks for a file named config.json in the current working directory before falling back to defaults.
| Property | Description |
|---|---|
interactiveLogin | true or false. Enables a login screen on the /authorize endpoint. Defaults to true in standalone mode, false in library mode. |
loginPagePath | Path to a custom HTML login page. The page must contain a form that posts username and optionally claims. See src/test/resources/login.example.html for an example. |
staticAssetsPath | Path to a directory of static assets served under /static. E.g. http://localhost:8080/static/myimage.svg. |
rotateRefreshToken | true or false. When true, a new refresh token is issued on each refresh grant, invalidating the previous one. |
httpServer | The HTTP server implementation to use: MockWebServerWrapper (default, supports takeRequest()) or NettyWrapper (required for HTTPS). Can also be a JSON object: {"type": "NettyWrapper", "ssl": {...}}. |
tokenCallbacks | A list of RequestMappingTokenCallback objects that define which claims to return based on request parameters. |
Additional token provider options:
{
"tokenProvider": {
"keyProvider": {
"algorithm": "ES256"
},
"systemTime": "2020-01-21T00:00:00Z"
}
}
The first path segment in any URL is the issuerId. Both OIDC and OAuth2 AS metadata endpoints are served:
GET /{issuerId}/.well-known/openid-configuration
GET /{issuerId}/.well-known/oauth-authorization-server
http://localhost:8080/default/.well-known/openid-configuration{
"issuer": "http://localhost:8080/default",
"authorization_endpoint": "http://localhost:8080/default/authorize",
"token_endpoint": "http://localhost:8080/default/token",
"jwks_uri": "http://localhost:8080/default/jwks",
"userinfo_endpoint": "http://localhost:8080/default/userinfo",
"introspection_endpoint": "http://localhost:8080/default/introspect",
"revocation_endpoint": "http://localhost:8080/default/revoke",
"end_session_endpoint": "http://localhost:8080/default/endsession"
}
Introspect (POST /{issuerId}/introspect) requires an Authorization header. Either Authorization: Bearer <token> or Authorization: Basic <credentials> is accepted. Requests without it will receive 400 invalid_client.
Revocation (POST /{issuerId}/revoke) only supports token_type_hint=refresh_token. Passing any other value returns 400 unsupported_token_type.
server.wellKnownUrl("default") // OIDC discovery URL
server.oauth2AuthorizationServerMetadataUrl("default") // OAuth2 AS metadata URL
server.tokenEndpointUrl("default")
server.jwksUrl("default")
server.userInfoUrl("default")
server.introspectUrl("default")
server.revocationEndpointUrl("default")
server.endSessionEndpointUrl("default")
server.baseUrl() // server root URL
navikt.github.io/mock-oauth2-server
This project is maintained by @navikt.
To raise an issue or question, open an issue in this repository.
For internal NAV contact, use the Slack channel #nais.
See the release notes for the full version history.
Fork the repo, check out a new branch, and build with:
./gradlew build
See CONTRIBUTING.md for more details.
This library is licensed under the MIT License.
See MIGRATION.md for upgrade instructions between versions.
46 followers · starred Feb 2023
13 followers · starred Aug 2024
24 followers · starred Aug 2024
22 followers · starred Jun 2023
Kotlin
94.4%
Java
2.9%
FreeMarker
1.9%