Skip to main content

Security starter

jeap-spring-boot-security-starter turns a service into a Spring Security OAuth2 resource server: it validates the JWT bearer access token on every request, converts it into a JeapAuthenticationToken, and lets the application authorize the request against the roles carried in that token. It is the standard, mandated way to protect REST APIs in jEAP services — per the jEAP guidelines, every REST endpoint must be authenticated and authorized, including endpoints only called internally.

Calling other secured services is covered by jeap-spring-boot-security-client-starter; testing protected endpoints is covered by jeap-spring-boot-security-starter-test.

Add it

<dependency>
<groupId>ch.admin.bit.jeap</groupId>
<artifactId>jeap-spring-boot-security-starter</artifactId>
</dependency>

The starter auto-configures the resource server, a default deny-all SecurityFilterChain, token conversion and (optionally) introspection and the current-user endpoint. It targets the Spring WebMvc stack. The WebFlux/reactive stack is no longer supported.

Authentication

Authentication contexts

Authorization always happens within a context. jEAP distinguishes three, each issued and validated differently and exposed via JeapAuthenticationToken.getJeapAuthenticationContext() (parsed from the ctx JWT claim):

JeapAuthenticationContextMeaning
USERA request by a human (natural person, employee, or business-partner user) via a UI or mobile app. The token lifetime is at most 5 minutes.
SYSA service-to-service request made by an internal system using a technical user, typically via OAuth2 client credentials.
B2BA request from an external partner system arriving through the B2B/API gateway, bound to one business partner via an API subscription.

A context can also be propagated horizontally: a service may forward the token it received to a downstream service (see the client starter).

What the starter validates

For every incoming token the resource server enforces the jEAP token requirements:

  • the signature algorithm is RS256/RS512alg=none and weak algorithms are rejected;
  • the iss (issuer) claim matches a configured authorization server, and the signing key fits that issuer;
  • the aud (audience) claim matches the service's resource-id (or spring.application.name);
  • the authentication context (ctx) of the token is allowed for that authorization server.

On success the Authentication placed in the SecurityContext is a JeapAuthenticationToken (ch.admin.bit.jeap.security.resource.token), a JwtAuthenticationToken carrying the JWT claims plus the parsed userRoles and businessPartnerRoles (see Authorization).

Minimal configuration

jeap:
security:
oauth2:
resourceserver:
authorization-server:
issuer: https://keycloak.example.ch/auth/realms/myrealm

jwk-set-uri defaults to {issuer}/protocol/openid-connect/certs. The default audience is spring.application.name; set resource-id when tokens carry a restricted audience.

Multiple authorization servers and the B2B gateway

Trust several issuers at once with auth-servers[*], and configure a separate B2B gateway issuer (which defaults to the B2B context):

jeap:
security:
oauth2:
resourceserver:
auth-servers:
- issuer: https://keycloak.example.ch/auth/realms/users
authentication-contexts: [USER]
- issuer: https://keycloak.example.ch/auth/realms/system
authentication-contexts: [SYS]
b2b-gateway:
issuer: https://b2b-gw.example.ch
jwk-set-uri: https://b2b-gw.example.ch/certs

Customizing the filter chain

By default the starter protects all URIs with OAuth2 (plus CORS and CSRF). To expose a public API, a Swagger UI behind basic auth, or a self-contained-system UI, declare your own SecurityFilterChain bean with a higher precedence (lower @Order value) and a tight RequestMatcher — the first chain whose matcher accepts the request wins, so keep custom matchers narrow. The starter's chains use orders relative to Ordered.LOWEST_PRECEDENCE so applications can override them; use a dedicated role (hasRole(...), not just fullyAuthenticated()) for any custom authentication, and avoid HTTP session management.

Key properties

Prefix: jeap.security.oauth2.resourceserver.

PropertyDefaultDescription
resource-idExpected audience for tokens with a restricted audience
application-name${spring.application.name}Default expected audience when resource-id is unset
system-nameSystem name; setting it activates semantic-role authorization
authorization-server.issuerToken issuer (shortcut for a single auth server)
authorization-server.jwk-set-uri{issuer}/protocol/openid-connect/certsJWKS endpoint for signing certificates
authorization-server.authentication-contexts[USER, SYS]Allowed contexts for this auth server
authorization-server.jwks-connect-timeout-in-millis15000Connect timeout for fetching the JWKS
authorization-server.jwks-read-timeout-in-millis15000Read timeout for fetching the JWKS
b2b-gateway.issuer / b2b-gateway.jwk-set-uriB2B gateway issuer and JWKS endpoint (both required)
b2b-gateway.authentication-contexts[B2B]Allowed contexts for the B2B gateway
auth-servers[*]List of additional auth servers (same fields as authorization-server)
introspection.modeResource-level introspection mode (NONE, EXPLICIT, ALWAYS, LIGHTWEIGHT, CUSTOM)
log-user-accessfalseRegister a filter that logs each authenticated user's access
log.authentication-failure.enabledfalseLog failed authentications (invalid/missing bearer tokens)
log.access-denied.enabledfalseLog authorization failures (authenticated but not permitted)
log.access-denied.debugfalseAdd debug detail to denied-request logs (needs the line above)

Current-user endpoint

When enabled, the starter exposes the authenticated user's profile for a frontend to read, serialized from the JeapCurrentUser view (subject, name, username, roles, business-partner roles, locale, ...):

PropertyDefaultDescription
jeap.security.oauth2.current-user-endpoint.enabledfalseEnable the current-user endpoint
jeap.security.oauth2.current-user-endpoint.path/api/current-userPath of the GET endpoint

Authorization

Once a request is authenticated, the starter authorizes it against the roles carried in the JWT. jEAP distinguishes functional authorization (may the user invoke this operation? — checked on the REST layer before the call) from data authorization (may the user access these data? — often checked after the data is loaded, e.g. against a business partner or tenant). Authorization is a property of the REST API and must be performed on the REST layer.

Roles come from two JWT claims:

  • userroles — roles the user holds independently of any business partner (e.g. admins, technical users);
  • bproles — roles the user holds for a specific business partner, a { partnerId: [roles] } map.

The starter supports two role models, simple and semantic (plus application-defined authorities, derived in your own code from coarse roles — out of scope here).

Choosing a model

ModelActivationRole shape
Simpleresourceserver.system-name must NOT be setOpaque strings, e.g. admin
Semanticresourceserver.system-name must be setStructured system_%tenant_@resource_#operation

Semantic roles are simple roles whose parts have a defined meaning: system (the application that authorizes against the role; mandatory), tenant (which mandant may exercise it), resource (the resource type), and operation. Parts are separated by special characters so each is unambiguous; omitting a part acts as a wildcard for it (any part except system may be omitted). Two syntaxes exist — the standard system_%tenant_@resource_#operation and an eIAM-compatible alternative system_:tenant_@resource_!operation (eIAM disallows % and # in role names). SemanticApplicationRole parses both automatically.

Query methods

Both models expose the same query methods; semantic roles take role components instead of a single role string:

Method (simple)Method (semantic)Meaning
hasRole(role)hasRole(resource, operation)Has the role for at least one partner or independent of one
hasRoleForPartner(role, partner)hasRoleForPartner(resource, operation, partner)Has the role for the given business partner (bproles)
hasRoleForAllPartners(role)hasRoleForAllPartners(resource, operation)Has the role independent of any partner (userroles)
getPartnersForRole(role)getPartnersForRole(resource, operation)Which partners the user holds the role for

Semantic queries may supply fewer components: hasOperation(operation), hasRole(resource, operation) or hasRole(tenant, resource, operation) — only the supplied components are checked.

Declarative authorization

The query methods are available in the SpEL of Spring Security's @PreAuthorize / @PostAuthorize:

// simple roles
@PreAuthorize("hasRole('admin')")
public Data getData() { ... }

@PreAuthorize("hasRoleForPartner('reader', #partnerId)")
public Partner getPartner(String partnerId) { ... }
// semantic roles
@PreAuthorize("hasRole('anmeldung', 'lesen')")
public Data getData() { ... }

@PreAuthorize("hasRoleForPartner('anmeldung', 'lesen', #partnerId)")
public Partner getPartner(String partnerId) { ... }

IntelliJ does not recognise these custom methods, so there is no auto-completion inside the SpEL string.

Programmatic authorization

Some checks need loaded data first. Inject the matching authorization bean and call the same methods in code, throwing AccessDeniedException on failure:

import ch.admin.bit.jeap.security.resource.authentication.ServletSimpleAuthorization;

@RequiredArgsConstructor
class PartnerService {
private final ServletSimpleAuthorization jeapAuthorization;

Partner findPartner(String partnerId) {
Partner partner = repository.load(partnerId);
if (!jeapAuthorization.hasRoleForPartner("reader", partner.getPartnerId())) {
throw new AccessDeniedException("Missing role for partner " + partner.getPartnerId());
}
return partner;
}
}

For semantic roles inject ch.admin.bit.jeap.security.resource.semanticAuthentication.ServletSemanticAuthorization instead (only created when system-name is set). It additionally offers hasOperation(...), getAllRolesForOperation(...) and partner-scoped variants.

The authentication token

JeapAuthenticationToken (ch.admin.bit.jeap.security.resource.token) is the Authentication in the SecurityContext. Besides getUserRoles() and getBusinessPartnerRoles() it exposes claims via getClientId(), getTokenSubject(), getPreferredUsername(), getTokenName(), getTokenGivenName(), getTokenFamilyName(), getTokenExtId(), getTokenLocale(), getAdminDirUID() and getJeapAuthenticationContext(). A read-only view of the user profile is also available through the JeapCurrentUser interface (ch.admin.bit.jeap.security.user).