英文:
Spring Auth Server doesn't authorize HTTP requests to secured endpoints by the passed Bearer token
问题
关键点
我正在使用 Spring Boot 3.1.0 开发一个应用程序,使用 Spring授权服务器 实现OAuth 2.1服务器,用于Auth Code Flow与PKCE。
OAuth运行良好,但一旦我继续处理服务API部分并对其进行安全保护,我的应用程序就拒绝对传递在标头中的Bearer令牌的API端点的传入http请求进行授权。
主要问题
如何在这个单模块Web服务器中使用Bearer令牌身份验证来保护我的REST API端点?<br>这可能吗?我应该怎么做?
测试用例
Spring应用程序日志:
# 我尝试用OAuth代码交换访问令牌
23:16:44.991 [nio-8080-exec-4] o.s.security.web.FilterChainProxy: Securing POST /oauth2/token
23:16:45.059 [nio-8080-exec-4] o.s.a.w.OAuth2ClientAuthenticationFilter: Set SecurityContextHolder authentication to OAuth2ClientAuthenticationToken
# 然后我尝试使用此令牌进行默认OAuth提供的端点(结果:200 OK)
...
...
源代码
安全配置:
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
...
...
}
REST API控制器:
```java
@RestController
@RequestMapping("/api/user")
@RequiredArgsConstructor
public class UserApiController {
...
...
}
应用程序配置:
spring:
datasource:
...
...
...
jpa:
...
...
security:
oauth2:
resourceserver:
...
...
logging:
...
...
server:
...
...
Maven项目配置的一部分:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.0</version>
<relativePath/>
</parent>
<dependencies>
<!-- Spring Boot Starter -->
...
...
<!-- Thymeleaf Extras: Spring Security 5 -->
...
...
<!-- PostgreSQL JDBC Driver -->
...
...
<!-- Lombok -->
...
...
</dependencies>
英文:
The key point
I am developing an application in Spring Boot 3.1.0 using the Spring Authorization Server to implement an OAuth 2.1 server for Auth Code Flow with PKCE.
The OAuth works perfectly, but as soon as I continued to work on the service API part and secured it, my application refused to authorize incoming http requests to API endpoints with the Bearer token passed in the header.
The main question
How to secure my REST API endpoints with Bearer token authentication in this single-module web-server?<br>
Is it possible? What I should to do?
The test case
Spring application logs:
# I try to exchange OAuth code for an access token
23:16:44.991 [nio-8080-exec-4] o.s.security.web.FilterChainProxy : Securing POST /oauth2/token
23:16:45.059 [nio-8080-exec-4] o.s.a.w.OAuth2ClientAuthenticationFilter : Set SecurityContextHolder authentication to OAuth2ClientAuthenticationToken
# And then I try to use this token for default OAuth provided endpoints (result: 200 OK)
23:17:16.212 [nio-8080-exec-6] o.s.security.web.FilterChainProxy : Securing GET /userinfo
23:17:16.217 [nio-8080-exec-6] o.s.web.client.RestTemplate : HTTP POST http://localhost:8080/oauth2/introspect
23:17:16.221 [nio-8080-exec-6] o.s.web.client.RestTemplate : Accept=[application/json, application/*+json]
23:17:16.222 [nio-8080-exec-6] o.s.web.client.RestTemplate : Writing [{token=[2Jd2M-Pq3Cx8We9gKVpfosvAnNGjprCJoyA6-gHOH3t2_cbpVaGsmGkgJ1n9wzam_kvvL4cthCUwSCNRWrfm_uGZJUtFWJjL_jaaKla0p37MDwkPbrGGhoJOGLeGDSrC]}] with org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter
# Also for opaque token introspection (result: 200 OK)
23:17:16.227 [nio-8080-exec-5] o.s.security.web.FilterChainProxy : Securing POST /oauth2/introspect
23:17:16.294 [nio-8080-exec-5] o.s.a.w.OAuth2ClientAuthenticationFilter : Set SecurityContextHolder authentication to OAuth2ClientAuthenticationToken
23:17:16.307 [nio-8080-exec-6] o.s.web.client.RestTemplate : Response 200 OK
23:17:16.308 [nio-8080-exec-6] o.s.web.client.RestTemplate : Reading to [java.util.Map<java.lang.String, java.lang.Object>]
23:17:16.320 [nio-8080-exec-6] .s.r.a.OpaqueTokenAuthenticationProvider : Authenticated token
23:17:16.320 [nio-8080-exec-6] .s.r.w.a.BearerTokenAuthenticationFilter : Set SecurityContextHolder to BearerTokenAuthentication [Principal=org.springframework.security.oauth2.server.resource.introspection.OAuth2IntrospectionAuthenticatedPrincipal@49bc387c, Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=0:0:0:0:0:0:0:1, SessionId=null], Granted Authorities=[SCOPE_openid, SCOPE_user:read, SCOPE_user:write]]
# After that I try to send request to my REST API controller (result: 401 + redirect)
23:17:43.504 [nio-8080-exec-8] o.s.security.web.FilterChainProxy : Securing GET /api/user/get
23:17:43.504 [nio-8080-exec-8] o.s.s.w.a.AnonymousAuthenticationFilter : Set SecurityContextHolder to anonymous SecurityContext
23:17:43.504 [nio-8080-exec-8] o.s.s.w.session.SessionManagementFilter : Request requested invalid session id 9FC445DE02E5AA86CF6C7D898290112F
23:17:43.505 [nio-8080-exec-8] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to team.flow.server.api.controller.UserApiController#getUser(Long, Authentication)
23:17:43.505 [nio-8080-exec-8] o.s.s.w.s.HttpSessionRequestCache : Saved request http://localhost:8080/api/user/get?continue to session
23:17:43.505 [nio-8080-exec-8] o.s.s.web.DefaultRedirectStrategy : Redirecting to http://localhost:8080/auth/sign-in
23:17:43.512 [nio-8080-exec-9] o.s.security.web.FilterChainProxy : Securing GET /auth/sign-in
23:17:43.512 [nio-8080-exec-9] o.s.s.w.a.AnonymousAuthenticationFilter : Set SecurityContextHolder to anonymous SecurityContext
23:17:43.512 [nio-8080-exec-9] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to team.flow.server.auth.controller.AuthFrontController#handleDefaultRequest()
23:17:43.512 [nio-8080-exec-9] o.s.security.web.FilterChainProxy : Secured GET /auth/sign-in
23:17:43.512 [nio-8080-exec-9] o.s.web.servlet.DispatcherServlet : GET "/auth/sign-in", parameters={}
23:17:43.512 [nio-8080-exec-9] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to team.flow.server.auth.controller.AuthFrontController#handleDefaultRequest()
23:17:43.513 [nio-8080-exec-9] o.s.web.servlet.DispatcherServlet : Completed 200 OK
Postman console screenshot for the last request:
The source code
Security configuration:
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private static final List<String> FLOW_OAUTH2_SCOPES = List.of(
"openid",
"user:read", "user:write"
);
private final UserRepository userRepository;
private final FlowAuthenticationHandler authenticationHandler;
@Bean
@Order(1)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
.oidc(Customizer.withDefaults());
return http
.exceptionHandling((exceptions) -> exceptions
.defaultAuthenticationEntryPointFor(
new LoginUrlAuthenticationEntryPoint("/auth"),
new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
))
.oauth2ResourceServer((resourceServer) -> resourceServer
.opaqueToken(Customizer.withDefaults()))
.build();
}
@Bean
@Order(2)
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(authorizeRequests -> authorizeRequests
.requestMatchers("/error", "/flow-web/**", "/favicon.ico").permitAll()
.requestMatchers("/auth/complete").authenticated()
.requestMatchers("/auth/**", "/logout").permitAll()
.requestMatchers("/oauth2/code").permitAll()
.requestMatchers(HttpMethod.GET, "/api/user/**").hasAuthority("SCOPE_user:read")
.requestMatchers(HttpMethod.POST, "/api/user/**").hasAuthority("SCOPE_user:write")
.anyRequest().authenticated())
.sessionManagement(sessionManagement -> sessionManagement
.maximumSessions(1))
.formLogin(formLogin -> formLogin
.loginPage("/auth/sign-in")
.loginProcessingUrl("/auth/sign-in")
.successHandler(authenticationHandler)
.failureHandler(authenticationHandler)
.usernameParameter("email")
.passwordParameter("password"))
.logout(logout -> logout
.deleteCookies("JSESSIONID")
.logoutUrl("/logout")
.logoutSuccessUrl("/auth"))
.build();
}
@Bean
public UserDetailsService userDetailsService() {
return new FlowUserDetailsService(userRepository);
}
@Bean
public RegisteredClientRepository registeredClientRepository() {
RegisteredClient flowClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("")
.clientName("")
.clientSecret("")
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.redirectUri("http://localhost:8080/oauth2/code")
.postLogoutRedirectUri("http://localhost:8080/auth")
.scopes(scopes -> scopes.addAll(FLOW_OAUTH2_SCOPES))
.clientSettings(ClientSettings.builder()
.requireAuthorizationConsent(false)
.requireProofKey(true)
.build())
.tokenSettings(TokenSettings.builder()
.accessTokenFormat(OAuth2TokenFormat.REFERENCE) // yes, I use opaque tokens here
.authorizationCodeTimeToLive(Duration.ofSeconds(30))
.accessTokenTimeToLive(Duration.ofDays(3))
.refreshTokenTimeToLive(Duration.ofDays(14))
.reuseRefreshTokens(false)
.build())
.build();
return new InMemoryRegisteredClientRepository(flowClient);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityContextRepository securityContextRepository() {
return new HttpSessionSecurityContextRepository();
}
REST API Controller:
@RestController
@RequestMapping("/api/user")
@RequiredArgsConstructor
public class UserApiController {
private final UserApiService userApiService;
@GetMapping("/get")
public ResponseEntity<?> getUser(@RequestParam(name = "id", required = false) Long userId) throws ApiException {
if (userId < 1) {
return ResponseEntity.badRequest().build();
}
User user = userApiService.getUser(userId);
return ResponseEntity.ok(UserModel.constructFrom(user));
}
@GetMapping("/meta/get")
public ResponseEntity<?> getUserMeta(@RequestParam(name = "id", required = false) Long userId) throws ApiException {
if (userId < 1) {
return ResponseEntity.badRequest().build();
}
UserMeta userMeta = userApiService.getUserMeta(userId);
return ResponseEntity.ok(UserMetaModel.constructFrom(userMeta));
}
}
Application config:
spring:
datasource:
driver-class-name: org.postgresql.Driver
url: jdbc:postgresql://127.0.0.1/${DATABASE}
username: ${USERNAME}
password: ${PASSWORD}
jpa:
properties:
hibernate:
dialect: org.hibernate.dialect.PostgreSQLDialect
hibernate:
ddl-auto: update
open-in-view: false
security:
oauth2:
resourceserver:
opaquetoken:
introspection-uri: http://localhost:8080/oauth2/introspect
client-id: my_client_id
client-secret: my_client_secret
logging:
level:
root: INFO
'[org.springframework.web]': DEBUG
'[org.springframework.security]': DEBUG
'[org.springframework.security.oauth2]': DEBUG
org.springframework.security.web.FilterChainProxy: DEBUG
server:
servlet:
session:
cookie:
same-site: lax
error:
whitelabel:
enabled: false
path: /error
Part of maven project configuration:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.0</version>
<relativePath/>
</parent>
<dependencies>
<!-- Spring Boot Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-authorization-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<!-- Thymeleaf Extras: Spring Security 5 -->
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
<version>3.1.1.RELEASE</version>
<scope>compile</scope>
</dependency>
<!-- PostgreSQL JDBC Driver -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
答案1
得分: 2
你应该注意,在PKCE方法中,不使用客户端密钥,因为PKCE的目的是不在前端应用程序中存储任何客户端密钥,以避免安全问题。
通过使用PKCE,每当客户端发送请求到授权服务器以获取授权码时,都会生成一个新的哈希码来验证客户端,而不是使用客户端密钥。
你可以在互联网上搜索挑战码和代码验证器,以获取有关用于验证客户端的哈希码的更多信息,而不是客户端密钥。
英文:
You should notice that in the PKCE approach, no client-secret is used because the purpose of PKCE is to not store any client-secret in the front-end application because of the security issues.
By using PKCE, every time the client sends a request to the authorization server to obtain the authorization-code, a new hash code is generated to verify the client instead of using client-secret.
you can search for challenge-code and code-verifier on internet to get more information about hash codes that are generated to verify the client instead of client-secret.
答案2
得分: 1
如何使用Bearer令牌认证来保护我的REST API端点?
使用资源服务器配置。
如何在已包含客户端配置的应用程序中执行此操作?
授权服务器端点和暴露Thymeleaf页面的端点都需要OAuth2客户端配置,其中请求使用会话进行保护(并需要CSRF保护)。
要将REST API端点保护为访问令牌(无会话、CSRF保护、登录或注销),请定义一个专用于资源服务器端点的第三个安全过滤器链。
要将defaultSecurityFilterChain
与OAuth2客户端配置保持默认状态,请将其顺序更改为@Order(3)
,并插入一个resourceServerFilterChain
,其顺序为**@Order(2)
,并使用安全匹配器http.securityMatcher("/api/**")
**,以便它仅匹配REST API路由,并让defaultSecurityFilterChain
处理所有未被具有较低@Order
的安全过滤器链拦截的请求。
您可以参考我的教程了解资源服务器配置以及同时具有OAuth2客户端和OAuth2资源服务器配置的应用程序(但不涉及Spring的授权服务器,因为我正在使用其他解决方案作为OpenID提供者)。
英文:
How to secure my REST API endpoints with Bearer token authentication?
With resource server configuration
How to do it in an application already containing client conf?
Both the authorization server endpoints and the ones exposing Thymeleaf pages require OAuth2 client configuration in which requests are secured with sessions (and require CSRF protection).
To have REST API endpoints secured with access tokens (and without session, CSRF protection, login or logout), define a third security filter-chain dedicated to resource server endpoints.
To keep your defaultSecurityFilterChain
with OAuth2 client config as default, change its order to @Order(3)
, and insert a resourceServerFilterChain
with @Order(2)
and a security-matcher like http.securityMatcher("/api/**")
so that it only matches REST API routes and lets the defaultSecurityFilterChain
process all requests that were intercepted by none of the security filter-chains with lower @Order
.
You might refer to my tutorials for resource server configuration and applications with both OAuth2 client and OAuth2 resource server configuration (but nothing there about Spring's authorization-server, as I'm using other solutions as OpenID Providers).
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论