Spring Boot Oauth Unautorized - spring

I'm trying to follow this tutorial in order to get oauth working.
But when I try to make the below request
curl -X POST -d "client_id=client-id&client_secret=secret&grant_type=password&username=demo&password=1234" http://localhost:8080/oauth/token
I get the following error message
{"timestamp":"2018-01-25T14:47:42.286+0000","status":401,"error":"Unauthorized","message":"Unauthorized","path":"/oauth/token"}
My AuthorizationServerConfiguration looks like the following
#Configuration
#EnableAuthorizationServer
class AuthorizationServerConfiguration : AuthorizationServerConfigurerAdapter() {
#Autowired
private val tokenStore: TokenStore? = null
#Autowired
private val userApprovalHandler: UserApprovalHandler? = null
#Autowired
#Qualifier("authenticationManagerBean")
private val authenticationManager: AuthenticationManager? = null
#Throws(Exception::class)
override fun configure(clients: ClientDetailsServiceConfigurer?) {
clients!!.inMemory()
.withClient("client-id")
.authorizedGrantTypes("password", "authorization_code", "refresh_token", "implicit")
.authorities("ROLE_CLIENT", "ROLE_TRUSTED_CLIENT")
.scopes("read", "write", "trust")
.secret("secret")
.accessTokenValiditySeconds(120)//Access token is only valid for 2 minutes.
.refreshTokenValiditySeconds(600)//Refresh token is only valid for 10 minutes.
}
#Throws(Exception::class)
override fun configure(endpoints: AuthorizationServerEndpointsConfigurer?) {
endpoints!!.tokenStore(tokenStore).userApprovalHandler(userApprovalHandler)
.authenticationManager(authenticationManager)
}
#Throws(Exception::class)
override fun configure(oauthServer: AuthorizationServerSecurityConfigurer?) {
oauthServer!!.realm(REALM + "/client")
}
companion object {
private val REALM = "MY_OAUTH_REALM"
}
}
My ResourceServerConfiguration looks like the following
#Configuration
#EnableResourceServer
class ResourceServerConfiguration : ResourceServerConfigurerAdapter() {
override fun configure(resources: ResourceServerSecurityConfigurer?) {
resources!!.resourceId(RESOURCE_ID).stateless(false)
}
#Throws(Exception::class)
override fun configure(http: HttpSecurity) {
http.anonymous().disable()
.requestMatchers().antMatchers("/users/**")
.and().authorizeRequests()
.antMatchers("/users/**").access("hasRole('ADMIN')")
.and().exceptionHandling().accessDeniedHandler(OAuth2AccessDeniedHandler())
}
companion object {
private val RESOURCE_ID = "my_rest_api"
}
}
My OAuth2SecurityConfiguration looks like the following
#Configuration
#EnableWebSecurity
class OAuth2SecurityConfiguration : WebSecurityConfigurerAdapter() {
#Autowired
private val clientDetailsService: ClientDetailsService? = null
#Autowired
#Throws(Exception::class)
fun globalUserDetails(auth: AuthenticationManagerBuilder) {
auth.inMemoryAuthentication()
.withUser("bill").password("abc123").roles("ADMIN").and()
.withUser("demo").password("1234").roles("USER")
}
#Throws(Exception::class)
override fun configure(http: HttpSecurity) {
http
.csrf().disable()
.anonymous().disable()
.authorizeRequests()
.antMatchers("/oauth/token").permitAll()
}
#Bean
#Throws(Exception::class)
override fun authenticationManagerBean(): AuthenticationManager {
return super.authenticationManagerBean()
}
#Bean
fun tokenStore(): TokenStore {
return InMemoryTokenStore()
}
#Bean
#Autowired
fun userApprovalHandler(tokenStore: TokenStore): TokenStoreUserApprovalHandler {
val handler = TokenStoreUserApprovalHandler()
handler.setTokenStore(tokenStore)
handler.setRequestFactory(DefaultOAuth2RequestFactory(clientDetailsService))
handler.setClientDetailsService(clientDetailsService)
return handler
}
#Bean
#Autowired
#Throws(Exception::class)
fun approvalStore(tokenStore: TokenStore): ApprovalStore {
val store = TokenApprovalStore()
store.setTokenStore(tokenStore)
return store
}
}
Any clue about what I'm doing wrong?

The request of token requires Basic Authentication.
Try to add the following option in your request:
-u "client-id:secret"
The request:
curl -X POST -u "client-id:secret" -d "client_id=client-id&client_secret=secret&grant_type=password&username=demo&password=1234" http://localhost:8080/oauth/token

Related

Authentication manager is null after upgrading spring security

I am writing application in spring boot 2.7.4 version which has new version of spring security. So I need to rewrite my old code to the new one.
Here is my old security configuration with WebSecurityConfigurerAdapter
#Configuration
#EnableWebSecurity
class AppWebConfig(
val customUserDetailsService: CustomUserDetailsService,
val passwordEncoder: PasswordEncoder,
val tokensService: TokensService
) : WebSecurityConfigurerAdapter() {
#Throws(Exception::class)
override fun configure(auth: AuthenticationManagerBuilder) {
auth
.userDetailsService(customUserDetailsService)
.passwordEncoder(passwordEncoder)
}
#Throws(Exception::class)
override fun configure(http: HttpSecurity) {
http
.cors()
.and()
.csrf().disable()
.exceptionHandling()
//urls permissions...
.addFilter(AppAuthorizationFilter(authenticationManager(), tokensServicee))
}
}
And here is the new code
#Configuration
#EnableWebSecurity
class AppWebConfig(
val tokensService: TokensService,
) {
#Bean
#Throws(Exception::class)
fun authenticationManager(authenticationConfiguration: AuthenticationConfiguration): AuthenticationManager? {
return authenticationConfiguration.authenticationManager
}
#Bean
#Throws(Exception::class)
protected fun fitlerChain(http: HttpSecurity): SecurityFilterChain {
val authenticationManager = http.getSharedObject(AuthenticationManager::class.java)
return http
.cors()
.and()
.csrf().disable()
//urls permissions...
.addFilter(AppAuthorizationFilter(authenticationManager, tokensService))
.build()
}
Here is the AppAuthorizationFilter which hasn't changed in both versions and where the authenticationManager is used:
class AppAuthorizationFilter(
authenticationManager: AuthenticationManager,
tokensService: TokensService,
) : BasicAuthenticationFilter(authenticationManager) {
private val tokensService: TokensService
init { this.tokensService = tokensService }
#Throws(IOException::class, ServletException::class)
override fun doFilterInternal(
request: HttpServletRequest,
response: HttpServletResponse,
chain: FilterChain,
) {
val header = request.getHeader(Objects.requireNonNull(HttpHeaders.AUTHORIZATION))
if (header != null) {
val authorizedUser = tokensService.parseAccessToken(header)
SecurityContextHolder.getContext().authentication = authorizedUser
}
chain.doFilter(request, response)
}
}
And the problem is with authenticationManager from AppWebConfig. I get the error that this its null.
Caused by: java.lang.NullPointerException: authenticationManager must not be null at com.app.security.config.WebConfig.fitlerChain(WebConfig.kt:68)
I tried the solution I showed above by getting authenticationManager from shared objects http.getSharedObject(AuthenticationManager::class.java) but it does not work as you can see.
I solved the problem by getting authenticationManager from WebApplicationContext but I am not sure if it is the best way to do it
val authenticationManager = applicationContext.getBean("authenticationManager") as AuthenticationManager
Try to add this annotation (#Primary) to you authenticationManager bean. If didn't work, then go with the solution you mentioned.
#Bean
#Primary // try to add this
#Throws(Exception::class)
fun authenticationManager(authenticationConfiguration: AuthenticationConfiguration): AuthenticationManager? {
return authenticationConfiguration.authenticationManager
}
Please access the authentication manager once you build it or after it builds.
public SecurityFilterChain securityFilterChain(HttpSecurity http, ResourceProtectAware protectAware) throws Exception {
protectAware.addUrlAuthorization(http);
http.cors();
http.httpBasic().authenticationEntryPoint(entryPoint());
// http.addFilterBefore(spnegoAuthenticationFilter(configuration.getAuthenticationManager()), BasicAuthenticationFilter.class);
DefaultSecurityFilterChain build = http.build();
//access it after you build.
AuthenticationManager sharedObject = http.getSharedObject(AuthenticationManager.class);
return build;
}

webflux http basic on specific path only while all others use JWT

I have a spring boot app that uses JWT for all endpoints. Now I want to add an /actuator endpoint that uses basic auth to enable prometheus scraping metrics.
#EnableWebFluxSecurity
#EnableReactiveMethodSecurity
class SecurityConfig(
val userService: UserService
) {
#Bean
fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain? {
return http {
csrf { disable() }
formLogin { disable() }
httpBasic { disable() }
authorizeExchange {
authorize(ServerWebExchangeMatchers.pathMatchers(HttpMethod.OPTIONS, "/**"), permitAll)
// the following should not use JWT but basic auth
authorize(ServerWebExchangeMatchers.pathMatchers("/actuator"), authenticated)
authorize(anyExchange, authenticated)
}
oauth2ResourceServer {
jwt {
jwtAuthenticationConverter = customConverter()
}
}
}
}
}
In the MVC Stack I would use something like this:
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Configuration
#Order(1)
public static class ActuatorWebSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter {
#Value("${management.endpoints.web.base-path}")
private String managementPath;
#Value("${config.actuator.user.name}")
private String actuatorUser;
#Value("${config.actuator.user.password}")
private String actuatorPassword;
#Autowired
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser(actuatorUser)
.password(passwordEncoder().encode(actuatorPassword))
.authorities("ROLE_ACTUATOR");
}
#Bean
public PasswordEncoder passwordEncoder() {
return new Argon2PasswordEncoder();
}
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher(managementPath + "/**")
.cors().and()
.csrf().disable()
.authorizeRequests()
.anyRequest()
.hasRole("ACTUATOR")
.and()
.httpBasic();
}
}
#Configuration
#Order(2)
public static class ApiWebSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.cors().and()
.csrf().disable()
.authenticationProvider(...)
.authorizeRequests()
// ...
}
}
}
How does this translate to webflux?
Adding multiple beans is possible but requires using securityMatcher:
#EnableWebFluxSecurity
#EnableReactiveMethodSecurity
class SecurityConfig(
val userService: UserService
) {
#Value("\${config.actuator.user.name}")
private val actuatorUser: String? = null
#Value("\${config.actuator.user.password}")
private val actuatorPassword: String? = null
#Bean
#Order(1)
fun springSecurityFilterChainActuator(http: ServerHttpSecurity): SecurityWebFilterChain? {
val encoder: PasswordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder()
return http {
securityMatcher(ServerWebExchangeMatchers.pathMatchers("/actuator/**"))
csrf { disable() }
formLogin { disable() }
httpBasic {
authenticationManager = UserDetailsRepositoryReactiveAuthenticationManager(
MapReactiveUserDetailsService(
User
.withUsername(actuatorUser)
.password(encoder.encode(actuatorPassword))
.roles("ACTUATOR")
.build()
)
)
}
authorizeExchange {
authorize(anyExchange, hasRole("ACTUATOR"))
}
}
}
#Bean
#Order(2)
fun springSecurityFilterChainApi(http: ServerHttpSecurity): SecurityWebFilterChain? {
return http {
securityMatcher(ServerWebExchangeMatchers.pathMatchers("/**"))
csrf { disable() }
formLogin { disable() }
httpBasic { disable() }
authorizeExchange {
authorize(ServerWebExchangeMatchers.pathMatchers(HttpMethod.OPTIONS, "/**"), permitAll)
authorize(anyExchange, authenticated)
}
oauth2ResourceServer {
jwt {
jwtAuthenticationConverter = customConverter()
}
}
}
}
}

How to add a ResourceServer to an existing spring security config

I use the following security configuration for authorization. I now want to add token secured endpoints to this project.
However, whenever I send a request to the secured endpoint I always find myself redirected to the login-page ("/oauth_login") as if I were unauthorized.
What am I doing wrong here? I tried to debug this and it seems the overriden decode() function of my accessTokenConverter is never called when I try to access the endpoint with a valid token.
This is the security config I already have:
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
#PropertySource("application.properties")
#Order(1)
class SecurityConfig(
private val userDetailsService: CustomUserDetailsService,
private val inMemoryClientRegistrationRepository: InMemoryClientRegistrationRepository,
private val secretAuthenticationFilter: SecretAuthenticationFilter
) : WebSecurityConfigurerAdapter() {
#Bean
#Throws(Exception::class)
override fun authenticationManager() = super.authenticationManager()
#Throws(Exception::class)
override fun configure(auth: AuthenticationManagerBuilder?) {
auth!!.userDetailsService(userDetailsService)
.passwordEncoder(BCryptPasswordEncoder(10))
}
#Throws(Exception::class)
override fun configure(http: HttpSecurity) {
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
http.addFilterAfter(secretAuthenticationFilter, UsernamePasswordAuthenticationFilter::class.java)
http
.authorizeRequests()
.antMatchers("/oauth_login")
.permitAll()
.antMatchers("/accounts/password")
.permitAll()
.anyRequest()
.authenticated()
.permitAll()
.and()
.logout()
.logoutUrl("/logoutconfirm")
.logoutSuccessUrl("/oauth_login")
.invalidateHttpSession(true)
.and()
.oauth2Login().loginPage("/oauth_login")
.authorizationEndpoint()
.baseUri("/oauth2/authorize-client")
.authorizationRequestRepository(authorizationRequestRepository())
.authorizationRequestResolver(CustomAuthorizationRequestResolver(inMemoryClientRegistrationRepository, "/oauth2/authorize-client"))
.and()
.tokenEndpoint()
.accessTokenResponseClient(accessTokenResponseClient())
.and()
.defaultSuccessUrl("/loginSuccess")
.failureUrl("/loginFailure")
.addObjectPostProcessor(object : ObjectPostProcessor<Any> {
override fun <O : Any> postProcess(obj: O) = when (obj) {
is OAuth2LoginAuthenticationProvider -> CustomOAuth2LoginAuthenticationProvider(obj) as O
is LoginUrlAuthenticationEntryPoint -> customizeLoginUrlAuthenticationEntryPoint(obj) as O
else -> obj
}
})
}
This is the ResourceServerConfig I want to add:
#Configuration
#EnableResourceServer
#Order(2)
class ResourceServerConfig(
private val defaultTokenServices: DefaultTokenServices
) : ResourceServerConfigurerAdapter() {
override fun configure(config: ResourceServerSecurityConfigurer) {
config.tokenServices(tokenServices())
}
override fun configure(http: HttpSecurity) {
http.authorizeRequests().anyRequest().authenticated()
}
fun tokenStore() = JwtTokenStore(accessTokenConverter())
fun accessTokenConverter(): JwtAccessTokenConverter {
val converter = object : JwtAccessTokenConverter() {
override fun decode(token: String) = if (token.isCorrectJwtToken(token)) {
super.decode(token)
} else {
mapOf()
}
}
val keyStoreKeyFactory = KeyStoreKeyFactory(ClassPathResource("mykeys.jks"),
"mykeys".toCharArray())
converter.setKeyPair(keyStoreKeyFactory.getKeyPair("mykeys"))
return converter
}
fun tokenServices() = DefaultTokenServices().apply {
setTokenStore(tokenStore())
setSupportRefreshToken(true)
}
This is the secured endpoint I want to be able to access with a valid token:
#PreAuthorize("hasAuthority('ROLE_USER')")
#PostMapping("/accounts/password")
fun updatePassword(#RequestBody newPassword: JsonWrappedValue<String>): Boolean {
// Update password
}
I found a solution to my problem.
I merged all configs into a single file so the security config looks like that
#EnableAuthorizationServer
#EnableResourceServer
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
#PropertySource("application.properties")
class SecurityConfig(
private val accountRepository: AccountRepository,
private val userDetailsService: CustomUserDetailsService,
private val inMemoryClientRegistrationRepository: InMemoryClientRegistrationRepository,
private val secretAuthenticationFilter: SecretAuthenticationFilter
) : AuthorizationServerConfigurer, ResourceServerConfigurer, WebSecurityConfigurerAdapter()
This made me able to finally reach my endpoint with a token but destroyed my social login.
Then I had to fix the configure(http: HttpSecurity) method as the ResourceServerConfig's default implementation sets the SessionCreationPolicy to SessionCreationPolicy.Never.
This caused my social login to get destroyed because the request parameters containing redirectUri and so on could not be restored. After adding
http.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.ALWAYS)
to configure(http: HTTPSecurity) the complete configuration works well.

Roles Hierarchy in Spring Webflux Security

I have implemented Webflux security by implementing:
ReactiveUserDetailsService
ReactiveAuthenticationManager
ServerSecurityContextRepository
Now, I am trying to introduce RoleHierarchy following the docs here: Role Hierarchy Docs
I have a user with role USER but he is getting 403 Denied on hitting a controller annotated with GUEST role. Role hierarchy is: "ROLE_ADMIN > ROLE_USER ROLE_USER > ROLE_GUEST"
#Configuration
#EnableWebFluxSecurity
#EnableReactiveMethodSecurity
public class SecurityConfig {
private final DaoAuthenticationManager reactiveAuthenticationManager;
private final SecurityContextRepository securityContextRepository;
private static final String ROLE_HIERARCHIES = "ROLE_ADMIN > ROLE_USER ROLE_USER > ROLE_GUEST";
#Autowired
public SecurityConfig(DaoAuthenticationManager reactiveAuthenticationManager,
SecurityContextRepository securityContextRepository) {
this.reactiveAuthenticationManager = reactiveAuthenticationManager;
this.securityContextRepository = securityContextRepository;
}
#Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
return http
.csrf().disable()
.formLogin().disable()
.httpBasic().disable()
.authenticationManager(reactiveAuthenticationManager)
.securityContextRepository(securityContextRepository)
.authorizeExchange()
.anyExchange().permitAll()
.and()
.logout().disable()
.build();
}
#Bean(name = "roleHierarchy")
public RoleHierarchy roleHierarchy() {
RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
roleHierarchy.setHierarchy(ROLE_HIERARCHIES);
return roleHierarchy;
}
#Bean(name = "roleVoter")
public RoleVoter roleVoter() {
return new RoleHierarchyVoter(roleHierarchy());
}
}
#Component
public class DaoAuthenticationManager implements ReactiveAuthenticationManager {
private final DaoUserDetailsService userDetailsService;
private final Scheduler scheduler;
#Autowired
public DaoAuthenticationManager(DaoUserDetailsService userDetailsService,
Scheduler scheduler) {
Assert.notNull(userDetailsService, "userDetailsService cannot be null");
this.userDetailsService = userDetailsService;
this.scheduler = scheduler;
}
#Override
public Mono<Authentication> authenticate(Authentication authentication) {
final String username = authentication.getName();
return this.userDetailsService.findByUsername(username)
.publishOn(this.scheduler)
.switchIfEmpty(
Mono.defer(() -> Mono.error(new UsernameNotFoundException("Invalid Username"))))
.map(u -> new UsernamePasswordAuthenticationToken(u, u.getPassword(),
u.getAuthorities()));
}
}
#Component
public class SecurityContextRepository implements ServerSecurityContextRepository {
private final DaoAuthenticationManager authenticationManager;
#Autowired
public SecurityContextRepository(DaoAuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
#Override
public Mono<Void> save(ServerWebExchange swe, SecurityContext sc) {
throw new UnsupportedOperationException("Not supported yet.");
}
#Override
public Mono<SecurityContext> load(ServerWebExchange swe) {
ServerHttpRequest request = swe.getRequest();
if (request.getHeaders().containsKey("userName") &&
!Objects.requireNonNull(request.getHeaders().get("userName")).isEmpty()) {
String userName = Objects.requireNonNull(swe
.getRequest()
.getHeaders()
.get("userName")).get(0);
Authentication auth = new UsernamePasswordAuthenticationToken(userName,
Security.PASSWORD);
return this.authenticationManager.authenticate(auth).map(SecurityContextImpl::new);
} else {
return Mono.empty();
}
}
}
Anyway to get the role hierarchy thing working in Webflux security.
EDIT
Controller:
#GetMapping
#PreAuthorize("hasRole('USER')")
public Mono<Device> getDevice(#RequestParam String uuid) {
return deviceService.getDevice(uuid);
}
Normal role authorization is working for me, whats not working is the hierarchy part.
Here a very naive solution by overriding DefaultMethodSecurityExpressionHandler.
I supposed you annotated your controller with this king of expression : #PreAuthorize("hasRole('ROLE_USER')")
securityConfig.java
#Configuration
#EnableWebFluxSecurity
#EnableReactiveMethodSecurity
public class SecurityConfig {
private final DaoAuthenticationManager reactiveAuthenticationManager;
private final SecurityContextRepository securityContextRepository;
private static final String ROLE_HIERARCHY = "ROLE_ADMIN > ROLE_USER ROLE_USER > ROLE_GUEST";
#Autowired
public SecurityConfig(DaoAuthenticationManager reactiveAuthenticationManager,
SecurityContextRepository securityContextRepository) {
this.reactiveAuthenticationManager = reactiveAuthenticationManager;
this.securityContextRepository = securityContextRepository;
}
#Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
return http
.csrf().disable()
.formLogin().disable()
.httpBasic().disable()
.authenticationManager(reactiveAuthenticationManager)
.securityContextRepository(securityContextRepository)
.authorizeExchange()
.anyExchange().permitAll()
.and()
.logout().disable()
.build();
}
#Bean
public RoleHierarchy roleHierarchy() {
RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
roleHierarchy.setHierarchy(ROLE_HIERARCHY);
return roleHierarchy;
}
// Overriding spring default bean
#Bean
public DefaultMethodSecurityExpressionHandler methodSecurityExpressionHandler(RoleHierarchy roleHierarchy) {
DefaultMethodSecurityExpressionHandler handler = new DefaultMethodSecurityExpressionHandler();
handler.setRoleHierarchy(roleHierarchy);
return handler;
}
}
Then you have to authorize spring bean overriding by modifying your application property file:
application.properties
spring.main.allow-bean-definition-overriding=true
Sources : issue 1 issue role hierarchy doc
Going a little bit further... This part can be optimized and cleaner.
Using url patterns setup from ServerHttpSecurity object.
Note that the following setup won't use role hierarchy :
#Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
return http
.csrf().disable()
.formLogin().disable()
.httpBasic().disable()
.authenticationManager(reactiveAuthenticationManager)
.securityContextRepository(securityContextRepository)
.authorizeExchange()
.pathMatchers("/user/**").hasRole("ROLE_USER") // This won't use role hierarchy because it will use implemention of hasRole defined in your 'reactiveAuthenticationManager'
.anyExchange().permitAll()
.and()
.logout().disable()
.build();
}
A solution could be to create your own implementation of ReactiveAuthorizationManager and overriding check method in order to call access(...) from your http object (ServerHttpSecurity). Ie :
public class CustomReactiveAuthorizationManager<T> implements ReactiveAuthorizationManager<T> {
private final static Logger logger = LoggerFactory.getLogger(CustomReactiveAuthorizationManager.class);
private final RoleHierarchyVoter roleHierarchyVoter;
private final String authority;
CustomReactiveAuthorizationManager(String role, RoleHierarchy roleHierarchy) {
this.authority = ROLE_PREFIX + role;
this.roleHierarchyVoter = new RoleHierarchyVoter(roleHierarchy);
}
#Override
public Mono<AuthorizationDecision> check(Mono<Authentication> authentication, T object) {
return authentication
.map(a -> {
ConfigAttribute ca = (ConfigAttribute) () -> authority;
int voteResult = roleHierarchyVoter.vote(a, object, Collections.singletonList(ca));
boolean isAuthorized = voteResult == AccessDecisionVoter.ACCESS_GRANTED;
return new AuthorizationDecision(isAuthorized);
})
.defaultIfEmpty(new AuthorizationDecision(false))
.doOnError(error -> logger.error("An error occured voting decision", error));
}
}
and then calling access method :
#Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http, RoleHierarchy roleHierarchy() {
return http
.csrf().disable()
.formLogin().disable()
.httpBasic().disable()
.authenticationManager(reactiveAuthenticationManager)
.securityContextRepository(securityContextRepository)
.authorizeExchange()
.pathMatchers("/user/**").access(new CustomReactiveAuthorizationManager<>("USER", roleHierarchy))
.anyExchange().permitAll()
.and()
.logout().disable()
.build();
}
One way I was able to achieve role hierarchy in Webflux was by creating custom annotations.
#Target(ElementType.METHOD)
#Retention(RetentionPolicy.RUNTIME)
#PreAuthorize("hasRole('ADMIN')")
public #interface IsAdmin {
}
#Target(ElementType.METHOD)
#Retention(RetentionPolicy.RUNTIME)
#PreAuthorize("hasAnyRole('ADMIN', 'USER')")
public #interface IsUser {
}
#Target(ElementType.METHOD)
#Retention(RetentionPolicy.RUNTIME)
#PreAuthorize("hasAnyRole('ADMIN', 'USER', 'GUEST')")
public #interface IsGuest {
}
–––––––––––––––––
And annotating the controllers like this:
#GetMapping
#IsUser
public Mono<Device> getDevice(#RequestParam String uuid) {
return deviceService.getDevice(uuid);
}
#PostMapping
#IsAdmin
#ResponseStatus(HttpStatus.CREATED)
public Mono<Device> createDevice(#Valid #RequestBody Device device) {
return deviceService.createDevice(device);
}

Spring WebMvcTest with post returns 403

I'm wondering where the issue is with my code, every time I run a post test (irrespective of what controller it targets, or method), I return a 403 error, when in some cases I expect a 401, and in others a 200 response (with auth).
This is a snippet from my controller:
#RestController
#CrossOrigin("*")
#RequestMapping("/user")
class UserController #Autowired constructor(val userRepository: UserRepository) {
#PostMapping("/create")
fun addUser(#RequestBody user: User): ResponseEntity<User> {
return ResponseEntity.ok(userRepository.save(user))
}
}
And my unit test targeting this controller
#RunWith(SpringRunner::class)
#WebMvcTest(UserController::class)
class UserControllerTests {
#Autowired
val mvc: MockMvc? = null
#MockBean
val repository: UserRepository? = null
val userCollection = mutableListOf<BioRiskUser>()
#Test
fun testAddUserNoAuth() {
val user = BioRiskUser(
0L,
"user",
"password",
mutableListOf(Role(
0L,
"administrator"
)))
repository!!
`when`(repository.save(user)).thenReturn(createUser(user))
mvc!!
mvc.perform(post("/create"))
.andExpect(status().isUnauthorized)
}
private fun createUser(user: BioRiskUser): BioRiskUser? {
user.id=userCollection.count().toLong()
userCollection.add(user)
return user
}
}
What am I missing?
As requested, my security config...
#Configuration
#EnableWebSecurity
class SecurityConfig(private val userRepository: UserRepository, private val userDetailsService: UserDetailsService) : WebSecurityConfigurerAdapter() {
#Bean
override fun authenticationManagerBean(): AuthenticationManager {
return super.authenticationManagerBean()
}
override fun configure(auth: AuthenticationManagerBuilder) {
auth.authenticationProvider(authProvider())
}
override fun configure(http: HttpSecurity) {
http
.csrf().disable()
.cors()
.and()
.httpBasic()
.realmName("App Realm")
.and()
.authorizeRequests()
.antMatchers("/img/*", "/error", "/favicon.ico", "/doc")
.anonymous()
.anyRequest().authenticated()
.and()
.logout()
.invalidateHttpSession(true)
.clearAuthentication(true)
.logoutSuccessUrl("/user")
.permitAll()
}
#Bean
fun authProvider(): DaoAuthenticationProvider {
val authProvider = CustomAuthProvider(userRepository)
authProvider.setUserDetailsService(userDetailsService)
authProvider.setPasswordEncoder(encoder())
return authProvider
}
}
and the auth provider
class CustomAuthProvider constructor(val userRepository: UserRepository) : DaoAuthenticationProvider() {
override fun authenticate(authentication: Authentication?): Authentication {
authentication!!
val user = userRepository.findByUsername(authentication.name)
if (!user.isPresent) {
throw BadCredentialsException("Invalid username or password")
}
val result = super.authenticate(authentication)
return UsernamePasswordAuthenticationToken(user, result.credentials, result.authorities)
}
override fun supports(authentication: Class<*>?): Boolean {
return authentication?.equals(UsernamePasswordAuthenticationToken::class.java) ?: false
}
}
In my case, the csrf-Protection seems to be still active in my WebMvcTest (even if disabled in your configuration).
So to workaround this, I simply changed my WebMvcTest to something like:
#Test
public void testFoo() throws Exception {
MvcResult result = mvc.perform(
post("/foo").with(csrf()))
.andExpect(status().isOk())
.andReturn();
// ...
}
So the missing .with(csrf()) was the problem in my case.
You need to add #ContextConfiguration(classes=SecurityConfig.class) to the top of your UserControllerTests class after the #WebMvcTest(UserController::class) annotation.
Your problem comes from the CSRF, if you enable debug logging the problem will become obvious, and it comes from the fact that #WebMvcTest load only the web layer and not the whole context, your KeycloakWebSecurityConfigurerAdapter is not loaded.
The loaded config comes from org.springframework.boot.autoconfigure.security.servlet.DefaultConfigurerAdapter (= to org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter
WebSecurityConfigurerAdapter contains crsf().
As of today you have 3 options to resolve this:
Options 1
Create a WebSecurityConfigurerAdapter inside your test class.
The solution suits you if you have only few #WebMvcTest annotated class in your project.
#ExtendWith(SpringExtension.class)
#WebMvcTest(controllers = {MyController.class})
public class MyControllerTest {
#TestConfiguration
static class DefaultConfigWithoutCsrf extends WebSecurityConfigurerAdapter {
#Override
protected void configure(final HttpSecurity http) throws Exception {
super.configure(http);
http.csrf().disable();
}
}
...
}
Options 2
Create a WebSecurityConfigurerAdapter inside a superclass and make your test extend from it.
The solution suits you if you have multiple #WebMvcTest annotated class in your project.
#Import(WebMvcTestWithoutCsrf.DefaultConfigWithoutCsrf.class)
public interface WebMvcCsrfDisabler {
static class DefaultConfigWithoutCsrf extends WebSecurityConfigurerAdapter {
#Override
protected void configure(final HttpSecurity http) throws Exception {
super.configure(http);
http.csrf().disable();
}
}
}
#ExtendWith(SpringExtension.class)
#WebMvcTest(controllers = {MyControllerTest .class})
public class MyControllerTest implements WebMvcCsrfDisabler {
...
}
Options 3
Use the spring-security csrf SecurityMockMvcRequestPostProcessors.
This solution is bulky and prone to error, checking for permission denial and forgeting the with(csrf()) will result in false positive test.
#ExtendWith(SpringExtension.class)
#WebMvcTest(controllers = {MyController.class})
public class MyControllerTest {
...
#Test
public void myTest() {
mvc.perform(post("/path")
.with(csrf()) // <=== THIS IS THE PART THAT FIX CSRF ISSUE
.content(...)
)
.andExpect(...);
}
}
Here's an issue:
override fun configure(http: HttpSecurity) {
http
.csrf().disable()
.cors()
.and()
.httpBasic()
.realmName("App Realm")
.and()
.authorizeRequests()
.antMatchers("/img/*", "/error", "/favicon.ico", "/doc")
.anonymous()
.anyRequest().authenticated()
.and()
.logout()
.invalidateHttpSession(true)
.clearAuthentication(true)
.logoutSuccessUrl("/user")
.permitAll()
}
More particularly here:
.anyRequest().authenticated()
You're requiring for each request to be authenticated, therefore you get 403.
This tutorial explains well how to perform testing with mock user.
The easy way is to have something like this:
#RunWith(SpringRunner.class)
#SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class SecuredControllerRestTemplateIntegrationTest {
#Autowired
private val template: TestRestTemplate
#Test
fun createUser(): Unit {
val result = template.withBasicAuth("username", "password")
.postForObject("/user/create", HttpEntity(User(...)), User.class)
assertEquals(HttpStatus.OK, result.getStatusCode())
}
}

Resources