I have OpenID Connect based authn working in a Spring Cloud Gateway app with Okta and Google. However, when I use the TokenRely filter (to send the JWT to upstream services), the access_token value returned by the request to https://www.googleapis.com/oauth2/v4/token is sent instead of the id_token (JWT). However, this works with Okta so I'm not sure if this an issue with Google's API or Spring?
The access_token (and not the id_token JWT) is being returned for getTokenValue (from TokenRelayGatewayFilterFactory):
private ServerWebExchange withBearerAuth(ServerWebExchange exchange,OAuth2AccessToken accessToken) {
return exchange.mutate()
.request(r -> r.headers(
headers -> headers.setBearerAuth(accessToken.getTokenValue())))
.build();
}
There's nothing wrong with the TokenRelayGatewayFilterFactory impl there ^ just including it here for context of where the accessToken.getTokenValue method is being invoked. That code works great with Okta.
accessToken.getTokenValue() should return the id_token for OIDC flow. This works with Okta, but not Google, so could be a configuration issue or something weird with Google's OIDC flow?
Here's an example of the response from the token endpoint from Google to show what they return:
HTTP/1.1 200 OK
Content-length: 1178
X-xss-protection: 1; mode=block
X-content-type-options: nosniff
Transfer-encoding: chunked
Vary: Origin, X-Origin, Referer
Server: ESF
-content-encoding: gzip
Cache-control: private
Date: Tue, 12 Feb 2019 20:44:18 GMT
X-frame-options: SAMEORIGIN
Alt-svc: quic=":443"; ma=2592000; v="44,43,39"
Content-type: application/json; charset=utf-8
{
"access_token": "ya29.....",
"id_token": "eyJhb...",
"expires_in": 3600,
"token_type": "Bearer",
"scope": "openid",
"refresh_token": "1/..."
}
The JWT is in the id_token field.
I set the following properties in my application.properties (sensitive redacted):
spring.security.oauth2.client.registration.google.client-id=****
# Client Secret in ~/.spring-boot-devtools.properties
spring.security.oauth2.client.registration.google.scope=openid,email,profile
spring.security.oauth2.client.provider.google.authorization-uri=https://accounts.google.com/o/oauth2/v2/auth
spring.security.oauth2.client.provider.google.token-uri=https://www.googleapis.com/oauth2/v4/token
spring.security.oauth2.client.provider.google.user-info-uri=https://www.googleapis.com/oauth2/v3/userinfo
spring.security.oauth2.client.provider.google.jwk-set-uri=https://www.googleapis.com/oauth2/v3/certs
spring.security.oauth2.resourceserver.jwt.issuer-uri=https://accounts.google.com
spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://www.googleapis.com/oauth2/v3/certs
springBootVersion = 2.1.2.RELEASE
springCloudVersion = Greenwich.RELEASE
springVersion = 5.1.4.RELEASE
Sorry, I didn't include my security config (webflux):
@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http.csrf().disable().authorizeExchange()
.matchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
.pathMatchers("/actuator/**", "/**/api-docs", "/**/swagger-ui.html").permitAll()
.anyExchange().authenticated()
.and()
.oauth2Login()
.and()
.oauth2ResourceServer()
.jwt();
return http.build();
}
I worked around this with my own WebFillter that does:
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
return exchange.getPrincipal()
.filter(token -> token instanceof OAuth2AuthenticationToken)
.cast(OAuth2AuthenticationToken.class)
.map(authentication -> authentication.getPrincipal())
.filter(principal -> principal instanceof OidcUser)
.cast(OidcUser.class)
.map(OidcUser::getIdToken)
.map(token -> withBearerAuth(exchange, token.getTokenValue()))
.defaultIfEmpty(exchange)
.flatMap(chain::filter);
}
@thelabdude
when I use the TokenRely filter (to send the JWT to upstream services), the
access_tokenvalue returned by the request tohttps://www.googleapis.com/oauth2/v4/tokenis sent instead of theid_token(JWT)
The ID Token is only used by the OAuth Client (relying party) to authenticate the user as it relies on the ID Token to successfully authenticate the user. The ID Token is rarely used to pass to protected resources. The Access Token is typically what is used to pass to protected resources on Resource Servers.
The Token Relay filter is behaving as expected so I'm going to close this issue. Also, in the future please log this issue with the Spring Cloud Gateway project. Thank you.
@jgrandja Ok, I understand about the access_token vs. id_token, but there's still an issue with the Google OIDC implementation. I switched my code back to using the TokenRelay filter and now my upstream service is showing this:
2019-02-13 09:03:28.177 DEBUG 77255 --- [tp1602101011-23] o.s.b.a.audit.listener.AuditListener : AuditEvent [timestamp=2019-02-13T16:03:28.176Z, principal=ya29.GlyuBq5y1k1snYrIWAYYNmooHsf...-VUVx9t0QBwOZ7vbw, type=AUTHENTICATION_FAILURE, data={details=org.springframework.security.web.authentication.WebAuthenticationDetails@b364: RemoteIpAddress: 192.168.1.10; SessionId: null, type=org.springframework.security.authentication.ProviderNotFoundException, message=No AuthenticationProvider found for org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken}]
2019-02-13 09:03:28.177 DEBUG 77255 --- [tp1602101011-23] .s.a.DefaultAuthenticationEventPublisher : No event was found for the exception org.springframework.security.oauth2.core.OAuth2AuthenticationException
2019-02-13 09:03:28.177 DEBUG 77255 --- [tp1602101011-23] .o.s.r.w.BearerTokenAuthenticationFilter : Authentication request for failed: org.springframework.security.oauth2.core.OAuth2AuthenticationException: An error occurred while attempting to decode the Jwt: Invalid token
So whatever Google is putting in the access_token is certainly not a JWT and isn't valid upstream. It works with Okta. So there's still an issue here and I don't think it is Gateway related.
So whatever Google is putting in the
access_tokenis certainly not a JWT
Google does not issue JWT formatted access tokens - it's opaque. Okta is JWT. The protected resource that receives the access token should be configured as such to know what the expected access token format is - opaque vs. JWT.
Ah I see now thank you. Sorry for the confusion there. What I'm trying to do is pass a JWT from the Gateway to upstream services as a mechanism for identifying users and their roles (JWT for stateless sessions basically). I guess I got lulled into thinking this was a valid approach because of Okta using the JWT for their access_token. Do you have any recommendations on how to achieve this with Google? Are there examples of this in Spring Security? This is the doc that led me down this path: https://docs.spring.io/spring-security/site/docs/current/reference/html/webflux-oauth2.html#webflux-oauth2-resource-server ...
@thelabdude The one thing to keep in mind is that the provider that issues the token is the provider that validates the token. So if you have a token issued by Google, than your downstream service needs to validate the token using Google - either through token introspection endpoint (if opaque) or local public key if JWT (not the case for google). Here is a full sample demonstrating the client and resource server features available in Spring Security 5.1 https://github.com/jgrandja/oauth2-protocol-patterns
Thank you for the explanation and link. Spring Security is truly amazing!
https://tools.ietf.org/html/draft-ietf-oauth-browser-based-apps-04#section-6.2
JavaScript Applications with a Backend
The Application Server utilizes its own session with the browser to store the access token. When the JavaScript application in the browser wants to make a request to the Resource Server, it MUST instead make the request to the Application Server, and the Application Server will make the request with the access token to the Resource Server (C), and forward the response (D) back to the browser.
As soon as id/access/refresh tokens exist only in scope of Application Server session logout will make it impossible to reuse id token to access resource server. This means there is no reason to use opaque access token as do not need to check with Authorization server if session is still active as with 6.3. JavaScript Applications without a Backend
Also some Authorization server implementations allow only id token custom claims.