Spring Cloud Gateway: Combined security? (X.509 + HTTP headers) - spring-security

I'm building an API gateway using Spring Cloud Gateway and I'd like to secure it.
In our current architecture the API gateway will be hidden behind a portal which authenticates using a client certificate (issued for a system user) and sends the name of the real user calling the service in an HTTP header.
I'm looking for a way to configure Spring Security to both verify the certificate (so that nobody else can call the GW) but at the same time construct the principal from the provided header (after the cert check succeeds).
If I use the standard x509 from Spring Security (with provided subjectPrincipalRegex) the user service receives just the matching part as the user ID so I can construct the Principal from the username in the cert (which is still the same).
I.e. I'm looking for something like this
No client cert or invalid client cert => anonymous user (still can have access to some routes)
Valid certificate => obtain UserDetails based on the HTTP header (which can still result in user not found in the DB, i.e. anonymous access).
Note: Since Spring Cloud Gateway is reactive I'm looking for solution applicable to Spring WebFlux Security but I believe that the concepts are the same as in "standard non-WebFlux" security

Related

How does keycloak determine which signature algorithm to use?

I'm writing an application that uses keycloak as its user authentication service. I have normal users, who log in to keycloak from the frontend (web browsers), and service users, who log in from the backend (PHP on IIS). However, when I log in from the backend, keycloak uses HS256 as its signature algorithm for the access token, and thus rejects it for further communication because RS256 is set in the realm and client settings. To get around this issue, I would like to "pretend to be the frontend" to get RS256 signed access tokens for my service users.
For security reasons, I cannot give the HS256 key to the application server, as it's symmetrical and too many people can access the server's code.
I am currently debugging the issue using the same user/pw/client id/grant type both on the frontend and the backend, so that cannot be the issue.
So far I have tried these with no luck:
copying the user agent
copying every single HTTP header (Host, Accept, Content-Type, User-Agent, Accept-Encoding, Connection, even Content-Length is the same as the form data is the same)
double checking if the keycloak login is successful or not - it is, it's just that it uses the wrong signature algorithm
So how does keycloak determine which algorithm to sign tokens with? If it's different from version to version, where should I look in keycloak's code for the answer?
EDIT: clarification of the flow of login and reasons why backend handles it.
If a user logs in, this is what happens:
client --[login data]--> keycloak server
keycloak server --[access and refresh token with direct token granting]--> client
client --[access token]--> app server
(app server validates access token)
app server --[data]--> client
But in some occasions the fifth step's data is the list of users that exist in my realm. The problem with this is that keycloak requires one to have the view-users role to list users, which only exists in the master realm, so I cannot use the logged in user's token to retrieve it.
For this case, I created a special service user in the master realm that has the view-users role, and gets the data like this:
client --[asks for list of users]--> app server
app server --[login data of service user]--> keycloak server
keycloak server --[access token with direct granting]-->app server
app server --[access token]--> keycloak server's get user list API endpoint
(app server filters detailed user data to just a list of usernames)
app server --[list of users]--> client
This makes the the list of usernames effectively public, but all other data remains hidden from the clients - and for security/privacy reasons, I want to keep it this way, so I can't just put the service user's login data in a JS variable on the frontend.
In the latter list, step 4 is the one that fails, as step 3 returns a HS256 signed access token. In the former list, step 2 correctly returns an RS256 signed access token.
Thank you for the clarification. If I may, I will answer your question maybe differently than expected. While you focus on the token signature algorithm, I think there are either mistakes within your OAuth2 flows regarding their usage, or you are facing some misunderstanding.
The fact that both the backend and frontend use "Direct Access Granting" which refers to the OAuth2 flow Resources Owner Credentials Grant is either a false claim or is a mistake in your architecture.
As stated by Keycloak's own documentation (but also slightly differently in official OAuth.2 references):
Resource Owner Password Credentials Grant (Direct Access Grants) ... is used by REST clients that want to obtain a token on behalf of a
user. It is one HTTP POST request that contains the credentials of the
user as well as the id of the client and the client’s secret (if it is
a confidential client). The user’s credentials are sent within form
parameters. The HTTP response contains identity, access, and refresh
tokens.
As far as I can see the application(s) and use case(s) you've described do NOT need this flow.
My proposal
Instead what I'd have seen in your case for flow (1) is Authorization Code flow ...
assuming that "Client" refers to normal users in Browser (redirected to Keycloak auth. from your front app)
and assuming you do not actually need the id and access tokens back in your client, unless you have a valid reasonable reason. As the flows allowing that are considered legacy/deprecated and no more recommended. In this case, we would be speaking of Implicit Flow (and Password Grant flow is also discouraged now).
So I think that the presented exchange (first sequence with points 1 to 5 in your post) is invalid at some point.
For the second flow (backend -> list users), I'd propose two modifications:
Allow users to poll the front end application for the list of users and in turn the front-end will ask the backend to return it. The backend having a service account to a client with view-roles will be able to get the required data:
Client (logged) --> Request list.users to FRONTEND app --> Get list.users from BACKEND app
(<--> Keycloak Server)
<----------------------------------------- Return data.
Use Client Credentials Grant (flow) for Backend <> Keycloak exchanges for this use case. The app will have a service account to which you can assign specific scopes+roles. It will not work on-behalf of any user (even though you could retrieve the original requester another way!) but will do its work in a perfectly safe manner and kept simple. You can even define a specific Client for these exchanges that would be bearer-only.
After all if you go that way you don't have to worry about tokens signature or anything like that. This is handled automatically according to the scheme, flow and parties involved. I believe that by incorrectly making use of the flows you end up having to deal with tricky token issues. According to me that is the root cause and it will be more helpful than focusing on the signature problem. What do you think?
Did I miss something or am I completely wrong...?
You tell me.

Spring Boot + Security OAuth2.0 Client with Custom Provider

I am creating a OAuth2.0 client for a custom OAuth2 provider in Spring Boot + Security (version 5) application.
Below is the application.properties which has all the configuration and there is no additional configuration class in my project.
spring.security.oauth2.client.registration.xxxxxxxxx.client-id=XXXXXXXXXX
spring.security.oauth2.client.registration.xxxxxxxxx.client-secret=XXXXXXXXXX
spring.security.oauth2.client.registration.xxxxxxxxx.scope=openid
spring.security.oauth2.client.registration.xxxxxxxxx.redirect-uri-template=http://localhost:8080/login/oauth2/code/xxxxxxxxx
spring.security.oauth2.client.registration.xxxxxxxxx.client-name=xxxxxxxxx
spring.security.oauth2.client.registration.xxxxxxxxx.provider=xxxxxxxxx
spring.security.oauth2.client.registration.xxxxxxxxx.client-authentication-method=basic
spring.security.oauth2.client.registration.xxxxxxxxx.authorization-grant-type=authorization_code
spring.security.oauth2.client.provider.xxxxxxxxx.authorization-uri=https://api.xxxxxxxxx.com/authorize
spring.security.oauth2.client.provider.xxxxxxxxx.token-uri=https://api.xxxxxxxxx.com/token
spring.security.oauth2.client.provider.xxxxxxxxx.user-info-uri=https://api.xxxxxxxxx.com/userinfo?schema=openid
spring.security.oauth2.client.provider.xxxxxxxxx.user-name-attribute=name
spring.security.oauth2.client.provider.xxxxxxxxx.user-info-authentication-method=header
When i hit http://localhost:8080/ it redirects properly to provider's login page and after successful login it redirects back to my application.
Now the problem is when it redirects then it shows below error message.
I have googled for this error but didn't get any proper answer. Also, the OAuth2 provider didn't share such URL.
After research I came to know that i need to set below property. Should it be provided by Auth Provider?
spring.security.oauth2.client.provider.pepstores.jwk-set-uri
What exactly I am missing here in configuration?
Finally, the problem is solved. I just need to configure the jwk URI which should be provided by the Auth provider.
Below the final configuration for customer Auth Provider.
spring.security.oauth2.client.registration.xxxxxxxxx.client-id=XXXXXXXXXX
spring.security.oauth2.client.registration.xxxxxxxxx.client-secret=XXXXXXXXXX
spring.security.oauth2.client.registration.xxxxxxxxx.scope=openid
spring.security.oauth2.client.registration.xxxxxxxxx.redirect-uri-template=http://localhost:8080/login/oauth2/code/xxxxxxxxx
spring.security.oauth2.client.registration.xxxxxxxxx.client-name=xxxxxxxxx
spring.security.oauth2.client.registration.xxxxxxxxx.provider=xxxxxxxxx
spring.security.oauth2.client.registration.xxxxxxxxx.client-authentication-method=basic
spring.security.oauth2.client.registration.xxxxxxxxx.authorization-grant-type=authorization_code
spring.security.oauth2.client.provider.xxxxxxxxx.authorization-uri=https://api.xxxxxxxxx.com/authorize
spring.security.oauth2.client.provider.xxxxxxxxx.token-uri=https://api.xxxxxxxxx.com/token
spring.security.oauth2.client.provider.xxxxxxxxx.user-info-uri=https://api.xxxxxxxxx.com/userinfo?schema=openid
spring.security.oauth2.client.provider.xxxxxxxxx.user-name-attribute=name
spring.security.oauth2.client.provider.xxxxxxxxx.user-info-authentication-method=header
spring.security.oauth2.client.provider.xxxxxxxxx.jwk-set-uri=https://api.xxxxxxxxx.com/jwks
Thanks
When you receive JWT in client application, you need to verify the signature of JWT. To verify the signature you need public key of Auth provider. As per OAuth specifications, Auth provider can expose the public key through a URI and client can use this URI to get the public key to validate the JWT. This is what is missing in your configuration.

URLSession | Authentication Challenge Response | NTLM

I am aware of How to Respond to an Authentication Challengelike we have NTLM Authentication as there are 3 options.
Provide authentication credentials.
Attempt to continue without credentials.
Cancel the authentication request.
But just want to know the thoughts here, when we go with the first option Provide authentication credentials we pass the username and password URLCredential is there any possibility of leakage of credentials, is it secure to pass the credentials, what is happening behind the screens? how Apple network API sending the credentials to the server?
Yes, we can set the policies like server domain, failure count etc. but from the security point of view is it safe? from Man in Middle Attack (MIMA) or anything else?
Maybe the way I have posted my question is not clear but I was looking more from the Application credential security point of view with NTLM Authentication and after lots of Google, I have found, how’s NTLM works and it’s pretty interesting to see that client don’t share the password with the server. here are the steps as follow.
The client makes the request to the server.
The server needs to validate the user because there is no identity so server generates 16 bytes random number called as the challenge and sends it to the client.
Client hash this challenge with the user’s password and return it back to the server that is called the response it also includes username as plain text and challenge sent to the client.
The server sends everything to the domain controller and it uses the username to retrieve the hash of the user’s password from security account manager database and hash the challenge.
Domain controller shares the response back to the server if they are identical then authentication is successful otherwise a failure.
So the interesting part is here that Network API doesn’t share the password with the server it means it very secure.
I hope it will help others, For More.
There are multiple types of challenges, and the answer to your question depends on what type of challenge you're talking about. Each challenge has a protection space, which basically tells what type of challenge you're responding to.
To answer your question for the most common protection spaces:
Basic password-based authentication (NSURLAuthenticationMethodHTTPBasic): The credential you pass is sent in cleartext to the server (HTTP) or encrypted by the session key (HTTPS).
Digest authentication (NSURLAuthenticationMethodHTTPDigest): The credential you pass is cryptographically hashed with a nonce provided by the server, and only the resulting hashed token gets sent over the network.
NTLM authentication (NSURLAuthenticationMethodNTLM): The credential you pass is cryptographically hashed with a nonce sent by the server, and only the resulting hashed token gets sent over the network.
Client Certificate authentication (NSURLAuthenticationMethodClientCertificate): The certificate is sent to the server, but not the private key data. The client uses the private key to sign the prior TLS handshake data as a means of letting the server verify that the client really does have the private key associated with that cert.
Server certificate validation (NSURLAuthenticationMethodServerTrust): If you pass a certificate obtained from the server, you MUST validate it first, or else you effectively reduce the level of security to that of HTTP (i.e. any server can send any cert and you'll be saying to trust that cert when talking to the server).
The list above covers the most common protection spaces. Kerberos is its own animal, and I don't know anything at all about how that works. And there's also the "Form" protection space, which is just a placeholder for custom authentication that you can use in various parts of your app's code, but is not actually supported in any meaningful way.
It is worth noting that Basic, Digest, and NTLM authentication provide no protection against man-in-the-middle attacks if the attacker can alter data in transit, because the authentication token provided does not depend on the rest of the request in any way. Thus, these are really suitable only for use over an encrypted channel (HTTPS).

Configuration of Microsoft Graph OAuth2 authentication in Spring Security - Error AADSTS90014

I am writing an SSO provider for MS Graph APIs Azure AD v2 endpoint leveraging Spring OAuth2.
I am progressing with the implementation and constant testing but I stumbled upon an error returned by AAD which is puzzling me. After all, this should all be plain standard OAuth 2 flow.
I successfully configured my application on MS dev portal, providing a localhost redirect URL (which, for the record, is the only supporting the http scheme. Kudos to MS). So when I invoke http://localhost/myapp/auth/office365 Spring security successfully intercepts the invocation, provides a correct redirect to my browser with client ID to https://login.microsoftonline.com/common/oauth2/v2.0/authorize with expected parameters.
Microsoft shows a consent screen to me, after which I get redirected back to my Spring Security application via HTTP GET with expected authorization code parameter.
The problem is that when the application tries to negotiate the given authorization code for a bearer token headaches start. Spring Security invokes a POST to https://login.microsoftonline.com/common/oauth2/v2.0/token but ends in 401 error.
Here is the stack trace
error="invalid_request", error_description="AADSTS90014: The request body must contain the following parameter: 'client_id'.
Trace ID: 9acd2a10-1cfb-443f-9c57-78d608c00c00
Correlation ID: bf063914-8926-4e8f-b102-7522d0e3b0af
Timestamp: 2017-10-09 15:51:44Z", correlation_id="bf063914-8926-4e8f-b102-7522d0e3b0af", error_codes="[90014]", timestamp="2017-10-09 15:51:44Z", trace_id="9acd2a10-1cfb-443f-9c57-78d608c00c00"
at org.springframework.security.oauth2.common.exceptions.OAuth2ExceptionJackson2Deserializer.deserialize(OAuth2ExceptionJackson2Deserializer.java:100)
at org.springframework.security.oauth2.common.exceptions.OAuth2ExceptionJackson2Deserializer.deserialize(OAuth2ExceptionJackson2Deserializer.java:33)
at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4001)
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3072)
at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.readJavaType(AbstractJackson2HttpMessageConverter.java:235)
at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.readInternal(AbstractJackson2HttpMessageConverter.java:215)
at org.springframework.http.converter.AbstractHttpMessageConverter.read(AbstractHttpMessageConverter.java:193)
at org.springframework.security.oauth2.client.token.OAuth2AccessTokenSupport$AccessTokenErrorHandler.handleError(OAuth2AccessTokenSupport.java:235)
at org.springframework.web.client.RestTemplate.handleResponse(RestTemplate.java:700)
at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:653)
at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:621)
at org.springframework.security.oauth2.client.token.OAuth2AccessTokenSupport.retrieveToken(OAuth2AccessTokenSupport.java:137)
at org.springframework.security.oauth2.client.token.grant.code.AuthorizationCodeAccessTokenProvider.obtainAccessToken(AuthorizationCodeAccessTokenProvider.java:209)
at org.springframework.security.oauth2.client.token.AccessTokenProviderChain.obtainNewAccessTokenInternal(AccessTokenProviderChain.java:148)
at org.springframework.security.oauth2.client.token.AccessTokenProviderChain.obtainAccessToken(AccessTokenProviderChain.java:121)
at org.springframework.security.oauth2.client.OAuth2RestTemplate.acquireAccessToken(OAuth2RestTemplate.java:221)
at org.springframework.security.oauth2.client.OAuth2RestTemplate.getAccessToken(OAuth2RestTemplate.java:173)
at org.springframework.security.oauth2.client.filter.OAuth2ClientAuthenticationProcessingFilter.attemptAuthentication(OAuth2ClientAuthenticationProcessingFilter.java:105)
at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:212)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331)
at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:200)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331)
I have looked into Spring security implementation to find the cause,.
It happens that the error message error="invalid_request", error_description="AADSTS90014: The request body must contain the following parameter: 'client_id'. is self explanatory: MS Graph wants the client ID (which is still supplied by the basic authentication header) in the request body. Stop for a moment. I want to use plain old Spring Security and not third-party specific jars in order not to pollute my classpath.
Looking into Java source code of Spring OAuth 2 the problem is damn clear. Spring uses the client ID only in getParametersForAuthorizeRequest, which is used to generate the redirect URL. When it comes to getParametersForTokenRequest the client ID is not specified in the form.
Question: who is right here? How do I tell Spring that MS wants the client id in the token request after an authorization code has been obtained?
Just to clarify, you're not actually authenticating with or against Microsoft Graph. You're actually authenticating against Azure Active Directory. The Microsoft Graph API accepts the bearer token you'll end up with but it doesn't issue the access token itself.
It isn't clear which endpoint you're using for the Authorization Code flow, AAD has two of them: v1 and v2. The primary difference being that v2 uses a central registration and can authenticate both work/school and personal accounts.
Regardless of the endpoint, you do need to supply the clientid in the request body when you're requesting an access token. There are actually several values you need to provide in the body. Also note that these need to be provided as application/x-www-form-urlencoded.
For the v1 endpoint you provide (line breaks for readability only):
grant_type=authorization_code
&client_id={client-id}
&code={authoization-code}
&redirect_uri={redirect-uri}
&client_secret={client-secret}
&resource={resource-uri}
The v2 endpoint is almost identical but uses scope instead of resource:
grant_type=authorization_code
&client_id={client-id}
&code={authoization-code}
&redirect_uri={redirect-uri}
&client_secret={client-secret}
&scope={scopes}
OP's edit
Now, back to Spring Security. Spring by default uses an HTTP basic authentication scheme against Azure AD. In that scheme, the client ID and secret are encoded into the HTTP Authorization header, then the form only contains the authorization code and state parameter, so here is why I (the OP, ndr) was puzzled about why AAD refused the authorization.
In order to pass client ID and secret into the form, we can tell Spring Security to use a different supported authentication scheme. The form authentication scheme will push the client ID and secret into the form.
The below code works and retrieves the access token.
<oauth2:resource
id="msAdAuthenticationSource"
client-id="${oauth.appId}"
client-secret="${oauth.appSecret}"
type="authorization_code"
authentication-scheme="form"
client-authentication-scheme="form"
use-current-uri="true"
user-authorization-uri="${oauth.authorizationUri}"
access-token-uri="${oauth.accessTokenUri}"
scope="${oauth.scopes}"
pre-established-redirect-uri="${oauth.redirectUri}" />
Please note the two
authentication-scheme="form"
client-authentication-scheme="form"
Problem solved, a lot more to come!

Spring Security and Google OpenID Connect migration

Questions:
1) What's the best way to integrate OpenID Connect authentication into a webapp that uses Spring Security for authentication?
2) Is there any way - either from the MITREid side of things or the Google Accounts side of things - to get the MITREid OpenID Connect authentication filter to work with Google's OpenID Connect service?
I'm sure answers to these questions will be useful for any developer that uses the Spring Security OpenID module to authenticate with Google.
Detail:
My webapp uses Spring Security's OpenID module (<openid-login .../>) for authentication with Google Accounts as the Identity Provider. ie., users authenticate using their Google Apps or GMail email address.
Recently, whenever users authenticate, they receive this warning message from Google accounts:
Important notice: OpenID2 for Google accounts is going away on April
20, 2015.
So Google is dropping support for OpenID, will turn it off completely in April 2015, and states that you must switch to the OpenID Connect protocol if you want to authenticate with Google Accounts.
I was hoping Spring Security would have built-in support for OpenID Connect, just like it has built-in support for OpenID. e.g. something like an <openid-connect-login .../> element. But my searches have turned up no such support.
The best candidate I've found so far is MITREid Connect . It includes a Spring Security authentication filter named OIDCAuthenticationFilter for OpenID Connect. The problem is, it does not interoperate with Google's OpenID Connect implementation.
I tried cloning the MITREid simple-web-app and configured it to authenticate (using OpenID Connect) with Google Accounts. But it did not work because it depends on a nonce which Google's OpenID Connect implementation does not support. The error message from Google accounts was:
Parameter not allowed for this message type: nonce
Next I tried plugging my own implementation of MITREid's AuthRequestUrlBuilder interface into the MITREid configuration. The only difference between my implementation and MITREid's implementation was that I did not send the nonce.
Not sending the nonce made Google's OpenID Connect implementation happy but MITREid threw an exception when it couldn't find a nonce in the Google authentication response. The error message was:
Authentication Failed: ID token did not contain a nonce claim
I tracked the MITREid exception down to these lines in MITREID'S OIDCAuthenticationFilter:
// compare the nonce to our stored claim
String nonce = idClaims.getStringClaim("nonce");
if (Strings.isNullOrEmpty(nonce)) {
logger.error("ID token did not contain a nonce claim.");
throw new AuthenticationServiceException("ID token did not contain a nonce claim.");
}
But there is no way for me to extend MITREid's implementation to ignore the nonce. So close but yet so far! If Google Accounts would accept the nonce or MITREid could be configured to ignore the nonce then we'd have a solution.
Within the MITREid Connect issues list on github I've found others have run into these similar issues:
1) #726 - Documentation on using client with Google as authentication provider
2) #704 - Add a useNonce attribute into ServerConfiguration to indicate if the IdP accepts the nonce value into its requests.
So I am stuck. Come April 2015 Google will shutdown Open ID authentication.
Some relevant links:
1) https://support.google.com/accounts/answer/6135882
2) https://www.tbray.org/ongoing/When/201x/2014/03/01/OpenID-Connect
3) https://github.com/mitreid-connect
4) https://github.com/mitreid-connect/OpenID-Connect-Java-Spring-Server/blob/master/openid-connect-client/src/main/java/org/mitre/openid/connect/client/OIDCAuthenticationFilter.java
5) https://github.com/mitreid-connect/simple-web-app
6) https://github.com/mitreid-connect/OpenID-Connect-Java-Spring-Server/blob/master/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/impl/PlainAuthRequestUrlBuilder.java
7) https://github.com/mitreid-connect/OpenID-Connect-Java-Spring-Server/issues/726
8) https://github.com/mitreid-connect/OpenID-Connect-Java-Spring-Server/pull/704
2015-02-18 Update
Functionality has recently been added to the development branch of mitreid-connect for disabling the nonce - therefore making Google's OIDC server happy. Thankfully, mitreid-connect has also provided some guidance on interoperating with Google .
Unfortunately the "nonceEnabled" change is not yet available in Maven central but hopefully that will change soon.
AFAIK, there is no clean and easy Spring Security migration from OpenID to OpenID Connect authentication. Implementing OpenID authentication with Spring Security is straight-forward using the well documented <openid-login/> but there exists no analog for OpenID Connect.
The MITREid alternative is still on a development branch and unavailable at Maven Central and therefore not a candidate.
In the comments, Chuck Mah points to How to implement Openid connect and Spring Security where Romain F. provides the sample code.
Romain's sample code pointed me in the right direction. Given time is running out, I went with romain's approach, which was to write a custom Spring Security AuthenticationFilter that uses spring-security-oauth2 to query the oauth2 api userinfo endpoint (for Google that's https://www.googleapis.com/oauth2/v2/userinfo). The assumption is that if we are able to successfully query the userinfo endpoint then the user has successfully authenticated so we can trust the information returned - eg the user's email address.
When i first started learning about OpenID Connect the “id token” seemed to be the central concept. However, browsing the spring-security-oauth2 source code, it appears to be ignored. This leads to the question, what’s the point of the ID token if we can authenticate without it (by simply querying oauth2 userinfo endpoint)?
A minimalist solution - which i would prefer - would simply return a validated ID token. There would be no need to query the userinfo endpoint. But no such solution exists in the form of a Spring Security authentication filter.
My webapp was not a spring-boot app like romain's. spring-boot does alot of configuration behind the scenes. Here are some of the problems/solutions I encountered along the way:
problem: HTTP Status 403 - Expected CSRF token not found. Has your session expired?
solution: java config: httpSecurity.csrf().disable()
problem: HTTP Status 500 - Error creating bean with name 'scopedTarget.googleOAuth2RestTemplate': Scope 'session' is not active for the current thread;
solution: java config: OAuth2RestTemplate does not need to be session scoped (OAuth2ClientContext is already session scoped and that's all that's necessary)
problem: HTTP Status 500 - Error creating bean with name 'scopedTarget.oauth2ClientContext': Scope 'session' is not active for the current thread;
solution: web.xml: add RequestContextListener
explanation: because the oauth2ClientContext session-scoped bean is accessed outside the scope of the Spring MVC DispatcherServlet (it is being accessed from OpenIdConnectAuthenticationFilter, which is part of the Spring Security filter chain).
<listener>
<listener-class>org.springframework.web.context.request.RequestContextListener</listener-class>
</listener>
problem: org.springframework.security.oauth2.client.resource.UserRedirectRequiredException: A redirect is required to get the users approval.
solution: web.xml: Add filter definition immediately PRECEEDING springSecurityFilterChain
<filter>
<filter-name>oauth2ClientContextFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>oauth2ClientContextFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
Unfortunately, OpenID Connect does not allow us to request only email scope.
When our users authenticated using OpenID they would see a consent screen like "webapp would like to view your email address" with which they were comfortable. Now we must request scopes openid email resulting in a consent screen asking the user to share their entire public profile with us ... which we really don't need or want ... and users are less comfortable with this consent screen.

Resources