Spring Security 6.2 and spring-authorization-server

84 views Asked by At

I'm migrating Spring 5.2./Spring Security 5.8 backend to Spring 6.1/Spring Security 6.2 together with ditching spring-security-oauth2 and adding spring-security-oauth2-authorization-server and it looks a bit complicated.

First of all, current architecture:

  1. Users are mobile apps that authenticates/authorises using username/password.
  2. Auth/authz is performed by backend against LDAP Server (AD basically) using provided user credentials
  3. Access/refresh tokens are generated at the same time and saved into local DB
  4. There are additional simplified auth flows based on auth_id/api_keys where passwords are not involved.

So there is no redirects, nothing complex. Oauth2 is used in a very simplified way.

Also:

  1. Everything is defined in spring-security.xml
  2. No Spring Boot is involved and I would like not to add in unless it strictly necessary.

What I would like to have:

  1. Same simplicity — not redirects, getting access token, refresh token together with single request, keep using LDAP.
  2. Be able to easily use the whole power of spring-security-oauth2-authorization-server when needed if I add another resource/auth server.

So I did some research through the documentation, samples, code and ended up with the following assumptions and config (see below):

  1. I would probably need to stick to Authorization Grant = Client Credentials
  2. I should add LDAP password check to authentication providers of 'token endpoint'

I'm using the following config and I'm able to generate/save access token by performing request to /oauth2/token endpoint but I have a few issues there:

  1. I'm not able to switch to ClientAuthenticationMethod.NONE. I don't need it.
  2. Refresh token is not generated along with access token
  3. I'm not able to perform a call with obtained access token (Authorization: Bearer ...) — I'm getting 401
  4. It's not clear how to provide user details (UserDetailsService) fetched from LDAP during auth. Should I save them into oauth2_authorization table together with access token etc.?

Here is my config. Any help is appreciated.

package accessmanager;

import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.jose.jws.JwsAlgorithm;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.security.oauth2.jwt.*;
import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientCredentialsAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
import org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat;
import org.springframework.security.oauth2.server.authorization.token.*;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.util.StringUtils;

import javax.sql.DataSource;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.time.Instant;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;

import static org.springframework.security.config.Customizer.withDefaults;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    private final CheckRolesLdapAuthenticationProvider checkRolesLdapAuthenticationProvider;
    private final DataSource dataSource;
    private final UserRepository userRepository;

    @Autowired
    public SecurityConfig(CheckRolesLdapAuthenticationProvider checkRolesLdapAuthenticationProvider,
                            DataSource dataSource, UserRepository userRepository) {
        this.checkRolesLdapAuthenticationProvider = checkRolesLdapAuthenticationProvider;
        this.dataSource = dataSource;
        this.userRepository = userRepository;
    }

    @Bean
    public JdbcTemplate jdbcTemplate() {
        return new JdbcTemplate(dataSource);
    }

    @Bean
    public JdbcOAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate,
                                                                RegisteredClientRepository registeredClientRepository) {
        return new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);
    }

    @Bean
    public PasswordEncoder noOpPasswordEncoder() {
        return new PasswordEncoder() {
            @Override
            public String encode(CharSequence rawPassword) {
                return rawPassword.toString();
            }

            @Override
            public boolean matches(CharSequence rawPassword, String encodedPassword) {
                return rawPassword.equals(encodedPassword);
            }
        };
    }

    @Bean
    @Order(1)
    public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
        OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
        http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
                .tokenEndpoint(tokenEndpoint ->
                        tokenEndpoint
                                .accessTokenRequestConverter(
                                        request -> {
                                            final Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication();
                                            return
                                                    new OAuth2ClientCredentialsAuthenticationToken(
                                                            clientPrincipal,
                                                            Collections.singleton("write"),
                                                            request.getParameterMap()
                                                                    .entrySet()
                                                                    .stream()
                                                                    .collect(
                                                                            Collectors.toMap(Map.Entry::getKey, p -> p.getValue()[0])));
                                        })
                )
                .clientAuthentication(auth ->
                        auth.authenticationProviders(list -> list.add(checkRolesLdapAuthenticationProvider)
                        ))
                .oidc(Customizer.withDefaults()); // Enable OpenID Connect 1.0

        http
//                .exceptionHandling((exceptions) -> exceptions
//                        .defaultAuthenticationEntryPointFor(
//                                new BasicAuthenticationEntryPoint(),
//                                new MediaTypeRequestMatcher(MediaType.ALL)
//                        )
//                )
                .oauth2ResourceServer((resourceServer) -> resourceServer.jwt(Customizer.withDefaults()));

        return http.build();
    }

    @Bean
    @Order(2)
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .csrf(CsrfConfigurer::disable)
                .authorizeHttpRequests((authorize) ->
                                authorize
                                    .requestMatchers("/oauth2/**", "/api/v*/auth").permitAll()
                                    .requestMatchers("/api/v*/**").authenticated()

                )
                .httpBasic(withDefaults());

        return http.build();
    }

    @Bean
    OAuth2TokenGenerator<?> tokenGenerator(JWKSource<SecurityContext> jwkSource) {
        final JwtEncoder jwtEncoder = new NimbusJwtEncoder(jwkSource);
        final OAuth2TokenGenerator<Jwt> jwtGenerator = new JwtGenerator(new NimbusJwtEncoder(jwkSource));
        final OAuth2AccessTokenGenerator accessTokenGenerator = new OAuth2AccessTokenGenerator();
        final OAuth2RefreshTokenGenerator refreshTokenGenerator = new OAuth2RefreshTokenGenerator();
        return new DelegatingOAuth2TokenGenerator(
                jwtGenerator, accessTokenGenerator, refreshTokenGenerator);
    }

    @Bean
    public RegisteredClientRepository registeredClientRepository() {
        RegisteredClient oidcClient = RegisteredClient.withId(UUID.randomUUID().toString())
                .clientId("mobile")
                .clientSecret("{noop}secret")
                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
                .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
                .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
                .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
                .redirectUri("http://127.0.0.1:8081/login/oauth2/code/mobile")
                .postLogoutRedirectUri("http://127.0.0.1:8081/")
                .scope("write")
                .clientSettings(ClientSettings.builder().requireAuthorizationConsent(false).build())
                .build();

        return new InMemoryRegisteredClientRepository(oidcClient);
    }

    //https://docs.spring.io/spring-authorization-server/reference/getting-started.html
    @Bean
    public JWKSource<SecurityContext> jwkSource() {
        KeyPair keyPair = generateRsaKey();
        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 new ImmutableJWKSet<>(jwkSet);
    }

    private static KeyPair generateRsaKey() {
        KeyPair keyPair;
        try {
            KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
            keyPairGenerator.initialize(2048);
            keyPair = keyPairGenerator.generateKeyPair();
        }
        catch (Exception ex) {
            throw new IllegalStateException(ex);
        }
        return keyPair;
    }

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

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

    @Bean
    public UserDetailsService userDetailsService() {
        // how to route this to details received from LDAP on authorization?
        return username -> {
            return new UserDetails() {
                @Override
                public Collection<? extends GrantedAuthority> getAuthorities() {
                    return Collections.emptyList();
                }

                @Override
                public String getPassword() {
                    return null;
                }

                @Override
                public String getUsername() {
                    return username;
                }

                @Override
                public boolean isAccountNonExpired() {
                    return true;
                }

                @Override
                public boolean isAccountNonLocked() {
                    return true;
                }

                @Override
                public boolean isCredentialsNonExpired() {
                    return true;
                }

                @Override
                public boolean isEnabled() {
                    return true;
                }
            };
        };
    }
}
0

There are 0 answers