Talk about the password authentication of spring securityouth2.

  spring-security

Order

This article mainly talks about the authentication of spring security oauth2 in password mode.

/oauth/token

This is mainly seen in the previous article, which explained how to intercept /oauth/token. One point that needs to be emphasized is that if password authorization mode is supported, additional support for user login authentication is required.

Password mode

This mode requires additional processing, first passing the filter authentication, and then entering endpoint
spring-security-oauth2-2.0.14.RELEASE-sources.jar! /org/springframework/security/oauth2/provider/endpoint/TokenEndpoint.java

@RequestMapping(value = "/oauth/token", method=RequestMethod.POST)
    public ResponseEntity<OAuth2AccessToken> postAccessToken(Principal principal, @RequestParam
    Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {

        if (!(principal instanceof Authentication)) {
            throw new InsufficientAuthenticationException(
                    "There is no client authentication. Try adding an appropriate authentication filter.");
        }

        String clientId = getClientId(principal);
        ClientDetails authenticatedClient = getClientDetailsService().loadClientByClientId(clientId);

        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) {
            oAuth2RequestValidator.validateScope(tokenRequest, authenticatedClient);
        }
        if (!StringUtils.hasText(tokenRequest.getGrantType())) {
            throw new InvalidRequestException("Missing grant type");
        }
        if (tokenRequest.getGrantType().equals("implicit")) {
            throw new InvalidGrantException("Implicit grant type not supported from token endpoint");
        }

        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 (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)));
        }

        OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
        if (token == null) {
            throw new UnsupportedGrantTypeException("Unsupported grant type: " + tokenRequest.getGrantType());
        }

        return getResponse(token);

    }

Here, from GetTokenRanker (). Grant (TokenRequest. GetGranttype (), TokenRequest) here, token will be obtained according to different authentication methods, among which password is special and requires user account password authentication. The matching granter here is ResourceOwnersPasswordTokenRanker

spring-security-oauth2-2.0.14.RELEASE-sources.jar! /org/springframework/security/oauth2/provider/password/ResourceOwnerPasswordTokenGranter.java

public class ResourceOwnerPasswordTokenGranter extends AbstractTokenGranter {

    private static final String GRANT_TYPE = "password";

    private final AuthenticationManager authenticationManager;

    public ResourceOwnerPasswordTokenGranter(AuthenticationManager authenticationManager,
            AuthorizationServerTokenServices tokenServices, ClientDetailsService clientDetailsService, OAuth2RequestFactory requestFactory) {
        this(authenticationManager, tokenServices, clientDetailsService, requestFactory, GRANT_TYPE);
    }

    protected ResourceOwnerPasswordTokenGranter(AuthenticationManager authenticationManager, AuthorizationServerTokenServices tokenServices,
            ClientDetailsService clientDetailsService, OAuth2RequestFactory requestFactory, String grantType) {
        super(tokenServices, clientDetailsService, requestFactory, grantType);
        this.authenticationManager = authenticationManager;
    }

    @Override
    protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {

        Map<String, String> parameters = new LinkedHashMap<String, String>(tokenRequest.getRequestParameters());
        String username = parameters.get("username");
        String password = parameters.get("password");
        // Protect from downstream leaks of password
        parameters.remove("password");

        Authentication userAuth = new UsernamePasswordAuthenticationToken(username, password);
        ((AbstractAuthenticationToken) userAuth).setDetails(parameters);
        try {
            userAuth = authenticationManager.authenticate(userAuth);
        }
        catch (AccountStatusException ase) {
            //covers expired, locked, disabled cases (mentioned in section 5.2, draft 31)
            throw new InvalidGrantException(ase.getMessage());
        }
        catch (BadCredentialsException e) {
            // If the username/password are wrong the spec says we should send 400/invalid grant
            throw new InvalidGrantException(e.getMessage());
        }
        if (userAuth == null || !userAuth.isAuthenticated()) {
            throw new InvalidGrantException("Could not authenticate user: " + username);
        }
        
        OAuth2Request storedOAuth2Request = getRequestFactory().createOAuth2Request(client, tokenRequest);        
        return new OAuth2Authentication(storedOAuth2Request, userAuth);
    }
}

You can see here to obtain the user name and password, and then authenticate. There is a question here. Didn’t the userDetailsService already be set up as ClientDetails SuserDetails Service before, and how is password authentication for users still supported here?

The userDetailsService was previously specified here as clientdetailsuserdetailsservice.
spring-security-oauth2-2.0.14.RELEASE-sources.jar! /org/springframework/security/oauth2/config/annotation/web/configurers/AuthorizationServerSecurityConfigurer.java

    @Override
    public void init(HttpSecurity http) throws Exception {

        registerDefaultAuthenticationEntryPoint(http);
        if (passwordEncoder != null) {
            ClientDetailsUserDetailsService clientDetailsUserDetailsService = new ClientDetailsUserDetailsService(clientDetailsService());
            clientDetailsUserDetailsService.setPasswordEncoder(passwordEncoder());
            http.getSharedObject(AuthenticationManagerBuilder.class)
                    .userDetailsService(clientDetailsUserDetailsService)
                    .passwordEncoder(passwordEncoder());
        }
        else {
            http.userDetailsService(new ClientDetailsUserDetailsService(clientDetailsService()));
        }
        http.securityContext().securityContextRepository(new NullSecurityContextRepository()).and().csrf().disable()
                .httpBasic().realmName(realm);
    }

AuthorizationServerSecurityConfigurer and AuthorizationServerEndPointConfigurer

The answer is in this detail. Compared with TokenEndpoint, the previous configuration is Authorized ServerSecurityConfigurer, which is equivalent to pre authentication, while Authorized ServerEndpointConfigurer is equivalent to after authentication.

When I talked about password mode earlier, I specifically mentioned the need for additional configuration.

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        //todo 这里额外指定了/oauth/token的password模式要使用的userDetailsService
        endpoints.authenticationManager(authenticationManager);
        endpoints.userDetailsService(userDetailsService);
        endpoints.tokenStore(tokenStore());
    }

Yes, the authenticationManager and userDetailsService used after entering TokenEndpoint are specified here.

The benefits of the separation of authenticationManager and userDetailsService are also shown here.

AuthorizationServerEndpointsConfigurer

spring-security-oauth2-2.0.14.RELEASE-sources.jar! /org/springframework/security/oauth2/config/annotation/web/configurers/AuthorizationServerEndpointsConfigurer.java

public AuthorizationServerEndpointsConfigurer userDetailsService(UserDetailsService userDetailsService) {
        if (userDetailsService != null) {
            this.userDetailsService = userDetailsService;
            this.userDetailsServiceOverride = true;
        }
        return this;
    }

After the userDetailsService is set, the userDetailsServiceOverride is true and then configured externally.
spring-security-oauth2-2.0.14.RELEASE-sources.jar! /org/springframework/security/oauth2/config/annotation/web/configuration/AuthorizationServerSecurityConfiguration.java

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        AuthorizationServerSecurityConfigurer configurer = new AuthorizationServerSecurityConfigurer();
        FrameworkEndpointHandlerMapping handlerMapping = endpoints.oauth2EndpointHandlerMapping();
        http.setSharedObject(FrameworkEndpointHandlerMapping.class, handlerMapping);
        configure(configurer);
        http.apply(configurer);
        String tokenEndpointPath = handlerMapping.getServletPath("/oauth/token");
        String tokenKeyPath = handlerMapping.getServletPath("/oauth/token_key");
        String checkTokenPath = handlerMapping.getServletPath("/oauth/check_token");
        if (!endpoints.getEndpointsConfigurer().isUserDetailsServiceOverride()) {
            UserDetailsService userDetailsService = http.getSharedObject(UserDetailsService.class);
            endpoints.getEndpointsConfigurer().userDetailsService(userDetailsService);
        }
        // @formatter:off
        http
            .authorizeRequests()
                .antMatchers(tokenEndpointPath).fullyAuthenticated()
                .antMatchers(tokenKeyPath).access(configurer.getTokenKeyAccess())
                .antMatchers(checkTokenPath).access(configurer.getCheckTokenAccess())
        .and()
            .requestMatchers()
                .antMatchers(tokenEndpointPath, tokenKeyPath, checkTokenPath)
        .and()
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.NEVER);
        // @formatter:on
        http.setSharedObject(ClientDetailsService.class, clientDetailsService);
    }

It is additionally judged here that the userDetailsService is only set if it is not overwritten.

Other

Why didn’t authorizationserversecurityconfigurer need to specify an authenticationManager, because the internal configuration helps you to configure it
spring-security-oauth2-2.0.14.RELEASE-sources.jar! /org/springframework/security/oauth2/config/annotation/web/configurers/AuthorizationServerSecurityConfigurer.java

private ClientCredentialsTokenEndpointFilter clientCredentialsTokenEndpointFilter(HttpSecurity http) {
        ClientCredentialsTokenEndpointFilter clientCredentialsTokenEndpointFilter = new ClientCredentialsTokenEndpointFilter(
                frameworkEndpointHandlerMapping().getServletPath("/oauth/token"));
        clientCredentialsTokenEndpointFilter
                .setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
        OAuth2AuthenticationEntryPoint authenticationEntryPoint = new OAuth2AuthenticationEntryPoint();
        authenticationEntryPoint.setTypeName("Form");
        authenticationEntryPoint.setRealmName(realm);
        clientCredentialsTokenEndpointFilter.setAuthenticationEntryPoint(authenticationEntryPoint);
        clientCredentialsTokenEndpointFilter = postProcess(clientCredentialsTokenEndpointFilter);
        http.addFilterBefore(clientCredentialsTokenEndpointFilter, BasicAuthenticationFilter.class);
        return clientCredentialsTokenEndpointFilter;
    }

See here from http.getsharedbject (authenticationmanager.class) and then specify.

Summary

  • For those requesting /oauth/token, if the configuration supports allowformauthenticationforclients and the url contains client_id and client_secret, clientcredentialstokendpointfilter will be used.
  • For those requesting /oauth/token, if there is no support for AllowFormatAuthenticationForClients or there is support but there is no client_id and client_secret in the url, go through basic authentication.
  • After the client detail authentication (go through the configuration of authorizationserversecurityconfigurer) succeeds, if it is password mode, go through user account password authentication.
  • The password is to use the userDetailsService configured in authorizationserdenedpointconfigurator to authenticate.