- Java 99.5%
- Shell 0.5%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| .github/workflows | ||
| .mvn | ||
| .run | ||
| jme-swagger-auth-scs | ||
| jme-swagger-service | ||
| jme-swagger-test | ||
| .gitignore | ||
| .trivyignore | ||
| CHANGELOG.md | ||
| CONTRIBUTING.md | ||
| exclude-patterns.txt | ||
| LICENSE | ||
| mvnw | ||
| mvnw.cmd | ||
| pom.xml | ||
| publiccode.yml | ||
| README.md | ||
| SECURITY.md | ||
| setPomVersions.sh | ||
| THIRD-PARTY-LICENSES.md | ||
JME Swagger Example
This example shows how a jEAP microservice documents its REST APIs and offers them for interactive use in a Swagger UI: springdoc-openapi generates OpenAPI documents from the controllers and DTOs of the service, and the jEAP Swagger starter serves them, decides who may read them and connects the Authorize button of the Swagger UI to the OAuth 2.0 authorization server of the service.
The example is self-contained: it brings its own authorization server, so the personal OAuth 2.0 login in the Swagger UI and a call to a protected endpoint can be tried out on a developer machine without any infrastructure. Everything below can be executed as it is written, starting from a fresh clone.
The example uses the following jEAP libraries:
- jeap-spring-boot-swagger-starter: springdoc-openapi and
the auto configuration behind the
jeap.swagger.*properties. This starter is what the example is about. - jeap-spring-boot-security-starter: turns the service into an OAuth 2.0 resource server. It is not required by the Swagger starter, it is part of the example to show that an OAuth 2.0 protected API can be called from the Swagger UI.
- jeap-spring-boot-monitoring-starter: the actuator endpoints, which springdoc documents as an additional API group.
- jeap-oauth-mock-server: the authorization server of this example. It issues tokens to anyone who asks and must therefore never be used outside of development and test environments.
Modules
- jme-swagger-service: The documented example service. It offers a small messages API in two versions and in two API groups, an internal and an external one, and it is the service whose Swagger UI you open below.
- jme-swagger-auth-scs: An instance of the jEAP OAuth mock server, acting as the authorization server of the example. It issues the access tokens that the Swagger UI obtains when a user presses Authorize.
- jme-swagger-test: The integration tests of the example, plus the reusable HTTP checks (SwaggerSmokeChecks) they are built from.
Prerequisites
To use this project, ensure you have the following installed:
- Java Development Kit (JDK): Version 25.
- Google Chrome (optional): Only needed for the browser-based integration test, see Integration tests. Everything else works without it.
Note: Use the provided maven wrapper to build and run the project. It compiles what it needs, so no separate build step is required for the walkthrough below.
Getting started
The walkthrough starts the two services on the command line. In IntelliJ IDEA the same three starts are available as the run configurations in .run: jme-swagger-auth-scs [local], jme-swagger-service [local] and jme-swagger-service [local,secured] (the last one for Trying the SECURED variant locally).
1. Start the authorization server
In a first terminal:
./mvnw --projects jme-swagger-auth-scs spring-boot:run -Dspring-boot.run.profiles=local
It starts on port 8081 and logs the client and the user it has been configured with:
Base URL: http://localhost:8081/jme-swagger-auth-scs
Adding client from configuration: jme-swagger-tester
Adding User from configuration: user (Maxine Muster)
Both come from application.yml: jme-swagger-tester is the
OAuth 2.0 client of the Swagger UI, and user is the demo user you will log in as. The user has the roles
jme_@swagger_#read and jme_@swagger_#write for the business partner 12345, which are exactly the roles the
operations of the example service require.
2. Start the example service
In a second terminal:
./mvnw --projects jme-swagger-service spring-boot:run -Dspring-boot.run.profiles=local
It starts on port 8080 and prints the URL of its Swagger UI:
----------------------------------------------------------
jme-swagger-service is running!
SwaggerUI: http://localhost:8080/jme-swagger-service/swagger-ui.html
Profile(s): [local]
----------------------------------------------------------
3. Open the Swagger UI
Open http://localhost:8080/jme-swagger-service/swagger-ui.html in a browser. The URL redirects to
/swagger-ui/index.html, the Swagger UI of the service.
What you see is the Messaging API, the internal API of the service. The selection box at the top right lets you
choose another API definition; it lists them sorted by name, and the example serves three:
| API definition | Contents |
|---|---|
Actuator |
The actuator endpoints, documented automatically because springdoc.show-actuator is true in the jEAP starter |
External Messaging API |
The reduced API for external consumers: /api/messages/mine |
Messaging API |
The internal API of the service: /api/messages (deprecated V1) and /api/messages/v2 |
Swagger UI shows the first definition of that list ( which would be the Actuator group here) unless it is told
otherwise. The example sets springdoc.swagger-ui.urls-primary-name: Messaging API in application.yml
and that definition is then the one that is pre-selected in the Swagger UI.
That the Swagger UI is accessible at all and without login is the effect of jeap.swagger.status: OPEN in
application-local.yml. Without it the service would use
the default of the starter, DISABLED, and answer 403 here — see
What the jEAP Swagger starter does.
4. Check that the documentation is open but the API is not
While the documentation is public (in the local profile), the documented API is not. Executing one of the API's operations
before logging in will result in a 401 error.
5. Log in through the Swagger UI
Press the Authorize button at the top right of the Swagger UI. The dialog Available authorizations lists one section per authentication scheme and, for OAuth 2.0, one section per grant type the authorization server announces:
OIDC (OAuth2, authorization_code with PKCE) is the one to use in this example,
for details see Why the Authorize dialog offers client_credentials.
In the section OIDC (OAuth2, authorization_code with PKCE):
client_idis already filled in withjme-swagger-tester(configured withspringdoc.swagger-ui.oauth.client-idin application.yml).- Leave
client_secretempty. The Swagger UI is a public client here: it cannot keep a secret, and it proves instead with PKCE that it is the same client that started the login. - Tick the scope
openid. - Press Authorize.
A window with the login form of the mock authorization server opens (Login — OpenID Connect Mock Server). It is already filled in for you:
- Username:
user, the only user of this example, named Maxine Muster. - Business Partner Roles:
12345:jme_@swagger_#readand12345:jme_@swagger_#writeare ticked. These end up in the access token and are what the API operations check. - User Roles:
jme_@swagger_#readandjme_@swagger_#writeare offered as well but are not ticked. They are the same roles without a business partner; the operations of this example authorize per business partner, so the token does not need them and leaving them unticked keeps visible which of the two the example uses. - No password has to be typed, the mock server accepts a fixed one that the form sends for you.
Press Submit. The window closes, the button in the section now reads Logout, and the padlocks of the operations are closed. Press Close to leave the dialog.
6. Try an operation out
The Swagger UI now sends the access token with every call. springdoc.swagger-ui.tryItOutEnabled is true in the jEAP
starter, so Try it out is already active and the input fields are editable right away.
Create a message: open PUT /api/messages/v2/{messageId} under the tag Messages (V2), enter 1 as messageId and
press Execute. The request body is pre-filled with the example values of the @Schema annotations of
Message2Dto; the
read-only fields id and timeSend are not part of it because the server sets them:
{
"text": "Hello World",
"receiver": "12345"
}
The response is 201 with the stored message:
{
"id": "1",
"text": "Hello World",
"receiver": "12345",
"timeSend": "2026-08-03T21:15:11.542229288+02:00"
}
Read it back with GET /api/messages/v2 — it answers 200 with a list containing the message. Selecting
External Messaging API in the API selection box shows the same message through the reduced external interface,
GET /api/messages/mine; authorizing works there in exactly the same way.
To see the other side again, press Authorize, then Logout, and execute GET /api/messages/v2 once more: it now
answers 401.
7. Stop the services
Stop both terminals with Ctrl+C. The example keeps its messages in memory only, so nothing is left behind.
What the jEAP Swagger starter does
Adding a single dependency to a service is enough to get everything shown above:
<dependency>
<groupId>ch.admin.bit.jeap</groupId>
<artifactId>jeap-spring-boot-swagger-starter</artifactId>
</dependency>
The starter pulls in springdoc-openapi (springdoc-openapi-starter-webmvc-ui), applies the jEAP defaults for it, adds
a security filter chain for the Swagger paths and, if the service is a jEAP resource server, registers the OpenAPI
security scheme that makes the Authorize button work.
No version is given in the snippet: it is managed by the jEAP Spring Boot parent this example inherits from
(ch.admin.bit.jeap:jeap-spring-boot-parent, see the root pom.xml), which is where a jEAP service gets a
consistent set of versions from.
The starter itself is documented on two pages of the jEAP documentation, which go beyond what is shown here:
- Swagger starter
— the reference of the starter and of its
jeap.swagger.*properties. - Swagger / OpenAPI — how APIs are documented and published in jEAP.
Who may read the documentation: jeap.swagger.status
The property jeap.swagger.status decides what happens with the Swagger UI and the generated OpenAPI documents:
| Status | Effect | Typical use |
|---|---|---|
DISABLED |
Default. Denied for everyone, the service answers 403. There is no way to authenticate. |
Production environments |
OPEN |
Served to everyone without authentication. | Local development, development environments |
SECURED |
Served after HTTP basic authentication with the configured Swagger credentials. | Environments where the documentation stays internal |
CUSTOM |
The starter does not configure any security for the Swagger paths at all — the application does it itself. | Special cases not covered by the three above |
The OAuth 2.0 login of the Swagger UI
As soon as jeap.security.oauth2.resourceserver.authorization-server.issuer is configured, the starter registers an
OpenAPI security scheme named OIDC of type openIdConnect, pointing at the discovery document of that authorization
server. A service marks its operations as protected by referring to that scheme, which this example does once for all
operations in
SwaggerConfig:
@OpenAPIDefinition(
// ...
security = {@SecurityRequirement(name = "OIDC")}
)
That is what puts the Authorize button into the Swagger UI, the padlocks onto the operations and the access token into the requests of Try it out.
Trying the SECURED variant locally
The profile application-secured.yml switches the
service to jeap.swagger.status: SECURED and sets a password. Start the service with it, in addition to the local
profile (the authorization server can keep running):
./mvnw --projects jme-swagger-service spring-boot:run -Dspring-boot.run.profiles=local,secured
Opening http://localhost:8080/jme-swagger-service/swagger-ui.html now makes the browser ask for credentials. Enter the
user swagger (the default of jeap.swagger.secured.username) and the password secret and the Swagger UI appears,
behaving exactly as before, personal OAuth 2.0 login included.
How this example uses springdoc-openapi
Apart from the jeap.swagger.* properties, this example documents its API the standard springdoc way. The interesting
parts, in the order in which springdoc uses them:
- One OpenAPI definition per service.
SwaggerConfig carries the
@OpenAPIDefinitionwith the title, description, version, contact and external documentation link that all APIs of the service share, plus the security requirement described above. - One
GroupedOpenApibean per API. InternalApiSwaggerConfig and ExternalApiSwaggerConfig each declare one, selecting their operations by package and path. The group name is what the API selection box of the Swagger UI shows and what appears in the document URL/api-docs/{group}(URL encoded — the group names of this example contain a space, so the document is at/api-docs/Messaging%20API). The external group also shows howaddOpenApiCustomizer(...)overrides parts of the shared definition — here the contact, the external documentation link and the version, all of which differ for an external audience. - API versioning. The messages API exists twice: the deprecated
V1, whose
message IDs must be UUIDs, and the current
V2, whose
IDs can be any string. Both live in the same API group under different paths, so consumers see the old and the new
operations side by side and can migrate at their own pace.
@Deprecatedon the controller and on the DTO carries over into the OpenAPI document, and the Swagger UI strikes the deprecated operations through. info.version. The OpenAPI specification requires a version in every document. It is the version of the API the document describes as a whole, not of a single operation: the shared definition declares2.0.0, the current major version of the messages API, and that the deprecated V1 is still part of the same document is visible in its paths and tags. The external API is a different API with a lifecycle of its own and is still at1.0.0, which its group sets with a customizer.- Annotations on the controllers.
@Tagdocuments the group of operations a controller contributes,@Operationits individual operations and@ApiResponsethe responses that are not obvious from the return type, such as the404ofreadSingle. - Annotations on the DTOs.
@Schemaon the fields of Message2Dto adds the description, marks fields as required (requiredMode) or as set by the server (accessMode = READ_ONLY) and gives anexample. The examples are what makes Try it out usable without typing.
See springdoc.org for the springdoc reference documentation and the jEAP documentation on Swagger / OpenAPI for the jEAP specific parts.
Why the Authorize dialog offers client_credentials
The Authorize dialog shows a client_credentials section (and a refresh_token and a token exchange one) although
this example only demonstrates the personal login of a user with the authorization code flow and PKCE. Filling those
sections in leads to an error instead of a token. The reason is a chain of defaults, and it is worth understanding.
- The jEAP Swagger starter registers the security scheme as
openIdConnectand not asoauth2. Such a scheme has no list of flows of its own — it only points at the OpenID Connect discovery document of the authorization server. - The Swagger UI therefore reads the available grant types out of that discovery document and renders one section per
entry of
grant_types_supported. - An authorization server lists there what it supports in general, not what a specific client is allowed to use.
- The client of the Swagger UI,
jme-swagger-tester, is registered as a public client in the user context (context: "USER"in application.yml), which means the authorization code grant and nothing else, and it has no client secret. Aclient_credentialsrequest with this client is rejected by the authorization server.
This is intentional: a machine-to-machine client that logs in with client_credentials has no user, and the operations
of this example are authorized with the roles of a user. The alternative — overriding the security scheme of the
starter with a hand-written oauth2 scheme that lists only the authorization code flow — would hide the other sections
but also duplicate configuration that the starter and the authorization server already provide.
Integration tests
./mvnw verify
builds the project and runs
SwaggerExampleIT, an
end-to-end test of everything described in this README. It starts the mock authorization server and three instances of
the example service — one per jeap.swagger.status the example demonstrates — as separate mvnw spring-boot:run
processes on reserved free ports, so it does not interfere with services you started manually. It then verifies from
the outside that
- with
OPEN, the Swagger UI and the OpenAPI documents of both API groups are served without credentials, - the Swagger UI is wired for the authorization code flow with PKCE (redirect URL, client id and the PKCE flag),
- the documented API answers
401without an access token, - an access token obtained through a scripted authorization code flow with PKCE — the same flow the Authorize button runs — is accepted by the API,
- with
SECURED, the documentation needs the basic credentialsswagger/secretwhile the API still needs a token, - with
DISABLED, the documentation answers403for everyone.
The nested SwaggerUiPlaywrightIT repeats the login in a real browser with
Playwright: it opens the Swagger UI, presses Authorize, submits the login form of the
mock server and executes a protected operation with Try it out, expecting 200. It drives an installed Google Chrome
through the Playwright chrome channel and never downloads a browser; if no system Chrome is found, the test is
skipped and the build stays green. Where a browser is part of the build environment a build can pass
-DrequireBrowser=true and a missing browser fails instead of skipping.
The checks themselves live in
SwaggerSmokeChecks in
src/main/java, and are published as part of the jme-swagger-test artifact.
AfterDeploymentSmokeTestIT
runs the same smoke tests on already running services.
./mvnw verify -pl jme-swagger-test -DdeployStage=local -Dit.test=AfterDeploymentSmokeTestIT
Without -DdeployStage=local that test is skipped, because there is nothing running to check.
Note
This repository is part of the open source distribution of JME. See github.com/jme-admin-ch/jme for more information.
Changes
This project is versioned using Semantic Versioning and all changes are documented in CHANGELOG.md following the format defined in Keep a Changelog.
License
This repository is Open Source Software licensed under the Apache License 2.0.