spring-projects / spring-security

Spring Security
http://spring.io/projects/spring-security
Apache License 2.0
8.79k stars 5.9k forks source link

Optimize OIDC for JWT based token to avoid user-info service call #5659

Closed gburboz closed 5 years ago

gburboz commented 6 years ago

Summary

Open ID Connect Core 1.0 specification does not mandate invocation of UserInfo Endpoint and set of Standard Claims can be returned in either ID Token and/or Access Token as JWT

@jgrandja please review this issue which came out based off discussion with @jzheaux on issue #5629

Actual Behavior

Currently OAuth client provider user-info-uri config is mandatory and this HTTP call is always invoked even though in some cases we already have necessary info already available.

Expected Behavior

When OAuth client provider user-info-uri is not provided and openid scope is mentioned (OP is OIDC), then get the claims from ID Token (which is always JWT) and additionally from Access Token if that is also a JWT.

Alternatively have an additional config parameter to drive this behavior so that it can also be used with plain OAuth without OIDC as we already drive user identity with user-name-attribute and rest of the claims can just be considered additional info.

e.g.: In case of Google Open ID Connect we can obtain user information from the Google's OIDC - ID token without having to invoke an additional UserInfo endpoint.

Version

Spring Security 5

jgrandja commented 6 years ago

@gburboz

Currently OAuth client provider user-info-uri config is mandatory and this HTTP call is always invoked even though in some cases we already have necessary info already available

The user-info-uri is required for standard OAuth 2.0 Providers (implemented by DefaultOAuth2UserService) but it's not mandatory for OIDC Providers (implemented by OidcUserService).

If you look at the logic in OidcUserService.shouldRetrieveUserInfo() you will see that the UserInfo Endpoint is called if ALL these conditions are met:

Otherwise the UserInfo Endpoint is not called.

Does this help?

gburboz commented 6 years ago

I am not sure why would there be any restriction wrt. scope profile, email, address, phone for calling this service.

Seems it is made mandatory for DefaultOAuth2UserService.loadUser(...) which is what I see is being called.

Sample project that I am working is gb-oauth2-springboot-talk/Step-04-CustomProviderLogin where by for demo purpose I am configuring custom OAuth provider

My apologies but not sure how or when spring security uses OIDC vs plain OAuth for authentication.

jgrandja commented 5 years ago

@gburboz With regard to your comment

... not sure how or when spring security uses OIDC vs plain OAuth for authentication.

OpenID Connect authentication is triggered when the openid scope is included in the Authenticaton request. In this case OidcUserService is used and the UserInfo Endpoint might be called depending on the config of the ClientRegistration - see OidcUserService.shouldRetrieveUserInfo().

If the openid scope is NOT included in the Authentication Request than DefaultOAuth2UserService is used and the UserInfo Endpoint must be called in order to obtain UserInfo - given that an ID Token is not available in this flow.

I hope this explains things? I'm going to close this issue as this works as expected.

gburboz commented 5 years ago

@jgrandja , it does not work as expected by OIDC spec. Current spring-security design/implementation mandates invocation of user-info service by the client while no such restriction is imposed by spec. In certain scenarios like Google OIDC, same info is already available in id_token hence HTTP call to user-info service can be avoided.

jgrandja commented 5 years ago

@gburboz

Current spring-security design/implementation mandates invocation of user-info service by the client while no such restriction is imposed by spec

This is not correct. Have you reviewed the logic in OidcUserService.shouldRetrieveUserInfo() as mentioned in previous comment?

Given this Google client registration:

spring:
  security:
    oauth2:
      client:
        registration:
          google:
            client-id: client-id
            client-secret: secret
            scope: openid

The UserInfo endpoint will not be called.

And given this Google client registration:

spring:
  security:
    oauth2:
      client:
        registration:
          google:
            client-id: client-id
            client-secret: secret
        provider:
          google:
            user-info-uri:

The UserInfo endpoint will not be called because the user-info-uri is empty. Note: CommonOAuth2Provider.GOOGLE provides defaults so you need to override here if you don't want the UserInfo endpoint called.

If you are still having issues than please put together a sample that reproduces this and I'll take a look.

gburboz commented 5 years ago

I tried with spring-boot-starter-parent version 2.0.6.RELEASE , 2.1.0.RELEASE and 2.1.1.BUILD-SNAPSHOT with spring-security-oauth2-client and spring-security-oauth2-jose dependencies included.

Used below to configure Google provider without user-info-uri

spring.security.oauth2.client:
  registration:
    gbgoogle:
      client-name: Custom Provider Google
      client-id: your-google-client-id-here
      client-secret: your-google-client-secret-here
      authorization-grant-type: authorization_code
      client-authentication-method: post
      redirect-uri-template: "{baseUrl}/login/oauth2/code/{registrationId}"
      scope: openid profile email
  provider:
    gbgoogle:
      user-name-attribute: sub
      authorization-uri: https://accounts.google.com/o/oauth2/v2/auth
      token-uri: https://www.googleapis.com/oauth2/v4/token
      user-info-uri:
      jwk-set-uri: https://www.googleapis.com/oauth2/v3/certs

Considering scope has openid, user-info should not have been mandatory but I get following error on UI

Your login attempt was not successful, try again.

Reason: [missing_user_info_uri] Missing required UserInfo Uri in UserInfoEndpoint for Client Registration: gbgoogle

Following is part of exception stack trace with spring-boot-starter-parent version 2.0.6.RELEASE

00:54:50 DEBUG o.s.s.o.c.w.OAuth2LoginAuthenticationFilter : Authentication request failed: org.springframework.security.oauth2.core.OAuth2AuthenticationException: [missing_user_info_uri] Missing required UserInfo Uri in UserInfoEndpoint for Client Registration: gbgoogle
org.springframework.security.oauth2.core.OAuth2AuthenticationException: [missing_user_info_uri] Missing required UserInfo Uri in UserInfoEndpoint for Client Registration: gbgoogle
    at org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService.loadUser(DefaultOAuth2UserService.java:65)
    at org.springframework.security.oauth2.client.authentication.OAuth2LoginAuthenticationProvider.authenticate(OAuth2LoginAuthenticationProvider.java:128)
    at org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:174)
    at org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter.attemptAuthentication(OAuth2LoginAuthenticationFilter.java:166)
jgrandja commented 5 years ago

@gburboz Based on the stacktrace, OAuth2LoginAuthenticationProvider calls DefaultOAuth2UserService which is the standard OAuth 2.0 Authorization Code flow. This is not the OpenID Connect flow. The reason is because the scope you have configured is not comma delimited so the Set of ClientRegistration.scopes is actually one element of "openid profile email". Make sure you configure as scope: openid, profile, email. This will ensure that OidcAuthorizationCodeAuthenticationProvider is called instead which in turn will call OidcUserService to perform the OpenID Connect flow.

gburboz commented 5 years ago

Thanks @jgrandja , you are right about scope and after making changes you recommended it worked. I used space delimiter as that is what is mentioned in spec and it kind of worked with OP as it got what it expected but caused issues with spring-oauth.

May be we should update scope as list to eliminate the confusion and/or do not allow space char which is special for OAuth scope. This will help eliminate hard to detect bugs like this where by OAuth is used instead of OIDC even though scope openid is specified.

jgrandja commented 5 years ago

@gburboz This is not an issue directly related to Spring Security. Spring Boot reads these properties via it's YAML reader. It's up to the user to ensure they have a properly configured/formatted yaml properties.

May be we should update scope as list to eliminate the confusion and/or do not allow space char which is special for OAuth scope

scope should be Set as duplicates should be avoided.

mraible commented 5 years ago

@jgrandja Is this still true with Spring Security in Spring Boot 2.1.6?

If you look at the logic in OidcUserService.shouldRetrieveUserInfo() you will see that the UserInfo Endpoint is called if ALL these conditions are met:

  • user-info-uri != null
  • grant_type == authorization_code
  • accessToken.scopes contains any of profile, email, address, phone

Otherwise the UserInfo Endpoint is not called.

The reason I ask is that I'm having an issue getting all the user's attributes when my access token contains the following for scopes.

 "scp": [
  "openid",
  "profile"
 ],

When I have the following, it works (but the access token also has most of the user's attributes in it):

"scope": "openid jhipster email offline_access profile",

I'm using OIDC discovery and just defining an issuer-uri.

mraible commented 5 years ago

@jgrandja I think I can answer my own question. The UserInfo Endpoint is called when using oauth2Login() and issuer-uri. It's not called when using oauth2ResourceServer(). For now, I'll just add claims to my access token to resolve user info. I would like to know if it's possible to get user info when using oauth2ResourceServer() though. It seems like functionality that should be available.

jgrandja commented 5 years ago

@mraible The logic you outlined for OidcUserService.shouldRetrieveUserInfo() is correct. However, I just added an enhancement to this logic. Please see this comment for more info.

The UserInfo Endpoint is called only during an oauth2Login() flow. It is never called from a oauth2ResourceServer() since that flow is not related to OpenID Connect (oauth2Login()). However, nothing is stopping you to make a call to the UserInfo Endpoint using the access token received from the oauth2ResourceServer(), although this would be custom logic you would need to implement. If you decide to implement this logic, the claims returned from the UserInfo Endpoint cannot be added/enhanced to the access token since only the provider is allowed to create/sign an access token with all the necessary claims associated to it.

mathewthomaspallipuram commented 3 years ago

@jgrandja I am getting issue for Spring OAuth2 - when assigning the value to user-name-attribute; it gives the error “Missing attribute 'name' in attributes”

https://stackoverflow.com/questions/65912983/spring-oauth2-when-assigning-the-value-to-user-name-attribute-it-gives-the-err

ralphdov commented 2 years ago

This is an old thread but as I had the same question, perhaps it can help someone else. with our keykloack configuration, all requested information was already in the id token, so I did not want spring security to call the user info endpoint.

As explained in this discussion, the solution is to have the user-info-uri property empty.

Do not use spring.security.oauth2.provider.[provider].issuer-uri property as it will automatically retrieve all provider info including the user info uri.

instead provide : authorization-uri, token-uri and jwk-set-uri with this setup, spring security will not have the user-info-uri and will not call the user info endpoint. having a dedicated property to drive spring behaviour would have be more straight forward (with no need to understand spring security behavior) but it's fine as we have at at the end the possibility to configure what we want.

JangoCG commented 1 year ago

How can my application but then get an access? I have Spring Cloud API Gateway setup as a oauth2 client. And I now understand, that it has to have openid scope so it can call the user-info endpoint. But I need to get access tokens, which I can then pass downstream to my resource server. The token provided from Microsoft will just be for user-info (graph) endpoint but not for my custom APIs