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'的路由