From 404692d6715f678e2612e1d65e7adec254b340ca Mon Sep 17 00:00:00 2001 From: vsxd Date: Thu, 12 Mar 2026 00:26:33 +0800 Subject: [PATCH] Fix phase1 auth flow gaps --- deploy/skillhub-ingress.yaml | 32 +++++++++ server/skillhub-app/pom.xml | 5 ++ .../skillhub/controller/AuthController.java | 9 +-- .../src/main/resources/application.yml | 4 ++ .../controller/AuthControllerTest.java | 67 +++++++++++++++++++ .../org.mockito.plugins.MockMaker | 1 + .../skillhub/auth/config/SecurityConfig.java | 19 ++++++ .../auth/identity/IdentityBindingService.java | 33 +++++++++ .../auth/oauth/AccountDisabledException.java | 11 +++ .../auth/oauth/AccountPendingException.java | 11 +++ .../auth/oauth/CustomOAuth2UserService.java | 26 ++++--- .../auth/oauth/GitHubClaimsExtractor.java | 46 ++++++++++++- .../auth/oauth/OAuth2LoginFailureHandler.java | 35 ++++++++++ .../auth/oauth/OAuthClaimsExtractor.java | 3 +- .../token/ApiTokenAuthenticationFilter.java | 6 +- 15 files changed, 291 insertions(+), 17 deletions(-) create mode 100644 deploy/skillhub-ingress.yaml create mode 100644 server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/AuthControllerTest.java create mode 100644 server/skillhub-app/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker create mode 100644 server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/AccountDisabledException.java create mode 100644 server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/AccountPendingException.java create mode 100644 server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/OAuth2LoginFailureHandler.java diff --git a/deploy/skillhub-ingress.yaml b/deploy/skillhub-ingress.yaml new file mode 100644 index 00000000..350826d8 --- /dev/null +++ b/deploy/skillhub-ingress.yaml @@ -0,0 +1,32 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: skillhub + annotations: + nginx.ingress.kubernetes.io/limit-rps: "10" + nginx.ingress.kubernetes.io/limit-burst-multiplier: "3" + nginx.ingress.kubernetes.io/limit-connections: "20" + nginx.ingress.kubernetes.io/server-snippet: | + location ~ ^/(oauth2/authorization|login/oauth2/code|api/v1/auth|api/v1/search|api/v1/skills/.*/download|api/v1/namespaces) { + limit_req zone=default burst=30 nodelay; + } +spec: + ingressClassName: nginx + rules: + - host: skills.example.com + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: skillhub-web + port: + number: 80 + - path: /api + pathType: Prefix + backend: + service: + name: skillhub-server + port: + number: 8080 diff --git a/server/skillhub-app/pom.xml b/server/skillhub-app/pom.xml index 2cd5a376..6d153aab 100644 --- a/server/skillhub-app/pom.xml +++ b/server/skillhub-app/pom.xml @@ -70,6 +70,11 @@ spring-boot-starter-test test + + org.springframework.security + spring-security-test + test + com.h2database h2 diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/AuthController.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/AuthController.java index d3ebfe4b..dabb0bf2 100644 --- a/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/AuthController.java +++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/AuthController.java @@ -1,8 +1,9 @@ package com.iflytek.skillhub.controller; import com.iflytek.skillhub.auth.rbac.PlatformPrincipal; -import jakarta.servlet.http.HttpSession; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -15,9 +16,9 @@ import java.util.Map; public class AuthController { @GetMapping("/me") - public ResponseEntity> me(HttpSession session) { - PlatformPrincipal principal = (PlatformPrincipal) session.getAttribute("platformPrincipal"); - if (principal == null) { + public ResponseEntity> me(@AuthenticationPrincipal PlatformPrincipal principal, + Authentication authentication) { + if (principal == null || authentication == null || !authentication.isAuthenticated()) { return ResponseEntity.status(401).build(); } return ResponseEntity.ok(Map.of( diff --git a/server/skillhub-app/src/main/resources/application.yml b/server/skillhub-app/src/main/resources/application.yml index 67b74ae3..f1fac7f5 100644 --- a/server/skillhub-app/src/main/resources/application.yml +++ b/server/skillhub-app/src/main/resources/application.yml @@ -23,6 +23,10 @@ spring: password: skillhub_dev hikari: maximum-pool-size: 10 + data: + redis: + host: ${REDIS_HOST:localhost} + port: ${REDIS_PORT:6379} session: store-type: redis redis: diff --git a/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/AuthControllerTest.java b/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/AuthControllerTest.java new file mode 100644 index 00000000..dc296376 --- /dev/null +++ b/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/AuthControllerTest.java @@ -0,0 +1,67 @@ +package com.iflytek.skillhub.controller; + +import com.iflytek.skillhub.auth.rbac.PlatformPrincipal; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.List; +import java.util.Set; + +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@AutoConfigureMockMvc +@ActiveProfiles("test") +class AuthControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Test + void meShouldReturnUnauthorizedForAnonymousRequest() throws Exception { + mockMvc.perform(get("/api/v1/auth/me")) + .andExpect(status().isUnauthorized()); + } + + @Test + void meShouldReturnCurrentPrincipal() throws Exception { + PlatformPrincipal principal = new PlatformPrincipal( + 42L, + "tester", + "tester@example.com", + "https://example.com/avatar.png", + "github", + Set.of("SUPER_ADMIN") + ); + + var auth = new UsernamePasswordAuthenticationToken( + principal, + null, + List.of(new SimpleGrantedAuthority("ROLE_SUPER_ADMIN")) + ); + + mockMvc.perform(get("/api/v1/auth/me").with(authentication(auth))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.userId").value(42)) + .andExpect(jsonPath("$.displayName").value("tester")) + .andExpect(jsonPath("$.oauthProvider").value("github")) + .andExpect(jsonPath("$.platformRoles[0]").value("SUPER_ADMIN")); + } + + @Test + void providersShouldExposeGithubLoginEntry() throws Exception { + mockMvc.perform(get("/api/v1/auth/providers")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data[0].id").value("github")) + .andExpect(jsonPath("$.data[0].authorizationUrl").value("/oauth2/authorization/github")); + } +} diff --git a/server/skillhub-app/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/server/skillhub-app/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 00000000..fdbd0b15 --- /dev/null +++ b/server/skillhub-app/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-subclass diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/config/SecurityConfig.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/config/SecurityConfig.java index c3be6327..848e324e 100644 --- a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/config/SecurityConfig.java +++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/config/SecurityConfig.java @@ -1,16 +1,21 @@ package com.iflytek.skillhub.auth.config; import com.iflytek.skillhub.auth.oauth.CustomOAuth2UserService; +import com.iflytek.skillhub.auth.oauth.OAuth2LoginFailureHandler; import com.iflytek.skillhub.auth.oauth.OAuth2LoginSuccessHandler; import com.iflytek.skillhub.auth.token.ApiTokenAuthenticationFilter; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpStatus; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.authentication.HttpStatusEntryPoint; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.csrf.CookieCsrfTokenRepository; import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; @Configuration @EnableWebSecurity @@ -18,13 +23,16 @@ public class SecurityConfig { private final CustomOAuth2UserService customOAuth2UserService; private final OAuth2LoginSuccessHandler successHandler; + private final OAuth2LoginFailureHandler failureHandler; private final ApiTokenAuthenticationFilter apiTokenAuthenticationFilter; public SecurityConfig(CustomOAuth2UserService customOAuth2UserService, OAuth2LoginSuccessHandler successHandler, + OAuth2LoginFailureHandler failureHandler, ApiTokenAuthenticationFilter apiTokenAuthenticationFilter) { this.customOAuth2UserService = customOAuth2UserService; this.successHandler = successHandler; + this.failureHandler = failureHandler; this.apiTokenAuthenticationFilter = apiTokenAuthenticationFilter; } @@ -43,6 +51,7 @@ public class SecurityConfig { .requestMatchers( "/api/v1/health", "/api/v1/auth/providers", + "/api/v1/auth/me", "/api/v1/skills/**", "/api/v1/namespaces/**", "/actuator/health", @@ -56,6 +65,16 @@ public class SecurityConfig { .oauth2Login(oauth2 -> oauth2 .userInfoEndpoint(userInfo -> userInfo.userService(customOAuth2UserService)) .successHandler(successHandler) + .failureHandler(failureHandler) + ) + .sessionManagement(session -> session + .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) + ) + .exceptionHandling(exceptions -> exceptions + .defaultAuthenticationEntryPointFor( + new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED), + new AntPathRequestMatcher("/api/**") + ) ) .logout(logout -> logout .logoutUrl("/api/v1/auth/logout") diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/identity/IdentityBindingService.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/identity/IdentityBindingService.java index 658be587..91d8f43f 100644 --- a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/identity/IdentityBindingService.java +++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/identity/IdentityBindingService.java @@ -57,6 +57,13 @@ public class IdentityBindingService { bindingRepo.save(binding); } + if (user.getStatus() == UserStatus.PENDING) { + throw new com.iflytek.skillhub.auth.oauth.AccountPendingException(); + } + if (user.getStatus() == UserStatus.DISABLED) { + throw new com.iflytek.skillhub.auth.oauth.AccountDisabledException(); + } + Set roles = roleBindingRepo.findByUserId(user.getId()).stream() .map(rb -> rb.getRole().getCode()) .collect(Collectors.toSet()); @@ -66,4 +73,30 @@ public class IdentityBindingService { user.getAvatarUrl(), claims.provider(), roles ); } + + @Transactional + public void createPendingUserIfAbsent(OAuthClaims claims) { + IdentityBinding existingBinding = bindingRepo + .findByProviderCodeAndSubject(claims.provider(), claims.subject()) + .orElse(null); + if (existingBinding != null) { + UserAccount existingUser = userRepo.findById(existingBinding.getUserId()) + .orElseThrow(() -> new IllegalStateException("User not found for binding")); + if (existingUser.getStatus() == UserStatus.DISABLED) { + throw new com.iflytek.skillhub.auth.oauth.AccountDisabledException(); + } + throw new com.iflytek.skillhub.auth.oauth.AccountPendingException(); + } + + UserAccount user = new UserAccount( + claims.providerLogin(), + claims.email(), + (String) claims.extra().get("avatar_url") + ); + user.setStatus(UserStatus.PENDING); + user = userRepo.save(user); + + IdentityBinding binding = new IdentityBinding(user.getId(), claims.provider(), claims.subject(), claims.providerLogin()); + bindingRepo.save(binding); + } } diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/AccountDisabledException.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/AccountDisabledException.java new file mode 100644 index 00000000..a2dd1b19 --- /dev/null +++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/AccountDisabledException.java @@ -0,0 +1,11 @@ +package com.iflytek.skillhub.auth.oauth; + +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2Error; + +public class AccountDisabledException extends OAuth2AuthenticationException { + + public AccountDisabledException() { + super(new OAuth2Error("account_disabled", "Account is disabled", null)); + } +} diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/AccountPendingException.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/AccountPendingException.java new file mode 100644 index 00000000..a5cbaac6 --- /dev/null +++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/AccountPendingException.java @@ -0,0 +1,11 @@ +package com.iflytek.skillhub.auth.oauth; + +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2Error; + +public class AccountPendingException extends OAuth2AuthenticationException { + + public AccountPendingException() { + super(new OAuth2Error("account_pending", "Account pending approval", null)); + } +} diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/CustomOAuth2UserService.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/CustomOAuth2UserService.java index 151168f0..774d4bef 100644 --- a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/CustomOAuth2UserService.java +++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/CustomOAuth2UserService.java @@ -4,6 +4,8 @@ import com.iflytek.skillhub.auth.identity.IdentityBindingService; import com.iflytek.skillhub.auth.policy.AccessDecision; import com.iflytek.skillhub.auth.policy.AccessPolicy; import com.iflytek.skillhub.auth.rbac.PlatformPrincipal; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import com.iflytek.skillhub.domain.user.UserStatus; import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; @@ -14,6 +16,7 @@ import org.springframework.security.oauth2.core.user.DefaultOAuth2User; import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.stereotype.Service; import java.util.HashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.function.Function; @@ -47,21 +50,28 @@ public class CustomOAuth2UserService implements OAuth2UserService UserStatus.ACTIVE; - case PENDING_APPROVAL -> UserStatus.PENDING; - case DENY -> throw new OAuth2AuthenticationException( + if (decision == AccessDecision.PENDING_APPROVAL) { + identityBindingService.createPendingUserIfAbsent(claims); + throw new AccountPendingException(); + } + if (decision == AccessDecision.DENY) { + throw new OAuth2AuthenticationException( new OAuth2Error("access_denied", "Access denied by policy", null)); - }; + } - PlatformPrincipal principal = identityBindingService.bindOrCreate(claims, initialStatus); + PlatformPrincipal principal = identityBindingService.bindOrCreate(claims, UserStatus.ACTIVE); var attrs = new HashMap<>(oAuth2User.getAttributes()); attrs.put("platformPrincipal", principal); - return new DefaultOAuth2User(oAuth2User.getAuthorities(), attrs, "login"); + var authorities = new LinkedHashSet(oAuth2User.getAuthorities()); + principal.platformRoles().stream() + .map(role -> new SimpleGrantedAuthority("ROLE_" + role)) + .forEach(authorities::add); + + return new DefaultOAuth2User(authorities, attrs, "login"); } } diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/GitHubClaimsExtractor.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/GitHubClaimsExtractor.java index a79bc367..f7e807b9 100644 --- a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/GitHubClaimsExtractor.java +++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/GitHubClaimsExtractor.java @@ -1,24 +1,64 @@ package com.iflytek.skillhub.auth.oauth; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.stereotype.Component; +import org.springframework.web.client.RestClient; + +import java.util.Comparator; +import java.util.List; +import org.springframework.stereotype.Component; import java.util.Map; @Component public class GitHubClaimsExtractor implements OAuthClaimsExtractor { + + private final RestClient restClient = RestClient.builder() + .baseUrl("https://api.github.com") + .defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE) + .build(); + @Override public String getProvider() { return "github"; } @Override - public OAuthClaims extract(OAuth2User oAuth2User) { + public OAuthClaims extract(OAuth2UserRequest request, OAuth2User oAuth2User) { Map attrs = oAuth2User.getAttributes(); + GitHubEmail primaryEmail = loadPrimaryEmail(request); + String email = primaryEmail != null ? primaryEmail.email() : (String) attrs.get("email"); + boolean emailVerified = primaryEmail != null + ? primaryEmail.verified() + : attrs.get("email") != null; + return new OAuthClaims( "github", String.valueOf(attrs.get("id")), - (String) attrs.get("email"), - attrs.get("email") != null, + email, + emailVerified, (String) attrs.get("login"), attrs ); } + + private GitHubEmail loadPrimaryEmail(OAuth2UserRequest request) { + List emails = restClient.get() + .uri("/user/emails") + .header(HttpHeaders.AUTHORIZATION, "Bearer " + request.getAccessToken().getTokenValue()) + .retrieve() + .body(new org.springframework.core.ParameterizedTypeReference>() {}); + + if (emails == null || emails.isEmpty()) { + return null; + } + + return emails.stream() + .filter(GitHubEmail::verified) + .sorted(Comparator.comparing(GitHubEmail::primary).reversed()) + .findFirst() + .orElse(null); + } + + private record GitHubEmail(String email, boolean primary, boolean verified) {} } diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/OAuth2LoginFailureHandler.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/OAuth2LoginFailureHandler.java new file mode 100644 index 00000000..71b90d01 --- /dev/null +++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/OAuth2LoginFailureHandler.java @@ -0,0 +1,35 @@ +package com.iflytek.skillhub.auth.oauth; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +public class OAuth2LoginFailureHandler extends SimpleUrlAuthenticationFailureHandler { + + @Override + public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, + AuthenticationException exception) + throws IOException, ServletException { + if (exception instanceof AccountPendingException) { + getRedirectStrategy().sendRedirect(request, response, "/pending-approval"); + return; + } + if (exception instanceof AccountDisabledException) { + getRedirectStrategy().sendRedirect(request, response, "/access-denied"); + return; + } + if (exception instanceof org.springframework.security.oauth2.core.OAuth2AuthenticationException oauth2Exception + && "access_denied".equals(oauth2Exception.getError().getErrorCode())) { + getRedirectStrategy().sendRedirect(request, response, "/access-denied"); + return; + } + + super.onAuthenticationFailure(request, response, exception); + } +} diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/OAuthClaimsExtractor.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/OAuthClaimsExtractor.java index d6e347df..826b3c9b 100644 --- a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/OAuthClaimsExtractor.java +++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/OAuthClaimsExtractor.java @@ -1,8 +1,9 @@ package com.iflytek.skillhub.auth.oauth; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; import org.springframework.security.oauth2.core.user.OAuth2User; public interface OAuthClaimsExtractor { String getProvider(); - OAuthClaims extract(OAuth2User oAuth2User); + OAuthClaims extract(OAuth2UserRequest request, OAuth2User oAuth2User); } diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/token/ApiTokenAuthenticationFilter.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/token/ApiTokenAuthenticationFilter.java index 5e8a5b78..c16ff398 100644 --- a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/token/ApiTokenAuthenticationFilter.java +++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/token/ApiTokenAuthenticationFilter.java @@ -10,6 +10,7 @@ import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; @@ -53,7 +54,10 @@ public class ApiTokenAuthenticationFilter extends OncePerRequestFilter { user.getId(), user.getDisplayName(), user.getEmail(), user.getAvatarUrl(), "api_token", roles ); - var auth = new UsernamePasswordAuthenticationToken(principal, null, List.of()); + var authorities = roles.stream() + .map(role -> new SimpleGrantedAuthority("ROLE_" + role)) + .toList(); + var auth = new UsernamePasswordAuthenticationToken(principal, null, authorities); SecurityContextHolder.getContext().setAuthentication(auth); }); });