Skip to content

Design for implementation of OAuth2UserService

zhihaoguo edited this page Dec 15, 2022 · 3 revisions

Context

  • 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.
         }
     }
     

Whole picture.

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.

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

Analysis and improvement options for each step

Analysis and improvement options in step1

Analysis

  • Step1 aims to construct authorities.
  1. 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. image
  2. 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. image

Improvement options for 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.
  • Option2: Keep the authorities as it is.

Analysis and improvement options in step2

Analysis

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

  1. In Spring Security, if the userinfo endpoint url is set, it will get the userInfo and use it while constructing the return value.
  2. 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.
  3. 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?

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.

Improvement options for Spring Cloud Azure

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.

Analysis and improvement options in step3

Analysis

  • Step3 aims to construct idToken. Both implementaition use the same idToken from OidcUserRequest.

Improvement options

No improvement options, just keep the same as it is.

Analysis and improvement options in step4

Analysis

  • Step4 aims to construct nameAttributeKey.
  1. In Spring Security, the value of nameAttributeKey is IdTokenClaimNames.SUB, always "sub".
  2. 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".

Improvement options for Spring Cloud Azure

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

Analysis and improvement options in step5

Analysis

  • 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) {  
        //  
     }

Improvement options for Spring Cloud Azure

  • No improvement options, it depends on the param values.

Initial Design

  1. 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.
  2. 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.
  3. Step3-Construct IdToken: keep the same logic as it is.
  4. Step4-Construct nameAttributeKey: keep the same logic as it is, but will refactor the code.
  5. Step5-Construct Result: keep the same logic.

Final Design

This is the final design of the OAuth2UserService implementation:

  1. Step1-Construct Authorities: keep the same logic as it is.
  2. 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.
  3. Step3-Construct IdToken: keep the same logic as it is.
  4. Step4-Construct nameAttributeKey: keep the same logic as it is, but will refactor the code.
  5. Step5-Construct Result: keep the same logic.

Note

  • 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
Clone this wiki locally