-
Notifications
You must be signed in to change notification settings - Fork 2k
Design for implementation of OAuth2UserService
- Context
- Analysis and improvement options for each step
- Initial Design
- Final Design
- Note
-
Spring Security provides an interface OAuth2UserService which has only one api loadUser .
@FunctionalInterface public interface OAuth2UserService<R extends OAuth2UserRequest, U extends OAuth2User> { U loadUser(R userRequest) throws OAuth2AuthenticationException; }
-
Spring Security implements the interface with OidcUserService.
-
Spring Cloud Azure also implements the interface with AadOAuth2UserService.
-
Both implementaitions take an OidcUserRequest instance as input, and output a DefaultOidcUser instance.
// pseudocode of OidcUserService public class OidcUserService implements OAuth2UserService<OidcUserRequest, OidcUser> { // .... @Override public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException { // ... return a DefaultOidcUser instance. } }
// pseudocode of AadOAuth2UserService public class AadOAuth2UserService implements OAuth2UserService<OidcUserRequest, OidcUser> { // .... @Override public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException { // ... return a DefaultOidcUser instance. } }
Both implementaition of the api loadUser can broken down into 5 steps. The picture shows the difference between our Spring Cloud Azure and Spring security implementaition in a whole picture.
No | Step name | Implementaition In Spring Security | Implementaition In Spring Cloud Azure |
---|---|---|---|
Step1 | Construct Authorities | Authorities from accessToken scopes + "ROLE_USER" | Authorities from groupids + groupnames + roles |
Step2 | Construct UserInfo | get userInfo from userInfo endpoint if endpoint is not empty | uri is not passed, so skip constructing |
Step3 | Construct IdToken | IdToken from input request | IdToken from input request |
Step4 | Construct nameAttributeKey | IdTokenClaimNames.SUB("sub") | User passed nameAttributeKey, default == "name" |
Step5 | Construct Result | return new DefaultOidcUser(authorities, idToken, null ,nameAttributeKey ) |
return new DefaultOidcUser(authorities, idToken, userInfo , IdTokenClaimNames.SUB ) |
From the table we can see, the behavior is different between Spring Security and Spring Cloud Azure in each step except for step4.
- Step1 aims to construct authorities.
- In Spring Security, the field authorities is made up with
Authorities from accessToken scopes
+ROLE_USER
. Below is a sample to show the implementaition logic in Spring Security. - In Spring Cloud Azure, the field authorities is made up with
Roles from idToken
+User GroupIds
+User GroupNames
. This is a sample to show implementaition logic in Spring Cloud Azure.
- Option1: Based on the existing implementation, retrive scopes from accessToken and use the scopes to build authorities.
- Cons:
- It will introduce breaking changes.
- Cons:
- Option2: Keep the authorities as it is.
- Step2 aims to construct userInfo.
Construct userInfo, this means obtain the user attributes of the end-user from the UserInfo Endpoint and use it while constructing the return value.
- In Spring Security, if the userinfo endpoint url is set, it will get the userInfo and use it while constructing the return value.
- In Spring Cloud Azure, if the userinfo endpoint url is set, it will get the userInfo but will not use it while constructing the return value.
- In Spring Cloud Azure, the userinfo endpoint url is never set, so it will skip getting the userInfo from userinfo endpoint.
What is userinfo_endpoint
, is it required or not, is it always existing in Azure AD?
- From OpenID specification, we can see
userinfo_endpoint
is not a REQUIRED but RECOMMENDED claim openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata
userinfo_endpoint
RECOMMENDED. URL of the OP's UserInfo Endpoint [OpenID.Core]. This URL MUST use the https scheme and MAY contain port, path, and query parameter components.
-
In Azure Directory, the
userinfo_endpoint
will always returned while callinghttps://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration
.You can find the userInfo endpoint programmatically by reading the userinfo_endpoint field of the OpenID configuration document at https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration.
Option1: Don't pass userinfo endpoint and never construct userInfo.
- Azure AD suggest get user's information from idtoken.
The information in an ID token is a superset of the information available on UserInfo endpoint ... > we suggest getting the user's information from the token instead of calling the userInfo endpoint. https://learn.microsoft.com/en-us/azure/active-directory/develop/userinfo#consider-using-an-id-token-instead
- The user can get more infomation in an ID token, the information only in userInfo by default can be configured with optioinal claims and graph api. Pros:
- We can simplify Spring Cloud Azure improvementation, remove the oidcUserService dependency, reduce the calling to userInfo endpoint.
Option2: Pass userinfo endpoint url, construct userInfo and use the userInfo to construct result.
- Since Azure AD will return the userInfo endpoint at https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration, and the interface indicates to get userInfo from userinfo endpoint if possible, in terms of interface implementation, we should pass pass the url to get userInfo and build result with the userinfo.
- Step3 aims to construct idToken. Both implementaition use the same idToken from OidcUserRequest.
No improvement options, just keep the same as it is.
- Step4 aims to construct nameAttributeKey.
- In Spring Security, the value of nameAttributeKey is IdTokenClaimNames.SUB, always "sub".
- In Spring Cloud Azure, the value of nameAttributeKey can be passed by user with
spring.cloud.azure.active-directory.user-name-attribute
, is user not pass the value, the default value of nameAttributeKey is "name".
No improvement options, just keep the same as it is.
- In Azure AD idToken, the value of "sub" claim is not human readable, the "name" can better represents the meaning of this nameAttributeKey field:
the key used to access the user's "name"
.
-
Step5 aims to construct DefaultOidcUser as the result.
-
Both Spring Security and Spring Cloud Azure use the same constuctor in the end, only param values differs.
/** * @param authorities the authorities granted to the user * @param idToken * @param userInfo, may be {@code null} * @param nameAttributeKey */ public DefaultOidcUser(Collection<? extends GrantedAuthority> authorities, OidcIdToken idToken, OidcUserInfo userInfo, String nameAttributeKey) { // }
- No improvement options, it depends on the param values.
- Step1-Construct Authorities: Based on the existing implementation, retrive scopes from accessToken and use the scopes to build authorities.
- I prefer to choose option1: add the scopes in access token to authorities. Scope means permissions, it's also an important part of authorities.
- Step2-Construct userInfo: Pass userinfo endpoint url, construct userInfo and use the userInfo to construct result.
- I think it's more important to follow the definition of interface.
- Step3-Construct IdToken: keep the same logic as it is.
- Step4-Construct nameAttributeKey: keep the same logic as it is, but will refactor the code.
- Step5-Construct Result: keep the same logic.
This is the final design of the OAuth2UserService implementation:
- Step1-Construct Authorities: keep the same logic as it is.
- Step2-Construct userInfo: keep the same logic
- we will not get userInfo from userInfo endpoint, thus we could refactor our code and remove dependency of
oidcUserService
.
- we will not get userInfo from userInfo endpoint, thus we could refactor our code and remove dependency of
- Step3-Construct IdToken: keep the same logic as it is.
- Step4-Construct nameAttributeKey: keep the same logic as it is, but will refactor the code.
- Step5-Construct Result: keep the same logic.
- In the implementation of spring cloud azure, it implements a session level cache, this will not include in this feature spec scope.
- This is the Issue link
- Frequently Asked Questions
- Azure Identity Examples
- Configuration
- Performance Tuning
- Android Support
- Unit Testing
- Test Proxy Migration
- Azure Json Migration
- New Checkstyle and Spotbugs pattern migration
- Protocol Methods
- TypeSpec-Java Quickstart
- Getting Started Guidance
- Adding a Module
- Building
- Writing Performance Tests
- Working with AutoRest
- Deprecation
- BOM guidelines
- Release process
- Access helpers