spring security: Generate authentication from jwt - spring-security

Here my configuration:
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration {
#Bean
public JwtDecoder reactiveJwtDecoder() throws Exception {
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKey = new SecretKeySpec("JAC1O17W1F3QB9E8B4B1MT6QKYOQB36V".getBytes(), mac.getAlgorithm());
return NimbusJwtDecoder.withSecretKey(secretKey)
.macAlgorithm(MacAlgorithm.HS256)
.build();
}
#Bean
public SecurityFilterChain securityFilterChain(
HttpSecurity http
) throws Exception {
Customizer<OAuth2ResourceServerConfigurer<HttpSecurity>> oauth2Customizer = (config) -> config.jwt();
return http
.httpBasic().disable()
.csrf().disable()
.formLogin().disable()
.anonymous().disable()
.logout().disable()
.authorizeHttpRequests((authorize) -> authorize
.antMatchers("/actuator/**").permitAll()
.antMatchers("/gicar/**").permitAll()
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2Customizer)
.build();
}
}
Everything works fine.
I need to allow method execution according to jwt token information.
I know that in order to get that, I need to transform the jwt token to an Authentication object.
From my configuration on, what should I add to it, in order to generate an Authentication object and translate claims to authorities?
What I need to do is something like this:
#RestController
#RequestMapping(value = "/qdcf")
#RequiredArgsConstructor
#Timed
public class QdCFController {
private final UsuariRepository usuariRepository;
#GetMapping("/user")
#PreAuthorize("hasRole(ADMIN)")
public Optional<Usuari> user() {
return this.usuariRepository.findOne(UsuariSpecs.hasCodi("11111111A"));
}
}

Your configuration is already enough for Spring to create an Authentication object, the question is how it's going to get roles from your JWT.
To "guide" Spring and your JwtDecoder you should create and configure a bean of JwtAuthenticationConverter type, e.g. like this:
#Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
final JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
// choose a JWT claim name where authorities are stored on login
// authorities claim name defaults to "scope" and "scp" if this method is not used
grantedAuthoritiesConverter.setAuthoritiesClaimName("roles");
// here choose a scope prefix that was used
// prefix defaults to "SCOPE_" if this method is not used
grantedAuthoritiesConverter.setAuthorityPrefix("");
final JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter);
return jwtAuthenticationConverter;
}
This configuration will help JwtDecoder to translate any authority in the "roles" claim of JWT to GrantedAuthority with no prefix.

Related

Authentification manager + database

I am trying to set up an authentication system on my spring boot application using spring boot security but I am having trouble getting it to work with my database.
If I try to log in with the credentials of a user created in my InMemoryUserDetailsManager, my AuthenticationManager works completely (it throws an error if the credentials are wrong).
However, if I try with the credentials of a user in my database, it doesn't work because it seems to not be logged in with my AuthenticationManager.
I'm new to spring boot and spring security, sorry if my problem is a bit stupid.
Here is my code that might help you understand my problem:
#Bean
public InMemoryUserDetailsManager inMemoryUserDetailsManager(){
return new InMemoryUserDetailsManager(
User.withUsername("user1").password(passwordEncoder.encode("1234")).authorities("USER").build(),
User.withUsername("user2").password(passwordEncoder.encode("1234")).authorities("USER").build(),
User.withUsername("admin").password(passwordEncoder.encode("1234")).authorities("USER","ADMIN").build()
);
}
#Bean
public AuthenticationManager authenticationManager(UserDetailsService userDetailsService){
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setPasswordEncoder(passwordEncoder);
authProvider.setUserDetailsService(userDetailsService);
return new ProviderManager(authProvider);
}
#Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
return httpSecurity
.csrf(csrf ->csrf.disable())
.authorizeHttpRequests(auth->auth.requestMatchers("/auth/**").permitAll())
.authorizeHttpRequests(auth ->
auth.anyRequest().authenticated()
)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)
.httpBasic(Customizer.withDefaults())
.build();
}
#PostMapping("/login")
public Map<String,String> Login(#RequestBody User user){
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(user.getEmail(), user.getPassword())
);
My database is correctly configured and contains a user
For someone that could be in the same situation as me, u must add this in your code :
#Bean
public UserDetailsService userDetailsService(){
return email->userRepository.findByEmail(email).orElseThrow(()->new UsernameNotFoundException("User not found"));
}
Moreover, u must change your authenticationManager like this :
#Bean
public AuthenticationManager authenticationManager(){
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setPasswordEncoder(passwordEncoder);
authProvider.setUserDetailsService(userDetailsService());
return new ProviderManager(authProvider);
}

Is it neseccary to use AuthenticationManager to load user from database when authorizing in Spring security?

I have a controller that authorizes user:
#PostMapping
public ResponseEntity<Token> signInUser(Credentials credentials) {
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
credentials.getUsername(),
credentials.getPassword())
);
Token token = tokenService.generateToken(authentication);
return ResponseEntity.ok(token);
}
"Credentials" is just a simple data object, generated by swagger:
Credentials
AuthenticationManager bean:
#Bean
public AuthenticationManager authenticationManager(UserDetailsService userDetailsService) {
var authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService);
return new ProviderManager(authProvider);
}
As I understand, AuthenticationManager only retrieves user from the database and checks if passwords match. And I would like to write all this logic myself. Do I have to write my own implementation of AuthenticationManager or I can just write UserService that will not implement anything?
For example, like these:
UserController:
#PostMapping
public ResponseEntity<Token> signInUser(Credentials credentials) {
Token token = userService.signIn(credentials);
return ResponseEntity.ok(token);
}
UserService:
public Token signIn(Credentials credentials) {
Optional<UserEntity> userEntityOpt =
userEntityRepo.findByUsername(credentials.getUsername());
return tokenService.generateToken(userEntityOpt.get());
}
Validation and stuff omitted

How can a jwt protected resource server call userinfo?

The documentation at spring security is missing important detail. Our idp does not provide an introspection link, and our resource server is not a client in its own right. It receives JWT access tokens from the actual client, and "needs to know" details about the user associated with the access token.
In our case standard jwt processing gives us a useful start, but we need to fill out the authentication with the claims from userinfo.
How do we 1. get a baseline valid oauth2 authentication, 2. fill it out with the results of the userinfo call.
public class UserInfoOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
private final OpaqueTokenIntrospector delegate =
new NimbusOpaqueTokenIntrospector("https://idp.example.org/introspect", "client", "secret");
private final WebClient rest = WebClient.create();
#Override
public OAuth2AuthenticatedPrincipal introspect(String token) {
OAuth2AuthenticatedPrincipal authorized = this.delegate.introspect(token);
return makeUserInfoRequest(authorized);
}
}
Current implementation using a converter:
#Configuration
public class JWTSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired JwtConverterWithUserInfo jwtConverter;
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.cors()
.and()
.csrf().disable()
.authorizeRequests(authz -> authz
.antMatchers("/api/**").authenticated()
.anyRequest().permitAll())
.oauth2ResourceServer().jwt().jwtAuthenticationConverter(jwtConverter);
}
}
#Configuration
public class WebClientConfig {
/**
* Provides a Web-Client Bean containing the bearer token of the authenticated user.
*/
#Bean
WebClient webClient(){
HttpClient httpClient = HttpClient.create()
.responseTimeout(Duration.ofSeconds(5))
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000);
return WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
.defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
.filter(new ServletBearerExchangeFilterFunction())
.build();
}
}
#Component
#Log4j2
public class JwtConverterWithUserInfo implements Converter<Jwt, AbstractAuthenticationToken> {
#Autowired WebClient webClient;
#Value("${userinfo-endpoint}")
String userinfoEndpoint;
#SuppressWarnings("unchecked")
#Override
public AbstractAuthenticationToken convert(Jwt jwt) {
String token = jwt.getTokenValue();
log.debug("Calling userinfo endpoint for token: {}", token);
String identityType = jwt.getClaimAsString("identity_type");
Map<String,Object> userInfo = new HashMap<>();
if ("user".equals(identityType)) {
// invoke the userinfo endpoint
userInfo =
webClient.get()
.uri(userinfoEndpoint)
.headers(h -> h.setBearerAuth(token))
.retrieve()
.onStatus(s -> s.value() >= HttpStatus.SC_BAD_REQUEST, response -> response.bodyToMono(String.class).flatMap(body -> {
return Mono.error(new HttpException(String.format("%s, %s", response.statusCode(), body)));
}))
.bodyToMono(Map.class)
.block();
log.debug("User info Map is: {}",userInfo);
// construct an Authentication including the userinfo
OidcIdToken oidcIdToken = new OidcIdToken(jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(), jwt.getClaims());
OidcUserInfo oidcUserInfo = new OidcUserInfo(userInfo);
List<OidcUserAuthority> authorities = new ArrayList<>();
if (oidcIdToken.hasClaim("scope")) {
String scope = String.format("SCOPE_%s", oidcIdToken.getClaimAsString("scope"));
authorities.add(new OidcUserAuthority(scope, oidcIdToken, oidcUserInfo));
}
OidcUser oidcUser = new DefaultOidcUser(authorities, oidcIdToken, oidcUserInfo, IdTokenClaimNames.SUB);
//TODO replace this OAuth2 Client authentication with a more appropriate Resource Server equivalent
return new OAuth2AuthenticationTokenWithCredentials(oidcUser, authorities, oidcUser.getName());
} else {
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
if (jwt.hasClaim("scope")) {
authorities.add(new SimpleGrantedAuthority(String.format("SCOPE_%s", jwt.getClaimAsString("scope"))));
}
return new JwtAuthenticationToken(jwt, authorities);
}
}
}
public class OAuth2AuthenticationTokenWithCredentials extends OAuth2AuthenticationToken {
public OAuth2AuthenticationTokenWithCredentials(OAuth2User principal,
Collection<? extends GrantedAuthority> authorities,
String authorizedClientRegistrationId) {
super(principal, authorities, authorizedClientRegistrationId);
}
#Override
public Object getCredentials() {
return ((OidcUser) this.getPrincipal()).getIdToken();
}
}
Instead of a custom OpaqueTokenIntrospector, try a custom JwtAuthenticationConverter:
#Component
public class UserInfoJwtAuthenticationConverter implements Converter<Jwt, BearerTokenAuthentication> {
private final ClientRegistrationRepository clients;
private final JwtGrantedAuthoritiesConverter authoritiesConverter =
new JwtGrantedAuthoritiesConverter();
#Override
public BearerTokenAuthentication convert(Jwt jwt) {
// Spring Security has already verified the JWT at this point
OAuth2AuthenticatedPrincipal principal = invokeUserInfo(jwt);
Instant issuedAt = jwt.getIssuedAt();
Instant expiresAt = jwt.getExpiresAt();
OAuth2AccessToken token = new OAuth2AccessToken(
BEARER, jwt.getTokenValue(), issuedAt, expiresAt);
Collection<GrantedAuthority> authorities = this.authoritiesConverter.convert(jwt);
return new BearerTokenAuthentication(principal, token, authorities);
}
private OAuth2AuthenticatedPrincipal invokeUserInfo(Jwt jwt) {
ClientRegistration registration =
this.clients.findByRegistrationId("registration-id");
OAuth2UserRequest oauth2UserRequest = new OAuth2UserRequest(
registration, jwt.getTokenValue());
return this.oauth2UserService.loadUser(oauth2UserRequest);
}
}
And then wire into the DSL like so:
#Bean
SecurityFilterChain web(
HttpSecurity http, UserInfoJwtAuthenticationConverter authenticationConverter) {
http
.oauth2ResourceServer((oauth2) -> oauth2
.jwt((jwt) -> jwt.jwtAuthenticationConverter())
);
return http.build();
}
our resource server is not a client in its own right
oauth2-client is where Spring Security's support for invoking /userinfo lives and ClientRegistration is where the application's credentials are stored for addressing /userinfo. If you don't have those, then you are on your own to invoke the /userinfo endpoint yourself. Nimbus provides good support, or you may be able to simply use RestTemplate.

Success Handler for Multiple Spring Security SAML RelyingPartyRegistrations

I am using Spring Security 5.5.3 to hit two separate SAML Identity Providers (IDP) (similar to, say, Google and Facebook, but not OAuth).
So, in my Security configuration, I define the two RelyingPartyRegistration beans:
public RelyingPartyRegistration site1RelyingPartyRegistration() {
RelyingPartyRegistration registration = RelyingPartyRegistration.withRegistrationId("site1")
// rest of configuration
.build();
return registration;
}
public RelyingPartyRegistration site2RelyingPartyRegistration() {
RelyingPartyRegistration registration = RelyingPartyRegistration.withRegistrationId("site2")
// rest of configuration
.build();
return registration;
}
#Bean
public RelyingPartyRegistrationRepository relyingPartyRegistrationRepository() throws Exception {
Collection<RelyingPartyRegistration> registrations = Collections.unmodifiableList(
Arrays.asList(
site1RelyingPartyRegistration(),
site2elyingPartyRegistration())
);
InMemoryRelyingPartyRegistrationRepository repository = new InMemoryRelyingPartyRegistrationRepository(registrations);
return repository;
}
Then I define my SuccessHandler:
public class MySuccessHandler implements AuthenticationSuccessHandler {
#Autowired
private AuthenticationManager authenticationManager;
#Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException{
// need to determine whether the success came from site1 or site2
}
}
My question is, since I can authenticate from more than one Identity Provider, how can I find out which site it was? The only idea I have is to pull the entire <samlp:Response> from the Saml2AuthenticationToken and parse it to an XML document to get a single String attribute.
Is there an easier way?
In Spring Security 5.6, it's included in the Authentication:
Saml2AuthenticatedPrincipal principal =
(Saml2AuthenticatedPrincipal) authentication.getPrincipal();
String registrationId = principal.getRelyingPartyRegistrationId();
Before Spring Security 5.6, you can customize the Saml2AuthenticatedPrincipal in the authentication provider:
OpenSaml4AuthenticationProvider provider =
new OpenSaml4AuthenticationProvider();
provider.setResponseAuthenticationConverter((params) -> {
Response response = params.getResponse();
Saml2AuthenticationToken token = params.getToken();
RelyingPartyRegistration registration = token.getRelyingPartyRegistration();
String registrationId = registration.getRegistrationId();
// ... create custom authentication that contains the registration id
});

Spring Security OAuth2 Java Config for Google Login

I am migrating working XML configuration to Java configuration for Spring Security OAuth2 and using Google as the OAuth provider.
This is how my java configuration looks:
#Configuration
#EnableWebMvcSecurity
#EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
private static final List<String> scope;
static {
// Permissions to access email and profile
scope = new ArrayList<>(3);
scope.add("openid");
scope.add("email");
scope.add("profile");
}
#Autowired(required = true)
private UserService userService;
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Override
protected void configure(HttpSecurity http) throws Exception {
// #formatter:off
http.
authorizeRequests()
.antMatchers(HttpMethod.GET, "/","/public/**", "/resources/**","/resources/public/**").permitAll()
//.antMatchers("/google_oauth2_login").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/")
.loginProcessingUrl("/login")
.defaultSuccessUrl("/home")
.and()
.csrf().disable()
.logout()
.logoutSuccessUrl("/")
.logoutUrl("/logout")
.and()
.requiresChannel().anyRequest().requiresSecure()
.and()
.addFilterAfter(oAuth2ClientContextFilter(),ExceptionTranslationFilter.class)
.addFilterAfter(googleOAuth2Filter(),OAuth2ClientContextFilter.class)
.userDetailsService(userService);
// #formatter:on
}
#Override
protected void configure(AuthenticationManagerBuilder auth)
throws Exception {
// #formatter:off
auth
.authenticationProvider(googleOauth2AuthProvider())
.userDetailsService(userService);
// #formatter:on
}
#Bean
public GoogleOAuth2Filter googleOAuth2Filter() throws Exception {
GoogleOAuth2Filter filter = new GoogleOAuth2Filter(
"/google_oauth2_login",
"https://accounts.google.com/o/oauth2/auth",
oAuth2RestTemplate(auth2ProtectedResourceDetails()));
filter.setAuthenticationManager(authenticationManagerBean());
return filter;
}
#Bean
public GoogleOauth2AuthProvider googleOauth2AuthProvider() {
GoogleOauth2AuthProvider authProvider = new GoogleOauth2AuthProvider();
return authProvider;
}
#Bean
public OAuth2ProtectedResourceDetails auth2ProtectedResourceDetails() {
AuthorizationCodeResourceDetails auth2ProtectedResourceDetails = new AuthorizationCodeResourceDetails();
auth2ProtectedResourceDetails
.setClientAuthenticationScheme(AuthenticationScheme.form);
auth2ProtectedResourceDetails
.setAuthenticationScheme(AuthenticationScheme.form);
auth2ProtectedResourceDetails.setGrantType("authorization_code");
auth2ProtectedResourceDetails
.setClientId("the-client-id");
auth2ProtectedResourceDetails
.setClientSecret("the-client-secret");
auth2ProtectedResourceDetails
.setAccessTokenUri("https://accounts.google.com/o/oauth2/token");
auth2ProtectedResourceDetails.setScope(scope);
auth2ProtectedResourceDetails
.setUserAuthorizationUri("https://accounts.google.com/o/oauth2/auth");
auth2ProtectedResourceDetails.setUseCurrentUri(false);
auth2ProtectedResourceDetails
.setPreEstablishedRedirectUri("https://localhost/google_oauth2_login");
return auth2ProtectedResourceDetails;
}
#Bean
public OAuth2RestTemplate oAuth2RestTemplate(
OAuth2ProtectedResourceDetails resource) {
OAuth2RestTemplate oAuth2RestTemplate = new OAuth2RestTemplate(resource);
return oAuth2RestTemplate;
}
#Bean
public OAuth2ClientContextFilter oAuth2ClientContextFilter() {
OAuth2ClientContextFilter oAuth2ClientContextFilter = new OAuth2ClientContextFilter();
return oAuth2ClientContextFilter;
}
}
Note that I have disabled CSRF.
From my login page the user do gets redirected to Google login page
Problem:-
Google Permission Page just asks for "Have offline access".'Email/Profile' access request is missing.
The equivalent 'scope' attibute XML configuration :-
<oauth2:resource id="googleOauth2Resource" type="authorization_code"
client-id="the-client-id
client-secret="the-client-secret"
user-authorization-uri="https://accounts.google.com/o/oauth2/auth"
scope="openid email profile" use-current-uri="false"
client-authentication-scheme="form" pre-established-redirect-uri="https://localhost/google_oauth2_login" />
do correctly asks for email and profile permissions. Why?
Continuing anyway with the 'Have offline access' results in this exception:-
org.springframework.web.client.HttpClientErrorException: 400 Bad Request
at org.springframework.web.client.DefaultResponseErrorHandler.handleError(DefaultResponseErrorHandler.java:91)
at org.springframework.security.oauth2.client.token.OAuth2AccessTokenSupport$AccessTokenErrorHandler.handleError(OAuth2AccessTokenSupport.java:243)
at org.springframework.web.client.RestTemplate.handleResponseError(RestTemplate.java:592)
at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:550)
at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:514)
at org.springframework.security.oauth2.client.token.grant.code.AuthorizationCodeAccessTokenProvider.obtainAuthorizationCode(AuthorizationCodeAccessTokenProvider.java:145)
at org.springframework.security.oauth2.client.token.grant.code.AuthorizationCodeAccessTokenProvider.obtainAccessToken(AuthorizationCodeAccessTokenProvider.java:196)
at org.springframework.security.oauth2.client.token.AccessTokenProviderChain.obtainNewAccessTokenInternal(AccessTokenProviderChain.java:142)
at org.springframework.security.oauth2.client.token.AccessTokenProviderChain.obtainAccessToken(AccessTokenProviderChain.java:118)
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.OAuth2RestTemplate.createRequest(OAuth2RestTemplate.java:105)
while trying to get user profile using this code block:
#Override
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException,
IOException, ServletException {
logger.info("Google Oauth Filter Triggered!!");
SecurityContext context = SecurityContextHolder.getContext();
// auth null or not authenticated.
String code = request.getParameter("code");
Authentication dummyAuthentication = null;
if (StringUtils.isEmpty(code)) {
// Google authentication in progress. will return null.
logger.debug("Will set dummy user in context ");
dummyAuthentication = getDummyAuthenticationToken();
context.setAuthentication(dummyAuthentication);
// trigger google oauth2.
oauth2RestTemplate.postForEntity(authURI, null, Object.class);
return null;
} else {
// response from google received !!.
// remove dummy authentication from context.
//SecurityContextHolder.clearContext();
logger.debug("Response from Google Recieved !!");
// get user profile and prepare the authentication token object.
ResponseEntity<Object> forEntity = oauth2RestTemplate.getForEntity(
HTTPS_WWW_GOOGLEAPIS_COM_PLUS_V1_PEOPLE_ME_OPEN_ID_CONNECT,
Object.class);
#SuppressWarnings("unchecked")
Map<String, String> profile = (Map<String, String>) forEntity
.getBody();
CustomOAuth2AuthenticationToken authenticationToken = getOAuth2Token(
profile.get(EMAIL), profile.get(NAME));
authenticationToken.setAuthenticated(false);
return getAuthenticationManager().authenticate(authenticationToken);
}
}
Spring RestTemplate showing this in logs:
o.s.web.client.RestTemplate : POST request for "https://accounts.google.com/o/oauth2/auth" resulted in 400 (Bad Request); invoking error handler
2014-09-05 21:51:46.870 WARN 5836 --- [ qtp25546756-15] o.eclipse.jetty.servlet.ServletHandler : /google_oauth2_login
This same piece of code works while using with XML configuration.
UPDATE 1
I was able to fix the 'Offline Access' problem by changing scope to 'https://www.googleapis.com/auth/plus.profile.emails.read' & 'https://www.googleapis.com/auth/plus.login'.
Still getting bad request error while trying to get user profile
Please find source code for the problem here -
git clone https://kumarsambhavjain#bitbucket.org/kumarsambhavjain/spring-oauth2-login.git
Have you tried change profile URL to
https://www.googleapis.com/plus/v1/people/me/openIdConnect
See more: https://developers.google.com/+/api/openidconnect/getOpenIdConnect
I used your code to create a OAuth2 Spring Boot sample, quite similar, and I had same issue once I replaced profile URL to:
https://www.googleapis.com/plus/v1/people/me/openIdConnect
I resolved it by enabling Google+ API in Google Console:
Visit the Google API Console here:https://code.google.com/apis/console/?api=plus
Under the Services panel, make sure the Google+ API is turned "on".
In the APIs console, click API Access in the left menu.
Copy the API key presented towards the bottom. Include this API key in your HTTP request.
This process is explained in this question:
How to call https://www.googleapis.com/plus/v1/people/me at google

Resources