Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
gburboz opened this issue Aug 13, 2018 · 18 comments
Closed

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

gburboz opened this issue Aug 13, 2018 · 18 comments
Assignees
Labels
in: oauth2 An issue in OAuth2 modules (oauth2-core, oauth2-client, oauth2-resource-server, oauth2-jose) status: declined A suggestion or change that we don't feel we should currently apply

Comments

@gburboz
Copy link

gburboz commented Aug 13, 2018

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 jgrandja added the in: oauth2 An issue in OAuth2 modules (oauth2-core, oauth2-client, oauth2-resource-server, oauth2-jose) label Aug 14, 2018
@jgrandja
Copy link
Contributor

@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:

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

Otherwise the UserInfo Endpoint is not called.

Does this help?

@gburboz
Copy link
Author

gburboz commented Aug 14, 2018

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
Copy link
Contributor

@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
Copy link
Author

gburboz commented Nov 21, 2018

@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
Copy link
Contributor

@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
Copy link
Author

gburboz commented Nov 26, 2018

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
Copy link
Contributor

@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
Copy link
Author

gburboz commented Nov 26, 2018

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
Copy link
Contributor

@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.

@rwinch rwinch added the status: declined A suggestion or change that we don't feel we should currently apply label May 3, 2019
@mraible
Copy link
Contributor

mraible commented Jun 20, 2019

@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
Copy link
Contributor

mraible commented Jun 20, 2019

@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
Copy link
Contributor

@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
Copy link

mathewthomaspallipuram commented Jan 27, 2021

@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
Copy link

ralphdov commented Mar 26, 2022

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
Copy link

JangoCG commented Oct 10, 2023

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

@softagongoraiberia
Copy link

@JangoCG Good morning.
I'm in the same situation. I have a Spring Cloud Gateway and Azure Entra ID, so if the call to user-info is made, the access token obtained is graph, since it requires User.Read.
Were you able to avoid the call to (user-info)?

@JangoCG
Copy link

JangoCG commented Mar 13, 2025

@JangoCG Good morning.
I'm in the same situation. I have a Spring Cloud Gateway and Azure Entra ID, so if the call to user-info is made, the access token obtained is graph, since it requires User.Read.
Were you able to avoid the call to (user-info)?

Hi,
I did not find a direct Solution. What I did at the and is to use keycloak and add Azure Entra ID as an identity provider in keycloak. Then I integrated my spring cloud API gateway with keycloak instead.
Keycloak would handle the Azure Entra id stuff and just return a normal access token to my spring cloud api gateway. This worked really well, and with the added benefit of Keycloak being able to add more id providers and so in the future with no changes in my spring cloud API gateway

I hope this helps

@softagongoraiberia
Copy link

softagongoraiberia commented Mar 13, 2025

This way, I don't need to make a request to obtain the information, so token renewal no longer goes through Graph.
By doing this, Spring Boot uses OIDC.
I don’t define the user-info-uri and specify the scope as openid, api://{CLIENT_ID}/.default
As a result, Spring Cloud Gateway no longer requests the user-info-uri because Spring uses OIDC.

    spring:
      security:
        oauth2:
          client:
            provider:
              azure:
                authorization-uri: https://login.microsoftonline.com/${TENANT_ID}/oauth2/v2.0/authorize
                token-uri: https://login.microsoftonline.com/${TENANT_ID}/oauth2/v2.0/token
                jwk-set-uri: https://login.microsoftonline.com/${TENANT_ID}/discovery/v2.0/keys
            registration:
              azure:
                client-id: ${CLIENT_ID}
                client-secret: ${CLIENT_SECRET}
                scope: ${CLIENT_SCOPE}
                authorization-grant-type: authorization_code
                redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
          resourceserver:
            jwt:
              issuer-uri: https://login.microsoftonline.com/${TENANT_ID}/v2.0
              jwk-set-uri: https://login.microsoftonline.com/${TENANT_ID}/discovery/v2.0/keys

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: oauth2 An issue in OAuth2 modules (oauth2-core, oauth2-client, oauth2-resource-server, oauth2-jose) status: declined A suggestion or change that we don't feel we should currently apply
Projects
None yet
Development

No branches or pull requests

8 participants