Describe the bug Our application provides ability to configure authentication with OpenID Connect (following documentation https://docs.spring.io/spring-security/site/docs/5.4.x/reference/html5/#oauth2login-advanced-userinfo-endpoint (example99)
With Azure AD as OP authentication fails with following exception
Caused by: java.lang.IllegalArgumentException: Missing attribute 'preferred_username' in attributes at org.springframework.security.oauth2.core.user.DefaultOAuth2User.(DefaultOAuth2User.java:72) at org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService.loadUser(DefaultOAuth2UserService.java:116) at org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService.loadUser(OidcUserService.java:109)
We noticed the failure was cause by the fact that the attribute we configured as username attributes is an id token attribute.
While the org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser is merging userinfo and id token attributes, but as explained in documentation
The OidcUserService leverages the DefaultOAuth2UserService when requesting the user attributes at the UserInfo Endpoint.
The illegalArgumentException is thrown in DefaultOAuth2UserService because it looks only userInfo attributes
A thing that may be interesting to mention is while debugging the difference between a google authentication and Azure AD authentication, I saw for google in OidcUserService shouldRetrieveUserInfo(userRequest) return false (so DefaultOAuth2UserService is never called) and for Azure AD it returns true
To Reproduce Configure authentication to an AzureID OP with preferred_username attributes (which is provided in id token)
ClientRegistration.withRegistrationId("azuread")
//.........
.userNameAttributeName("preferred_username")
.build();
Expected behavior Authentication should work with preferred_username claim value used as authenticated principal name
Comment From: amergey
Note that as a workaround I configured spring security with my own implementation of DefaultOauth2UserService with just one line of code updated
String userNameAttributeName = IdTokenClaimNames.SUB ; //userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName()
It works in my case (the oidc authentication succeed with proper preferred_username used as user name), but this is not the correct fix for this issue
Comment From: jgrandja
@amergey Please see reference for ClientRegistration:
userNameAttributeName: The name of the attribute returned in the UserInfo Response that references the Name or Identifier of the end-user.
As noted, the userNameAttributeName is a configuration applicable to the claims in the UserInfo response NOT the claims in the ID Token.
We noticed the failure was cause by the fact that the attribute we configured as username attributes is an id token attribute.
I'm going to close this based on application misconfiguration.
Comment From: amergey
In this case why OidcUserService is using userNameAttributeName
to get claims in id token ? see code in org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService.getUser(OidcUserRequest, OidcUserInfo, Set
Comment From: amergey
@jgrandja I think my point raised above is valid and the decision to tag this bug as invalid was too fast. If you still think this bug is invalid, could you point me on how could I use an id token attribute as username attribute for an oidc authentication ?
Comment From: jgrandja
@amergey
why
OidcUserServiceis usinguserNameAttributeNameto get claims in id token
This is not correct. The OidcUserService is using userNameAttributeName to get claim value from UserInfo response claims.
As mentioned, in previous comment:
userNameAttributeName : The name of the attribute returned in the UserInfo Response that references the Name or Identifier of the end-user.
The userNameAttributeName is a configuration specific to the UserInfo response NOT the ID Token claims.
how could I use an id token attribute as username attribute for an oidc authentication
You could use a delegation-based strategy:
private OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService() {
final OidcUserService delegate = new OidcUserService();
return (userRequest) -> {
OidcUser oidcUser = delegate.loadUser(userRequest);
return new DefaultOidcUser(
oidcUser.getAuthorities(), oidcUser.getIdToken(), oidcUser.getUserInfo(), "preferred_username");
};
}
See examples in UserInfo Endpoint.
Comment From: amergey
@jgrandja if I am sorry to say I have debugged the code and OidcUserService is sometime using userNameAttributeName to get claim form id token, at least with google as OP
I have explained this in my bug description
When the code reaches org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService.loadUser(OidcUserRequest)
as this.shouldRetrieveUserInfo(userRequest) return false,
return getUser(userRequest, userInfo, authorities);is called with userInfo set to null
So org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser.DefaultOidcUser is using userNameAttributeName to get the username form id token, please debug it and you will see the same.
Comment From: jgrandja
@amergey
this.shouldRetrieveUserInfo(userRequest)return false
If the UserInfo endpoint is not called and...
DefaultOidcUseris usinguserNameAttributeNameto get the username form id token
Then userNameAttributeName should not be configured if the UserInfo endpoint is not configured. This still seems to be a misconfiguration issue.
Can you please provide a minimal reproducible sample so I can see exactly what you are experiencing. Without this sample, we will continue to go back and forth which is not very efficient. I will wait for your sample.
Comment From: amergey
@jgrandja It seems quite obvious to see what I explain looking at the code, but to move forward I have updated initial spring boot getting started to illustrate the behavior : gs-spring-boot.zip
- in /gs-spring-boot/initial/src/main/resources/application.yml put valid google client id client secret
- put a breakpoint in org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService.loadUser(OidcUserRequest)
-
navigate to http://localhost:8080 Authentication.getName is using email claim from id token
-
Then update application.yml to replace google with and Azure AD and replace
user-name-attribute: emailwithuser-name-attribute: preferred_usernameand it will fails withjava.lang.IllegalArgumentException: Missing attribute 'preferred_username' in attributeswhile preferred_username is in id token.
I do not undestand why you are saying this is a misconfiguration issue, if this is a misconfiguration, could you explain why this sample with google is not failing with IllegalArgumentException instead of using claim from id token ?
If the sample with google is working as expected for google, then it should work as well for Azure AD (as explained in this bug)
In any case their is a bug, either the sample with google should not work either the sample with Azure AD should work, but there is definitely something wrong there
Comment From: jgrandja
@amergey The gs-spring-boot/initial sample you provided did not work out of the box. I needed to add:
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
Regardless, this confirms that it's an application misconfiguration.
Given the default Google client configuration, the UserInfo endpoint is NOT called. And since user-name-attribute: email is configured it will look for that attribute in the available claims, which happens to be the ID Token. This is a side-effect, because that claim happens to come in the ID Token BUT the application has configured user-name-attribute, which specifies it should read it from the UserInfo claims.
I will restate again, if your ClientRegistration is configured with user-name-attribute then it should be configured to call the UserInfo endpoint and use that claim name for Authentication.getName(). But since it's NOT calling the UserInfo endpoint, then it's simply not configured correctly.