Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 12 additions & 8 deletions .github/workflows/release-maven.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
4 changes: 2 additions & 2 deletions backend/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.1.0</version>
<relativePath/>
<relativePath />
</parent>

<groupId>de.muenchen.oss.refarch</groupId>
<artifactId>refarch-backend</artifactId>
<version>0.0.1-SNAPSHOT</version>
<version>0.0.2-SNAPSHOT</version>

<name>refarch-backend</name>
<description>This is the default starting project for a backend project based on the reference architecture of it@M</description>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,15 @@
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;
import org.springframework.stereotype.Component;
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)
Expand All @@ -28,6 +29,7 @@ public class RequestResponseLoggingFilter extends OncePerRequestFilter {
private static final List<String> CHANGING_METHODS = List.of(HttpMethod.POST.name(), HttpMethod.PUT.name(), HttpMethod.PATCH.name(),
HttpMethod.DELETE.name());

private final AuthUtils authUtils;
private final SecurityProperties securityProperties;

/**
Expand All @@ -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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,35 +1,35 @@
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
* {@link SecurityContextHolder}.
*
* @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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}

}