spc16670
spc16670

Reputation: 584

Spring Security 5.2 Password Flow

I am trying to authenticate the user using the password flow in the latest version of Spring Security - 5.2.

The docs seem to suggest how to do that.

@Bean
public OAuth2AuthorizedClientManager passwordFlowAuthorizedClientManager(
        HttpClient httpClient,
        ClientRegistrationRepository clientRegistrationRepository,
        OAuth2AuthorizedClientRepository authorizedClientRepository) {

    HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory();
    requestFactory.setHttpClient(httpClient);

    DefaultPasswordTokenResponseClient c = new DefaultPasswordTokenResponseClient();
    RestTemplate client = new RestTemplate(requestFactory);
    client.setMessageConverters(Arrays.asList(
            new FormHttpMessageConverter(),
            new OAuth2AccessTokenResponseHttpMessageConverter()));
    client.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
    c.setRestOperations(client);

    OAuth2AuthorizedClientProvider authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder()
                .password(configurer -> configurer.accessTokenResponseClient(c))
                .refreshToken()
                .build();

    DefaultOAuth2AuthorizedClientManager authorizedClientManager =
            new DefaultOAuth2AuthorizedClientManager(
                    clientRegistrationRepository, authorizedClientRepository);
    authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);

    authorizedClientManager.setContextAttributesMapper(authorizeRequest -> {
        Map<String, Object> contextAttributes = new HashMap<>();
        String username = authorizeRequest.getAttribute(OAuth2ParameterNames.USERNAME);
        String password = authorizeRequest.getAttribute(OAuth2ParameterNames.PASSWORD);
        contextAttributes.put(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, username);
        contextAttributes.put(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, password);
        return contextAttributes;
    });

    return authorizedClientManager;
}



I execute the request, I can see the access token returned in HTTP header but the SecurityContext is not populated and the session user remains anonymous.

String username = "joe";
String password = "joe";
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
ClientRegistration r = clientRegistrationRepository.findByRegistrationId("keycloak");

OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId(r.getRegistrationId())
        .principal(authentication)
        .attributes(attrs -> {
            attrs.put(OAuth2ParameterNames.USERNAME, username);
            attrs.put(OAuth2ParameterNames.PASSWORD, password);
        })
        .build();
OAuth2AuthorizedClient authorizedClient = this.authorizedClientManager.authorize(authorizeRequest);

Any ideas?

Upvotes: 7

Views: 4164

Answers (2)

xardbaiz
xardbaiz

Reputation: 817

Probably .password(..) is not the best povider in this case, because by default DefaultPasswordTokenResponseClient extracts token from reponse body.

If,

.. the access token returned in HTTP header..

.. (which is strange for this type of flow 🤔), then we need to override getTokenResponse method with our own.

1. Create the copy of DefaultPasswordTokenResponseClient

2. Replace getTokenResponse method code with:

@Override
public OAuth2AccessTokenResponse getTokenResponse(OAuth2ClientCredentialsGrantRequest clientCredentialsGrantRequest) {
    Assert.notNull(clientCredentialsGrantRequest, "clientCredentialsGrantRequest cannot be null");
    RequestEntity<?> request = this.requestEntityConverter.convert(clientCredentialsGrantRequest);
    ResponseEntity<OAuth2AccessTokenResponse> response = getResponse(request);
    String token = response.getHeaders().getFirst("<accessToken header name>");
    return OAuth2AccessTokenResponse.withToken(token);
}

Then we can use this client as .password(...) provider configurer

.password(configurer -> configurer.accessTokenResponseClient(<instance of your client>))

Upvotes: 0

spc16670
spc16670

Reputation: 584

After reading into the documentation a bit more I do not think that Oauth 2 password flow in Spring Security 5.2 is supported the same way authorisation flow is. Spring Security 5.2 has password flow support for the http client which can cache the authorization request and refresh the token before it expires - but there is no end user password flow support in which the client proxies the credentials to the authorization server.

Of course, it is entirely possible to authenticate the end user by harvesting the credentials, implementing a custom AuthenticationProvider that swaps the credentials for a token with the authorization server and returns an OAuth2AuthenticationToken that is persisted to the context.

Upvotes: 2

Related Questions