Summary

I've been playing around with Microsoft's identity platform (v2.0) and it seems like it it doing things a bit differently than v1.0 (Azure AD). I've been using the issuer-uri to not have to specify all properties manually and it seems that OidcUserService#loadUser() is, by default, retrieving the user info from the user-info-uri (here provided by the .well-known/openid-configuration) since I had to add the profile scope to get access to some optional claims.

This call eventually hits DefaultOAuth2User#DefaultOAuth2User which fails with Missing attribute..." for thenameAttributeKeysince the endpoint only returns quite sparse claims. However, theid_tokendoes contain the desired attribute and eventually,OidcUserService#loadUser()would merge all claims from thisOAuth2Userand theid_tokenvia theDefaultOidcUserconstructor and then check that thenameAttributeKey` is present.

As a workaround, I can specify an empty user-info-uri to make OidcUserService#shouldRetrieveUserInfo return false but this seems a bit unclean.

Actual Behavior

An exception is thrown as IllegalArgumentException("Missing attribute '" + nameAttributeKey + "' in attributes"); from DefaultOAuth2User.

Expected Behavior

Ignore the missing nameAttributeKey from the user info endpoint and only verify this in the merged claims from this endpoint and the id_token (there is really no need to be strict on the user info endpoint only).

Configuration

spring.security.oauth2.client.provider.my-oauth-provider.issuer-uri=https://login.microsoftonline.com/<tenant_id>/v2.0
spring.security.oauth2.client.provider.my-oauth-provider.user-name-attribute=preferred_username

Version

5.2.0, but the code is still the same in current master

Comment From: jgrandja

@NicoK A valid nameAttributeKey must be supplied to DefaultOAuth2User so this behaviour cannot be changed. See DefaultOAuth2User.getName() to see how nameAttributeKey is being used. Also see the reference doc on userNameAttributeName

I've been playing around with Microsoft's identity platform (v2.0) and it seems like it it doing things a bit differently than v1.0 (Azure AD).

Can you provide a sample of the response from the UserInfo endpoint? Based on your configuration spring.security.oauth2.client.provider.my-oauth-provider.user-name-attribute=preferred_username, the response should contain preferred_username attribute or it will fail. This would be a misconfiguration error.

Comment From: NicoK

Hi @jgrandja, I agree for DefaultOAuth2User, however, I'm actually talking about how OidcUser is created

It that built via https://github.com/spring-projects/spring-security/blob/833bfd0c22a4f026b10d9878e410dbadc89d2b3e/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserService.java#L96 and is based on the id_token and on a DefaultOAuth2User object if shouldRetrieveUserInfo returns true.

According to https://openid.net/specs/openid-connect-core-1_0.html#UserInfo, only the sub field is actually required from the UserInfo endpoint, but that wouldn't be a problem, since a lot of claims are already present in the id_token. The problem is that this code builds upon DefaultOAuth2User which verifies that the userNameAttribute is present in the endpoint, not in the final OidcUser as would be required here. If it wasn't for that, the final OidcUser would have contained that attribute.

Let me illustrate with an example:

  1. By using the default endpoints through https://sts.windows.net/TENANT-ID/v2.0/.well-known/openid-configuration:
id_token.claims: {
  sub=...,
  ver=2.0,
  iss=https://login.microsoftonline.com/<tenant-id>/v2.0,
  oid=...,
  preferred_username=...,
  uti=...,
  nonce=...,
  tid=<tenant-id>,
  aud=[...],
  nbf=Tue Dec 03 11:21:36 CET 2019,
  idp=https://sts.windows.net/.../,
  name=...,
  exp=2019-12-03T11:26:36Z,
  iat=2019-12-03T10:21:36Z
}
userprofile: {
  sub=...,
  name=...,
  family_name=...,
  given_name=...,
  picture=https://graph.microsoft.com/v1.0/me/photo/$value
}
  1. Changing the endpoint to https://graph.microsoft.com/v1.0/me:
id_token.claims: {
  sub=...,
  ver=2.0,
  iss=https://login.microsoftonline.com/<tenant-id>/v2.0,
  oid=...,
  preferred_username=...,
  uti=...,
  nonce=...,
  tid=<tenant-id>,
  aud=[...],
  nbf=Tue Dec 03 11:29:52 CET 2019,
  idp=https://sts.windows.net/.../,
  name=...,
  exp=2019-12-03T11:34:52Z,
  iat=2019-12-03T10:29:52Z
}
userprofile: {
  @odata.context=https://graph.microsoft.com/v1.0/$metadata#users/$entity,
  businessPhones=[],
  displayName=...,
  givenName=...,
  jobTitle=null,
  mail=null,
  mobilePhone=null,
  officeLocation=null,
  preferredLanguage=en,
  surname=...,
  userPrincipalName=...,
  id=...
}

As you can see, the preferred_username is present in the id_token as requested by the scope [openid, profile] but just not in the user info endpoint.

Comment From: jgrandja

@NicoK

As you can see, the preferred_username is present in the id_token as requested by the scope [openid, profile] but just not in the user info endpoint.

The attributes returned from the ID Token and UserInfo endpoint is provider specific and may vary across provider implementations. The only required attributes are typically sub and iss in the ID Token. Almost all attributes are optional for UserInfo endpoint. If you want specific attributes to be returned from the UserInfo endpoint than you'll need to configure this on the provider side.

FYI, if you did not configure spring.security.oauth2.client.provider.my-oauth-provider.user-name-attribute=preferred_username than the default nameAttributeKey would be sub for OidcUser and we know this would work because sub is a required attribute. For all other attributes, you need to specify the correct attribute that you know will be returned in either the ID Token and/or UserInfo endpoint.

As you mentioned:

I've been playing around with Microsoft's identity platform (v2.0) and it seems like it it doing things a bit differently than v1.0 (Azure AD).

You'll need to configure things for v2.0 on the provider side so it behaves as you expect. Or you simply need to use an attribute like sub for your nameAttributeKey.

I'm going to close this as I don't see any issues on the client side. You need to look at the provider and customize the endpoint response there.

Comment From: NicoK

I think, I may have been a bit unclear, but you actually brought it to the point:

Expected behaviour

nameAttributeKey should be present in the ID Token OR the UserInfo

Actual behaviour

  • if UserInfo is fetched: nameAttributeKey must be present in UserInfo
  • if UserInfo is not fetched: nameAttributeKey must be present in ID token

This is even supported by the fact that actually fetching the UserInfo is optional and its intention should be to get and additional claims that can be useful.

Comment From: jgrandja

@NicoK

Slight correction:

  • if UserInfo is fetched: nameAttributeKey must be present in UserInfo OR ID token
  • if UserInfo is not fetched: nameAttributeKey must be present in ID token

Comment From: NicoK

yes, that is the expected behaviour but this is not what is currently happening and is actually what I tried to describe above

Comment From: jgrandja

@NicoK I guess I'm still not seeing an issue here. At this point, to clarify things for me, please put together a test that reproduces the issue you are having. Take a look at the tests in OidcUserServiceTests for a starting point for your test. You can post the test here.

Comment From: patricklucas

@jgrandja if I understand @NicoK correctly, the problem is that while there is no technical reason a claim nameAttributeKey needs to be returned by the UserInfo Endpoint, Spring Security implicitly requires it.

The sub claim is by definition the "primary key" for both the ID and Access Tokens as well as the response from the UserInfo endpoint, and so is sufficient to "join" the claims from the UserInfo Endpoint with the ID Token. Meanwhile, nameAttributeKey refers to which of these joined claims should be used within Spring to identify the user after the auth flow has completed.

In @NicoK's case, the OIDC provider does not include the nameAttributeKey in responses from the UserInfo Endpoint because it would be redundant with the original ID token that already included it—but Spring Security doesn't tolerate this.

Comment From: dawi

@NicoK @jgrandja I am having the same issue with Spring Security and Azure AD.

Therefore I configured the spring.security.oauth2.client.provider with empty user-info-uri:

spring.security.oauth2.client.provider.azure.issuer-uri=https://login.microsoftonline.com/xxx/v2.0
spring.security.oauth2.client.provider.azure.user-name-attribute=preferred_username
spring.security.oauth2.client.provider.azure.user-info-uri=

As a result shouldRetrieveUserInfo returns false and the oauth2UserService.loadUser(userRequest) is not executed and therefore does not fail.

I don't like this solution, but it seems that in Azure AD you can't configure the User-Info-Endpoint very much, you can only configure Access- and ID-Tokens.