From e1698e0641668e24f411c4e55ed173581ec3c75a Mon Sep 17 00:00:00 2001 From: Tobias Stadler <28538704+devtobi@users.noreply.github.com> Date: Tue, 28 Jul 2026 13:04:12 +0200 Subject: [PATCH 1/5] :sparkles: Username extraction claim configurable (#1740) --- .../filter/RequestResponseLoggingFilter.java | 8 +- .../security/SecurityProperties.java | 6 ++ .../refarch/backend/security/AuthUtils.java | 24 ++--- .../backend/security/AuthUtilsTest.java | 95 +++++++++++++++++++ 4 files changed, 118 insertions(+), 15 deletions(-) create mode 100644 backend/src/test/java/de/muenchen/oss/refarch/backend/security/AuthUtilsTest.java diff --git a/backend/src/main/java/de/muenchen/oss/refarch/backend/configuration/filter/RequestResponseLoggingFilter.java b/backend/src/main/java/de/muenchen/oss/refarch/backend/configuration/filter/RequestResponseLoggingFilter.java index b97aeb498..bbef48f78 100644 --- a/backend/src/main/java/de/muenchen/oss/refarch/backend/configuration/filter/RequestResponseLoggingFilter.java +++ b/backend/src/main/java/de/muenchen/oss/refarch/backend/configuration/filter/RequestResponseLoggingFilter.java @@ -10,6 +10,7 @@ import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.jspecify.annotations.NonNull; import org.springframework.boot.web.servlet.FilterRegistration; import org.springframework.http.HttpMethod; import org.springframework.security.core.context.SecurityContext; @@ -17,7 +18,7 @@ import org.springframework.web.filter.OncePerRequestFilter; /** - * This filter logs the username for requests. + * This filter logs the username from requests using the {@link AuthUtils} bean. */ @Component @FilterRegistration(urlPatterns = "/*", order = 1) @@ -28,6 +29,7 @@ public class RequestResponseLoggingFilter extends OncePerRequestFilter { private static final List CHANGING_METHODS = List.of(HttpMethod.POST.name(), HttpMethod.PUT.name(), HttpMethod.PATCH.name(), HttpMethod.DELETE.name()); + private final AuthUtils authUtils; private final SecurityProperties securityProperties; /** @@ -54,12 +56,12 @@ public enum LoggingMode { * {@inheritDoc} */ @Override - protected void doFilterInternal(final HttpServletRequest request, final HttpServletResponse response, final FilterChain filterChain) + protected void doFilterInternal(final @NonNull HttpServletRequest request, final @NonNull HttpServletResponse response, final FilterChain filterChain) throws ServletException, IOException { filterChain.doFilter(request, response); if (checkForLogging(request)) { log.info("User {} executed {} on URI {} with http status {}", - AuthUtils.getUsername(), + authUtils.getUsername(), request.getMethod(), request.getRequestURI(), response.getStatus()); diff --git a/backend/src/main/java/de/muenchen/oss/refarch/backend/configuration/security/SecurityProperties.java b/backend/src/main/java/de/muenchen/oss/refarch/backend/configuration/security/SecurityProperties.java index 67df25e5e..4e38cb7f7 100644 --- a/backend/src/main/java/de/muenchen/oss/refarch/backend/configuration/security/SecurityProperties.java +++ b/backend/src/main/java/de/muenchen/oss/refarch/backend/configuration/security/SecurityProperties.java @@ -26,6 +26,12 @@ public class SecurityProperties { */ @NotBlank private String clientId; + /** + * Name of the JWT claim that contains the username. + * Defaults to "preferred_username" if not configured. + */ + @NotBlank private String usernameClaim = "preferred_username"; + /** * URI of the endpoint used for fetching permissions, * see also {@link KeycloakPermissionsAuthoritiesConverter}. diff --git a/backend/src/main/java/de/muenchen/oss/refarch/backend/security/AuthUtils.java b/backend/src/main/java/de/muenchen/oss/refarch/backend/security/AuthUtils.java index e73ce7554..65ad9ab70 100644 --- a/backend/src/main/java/de/muenchen/oss/refarch/backend/security/AuthUtils.java +++ b/backend/src/main/java/de/muenchen/oss/refarch/backend/security/AuthUtils.java @@ -1,21 +1,23 @@ package de.muenchen.oss.refarch.backend.security; +import de.muenchen.oss.refarch.backend.configuration.security.SecurityProperties; +import lombok.RequiredArgsConstructor; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; +import org.springframework.stereotype.Component; /** - * Utilities for authentication data. + * Utility bean for authentication related data. */ -public final class AuthUtils { +@Component +@RequiredArgsConstructor +public class AuthUtils { public static final String NAME_UNAUTHENTICATED_USER = "unauthenticated"; - private static final String TOKEN_USER_NAME = "preferred_username"; - - private AuthUtils() { - } + private final SecurityProperties securityProperties; /** * Extracts the user name from the existing Spring Security Context via @@ -23,13 +25,11 @@ private AuthUtils() { * * @return the username or an "unauthenticated" if no {@link Authentication} exists */ - public static String getUsername() { + public String getUsername() { final Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - if (authentication instanceof JwtAuthenticationToken) { - final JwtAuthenticationToken jwtAuth = (JwtAuthenticationToken) authentication; - return (String) jwtAuth.getTokenAttributes().getOrDefault(TOKEN_USER_NAME, null); - } else if (authentication instanceof UsernamePasswordAuthenticationToken) { - final UsernamePasswordAuthenticationToken usernameAuth = (UsernamePasswordAuthenticationToken) authentication; + if (authentication instanceof JwtAuthenticationToken jwtAuth) { + return (String) jwtAuth.getTokenAttributes().get(securityProperties.getUsernameClaim()); + } else if (authentication instanceof UsernamePasswordAuthenticationToken usernameAuth) { return usernameAuth.getName(); } else { return NAME_UNAUTHENTICATED_USER; diff --git a/backend/src/test/java/de/muenchen/oss/refarch/backend/security/AuthUtilsTest.java b/backend/src/test/java/de/muenchen/oss/refarch/backend/security/AuthUtilsTest.java new file mode 100644 index 000000000..53b5c1371 --- /dev/null +++ b/backend/src/test/java/de/muenchen/oss/refarch/backend/security/AuthUtilsTest.java @@ -0,0 +1,95 @@ +package de.muenchen.oss.refarch.backend.security; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +import de.muenchen.oss.refarch.backend.configuration.security.SecurityProperties; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; + +@ExtendWith(MockitoExtension.class) +class AuthUtilsTest { + + private static final String USERNAME_CLAIM = "preferred_username"; + + @Mock + private SecurityProperties securityProperties; + + @InjectMocks + private AuthUtils authUtils; + + @AfterEach + void tearDown() { + SecurityContextHolder.clearContext(); + } + + @Test + void givenJwtWithUsernameClaim_thenReturnUsername() { + // given + when(securityProperties.getUsernameClaim()).thenReturn(USERNAME_CLAIM); + final Jwt jwt = Jwt.withTokenValue("token") + .header("alg", "none") + .claim(USERNAME_CLAIM, "john.doe") + .build(); + final JwtAuthenticationToken authentication = new JwtAuthenticationToken(jwt); + SecurityContextHolder.getContext().setAuthentication(authentication); + + // when + final String username = authUtils.getUsername(); + + // then + assertThat(username).isEqualTo("john.doe"); + } + + @Test + void givenJwtWithoutUsernameClaim_thenReturnUnauthenticatedUser() { + // given + when(securityProperties.getUsernameClaim()).thenReturn(USERNAME_CLAIM); + final Jwt jwt = Jwt.withTokenValue("token") + .header("alg", "none") + .claims(claims -> claims.put("other", "value")) + .build(); + final JwtAuthenticationToken authentication = new JwtAuthenticationToken(jwt); + SecurityContextHolder.getContext().setAuthentication(authentication); + + // when + final String username = authUtils.getUsername(); + + // then + assertThat(username).isNull(); + } + + @Test + void givenUsernamePasswordAuthentication_thenReturnUsername() { + // given + final UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken("jane.doe", "password"); + SecurityContextHolder.getContext().setAuthentication(authentication); + + // when + final String username = authUtils.getUsername(); + + // then + assertThat(username).isEqualTo("jane.doe"); + } + + @Test + void givenNoAuthentication_thenReturnUnauthenticatedUser() { + // given + SecurityContextHolder.clearContext(); + + // when + final String username = authUtils.getUsername(); + + // then + assertThat(username).isEqualTo(AuthUtils.NAME_UNAUTHENTICATED_USER); + } + +} From 32c8d7fc96e0817171b56f59202e0e4968e5ba70 Mon Sep 17 00:00:00 2001 From: Hans <11695964+hupling@users.noreply.github.com> Date: Wed, 29 Jul 2026 08:09:46 +0200 Subject: [PATCH 2/5] Add release method input to Maven release workflow --- .github/workflows/release-maven.yml | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/.github/workflows/release-maven.yml b/.github/workflows/release-maven.yml index 8583da5cd..c857352ae 100644 --- a/.github/workflows/release-maven.yml +++ b/.github/workflows/release-maven.yml @@ -4,14 +4,18 @@ name: release-maven on: workflow_dispatch: inputs: + release-method: + type: choice + description: "Release-Methode = patch, minor oder major" + required: true + options: + - patch + - minor + - major release-version: description: "Version to use when preparing a release (e.g., 1.2.3)" - required: true - default: "X.Y.Z" - development-version: - description: "Version to use for new local working copy (e.g., 1.2.4-SNAPSHOT)" - required: true - default: X.Y.Z-SNAPSHOT + required: false + default: "" app-path: type: choice description: Service-Name @@ -31,11 +35,11 @@ jobs: ARTIFACT_NAME: ${{ steps.release-maven-artifact.outputs.artifact-name }} steps: - id: release-maven-artifact - uses: it-at-m/lhm_actions/action-templates/actions/action-maven-release@4ed8f906bab8111fbd9a7af8e3f4129f9b721757 # v1.2.3 + uses: it-at-m/lhm_actions/action-templates/actions/action-maven-release@330-action-release-mvn-should-offer-input-variable-to-use-patchminormajor with: app-path: ${{ inputs.app-path }} releaseVersion: ${{ inputs.release-version }} - developmentVersion: ${{ inputs.development-version }} + release-method: ${{ inputs.release-method }} use-pr: "true" # the following variables are necessary to publish the packages to maven central # docs: https://it-at-m.github.io/lhm_actions/workflows.html#maven-central From 544337d51e0789b9b2e87bb320106cd506f8a3e6 Mon Sep 17 00:00:00 2001 From: Hans <11695964+hupling@users.noreply.github.com> Date: Wed, 29 Jul 2026 08:17:03 +0200 Subject: [PATCH 3/5] Update release-maven.yml --- .github/workflows/release-maven.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release-maven.yml b/.github/workflows/release-maven.yml index c857352ae..aa7f90cc4 100644 --- a/.github/workflows/release-maven.yml +++ b/.github/workflows/release-maven.yml @@ -39,7 +39,7 @@ jobs: with: app-path: ${{ inputs.app-path }} releaseVersion: ${{ inputs.release-version }} - release-method: ${{ inputs.release-method }} + releaseMethod: ${{ inputs.release-method }} use-pr: "true" # the following variables are necessary to publish the packages to maven central # docs: https://it-at-m.github.io/lhm_actions/workflows.html#maven-central From 33d5d396c5703b4dbc9db12596e23aad3c28c497 Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Wed, 29 Jul 2026 06:19:48 +0000 Subject: [PATCH 4/5] [maven-release-plugin] prepare release refarch-backend-0.0.1 --- backend/pom.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/pom.xml b/backend/pom.xml index 2ec3d5fd1..316868a86 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -6,12 +6,12 @@ org.springframework.boot spring-boot-starter-parent 4.1.0 - + de.muenchen.oss.refarch refarch-backend - 0.0.1-SNAPSHOT + 0.0.1 refarch-backend This is the default starting project for a backend project based on the reference architecture of it@M @@ -27,7 +27,7 @@ https://github.com/it-at-m/refarch-templates.git scm:git:https://github.com/it-at-m/refarch-templates.git scm:git:https://github.com/it-at-m/refarch-templates.git - HEAD + refarch-backend-0.0.1 From f1e6b19f533ec0f5dcdc1c786b4f4026c9d4a94f Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Wed, 29 Jul 2026 06:19:48 +0000 Subject: [PATCH 5/5] [maven-release-plugin] prepare for next development iteration --- backend/pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/pom.xml b/backend/pom.xml index 316868a86..73d459bc3 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -11,7 +11,7 @@ de.muenchen.oss.refarch refarch-backend - 0.0.1 + 0.0.2-SNAPSHOT refarch-backend This is the default starting project for a backend project based on the reference architecture of it@M @@ -27,7 +27,7 @@ https://github.com/it-at-m/refarch-templates.git scm:git:https://github.com/it-at-m/refarch-templates.git scm:git:https://github.com/it-at-m/refarch-templates.git - refarch-backend-0.0.1 + HEAD