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:
- Users are mobile apps that authenticates/authorises using username/password.
- Auth/authz is performed by backend against LDAP Server (AD basically) using provided user credentials
- Access/refresh tokens are generated at the same time and saved into local DB
- 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:
- Everything is defined in spring-security.xml
- No Spring Boot is involved and I would like not to add in unless it strictly necessary.
What I would like to have:
- Same simplicity — not redirects, getting access token, refresh token together with single request, keep using LDAP.
- 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):
- I would probably need to stick to Authorization Grant = Client Credentials
- 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:
- I'm not able to switch to ClientAuthenticationMethod.NONE. I don't need it.
- Refresh token is not generated along with access token
- I'm not able to perform a call with obtained access token (Authorization: Bearer ...) — I'm getting 401
- 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;
}
};
};
}
}