Spring身份验证服务器不通过传递的Bearer令牌对受保护的端点进行授权。

huangapple go评论63阅读模式
英文:

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&lt;java.lang.String, java.lang.Object&gt;]
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 &quot;/auth/sign-in&quot;, 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:
Spring身份验证服务器不通过传递的Bearer令牌对受保护的端点进行授权。

The source code

Security configuration:

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    private static final List&lt;String&gt; FLOW_OAUTH2_SCOPES = List.of(
            &quot;openid&quot;,
            &quot;user:read&quot;, &quot;user:write&quot;
    );

    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) -&gt; exceptions
                        .defaultAuthenticationEntryPointFor(
                                new LoginUrlAuthenticationEntryPoint(&quot;/auth&quot;),
                                new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
                        ))
                .oauth2ResourceServer((resourceServer) -&gt; resourceServer
                        .opaqueToken(Customizer.withDefaults()))
                .build();
    }

    @Bean
    @Order(2)
    public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
        return http
                .authorizeHttpRequests(authorizeRequests -&gt; authorizeRequests
                        .requestMatchers(&quot;/error&quot;, &quot;/flow-web/**&quot;, &quot;/favicon.ico&quot;).permitAll()
                        .requestMatchers(&quot;/auth/complete&quot;).authenticated()
                        .requestMatchers(&quot;/auth/**&quot;, &quot;/logout&quot;).permitAll()
                        .requestMatchers(&quot;/oauth2/code&quot;).permitAll()
                        .requestMatchers(HttpMethod.GET, &quot;/api/user/**&quot;).hasAuthority(&quot;SCOPE_user:read&quot;)
                        .requestMatchers(HttpMethod.POST, &quot;/api/user/**&quot;).hasAuthority(&quot;SCOPE_user:write&quot;)
                        .anyRequest().authenticated())
                .sessionManagement(sessionManagement -&gt; sessionManagement
                        .maximumSessions(1))
                .formLogin(formLogin -&gt; formLogin
                        .loginPage(&quot;/auth/sign-in&quot;)
                        .loginProcessingUrl(&quot;/auth/sign-in&quot;)
                        .successHandler(authenticationHandler)
                        .failureHandler(authenticationHandler)
                        .usernameParameter(&quot;email&quot;)
                        .passwordParameter(&quot;password&quot;))
                .logout(logout -&gt; logout
                        .deleteCookies(&quot;JSESSIONID&quot;)
                        .logoutUrl(&quot;/logout&quot;)
                        .logoutSuccessUrl(&quot;/auth&quot;))
                .build();
    }

    @Bean
    public UserDetailsService userDetailsService() {
        return new FlowUserDetailsService(userRepository);
    }

    @Bean
    public RegisteredClientRepository registeredClientRepository() {
        RegisteredClient flowClient = RegisteredClient.withId(UUID.randomUUID().toString())
                .clientId(&quot;&quot;)
                .clientName(&quot;&quot;)
                .clientSecret(&quot;&quot;)
                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
                .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
                .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
                .redirectUri(&quot;http://localhost:8080/oauth2/code&quot;)
                .postLogoutRedirectUri(&quot;http://localhost:8080/auth&quot;)
                .scopes(scopes -&gt; 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(&quot;/api/user&quot;)
@RequiredArgsConstructor
public class UserApiController {

    private final UserApiService userApiService;

    @GetMapping(&quot;/get&quot;)
    public ResponseEntity&lt;?&gt; getUser(@RequestParam(name = &quot;id&quot;, required = false) Long userId) throws ApiException {
        if (userId &lt; 1) {
            return ResponseEntity.badRequest().build();
        }

        User user = userApiService.getUser(userId);
        return ResponseEntity.ok(UserModel.constructFrom(user));
    }

    @GetMapping(&quot;/meta/get&quot;)
    public ResponseEntity&lt;?&gt; getUserMeta(@RequestParam(name = &quot;id&quot;, required = false) Long userId) throws ApiException {
        if (userId &lt; 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
    &#39;[org.springframework.web]&#39;: DEBUG
    &#39;[org.springframework.security]&#39;: DEBUG
    &#39;[org.springframework.security.oauth2]&#39;: 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:

    &lt;parent&gt;
        &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
        &lt;artifactId&gt;spring-boot-starter-parent&lt;/artifactId&gt;
        &lt;version&gt;3.1.0&lt;/version&gt;
        &lt;relativePath/&gt;
    &lt;/parent&gt;

    &lt;dependencies&gt;
        &lt;!-- Spring Boot Starter --&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
            &lt;artifactId&gt;spring-boot-starter&lt;/artifactId&gt;
        &lt;/dependency&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
            &lt;artifactId&gt;spring-boot-starter-data-jpa&lt;/artifactId&gt;
        &lt;/dependency&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
            &lt;artifactId&gt;spring-boot-starter-mail&lt;/artifactId&gt;
        &lt;/dependency&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
            &lt;artifactId&gt;spring-boot-starter-oauth2-authorization-server&lt;/artifactId&gt;
        &lt;/dependency&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
            &lt;artifactId&gt;spring-boot-starter-security&lt;/artifactId&gt;
        &lt;/dependency&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
            &lt;artifactId&gt;spring-boot-starter-thymeleaf&lt;/artifactId&gt;
        &lt;/dependency&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
            &lt;artifactId&gt;spring-boot-starter-validation&lt;/artifactId&gt;
        &lt;/dependency&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
            &lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt;
        &lt;/dependency&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
            &lt;artifactId&gt;spring-boot-starter-webflux&lt;/artifactId&gt;
        &lt;/dependency&gt;

        &lt;!-- Thymeleaf Extras: Spring Security 5 --&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;org.thymeleaf.extras&lt;/groupId&gt;
            &lt;artifactId&gt;thymeleaf-extras-springsecurity5&lt;/artifactId&gt;
            &lt;version&gt;3.1.1.RELEASE&lt;/version&gt;
            &lt;scope&gt;compile&lt;/scope&gt;
        &lt;/dependency&gt;

        &lt;!-- PostgreSQL JDBC Driver --&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;org.postgresql&lt;/groupId&gt;
            &lt;artifactId&gt;postgresql&lt;/artifactId&gt;
        &lt;/dependency&gt;

        &lt;!-- Lombok --&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;org.projectlombok&lt;/groupId&gt;
            &lt;artifactId&gt;lombok&lt;/artifactId&gt;
        &lt;/dependency&gt;
    &lt;/dependencies&gt;

答案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(&quot;/api/**&quot;) 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).

huangapple
  • 本文由 发表于 2023年6月15日 02:28:24
  • 转载请务必保留本文链接:https://go.coder-hub.com/76476558.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定