Spring Security without WebSecurityConfiguererAdapter, register two AuthenticationProvider - spring-boot

Since the deprication of WebSecurityConfiguererAdapter, I am not sure on how to implement my two custom AuthenticationProvider classes. Everything works in case I only use one of the AuthenticationProviders, but having two prevents both of them to function. I tried giving them #Order annotations but that doesn't work either. How would I register my custom AuthenticationProviders to make them work along each other and being called when the class defined in its support method matches. By the way, I'am a SpringBoot noob ;)
#Component
class AuthenticationService(
private val authenticationManager: AuthenticationManager,
) {
fun login(loginDto: LoginDto): Boolean {
authenticationManager.authenticate(
Token2(
loginDto.username, loginDto.password
)
)
return true
}
}
#Configuration
class ProdSecurityConfiguration {
#Bean
fun passwordEncoder(): PasswordEncoder {
return BCryptPasswordEncoder()
}
#Bean
fun authenticationManager(authenticationConfiguration: AuthenticationConfiguration): AuthenticationManager {
return authenticationConfiguration.authenticationManager
}
#Bean
fun filterChain(http: HttpSecurity): SecurityFilterChain {
http.csrf().disable().cors().disable()
http.authorizeRequests()
.antMatchers("/login", "/register").permitAll().anyRequest().authenticated().and().sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
return http.build()
}
}
#Component
class CredentialsAuthProvider : AuthenticationProvider {
override fun authenticate(authentication: Authentication): Authentication {
return UsernamePasswordAuthenticationToken(
"myUser#mailbox.org", "password"
)
}
override fun supports(authentication: Class<*>?): Boolean {
return authentication == Token1::class.java
}
}
#Component
class CredentialsAuthProvider2 : AuthenticationProvider {
override fun authenticate(authentication: Authentication): Authentication {
return UsernamePasswordAuthenticationToken(
"myUser#mailbox.org", "password"
)
}
override fun supports(authentication: Class<*>?): Boolean {
return authentication == Token2::class.java
}
}

I removed the #Component annotation from my custom AuthenticationProviders, I created #Beans in my SecurityConfiguration class for each custom AuthenticationProvider where I directly call their constructor instead of using springs DI. Then used them within my AthenticationManager Bean where I return a ProviderManager.
Code snippets for explanation:
#Configuration
class ProdSecurityConfiguration {
#Bean
fun credentialsAuthProvider(): AuthenticationProvider {
return CredentialsAuthProvider()
}
#Bean
fun credentialsAuthProvider2(): AuthenticationProvider {
return CredentialsAuthProvider2()
}
#Bean
fun authenticationManager(): AuthenticationManager {
return ProviderManager(listOf(credentialsAuthProvider(), credentialsAuthProvider2()))
}
#Bean
fun filterChain(http: HttpSecurity): SecurityFilterChain {
http.csrf().disable().cors().disable()
http.authorizeRequests()
.antMatchers("/login", "/register").permitAll().anyRequest().authenticated().and().sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
return http.build()
}
}

Related

What is the replacement for TokenStore, TokenServices and JwtAccessTokenConverter in Spring Security 5

I am upgrading Spring Boot from 2.3.12.RELEASE to 2.7.7 in my Kotlin project and found out that I have to change the code for Spring Security because Spring Security OAuth that was used in this project is no longer supported and has to be migrated to Spring Security 5+. I have this configuration that I want to migrate (some business data omitted):
#Configuration
#EnableResourceServer
#EnableGlobalMethodSecurity(prePostEnabled = true)
class ResourceServerConfig : ResourceServerConfigurerAdapter() {
#Value("...")
private val claimAud: String? = null
#Value("...")
private val urlJwk: String? = null
override fun configure(resources: ResourceServerSecurityConfigurer) {
resources.tokenStore(tokenStore())
resources.resourceId(claimAud)
}
#Bean
fun tokenStore(): TokenStore {
logger.info("JWK settings resource config: $urlJwk")
return JwkTokenStore(urlJwk, createJwtAccessTokenConverter())
}
#Throws(Exception::class)
override fun configure(http: HttpSecurity) {
http.anonymous().and().cors(withDefaults())
.authorizeRequests()
.mvcMatchers(BASE_PATH_PATTERN).permitAll()
.mvcMatchers(API_PATH_PATTERN).permitAll()
.mvcMatchers(ADMIN_PATH_PATTERN).authenticated()
}
#Bean
fun securityEvaluationContextExtension(): SecurityEvaluationContextExtension {
return SecurityEvaluationContextExtension()
}
#Bean
#Primary
fun tokenServices(): DefaultTokenServices {
val defaultTokenServices = DefaultTokenServices()
defaultTokenServices.setTokenStore(tokenStore())
return defaultTokenServices
}
#Bean
fun createJwtAccessTokenConverter(): JwtAccessTokenConverter? {
val converter = JwtAccessTokenConverter()
converter.accessTokenConverter = MyTokenConverter()
return converter
}
#Component
class MyTokenConverter : DefaultAccessTokenConverter(), JwtAccessTokenConverterConfigurer {
override fun extractAuthentication(claims: Map<String?, *>?): OAuth2Authentication {
val authentication = super.extractAuthentication(claims)
authentication.details = claims
return authentication
}
override fun configure(converter: JwtAccessTokenConverter) {
converter.accessTokenConverter = this
}
}
I don't know what to do with those methods related to TokenStore, TokenServices or TokenConverter, how to replace them.
I consulted the migrtation guide https://github.com/spring-projects/spring-security/wiki/OAuth-2.0-Migration-Guide but it seems that it lacks a lot of information, there is no specific guide anywhere actually for how to replace those components that I mentioned in my question.

Too many arguments for public open fun http()

I'm following this tutorial to implement a authentication system in Kotlin using Spring Boot
The code works nicely until this part:
#Configuration
#EnableWebSecurity
class MultiHttpSecurityConfig {
#Bean
public fun userDetailsService(): UserDetailsService {
val users: User.UserBuilder = User.withDefaultPasswordEncoder()
val manager = InMemoryUserDetailsManager()
manager.createUser(users.username("user").password("password").roles("USER").build())
manager.createUser(users.username("admin").password("password").roles("USER","ADMIN").build())
return manager
but in the next part i receive the following error and cant import anything:
#Order(1)
#Bean
open fun apiFilterChain(http: HttpSecurity): SecurityFilterChain {
http {
securityMatcher("/api/**")
authorizeRequests {
authorize(anyRequest, hasRole("ADMIN"))
}
httpBasic { }
}
return http.build()
}
#Bean
open fun formLoginFilterChain(http: HttpSecurity): SecurityFilterChain {
http {
authorizeRequests {
authorize(anyRequest, authenticated)
}
formLogin { }
}
return http.build()
}
}
Am I missing something? Is something necessary besides Spring Security dependency?
Try to add import :
import org.springframework.security.config.web.server.invoke

Spring in Kotlin: from 5.3 to 6.0 security Configuration

I'm facing lots of issues in doing Spring security configurations that I used to have in v5.3 applied in v6.
This is the file I had
#Configuration
#EnableWebSecurity
class WebSecurityConfiguration : WebSecurityConfigurerAdapter() {
#Autowired
lateinit var service: UserService
/**
* Will be resolved into: WebSecurityEntryPoint injected instance.
*/
#Autowired
lateinit var unauthorizedHandler: AuthenticationEntryPoint
#Autowired
lateinit var successHandler: WebSecurityAuthSuccessHandler
#Autowired
override fun configure(auth: AuthenticationManagerBuilder) {
auth.authenticationProvider(authenticationProvider())
}
override fun configure(http: HttpSecurity?) {
http
?.csrf()?.disable()
?.exceptionHandling()
?.authenticationEntryPoint(unauthorizedHandler)
?.and()
?.authorizeRequests()
/**
* Access to Notes and Todos API calls is given to any authenticated system user.
*/
?.antMatchers("/notes")?.authenticated()
?.antMatchers("/notes/**")?.authenticated()
?.antMatchers("/todos")?.authenticated()
?.antMatchers("/todos/**")?.authenticated()
/**
* Access to User API calls is given only to Admin user.
*/
?.antMatchers("/users")?.hasAnyAuthority("ADMIN")
?.antMatchers("/users/**")?.hasAnyAuthority("ADMIN")
?.and()
?.formLogin()
?.successHandler(successHandler)
?.failureHandler(SimpleUrlAuthenticationFailureHandler())
?.and()
?.logout()
}
#Bean
fun authenticationProvider(): DaoAuthenticationProvider {
val authProvider = DaoAuthenticationProvider()
authProvider.setUserDetailsService(service)
authProvider.setPasswordEncoder(encoder())
return authProvider
}
#Bean
fun encoder(): PasswordEncoder = BCryptPasswordEncoder(11)
#Bean
fun accessDecisionManager(): AccessDecisionManager {
val decisionVoters = Arrays.asList(
WebExpressionVoter(),
RoleVoter(),
AuthenticatedVoter()
)
return UnanimousBased(decisionVoters)
}
}
I used the documentation in Spring.io
https://spring.io/blog/2022/02/21/spring-security-without-the-websecurityconfigureradapter
and I'm just hitting the wall since then. their documentation is not helpful and the new dependencies aren't working the same.
how can this be done now?
P.S: I often keep getting this error:
Caused by: java.lang.ClassNotFoundException: org.springframework.security.core.context.DeferredSecurityContext
which i couldn't find anywhere
Okey... I managed to solve it this way
first I had to add the security dependency for v6
implementation("org.springframework.security:spring-security-core:6.0.1")
and I made the Security Configuration this way
#Configuration
#EnableWebSecurity
class SecurityConfiguration(
private val userService: UserService,
private val unauthorizedHandler: AuthenticationEntryPoint,
private val successHandler: WebSecurityAuthSuccessHandler
) {
/**
* Will be resolved into: WebSecurityEntryPoint injected instance.
*/
#Bean
fun myPasswordEncoder(): PasswordEncoder {
return BCryptPasswordEncoder(11)
}
#Primary
fun configureAuthentication(auth: AuthenticationManagerBuilder): AuthenticationManagerBuilder {
return auth.authenticationProvider(authenticationProvider())
}
#Bean
fun authenticationProvider(): DaoAuthenticationProvider {
val authProvider = DaoAuthenticationProvider()
authProvider.setUserDetailsService(userService)
authProvider.setPasswordEncoder(myPasswordEncoder())
return authProvider
}
#Bean
fun accessDecisionManager(): AccessDecisionManager {
val decisionVoter = listOf(
WebExpressionVoter(),
RoleVoter(),
AuthenticatedVoter()
)
return UnanimousBased(decisionVoter)
}
#Bean
fun configureHttpSecurity(httpSecurity: HttpSecurity): SecurityFilterChain {
httpSecurity
.csrf().disable()
.exceptionHandling()
.authenticationEntryPoint(unauthorizedHandler)
.and()
.authorizeHttpRequests()
/**
* Access to Notes and Todos API calls is given to any authenticated system user.
*/
.requestMatchers("/notes").authenticated()
.requestMatchers("/notes/**").authenticated()
.requestMatchers("/todos").authenticated()
.requestMatchers("/todos/**").authenticated()
/**
* Access to User API calls is given only to Admin user.
*/
.requestMatchers("/users").hasAnyAuthority("ADMIN")
.requestMatchers("/users/**").hasAnyAuthority("ADMIN")
.and()
.formLogin()
.successHandler(successHandler)
.failureHandler(SimpleUrlAuthenticationFailureHandler())
.and()
.logout()
return httpSecurity.build()
}
}

How to response custom json body on unauthorized requests while implementing custom authentication manager in webflux

I was trying to implement custom JWT token authentication while i am also handling global exceptions to customize response body for each type of exceptions. Everything is working fine except I would like to return custom json response when an unauthorized request is received instead of just 401 status code.
Below is my implementation for JwtServerAuthenticationConverter and JwtAuthenticationManager.
#Component
public class JwtServerAuthenticationConverter implements ServerAuthenticationConverter {
private static final String AUTH_HEADER_VALUE_PREFIX = "Bearer ";
#Override
public Mono<Authentication> convert(ServerWebExchange exchange) {
return Mono.justOrEmpty(exchange)
.flatMap(serverWebExchange -> Mono.justOrEmpty(
serverWebExchange
.getRequest()
.getHeaders()
.getFirst(HttpHeaders.AUTHORIZATION)
)
)
.filter(header -> !header.trim().isEmpty() && header.trim().startsWith(AUTH_HEADER_VALUE_PREFIX))
.map(header -> header.substring(AUTH_HEADER_VALUE_PREFIX.length()))
.map(token -> new UsernamePasswordAuthenticationToken(token, token))
;
}
}
#Component
public class JwtAuthenticationManager implements ReactiveAuthenticationManager {
private final JWTConfig jwtConfig;
private final ObjectMapper objectMapper;
public JwtAuthenticationManager(JWTConfig jwtConfig, ObjectMapper objectMapper) {
this.jwtConfig = jwtConfig;
this.objectMapper = objectMapper;
}
#Override
public Mono<Authentication> authenticate(Authentication authentication) {
return Mono.just(authentication)
.map(auth -> JWTHelper.loadAllClaimsFromToken(auth.getCredentials().toString(), jwtConfig.getSecret()))
.onErrorResume(throwable -> Mono.error(new JwtException("Unauthorized")))
.map(claims -> objectMapper.convertValue(claims, JWTUserDetails.class))
.map(jwtUserDetails ->
new UsernamePasswordAuthenticationToken(
jwtUserDetails,
authentication.getCredentials(),
jwtUserDetails.getGrantedAuthorities()
)
)
;
}
}
And below is my global exception handling which is working absolutely fine except the case where webflux return 401 from JwtServerAuthenticationConverter convert method.
#Configuration
#Order(-2)
public class ExceptionHandler implements WebExceptionHandler {
#Override
public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
exchange.getResponse().getHeaders().set("Content-Type", MediaType.APPLICATION_JSON_VALUE);
return buildErrorResponse(ex)
.flatMap(
r -> r.writeTo(exchange, new HandlerStrategiesResponseContext(HandlerStrategies.withDefaults()))
);
}
private Mono<ServerResponse> buildErrorResponse(Throwable ex) {
if (ex instanceof RequestEntityValidationException) {
return ServerResponse.badRequest().contentType(MediaType.APPLICATION_JSON).body(
Mono.just(new ErrorResponse(ex.getMessage())),
ErrorResponse.class
);
} else if (ex instanceof ResponseStatusException) {
ResponseStatusException exception = (ResponseStatusException) ex;
if (exception.getStatus().value() == 404) {
return ServerResponse.status(HttpStatus.NOT_FOUND).contentType(MediaType.APPLICATION_JSON).body(
Mono.just(new ErrorResponse("Resource not found - 404")),
ErrorResponse.class
);
} else if (exception.getStatus().value() == 400) {
return ServerResponse.status(HttpStatus.BAD_REQUEST).contentType(MediaType.APPLICATION_JSON).body(
Mono.just(new ErrorResponse("Unable to parse request body - 400")),
ErrorResponse.class
);
}
} else if (ex instanceof JwtException) {
return ServerResponse.status(HttpStatus.UNAUTHORIZED).contentType(MediaType.APPLICATION_JSON).body(
Mono.just(new ErrorResponse(ex.getMessage())),
ErrorResponse.class
);
}
ex.printStackTrace();
return ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR).contentType(MediaType.APPLICATION_JSON).body(
Mono.just(new ErrorResponse("Internal server error - 500")),
ErrorResponse.class
);
}
}
#RequiredArgsConstructor
class HandlerStrategiesResponseContext implements ServerResponse.Context {
private final HandlerStrategies handlerStrategies;
#Override
public List<HttpMessageWriter<?>> messageWriters() {
return this.handlerStrategies.messageWriters();
}
#Override
public List<ViewResolver> viewResolvers() {
return this.handlerStrategies.viewResolvers();
}
}
#Configuration
#EnableWebFluxSecurity
public class SecurityConfig {
#Bean
public SecurityWebFilterChain securityWebFilterChain(
ServerHttpSecurity http,
ReactiveAuthenticationManager jwtAuthenticationManager,
ServerAuthenticationConverter jwtAuthenticationConverter
) {
AuthenticationWebFilter authenticationWebFilter = new AuthenticationWebFilter(jwtAuthenticationManager);
authenticationWebFilter.setServerAuthenticationConverter(jwtAuthenticationConverter);
return http
.authorizeExchange()
.pathMatchers("/auth/login", "/auth/logout").permitAll()
.anyExchange().authenticated()
.and()
.addFilterAt(authenticationWebFilter, SecurityWebFiltersOrder.AUTHENTICATION)
.httpBasic()
.disable()
.csrf()
.disable()
.formLogin()
.disable()
.logout()
.disable()
.build();
}
#Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
So when i am hitting it with an invalid JWT token in header. This got handled by my ExceptioHandler class and I got below output which is great.
But when i hit it with empty jwt token I got this.
Now i would like to return the same body which i am returning in the case of invalid JWT token. but the problem is when empty token is provided its not even falling in handle method of ExceptionHandler class. thats why its not in my control like i did for JwtException in the same class. How could i do that please help?
I sort it out myself.
webflux provides ServerAuthenticationFailureHandler to handle custom response for that but unfortunately ServerAuthenticationFailureHandler not works and its a known issue so i created a failure route and write my custom response in it and setup login page.
.formLogin()
.loginPage("/auth/failed")
.and()
.andRoute(path("/auth/failed").and(accept(MediaType.APPLICATION_JSON)), (serverRequest) ->
ServerResponse
.status(HttpStatus.UNAUTHORIZED)
.body(
Mono.just(new ErrorResponse("Unauthorized")),
ErrorResponse.class
)
);

Spring reactive security

I am trying for reactive security and the unauthenticated calls are not going to auth manager.
#Configuration
#EnableWebFluxSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig{
#Autowired
private WebAuthenticationManager authenticationManager;
#Autowired
private ServerSecurityContextRepository securityContextRepository;
private static final String[] AUTH_WHITELIST = {
"/login/**",
"/logout/**",
"/authorize/**",
"/favicon.ico",
};
#Bean
public SecurityWebFilterChain securitygWebFilterChain(ServerHttpSecurity http) {
return http.exceptionHandling().authenticationEntryPoint((swe, e) -> {
return Mono.fromRunnable(() -> {
swe.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
});
}).accessDeniedHandler((swe, e) -> {
return Mono.fromRunnable(() -> {
swe.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
});
}).and().csrf().disable()
.formLogin().disable()
.httpBasic().disable()
.authenticationManager(authenticationManager)
.securityContextRepository(NoOpServerSecurityContextRepository.getInstance())
.authorizeExchange().pathMatchers(HttpMethod.OPTIONS).permitAll()
.pathMatchers(AUTH_WHITELIST).permitAll()
.anyExchange().authenticated().and().build();
}
#Bean
public PBKDF2Encoder passwordEncoder() {
return new PBKDF2Encoder();
}
}
WebAuthentication Manager,
#Component
public class WebAuthenticationManager implements ReactiveAuthenticationManager {
#Autowired
private JWTUtil jwtUtil;
#Override
public Mono<Authentication> authenticate(Authentication authentication) {
String authToken = authentication.getCredentials().toString();
String username;
try {
username = jwtUtil.getUsernameFromToken(authToken);
} catch (Exception e) {
username = null;
}
if (username != null && jwtUtil.validateToken(authToken)) {
Claims claims = jwtUtil.getAllClaimsFromToken(authToken);
List<String> rolesMap = claims.get("role", List.class);
List<Role> roles = new ArrayList<>();
for (String rolemap : rolesMap) {
roles.add(Role.valueOf(rolemap));
}
UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(
username,
null,
roles.stream().map(authority -> new SimpleGrantedAuthority(authority.name())).collect(Collectors.toList())
);
return Mono.just(auth);
} else {
return Mono.empty();
}
}
}
Here, I have registered my WebAuthentication manager in Securityconfig. But, still the unauthenticated calls are not going to the WebAuthenticationManager.
It is expected to go to AuthenticationManager when the protected URL's are hit. For ex,
http://localhost:8080/api/v1/user.
Not sure, why the calls are not going to AuthManager.
In non reactive, we have OncePerRequestFilter and the auth is being taken care over there. Not sure, how to implement the same for reactive.
You disabled all authentication mechanisms hence there is nothing calling your authentication manager. As you mentioned, you can implement authentication flow through filters.
Sample implementation of authentication filter:
#Bean
public AuthenticationWebFilter webFilter() {
AuthenticationWebFilter authenticationWebFilter = new AuthenticationWebFilter(authenticationManager);
authenticationWebFilter.setServerAuthenticationConverter(tokenAuthenticationConverter());
authenticationWebFilter.setRequiresAuthenticationMatcher(serverWebExchangeMatcher());
authenticationWebFilter.setSecurityContextRepository(NoOpServerSecurityContextRepository.getInstance());
return authenticationWebFilter;
}
Then add this filter to ServerHttpSecurity: http.addFilterBefore(webFilter(),SecurityWebFiltersOrder.HTTP_BASIC)
Then finally your authentication manager will be called.
You must provide few additional things to make it working.
Matcher to check if Authorization header is added to request:
#Bean
public ServerWebExchangeMatcher serverWebExchangeMatcher() {
return exchange -> {
Mono<ServerHttpRequest> request = Mono.just(exchange).map(ServerWebExchange::getRequest);
return request.map(ServerHttpRequest::getHeaders)
.filter(h -> h.containsKey(HttpHeaders.AUTHORIZATION))
.flatMap($ -> ServerWebExchangeMatcher.MatchResult.match())
.switchIfEmpty(ServerWebExchangeMatcher.MatchResult.notMatch());
};
}
Token converter responsible for getting token from request and preparing basic AbstractAuthenticationToken
#Bean
public ServerAuthenticationConverter tokenAuthenticationConverter() {
return exchange -> Mono.justOrEmpty(exchange)
.map(e -> getTokenFromRequest(e))
.filter(token -> !StringUtils.isEmpty(token))
.map(token -> getAuthentication(token));
}
I intentionally omitted implementation of getTokenFromRequest and getAuthentication because there is a lot of examples available.

Resources