My personal blog is blog.sqdyy.cn

Spring Security OAuth2 architecture

Spring Security OAuth2 is a class library encapsulated based on OAuth2 that provides the functionality needed to build the Authorization Server, Resource Server, and Client roles for Spring applications. Spring Security OAuth needs to work with the Spring Framework (Spring MVC) and functionality provided by Spring Security, In the case of using Spring Security OAuth to build Authorization Server, Resource Server and Client, the overall architecture of Spring Security OAuth2 is as follows:

  1. The resource owner passesUserAgentaccessclient, where authorization allows access to authorized endpoints,OAuth2RestTemplateWill createOAuth2The certificationRESTRequest, indicateUserAgentRedirected toAuthorization ServerAuthorization endpoint ofAuthorizationEndpoint.
  2. UserAgentaccessAuthorization ServerOf the authorization endpointauthorizeMethod, the authorization endpoint will require the authorization interface when authorization is not registered/oauth/confirm_accessIt is displayed to the resource owner, and the resource owner authorizes the permissionAuthorizationServerTokenServicesGenerate an authorization code or access token, and the generated token eventually passesuserAgentThe redirection is passed to the client.
  3. The client’sOAuth2RestTemplateAfter obtaining the authorization code, create a request to access the authorization serverTokenEndpointToken endpoint, token endpoint by callingAuthorizationServerTokenServicesTo verify the authorization code provided by the client and issue an access token in response to the client.
  4. The client’sOAuth2RestTemplateAccess the resource server by adding the access token obtained from the authorization server to the request header, and the resource server passesOAuth2AuthenticationManagercallResourceServerTokenServicesVerify the access token and the authentication information associated with the access token. After the access token authentication is successful, the resource request is returned to the client.

The execution process of the three Spring Security OAuth2 application roles has been briefly explained. Next, we will analyze the architecture and source code of the three roles one by one to deepen our understanding.


Authorization Server architecture

The authorization server mainly provides the authentication service of the resource owner. The client obtains authorization from the resource owner through the authorization server and then obtains the token issued by the authorization server. In this authentication process, two important endpoints are involved, an AuthorizationEndpoint, and a TokenEndpoint, TokenEndpoint. The following is a source code analysis of the internal running processes of these two endpoints.

AuthorizationEndpoint

First, let’s look at the process of accessing the AuthorizationEndpoint:

  1. UserAgentWill access the authorization serverAuthorizationEndpointURI (authorization endpoint) :/oauth/authorize, the call isauthorizeMethod is used to determine whether the user is authorized. If the user is authorized, a new authorization_code is issued, otherwise the user authorization page is displayed.
  2. authorizeIt calls firstClientDetailsServiceGet customer information and validate request parameters.
  3. thenauthorizeMethod passes the request parameters toUserApprovalHandlerCheck whether the client is registeredscopeAuthorization.
  4. When no authorization is registered, i.eapprovedforfalseThe request authorization interface is displayed to the resource owner/oauth/confirm_access.
  5. That’s the same thing as 4.
  6. The URI of the authorization endpoint of the authorization server that the resource owner accesses again after confirming authorization:/oauth/authorizeThe request parameter will be added this timeuser_oauth_approval, so another mapping method is calledapproveOrDeny.
  7. approveOrDenyWill be calleduserApprovalHandler.updateAfterApprovalThe decision to update depends on whether the user is authorized or notauthorizationRequestThe object of theapprovedProperties.
  8. userApprovalHandlerThe default implementation class of isApprovalStoreUserApprovalHandlerIts interior is throughApprovalStoretheaddApprovalsTo register the authorization information.

When the request parameter user_oAUTH_Approval is not carried, the authorize method is accessed, and the execution process corresponds to steps 1-5 above. If the user has been authorized, a new authorization_code is issued; otherwise, the user authorization page is displayed:

@RequestMapping(value = "/oauth/authorize")
public ModelAndView authorize(Map
       
         model, @RequestParam Map
        
          parameters, SessionStatus sessionStatus, Principal principal)
        ,>
       ,> {
  // Encapsulate the authentication request object according to request parameters ----> AuthorizationRequest
  // Pull out the authorization request first, using the OAuth2RequestFactory. 
  // All further logic should query off of the authorization request instead of referring back to the parameters map. 
  // The contents of the parameters map will be stored without change in the AuthorizationRequest object once it is created.
  AuthorizationRequest authorizationRequest = getOAuth2RequestFactory().createAuthorizationRequest(parameters);
  // Obtain the response_type type in the request parameter and perform a conditional check: response_type supports only tokens and codes, that is, tokens and authorization codes
  Set<String> responseTypes = authorizationRequest.getResponseTypes();
  if(! responseTypes.contains("token") && !responseTypes.contains("code")) {
    throw new UnsupportedResponseTypeException("Unsupported response types: " + responseTypes);
  }
  // Request parameters must carry client IDS
  if (authorizationRequest.getClientId() == null) {
    throw new InvalidClientException("A client id must be provided");
  }

  try {
    // The user must be authenticated by Spring Security before authorization can be completed using Spring Security OAuth2
    if(! (principalinstanceofAuthentication) || ! ((Authentication) principal).isAuthenticated()) {throw new InsufficientAuthenticationException(
          "User must be authenticated with Spring Security before authorization can be completed.");
    }
    // Obtain customer information
    ClientDetails client = getClientDetailsService().loadClientByClientId(authorizationRequest.getClientId());
    // Get the redirect URL, which can come from the request parameter or the client view, but you need to store it in the authorization request
    // The resolved redirect URI is either the redirect_uri from the parameters or the one from clientDetails.
    // Either way we need to store it on the AuthorizationRequest.
    String redirectUriParameter = authorizationRequest.getRequestParameters().get(OAuth2Utils.REDIRECT_URI);
    String resolvedRedirect = redirectResolver.resolveRedirect(redirectUriParameter, client);
    if(! StringUtils.hasText(resolvedRedirect)) {throw new RedirectMismatchException(
          "A redirectUri must be either supplied or preconfigured in the ClientDetails");
    }
    authorizationRequest.setRedirectUri(resolvedRedirect);
    // Validates the scope in the request parameters according to the client details
    // We intentionally only validate the parameters requested by the client (ignoring any data that may have been added to the request by the manager).
    oauth2RequestValidator.validateScope(authorizationRequest, client);

    // Check whether the requested user is authorized or has the permission to configure the default authorization. If access stoke already exists or is configured with default authorization, the object with authorization is returned
    / / use userApprovalHandler -- - > ApprovalStoreUserApprovalHandler
    // Some systems may allow for approval decisions to be remembered or approved by default. 
    // Check for such logic here, and set the approved flag on the authorization request accordingly.
    authorizationRequest = userApprovalHandler.checkForPreApproval(authorizationRequest,
        (Authentication) principal);
    // TODO: is this call necessary?
    / / if authorizationRequest. Approved to true, will skip the Approval page.
    boolean approved = userApprovalHandler.isApproved(authorizationRequest, (Authentication) principal);
    authorizationRequest.setApproved(approved);
    // Return the view containing the newly generated value of authorization_code (a random string of fixed length)
    // Validation is all done, so we can check for auto approval...
    if (authorizationRequest.isApproved()) {
      if (responseTypes.contains("token")) {
        return getImplicitGrantResponse(authorizationRequest);
      }
      if (responseTypes.contains("code")) {
        return newModelAndView(getAuthorizationCodeResponse(authorizationRequest, (Authentication) principal)); }}// Place auth request into the model so that it is stored in the sessionfor approveOrDeny to use.
    // That way we make sure that auth request comes from the session,
    // so any auth request parameters passed to approveOrDeny will be ignored and retrieved from the session.
    model.put("authorizationRequest", authorizationRequest);
    // Not authorized The user can choose whether to authorize the user on the authorization page
    return getUserApprovalPageResponse(model, authorizationRequest, (Authentication) principal);

  }
  catch (RuntimeException e) {
    sessionStatus.setComplete();
    throwe; }}Copy the code

The user confirms the authorization through the authorization page and accesses the authorized endpoint with the request parameter user_oAUTH_approval. The approveOrDeny method is executed, and the execution process corresponds to steps 6-7 above:

@RequestMapping(value = "/oauth/authorize", method = RequestMethod.POST, params = OAuth2Utils.USER_OAUTH_APPROVAL)
public View approveOrDeny(@RequestParam Map
       
         approvalParameters, Map
        
          model, SessionStatus sessionStatus, Principal principal)
        ,>
       ,> {
  // The user must be authenticated by Spring Security before authorization can be completed using Spring Security OAuth2
  if(! (principalinstanceof Authentication)) {
    sessionStatus.setComplete();
    throw new InsufficientAuthenticationException(
        "User must be authenticated with Spring Security before authorizing an access token.");
  }
  // Get the request parameters
  AuthorizationRequest authorizationRequest = (AuthorizationRequest) model.get("authorizationRequest");
  if (authorizationRequest == null) {
    sessionStatus.setComplete();
    throw new InvalidRequestException("Cannot approve uninitialized authorization request.");
  }

  try {
    // Get the response_type type in the request parameter
    Set<String> responseTypes = authorizationRequest.getResponseTypes();
    // Set the parameters for Approval
    authorizationRequest.setApprovalParameters(approvalParameters);
    // Update the approved attribute in the authorizationRequest object depending on whether the user is authorized.
    authorizationRequest = userApprovalHandler.updateAfterApproval(authorizationRequest,
        (Authentication) principal);
    boolean approved = userApprovalHandler.isApproved(authorizationRequest, (Authentication) principal);
    authorizationRequest.setApproved(approved);
    // Need to carry the redirection URI
    if (authorizationRequest.getRedirectUri() == null) {
      sessionStatus.setComplete();
      throw new InvalidRequestException("Cannot approve request when no redirect URI is provided.");
    }
    // The user denies authorization and responds with an error message to the client's redirect URL
    if(! authorizationRequest.isApproved()) {return new RedirectView(getUnsuccessfulRedirect(authorizationRequest,
          new UserDeniedAuthorizationException("User denied access"), responseTypes.contains("token")),
          false.true.false);
    }
    // In simplified mode, issue the access token directly
    if (responseTypes.contains("token")) {
      return getImplicitGrantResponse(authorizationRequest).getView();
    }
    // In the authorization code mode, generate an authorization code, store it, and return it to the client
    return getAuthorizationCodeResponse(authorizationRequest, (Authentication) principal);
  }
  finally{ sessionStatus.setComplete(); }}Copy the code

TokenEndpoint

Next, let’s look at the execution of TokenEndpoint:

  1. userAgentBy accessing the URI of the authorization server TokenEndpoint TokenEndpoint:/oauth/token, the call ispostAccessTokenMethod, mainly used to generate for the clientToken.
  2. postAccessTokenFirst of all, it callsClientDetailsServiceGet customer information and validate request parameters.
  3. Invoke the corresponding authorization mode implementation class generationToken.
  4. The corresponding authorization mode is implementedAbstractTokenGranterAbstract class, its membersAuthorizationServerTokenServicesCan be used to create, refresh, gettoken.
  5. AuthorizationServerTokenServicesThe default implementation class has onlyDefaultTokenServicesThrough which,The createAccessTokenMethod as you can seetokenHow it was created.
  6. The real operationtokenThe class isTokenStore, according to procedureTokenStoreDifferent implementations of the interface to produce and storetoken.

TokenEndpoint URI: /oauth/token

@RequestMapping(value = "/oauth/token", method=RequestMethod.POST)
public ResponseEntity<OAuth2AccessToken> postAccessToken(Principal principal, @RequestParam
Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {
  // The user must be authenticated by Spring Security before authorization can be completed using Spring Security OAuth2
  if(! (principalinstanceof Authentication)) {
    throw new InsufficientAuthenticationException(
        "There is no client authentication. Try adding an appropriate authentication filter.");
  }
  // Obtain the client information by the client Id
  String clientId = getClientId(principal);
  ClientDetails authenticatedClient = getClientDetailsService().loadClientByClientId(clientId);
  // Encapsulate the authentication request object according to request parameters ----> AuthorizationRequest
  TokenRequest tokenRequest = getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient);

  if(clientId ! =null && !clientId.equals("")) {
    // Only validate the client details if a client authenticated during this
    // request.
    if(! clientId.equals(tokenRequest.getClientId())) {// double check to make sure that the client ID in the token request is the same as that in the
      // authenticated client
      throw new InvalidClientException("Given client ID does not match authenticated client"); }}if(authenticatedClient ! =null) {
    // Check the scope in the request parameter according to the client's details, to prevent the client from getting more permissions
    oAuth2RequestValidator.validateScope(tokenRequest, authenticatedClient);
  }
  // No authorization mode is specified
  if(! StringUtils.hasText(tokenRequest.getGrantType())) {throw new InvalidRequestException("Missing grant type");
  }
  // Access to this endpoint should not be in simplified mode
  if (tokenRequest.getGrantType().equals("implicit")) {
    throw new InvalidGrantException("Implicit grant type not supported from token endpoint");
  }
  // If grant_type=authoraztion_code, the scope is cleared
  if (isAuthCodeRequest(parameters)) {
    // The scope was requested or determined during the authorization step
    if(! tokenRequest.getScope().isEmpty()) { logger.debug("Clearing scope of incoming token request"); tokenRequest.setScope(Collections.<String> emptySet()); }}// If grant_type=refresh_token, set scope for refreshing tokens
  if (isRefreshTokenRequest(parameters)) {
    // A refresh token has its own default scopes, so we should ignore any added by the factory here.
    tokenRequest.setScope(OAuth2Utils.parseParameterList(parameters.get(OAuth2Utils.SCOPE)));
  }
  // Generate a token for the client
  OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
  if (token == null) {
    throw new UnsupportedGrantTypeException("Unsupported grant type: " + tokenRequest.getGrantType());
  }
  return getResponse(token);
}
Copy the code

The key to the token endpoint is how to produce the token. Different authorization modes will implement different implementations based on the AbstractTokenGranter interface. AbstractTokenGranter will entrust AuthorizationServerTokenServices to create, refresh, access token. AuthorizationServerTokenServices default implementation DefaultTokenServices only, a simple method to extract its createAccessToken source can see:

// Generate the accessToken and RefreshToken
@Transactional
public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException {
  // First try to obtain the existing Token
  OAuth2AccessToken existingAccessToken = tokenStore.getAccessToken(authentication);
  OAuth2RefreshToken refreshToken = null;
  // if the existing accessToken accessToken is not empty and not invalidated, the existing accessToken is saved, and if the new accessToken is invalidated, the new accessToken is stored
  if(existingAccessToken ! =null) {
    if (existingAccessToken.isExpired()) {
      if(existingAccessToken.getRefreshToken() ! =null) {
        refreshToken = existingAccessToken.getRefreshToken();
        // The token store could remove the refresh token when the
        // access token is removed, but we want to
        // be sure...
        tokenStore.removeRefreshToken(refreshToken);
      }
      tokenStore.removeAccessToken(existingAccessToken);
    }
    else {
      // Re-store the access token in case the authentication has changed
      tokenStore.storeAccessToken(existingAccessToken, authentication);
      returnexistingAccessToken; }}// Create a refresh token if no refresh token is present, and re-create a refresh token if it expires.
  // Only create a new refresh token if there wasn't an existing one associated with an expired access token.
  // Clients might be holding existing refresh tokens, so we re-use it in the case that the old access token expired.
  if (refreshToken == null) {
    refreshToken = createRefreshToken(authentication);
  }
  // But the refresh token itself might need to be re-issued if it has expired.
  else if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
    ExpiringOAuth2RefreshToken expiring = (ExpiringOAuth2RefreshToken) refreshToken;
    if(System.currentTimeMillis() > expiring.getExpiration().getTime()) { refreshToken = createRefreshToken(authentication); }}// Generate a new access token and save it. Save the refresh token
  OAuth2AccessToken accessToken = createAccessToken(authentication, refreshToken);
  tokenStore.storeAccessToken(accessToken, authentication);
  // In case it was modified
  refreshToken = accessToken.getRefreshToken();
  if(refreshToken ! =null) {
    tokenStore.storeRefreshToken(refreshToken, authentication);
  }
  return accessToken;
}
Copy the code

Resource Server architecture

Resource servers are used to process clients’ access requests to protected resources and return corresponding requests. The resource server verifies that the client’s access token is valid and retrieves the authentication information associated with the access token. After the authentication information is obtained, the access token is verified to be within the allowed scope, and the processing behavior after the authentication is completed can be implemented similar to that of a normal application. Here’s how the resource server works:

  • (1) When the client starts to access the resource server, it passes throughOAuth2AuthenticationProcessingFilterThe interceptor is used to extract the access token from the request and then extract the authentication information from the tokenAuthenticationAnd store it in the context.
  • (2) OAuth2AuthenticationProcessingFilterIs called in the interceptorThe authenticate the AuthenticationManagerMethods To extract authentication information.
  • (2)OAuth2AuthenticationProcessingFilterThe interceptor will commission if an authentication error occursJoe AuthenticationEntryPointThe default implementation class isOAuth2AuthenticationEntryPoint.
  • (3)OAuth2AuthenticationProcessingFilterAfter the execution is complete, go to the next security filterExceptionTranslationFilter.
  • (3)ExceptionTranslationFilterFilters are used to handle exceptions thrown during system authentication and authorization. Interceptors delegate if an exception occursAccessDeniedHandlerThe default implementation class isOAuth2AccessDeniedHandler.
  • (4) After the authentication/authorization verification is successful, the resource requested by the customer is returned.

Resource server we want to care about it how to validate the client access token is valid, so we OAuth2AuthenticationProcessingFilter source of from the beginning, the function of the interceptor is extracted from the request access token, Then extract the Authentication information from the token and store it in the context:

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
    boolean debug = logger.isDebugEnabled();
    HttpServletRequest request = (HttpServletRequest)req;
    HttpServletResponse response = (HttpServletResponse)res;

    try {
        // Extract the token from the request, and then extract the Authorization information in the token
        Authentication authentication = this.tokenExtractor.extract(request);
        if (authentication == null) {
            if (this.stateless && this.isAuthenticated()) {
                if (debug) {
                    logger.debug("Clearing security context.");
                }

                SecurityContextHolder.clearContext();
            }

            if (debug) {
                logger.debug("No token in request, will continue chain."); }}else {
            request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE, authentication.getPrincipal());
            if (authentication instanceof AbstractAuthenticationToken) {
                AbstractAuthenticationToken needsDetails = (AbstractAuthenticationToken)authentication;
                needsDetails.setDetails(this.authenticationDetailsSource.buildDetails(request));
            }
            // Obtain the Authentication information carried by the token, and then save it to spring Security context for future use
            Authentication authResult = this.authenticationManager.authenticate(authentication);
            if (debug) {
                logger.debug("Authentication success: " + authResult);
            }
            this.eventPublisher.publishAuthenticationSuccess(authResult); SecurityContextHolder.getContext().setAuthentication(authResult); }}catch (OAuth2Exception var9) {
        SecurityContextHolder.clearContext();
        if (debug) {
            logger.debug("Authentication request failed: " + var9);
        }
        this.eventPublisher.publishAuthenticationFailure(new BadCredentialsException(var9.getMessage(), var9), new PreAuthenticatedAuthenticationToken("access-token"."N/A"));
        this.authenticationEntryPoint.commence(request, response, new InsufficientAuthenticationException(var9.getMessage(), var9));
        return;
    }

    chain.doFilter(request, response);
}
Copy the code

Code above mentioned Oauth2AuthenticationManager get a token authentication information to carry on certification, through the source code can learn it basically worked three steps:

public Authentication authenticate(Authentication authentication) throws AuthenticationException {

  if (authentication == null) {
    throw new InvalidTokenException("Invalid token (token not found)");
  }
  String token = (String) authentication.getPrincipal();
  // 1. Obtain the OAuuth2Authentication object through the token
  OAuth2Authentication auth = tokenServices.loadAuthentication(token);
  if (auth == null) {
    throw new InvalidTokenException("Invalid token: " + token);
  }
  // 2. Verify that the ID of the resource service is correct
  Collection<String> resourceIds = auth.getOAuth2Request().getResourceIds();
  if(resourceId ! =null&& resourceIds ! =null&&! resourceIds.isEmpty() && ! resourceIds.contains(resourceId)) {throw new OAuth2AccessDeniedException("Invalid token does not contain resource id (" + resourceId + ")");
  }
  // 3. Verify the client's access scope
  checkClientDetails(auth);

  if (authentication.getDetails() instanceof OAuth2AuthenticationDetails) {
    OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) authentication.getDetails();
    // Guard against a cached copy of the same details
    if(! details.equals(auth.getDetails())) {// Preserve the authentication details from the one loaded by token services
      details.setDecodedDetails(auth.getDetails());
    }
  }
  auth.setDetails(authentication.getDetails());
  auth.setAuthenticated(true);
  return auth;

}
Copy the code

After the verification by ExceptionTranslationFilter filter, can access to the resource.


Client architecture

Spring Security OAuth2 clients control access to the resources of other servers protected by OAuth 2.0. Configuration involves establishing connections between the relevant protected resource and users who have access to the resource. The client also needs to be able to store the user’s authorization code and access token.

The structure of the client code is not particularly complex, here contact the description of the architecture diagram, interested in the source code can follow the following process:

  1. First of all,UserAgentCall clientControllerBefore that, it will go throughOAuth2ClientContextFilterFilter, which is mainly used to capture what might happen in step 5UserRedirectRequiredExceptionIn order to redirect to the authorization server for reauthorization.
  2. The client-side service layer code needs to be injectedRestOperations->OAuth2RestOperationsThe implementation class of the interfaceOAuth2RestTemplate. It mainly provides access to authorization servers or resource serversRestAPI.
  3. OAuth2RestTemplateA member of theOAuth2ClientContextThe interface implementation class isDefaultOAuth2ClientContext. It verifies that the access token is valid, and if it is, perform step 6 to access the resource server.
  4. If the access token does not exist or has expiredAccessTokenProviderTo obtain the access token.
  5. AccessTokenProviderGets the access token based on the defined resource details and authorization type, and throws it if it cannot be obtainedUserRedirectRequiredException.
  6. Specify the access token obtained in 3 or 5 to access the resource server. If a token expiration exception occurs during access, initialize the saved access token and then go to Step 4.

The architecture diagram and part of the content are from the TERASOLUNA Server Framework (5.x) development guide.