diff --git a/.github/workflows/release-maven.yml b/.github/workflows/release-maven.yml
index 8583da5cd..aa7f90cc4 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 }}
+ 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
diff --git a/backend/pom.xml b/backend/pom.xml
index 2ec3d5fd1..73d459bc3 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.2-SNAPSHOT
refarch-backend
This is the default starting project for a backend project based on the reference architecture of it@M
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);
+ }
+
+}