Spring Authorization Server:使用重定向测试 OAuth 流

Posted

技术标签:

【中文标题】Spring Authorization Server:使用重定向测试 OAuth 流【英文标题】:Spring Authorization Server: Testing OAuth flow with redirects 【发布时间】:2022-01-19 10:49:02 【问题描述】:

我正在为使用 Spring Authorization Server 版本 0.2.1 构建的 OAuth 流程编写集成测试。我想测试对授权路由的调用是否重定向到我在测试中嵌入的“回调”控制器。这适用于 curl、postman 甚至 OAuth 客户端 - 但是我只想让它在集成测试中工作。

注意:我有一个稍微定制的流程,因为授权主体是使用来自另一个应用程序的传入 JWT 创建的。这通过利用注入的BearerTokenResolver 以及将我的授权服务器配置为也充当资源服务器来通过带有oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt) 的JWT 处理身份验证来工作。这基本上意味着如果我重定向具有所有 oauth 选项以及前面提到的 JWT 的 /login 调用,我可以从经过身份验证的用户的角度触发 OAuth 流。然后将其重定向到/oauth2/authorize——这一切都可以在测试之外进行。

测试具有以下(粗略)结构:


@LocalServerPort  // using SpringBootTest with webEnvironment= SpringBootTest.WebEnvironment.RANDOM_PORT
private int port;

// Capture the redirect that has the auth code
// if it returns the
@RestController
public static class TestRegisteredCallback 
    @GetMapping("/callback")
    public String callback(@RequestParam String code, @RequestParam String state)
        return code;
    


@Test
public void canGetAccessCode_existingConsent() throws Exception 
    // given an existing client
    Client client = setupNewClient();
    client.setRedirectUris(String.format("http://127.0.0.1:%s/callback", port));
    clientRepository.save(client);

    // and a consent against that client
    authorizationConsentRepository.save(setupNewConsent());
    // and a registered test controller for that clients callback

    String constructingUrl = String.format("/login?response_type=code" +
            "&client_id=clientid" +
            "&scope=read" +
            "&state=teststate" +
            "&redirect_uri=http://127.0.0.1:%s/callback" +
            "&nonce=testnonce" +
            "&access_token=%s", port, JWT_ACCESS_TOKEN);

    String url = UriComponentsBuilder
            .fromUriString(constructingUrl)
            .build()
            .encode()
            .toUri().toString();

    HttpHeaders requestHeaders = new HttpHeaders();
    requestHeaders.add("Cookie", "JSESSIONID=0778F764F69C8C17162D76EFD69F635C");
    HttpEntity requestEntity = new HttpEntity(null, requestHeaders);

    // call the /login - login will authenticate using provided JWT
    ResultActions resultActions = this.mockMvc
            .perform(get(url)
                .headers(requestHeaders)
            )
            .andDo(print());
    MvcResult result = resultActions.andReturn();

    // follow the redirect to /auth2/authorize
    ResultActions action2 = this.mockMvc.perform(
            get(
                    result.getResponse()
                            .getHeader("Location"))
                    .headers(requestHeaders)
                    )
            .andDo(print());
    MvcResult authorizeResult = action2.andReturn();
    
    // authorizeResult comes back as a 401 :(
   // should come as 302 to Location http://127.0.0.1:%s/callback ?

确实在过滤器链中允许/callback 路由:

antMatchers("/callback/**").permitAll()

相同的流程,但使用 curl 编写的效果很好。从 curl / 实际客户端运行时调试 SAS 主体被正确设置为 JwtAuthenticationToken之后和重定向之前。

在调试 SAS 时,我注意到 authorizationCodeRequestAuthentication 对象的主体是 AnonymousAuthenticationToken 的代码路径不起作用 - 至于工作代码路径,主体是 JwtAuthenticationToken

我认为这可能是由于会话?不过,我已尝试确保会话对于调用 /login 以及遵循重定向时的会话是相同的,并认为它有效,如在调试中查看相同的 cookie。

我在 mvc 调用中使用 print() - 这是输出:

按预期调用/login,进行身份验证并重定向到/oauth2/authorize:

MockHttpServletRequest:
      HTTP Method = GET
      Request URI = /login
       Parameters = response_type=[code], client_id=[clientid], scope=[read], state=[teststate], redirect_uri=[http://127.0.0.1:63440/callback], nonce=[testnonce], access_token=[eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c]
          Headers = [Cookie:"JSESSIONID=0778F764F69C8C17162D76EFD69F635C"]
             Body = null
    Session Attrs = SPRING_SECURITY_CONTEXT=SecurityContextImpl [Authentication=JwtAuthenticationToken [Principal=org.springframework.security.oauth2.jwt.Jwt@3ee32622, Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=127.0.0.1, SessionId=null], Granted Authorities=[]]]

Handler:
             Type = com.fanduel.oauthservice.auth.LoginRedirectController
           Method = com.fanduel.oauthservice.auth.LoginRedirectController#login(String, String, String, String, String, String, Authentication, HttpServletRequest, HttpSession)

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = null

ModelAndView:
        View name = redirect:/oauth2/authorize?response_type=code&client_id=clientid&scope=read&state=teststate&nonce=testnonce&redirect_uri=http://127.0.0.1:63440/callback
             View = null
            Model = null

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 302
    Error message = null
          Headers = [Content-Language:"en", X-Content-Type-Options:"nosniff", X-XSS-Protection:"1; mode=block", Cache-Control:"no-cache, no-store, max-age=0, must-revalidate", Pragma:"no-cache", Expires:"0", Location:"/oauth2/authorize?response_type=code&client_id=clientid&scope=read&state=teststate&nonce=testnonce&redirect_uri=http://127.0.0.1:63440/callback"]
     Content type = null
             Body = 
    Forwarded URL = null
   Redirected URL = /oauth2/authorize?response_type=code&client_id=clientid&scope=read&state=teststate&nonce=testnonce&redirect_uri=http://127.0.0.1:63440/callback
          Cookies = []

跟随重定向到/oauth2/authorize - 但在预期 302 时遇到 401

MockHttpServletRequest:
      HTTP Method = GET
      Request URI = /oauth2/authorize
       Parameters = response_type=[code], client_id=[clientid], scope=[read], state=[teststate], nonce=[testnonce], redirect_uri=[http://127.0.0.1:63440/callback]
          Headers = [Cookie:"JSESSIONID=0778F764F69C8C17162D76EFD69F635C"]
             Body = null
    Session Attrs = SPRING_SECURITY_SAVED_REQUEST=DefaultSavedRequest [http://localhost/oauth2/authorize?response_type=code&client_id=clientid&scope=read&state=teststate&nonce=testnonce&redirect_uri=http://127.0.0.1:63440/callback]

Handler:
             Type = null

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = null

ModelAndView:
        View name = null
             View = null
            Model = null

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 401
    Error message = null
          Headers = [X-Content-Type-Options:"nosniff", X-XSS-Protection:"1; mode=block", Cache-Control:"no-cache, no-store, max-age=0, must-revalidate", Pragma:"no-cache", Expires:"0", X-Frame-Options:"DENY"]
     Content type = null
             Body = 
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

【问题讨论】:

【参考方案1】:

我无法强制会话在mockMvc 调用之间保持。设置 cookie 并在重定向之间保持相同似乎不起作用。

但是我设法让会话通过调用,只需在第一个请求上抓取它并将其设置为后续请求。

        // first call - expect to redirect
        MvcResult loginResult = this.mockMvc
                .perform(get(url))
                .andDo(print())
                .andReturn();

        String redirectUrl = loginResult.getResponse().getHeader("Location");

        // GRAB THE SESSION - this is what fixed my problem. mockmvc doesn't maintain sessions for you.
        MockHttpSession session = (MockHttpSession) loginResult.getRequest().getSession();

        // follow redirect
        MvcResult authorizeResult = this.mockMvc
                .perform(get(redirectUrl).session(session)) // <--- now use that session
                .andDo(print())
                .andReturn();

【讨论】:

以上是关于Spring Authorization Server:使用重定向测试 OAuth 流的主要内容,如果未能解决你的问题,请参考以下文章

Spring Security:Authorization 授权

Spring Security(三十):9.5 Access-Control (Authorization) in Spring Security

如何修复 Spring Security Authorization 标头未通过?

Vaadin 21 Flow + Spring Security OAuth2:找不到'oauth2/authorization/google'的路由

Spring Authorization Server:使用重定向测试 OAuth 流

Spring Security Authorization - 管理员被拒绝访问