Spring Reactive Security API网关,带OpenID Connect,使用private_key



下面是这篇由两部分组成的教程。

  • 第1部分
  • 第2部分

描述的目标

API网关将作为执行OAuth 2的示例基于授权代码流的登录和会话管理。此外,它还将显示如何使用资源服务器所需的适当OAuth承载令牌。钥匙获取的访问/刷新令牌Keycloft永远不会暴露在浏览器中。

不同的是,我没有利用Keycloft,我使用的是Login.gov,根据我的经验,尽管它们的文档不错,但与它们集成可能会非常痛苦

问题

教程认为我可以逃脱一些基本的东西:

...
.oauth2Login()

以前,当我不使用网关范式或反应式编程模型时,我在安全配置上对oauth进行了大量调整,以与Login.gov集成,例如:

...
.oauth2Login()
.loginPage(DefaultLoginPageGeneratingFilter.DEFAULT_LOGIN_PAGE_URL)
.authorizationEndpoint()
.authorizationRequestResolver(new LoginGovAuthorizationRequestResolver(clientRegistrationRepository))
.authorizationRequestRepository(authorizationRequestRepository())
.and()
.tokenEndpoint()
.accessTokenResponseClient(accessTokenResponseClient())
.and()
.failureHandler(new LoginGovAuthenticationFailureHandler())
.successHandler(new LoginGovAuthenticationSuccessHandler())

很明显,这里发生了很多事情,但重点是:所有这些跳跃主要是为了做几件事(我还记得(:

  • 在请求解析程序中添加2个参数
    • acr_values(常量:可在配置authorization_uri中添加(
    • nonce(最少22个字符,至少需要一个解析器(
additionalParameters.put("acr_values", LoginGovConstants.LOGIN_GOV_LOA1)
additionalParameters.put("nonce", nonce)

此外,转换器显然需要我签署自己的JWT,并为其提供client_assertion和恒定的client_assertion_type参数:

@Override
RequestEntity<?> convert(OAuth2AuthorizationCodeGrantRequest authorizationCodeGrantRequest) {
RequestEntity<?> originalRequestEntity = super.convert(authorizationCodeGrantRequest)
String registrationId = resolveRegistrationId(authorizationCodeGrantRequest)
if(registrationId == LoginGovConstants.LOGIN_GOV_REGISTRATION_ID) {
ClientRegistration clientRegistration = clientRegistrationRepository.findByRegistrationId(registrationId)
String clientId = clientRegistration.clientId
String clientSecret = clientRegistration.clientSecret
String tokenUri = clientRegistration.providerDetails.tokenUri
Long expirationTime = LoginGovConstants.LOGIN_GOV_TOKEN_EXPIRATION_TIME
String jwt = JWT.create()
.withSubject(clientId)
.withIssuer(clientId)
.withAudience(tokenUri)
// Should be an un-guessable, random string generated by the client
.withJWTId(UUID.randomUUID().toString())
.withExpiresAt(new Date(System.currentTimeMillis() + expirationTime))
.sign(Algorithm.RSA256(keystoreUtil.rsaPublicKey(), keystoreUtil.rsaPrivateKey()))
HttpHeaders headers = originalRequestEntity.headers
MultiValueMap<String, String> formParameters = originalRequestEntity.body as MultiValueMap<String, String>
URI uri = originalRequestEntity.url
formParameters.add("client_assertion", jwt)
formParameters.add("client_assertion_type", LoginGovConstants.LOGIN_GOV_CLIENT_ASSERTION_TYPE)
return new RequestEntity<?>(formParameters, headers, HttpMethod.POST, uri)
}
}

只需说一句,在我实现之前的解决方案时,我认为Spring Security默认情况下应该提供一些额外的URL参数,这似乎是一个很大的倒退。因此,如果我们能在我之前的学徒基础上有所改进,我将不胜感激。

尽管如此,对于这种新的API网关方法,考虑到Webflux和一种反应式范式,我进一步感到困惑。

例如:

  • OAuth2AuthorizationCodeGrantRequestEntityConverterServerOAuth2AuthorizationCodeAuthenticationTokenConverter
  • OAuth2AuthorizationRequestResolverServerOAuth2AuthorizationRequestResolver

这些类之间有什么重叠(如果有的话(?

有没有一种更简单的方法来完成我以前用这些不同的Server*类所做的事情?

当前行为

按照我提到的教程,我有一个部分流程与我的网关,但图片并不完整:

spring:
application:
name: gateway
security:
oauth2:
client:
registration:
login-gov:
client-id: ${LOGIN_GOV_CLIENT_ID}
authorization-grant-type: authorization_code
redirect-uri: "{baseUrl}/{action}/oauth2/code/{registrationId}"
scope:
- openid
- email
provider:
login-gov:
authorization-uri: https://idp.int.identitysandbox.gov/openid_connect/authorize
token-uri: https://idp.int.identitysandbox.gov/api/openid_connect/token?client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer
user-info-uri: https://idp.int.identitysandbox.gov/api/openid_connect/userinfo
jwk-set-uri: https://idp.int.identitysandbox.gov/api/openid_connect/certs
user-name-attribute: sub

Login.gov使用private_key_jwt身份验证方法,这似乎意味着我没有"客户端机密"。这就是为什么我有必要在之前的实现中创建并签署自己的JWT的原因吗?

这是当我到达网关端点(/api/user/api/v1/savesearchs/(时网关的输出,该端点最终应该重定向到实际服务。

登录流按预期启动,我在integration login.gov页面上输入凭据。提交后,我最终被重定向回错误页面/login?error

2020-04-20 14:10:30.934 DEBUG 15352 --- [ctor-http-nio-5] o.s.w.s.adapter.HttpWebHandlerAdapter    : [eae7892d-10] HTTP GET "/api/user/api/v1/savesearches/"
2020-04-20 14:10:30.983 DEBUG 15352 --- [oundedElastic-2] o.s.w.s.s.DefaultWebSessionManager       : Created new WebSession.
2020-04-20 14:10:30.995 DEBUG 15352 --- [oundedElastic-2] .s.u.m.MediaTypeServerWebExchangeMatcher : httpRequestMediaTypes=[text/html, application/xhtml+xml, image/webp, image/apng, application/xml;q=0.9, application/signed-exchange;v=b3;q=0.9, */*;q=0.8]
2020-04-20 14:10:30.996 DEBUG 15352 --- [oundedElastic-2] .s.u.m.MediaTypeServerWebExchangeMatcher : Processing text/html
2020-04-20 14:10:30.996 DEBUG 15352 --- [oundedElastic-2] .s.u.m.MediaTypeServerWebExchangeMatcher : text/html .isCompatibleWith text/html = true
2020-04-20 14:10:30.997 DEBUG 15352 --- [oundedElastic-2] o.s.w.s.adapter.HttpWebHandlerAdapter    : [eae7892d-10] Completed 302 FOUND
2020-04-20 14:10:31.006 DEBUG 15352 --- [ctor-http-nio-5] o.s.w.s.adapter.HttpWebHandlerAdapter    : [eae7892d-11] HTTP GET "/oauth2/authorization/login-gov"
2020-04-20 14:10:31.017 DEBUG 15352 --- [ctor-http-nio-5] o.s.w.s.adapter.HttpWebHandlerAdapter    : [eae7892d-11] Completed 302 FOUND
2020-04-20 14:10:31.853 DEBUG 15352 --- [ctor-http-nio-5] o.s.w.s.adapter.HttpWebHandlerAdapter    : [eae7892d-12] HTTP GET "/api/user/api/v1/savesearches/"
2020-04-20 14:10:31.861 DEBUG 15352 --- [ctor-http-nio-5] .s.u.m.MediaTypeServerWebExchangeMatcher : httpRequestMediaTypes=[text/html, application/xhtml+xml, image/webp, image/apng, application/xml;q=0.9, application/signed-exchange;v=b3;q=0.9, */*;q=0.8]
2020-04-20 14:10:31.861 DEBUG 15352 --- [ctor-http-nio-5] .s.u.m.MediaTypeServerWebExchangeMatcher : Processing text/html
2020-04-20 14:10:31.861 DEBUG 15352 --- [ctor-http-nio-5] .s.u.m.MediaTypeServerWebExchangeMatcher : text/html .isCompatibleWith text/html = true
2020-04-20 14:10:31.861 DEBUG 15352 --- [ctor-http-nio-5] o.s.w.s.adapter.HttpWebHandlerAdapter    : [eae7892d-12] Completed 302 FOUND
2020-04-20 14:10:31.868 DEBUG 15352 --- [ctor-http-nio-5] o.s.w.s.adapter.HttpWebHandlerAdapter    : [eae7892d-13] HTTP GET "/oauth2/authorization/login-gov"
2020-04-20 14:10:31.874 DEBUG 15352 --- [ctor-http-nio-5] o.s.w.s.adapter.HttpWebHandlerAdapter    : [eae7892d-13] Completed 302 FOUND
2020-04-20 14:10:44.361 DEBUG 15352 --- [ctor-http-nio-5] o.s.w.s.adapter.HttpWebHandlerAdapter    : [eae7892d-14] HTTP GET "/login/oauth2/code/login-gov?code=d2236ca8-1458-4631-b067-057f461d2a71&state=pp0V-IpV75MSgq995i5IUoQvuaFGhBXBJyRiHsAhQvM%3D"
2020-04-20 14:10:44.382 DEBUG 15352 --- [ctor-http-nio-5] o.s.w.r.f.client.ExchangeFunctions       : [312e7ca5] HTTP POST https://idp.int.identitysandbox.gov/api/openid_connect/token?client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer
2020-04-20 14:10:44.950 DEBUG 15352 --- [ctor-http-nio-5] org.springframework.web.HttpLogging      : [312e7ca5] Writing form fields [grant_type, code, redirect_uri, client_id, code_verifier] (content masked)
2020-04-20 14:10:45.080 DEBUG 15352 --- [ctor-http-nio-5] o.s.w.r.f.client.ExchangeFunctions       : [312e7ca5] Response 400 BAD_REQUEST
2020-04-20 14:10:45.094 DEBUG 15352 --- [ctor-http-nio-5] org.springframework.web.HttpLogging      : [312e7ca5] Decoded [{error=Client assertion Nil JSON web token}]
2020-04-20 14:10:45.095 DEBUG 15352 --- [ctor-http-nio-5] o.s.w.s.adapter.HttpWebHandlerAdapter    : [eae7892d-14] Completed 302 FOUND
2020-04-20 14:10:45.099 DEBUG 15352 --- [ctor-http-nio-5] o.s.w.s.adapter.HttpWebHandlerAdapter    : [eae7892d-15] HTTP GET "/login?error"
2020-04-20 14:10:45.101 DEBUG 15352 --- [ctor-http-nio-5] o.s.w.s.adapter.HttpWebHandlerAdapter    : [eae7892d-15] Completed 200 OK

对我来说,突出的地方在这里非常清楚。我可以看到授权步骤成功,如重定向到具有codestate参数的/login/oauth2/code/login-gov?code=d2236ca8-1458-4631-b067-057f461d2a71&state=pp0V-IpV75MSgq995i5IUoQvuaFGhBXBJyRiHsAhQvM%3D所示。

令人困惑的是提供给令牌端点的参数:它只有我在上面的配置中提供的client_assertion_type,但它也应该有实际的client_assertion(JWT(。我想,这就是我之前为在自定义转换器中创建JWT所做的所有跳跃,但我一直不明白为什么Spring不为我做这件事?

我是不是错过了一些基本的配置步骤?如果没有,我可以使用一些帮助以响应式(ServerWebExchange(的方式重新实现转换器。

谢谢你的耐心。

FYI Spring Security 5还不支持private_key_jwt,这就是为什么您必须做额外的工作才能使其工作。

(添加一个答案,因为我没有足够的声誉来评论(

最新更新