Search code examples
spring-bootspring-authorization-server

Implement Spring Authorization Server with 2 custom identity providers


I want to create a custom implementation of Spring Authorization Server with 2 custom federation providers:

  1. First provider to authenticate clients with AuthorizationGrantType.AUTHORIZATION_CODE for React web clients with login page. Used to get users from Keycloak.
  2. Second provider for devices only with API access for devices - AuthorizationGrantType.CLIENT_CREDENTIALS. User credentials will fe fetched again from a second Cognito provider using API call.

Spring Security configuration:

    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {

        OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);

        http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
                .tokenEndpoint(tokenEndpoint ->
                {
                    tokenEndpoint.accessTokenRequestConverter(new ThirdPartyPreAuthenticationConverter());
                    tokenEndpoint.authenticationProvider(
                            new ApikeyAuthenticationProvider(
                                    jwtGenerator(jwkSource()),
                                    oAuth2BasicTokenClaimsProvider,
                                    apikeyRegisteredClientRepository(passwordEncoder()),
                                    passwordEncoder(),
                                    tokenGenerator));
                    tokenEndpoint.authenticationProvider(
                            new UsersAuthenticationProvider(
                                    jwtGenerator(jwkSource()),
                                    pegasusClient,
                                    usersRegisteredClientRepository(passwordEncoder()),
                                    passwordEncoder(),
                                    tokenGenerator));
                    tokenEndpoint.accessTokenResponseHandler(new DispatchGeneratedTokenHandler());
                });

        http.oauth2ResourceServer(oauth2ResourceServer ->
                oauth2ResourceServer.jwt(Customizer.withDefaults())
        );

        // @formatter:on
        return http.build();
    }

    @Bean
    public InMemoryRegisteredClientRepository apikeyRegisteredClientRepository(PasswordEncoder passwordEncoder) {
        RegisteredClient tokenExchangeClient = RegisteredClient.withId(UUID.randomUUID().toString())
                .clientId(clientId)
                .clientSecret(passwordEncoder.encode(clientSecret))
                .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
                .scope("internal.read")
                .scope("internal.write")
                .build();
        InMemoryRegisteredClientRepository registeredClientRepository = new InMemoryRegisteredClientRepository(tokenExchangeClient);
        registeredClientRepository.save(tokenExchangeClient);

        return registeredClientRepository;
    }

    @Bean
    @Primary
    public InMemoryRegisteredClientRepository usersRegisteredClientRepository(PasswordEncoder passwordEncoder) {
        RegisteredClient tokenExchangeClient = RegisteredClient.withId(UUID.randomUUID().toString())
                .clientId(clientId)
                .clientSecret(passwordEncoder.encode(clientSecret))
                .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
                .scope("internal.read")
                .scope("internal.write")
                .redirectUri("http://localhost:8080/login/oauth2/code/my-client") // Add at least one redirect URI
                .redirectUri("http://localhost:8080/authorized") // Another example URI
                .build();
        InMemoryRegisteredClientRepository registeredClientRepository = new InMemoryRegisteredClientRepository(tokenExchangeClient);
        registeredClientRepository.save(tokenExchangeClient);

        return registeredClientRepository;
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @SneakyThrows
    @Bean
    public JWKSource<SecurityContext> jwkSource() {
        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
        keyPairGenerator.initialize(2048);
        KeyPair keyPair = keyPairGenerator.generateKeyPair();
        RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
        RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
        RSAKey rsaKey = new RSAKey.Builder(publicKey)
                .privateKey(privateKey)
                .keyID(UUID.randomUUID().toString())
                .build();
        JWKSet jwkSet = new JWKSet(rsaKey);
        return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
    }

    @Bean
    public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
        return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
    }

    @Bean
    public JwtEncoder jwtGenerator(JWKSource<SecurityContext> jwkSource) {
        return new NimbusJwtEncoder(jwkSource);
    }

    @Bean
    public AuthorizationServerSettings authorizationServerSettings() {
        return AuthorizationServerSettings.builder().build();
    }

    @Bean
    public AuthenticationProvider apikeyAuthenticationProvider() {
        return new ApikeyAuthenticationProvider(
                jwtGenerator(jwkSource()),
                oAuth2BasicTokenClaimsProvider,
                apikeyRegisteredClientRepository(passwordEncoder()),
                passwordEncoder(),
                tokenGenerator);
    }

    @Bean
    @Primary
    public AuthenticationProvider usersAuthenticationProvider() {
        return new UsersAuthenticationProvider(
                jwtGenerator(jwkSource()),                    usersRegisteredClientRepository(passwordEncoder()),
                passwordEncoder(),
                tokenGenerator);
    }

User Provider:

public class UsersAuthenticationProvider implements AuthenticationProvider {
    ....

    public UsersAuthenticationProvider(...) {
        .........
    }

    @Override
    public OAuth2AccessTokenAuthenticationToken authenticate(Authentication authentication) throws AuthenticationException {
        OAuth2ThirdPartyClientCredentialsAuthenticationToken token =
                (OAuth2ThirdPartyClientCredentialsAuthenticationToken) authentication;

        RegisteredClient registeredClient =
                registeredClientRepository.findByClientId(token.getClientId());
        if(registeredClient == null){
            throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_CLIENT);
        }
        if(!encoder.matches(token.getClientSecret(), registeredClient.getClientSecret())) {
            throw new OAuth2AuthenticationException(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT);
        }

        Map<String, Object> claims = null; // !!! TEMPORARY disabled
this.jwtEncoder.encode(JwtEncoderParameters.from(jwsHeaderBuilder.build(), claimsBuilder.build()));
        OAuth2AccessToken jwt = tokenGenerator.generateToken(claims);
        return new OAuth2AccessTokenAuthenticationToken(
                registeredClient,
                authentication,
                jwt);
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return OAuth2ThirdPartyClientCredentialsAuthenticationToken.class.isAssignableFrom(authentication);
    }
}

Second provider:

@Primary
public class ApikeyAuthenticationProvider implements AuthenticationProvider {
    ...........

    public ApikeyAuthenticationProvider(........) {
        ..........
    }

    @Override
    public OAuth2AccessTokenAuthenticationToken authenticate(Authentication authentication) throws AuthenticationException {
        OAuth2ThirdPartyClientCredentialsAuthenticationToken token =
                (OAuth2ThirdPartyClientCredentialsAuthenticationToken) authentication;

        RegisteredClient registeredClient =
                registeredClientRepository.findByClientId(token.getClientId());
        if(registeredClient == null){
            throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_CLIENT);
        }
        if(!encoder.matches(token.getClientSecret(), registeredClient.getClientSecret())) {
            throw new OAuth2AuthenticationException(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT);
        }

        Map<String, Object> claims =
                apikeyAuthenticationProviderClient.getClaims(......);

        OAuth2AccessToken jwt = tokenGenerator.generateToken(claims);
        return new OAuth2AccessTokenAuthenticationToken(
                registeredClient,
                authentication,
                jwt);
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return OAuth2ThirdPartyClientCredentialsAuthenticationToken.class.isAssignableFrom(authentication);
    }
}

This setup is not working. Do you know what is the proper way to implement it?


Solution

  • Adding my implementation on top of baeldung's spring-auth-server guide by extending the authorization to support spring extension grant type

    Overall authorization that wraps User's OIDC flow and Device's access token flow are as illustrated: enter image description here

    That analogous to your requirement:

    1. First provider to authenticate clients with grant type AUTHORIZATION_CODE --> taken care by OIDC flow with username and password.

    1.A & B: When user tries to access the 127.0.0.1:8080/articles endpoint, it will redirect them to login endpoint where they has to be authenticated themselves by providing their username n passwordenter image description here 1.E After successful authentication, user get the resource data representation as json via web cliententer image description here

    1. Second provider for devices only with API access for devices grant type CLIENT_CREDENTIALS --> will be handled by extension grant type with client id n secret

    2.A Device has to obtain the access token via custom grant type with basic authorization as client id n secretenter image description here 2.C Device has now to approach the articles endpoint with the obtained bearer token to get the resource data enter image description here

    I'm hoping that these will give some path to proceed you to the next level. Added the complete code commit in GitHub and sequence flow in Medium for any further reference.