The following spring security config gives some unexpected behavior.
When making a request to some (non-health-check) endpoint (/user), in the browser and when using curl (via git bash on windows), an unauthenticated request returns an idp redirect as expected.
However, when using the WebTestClient, it returns 401 Unauthorized with www-authenticate: [Basic ...].
The request for basic authn in this context (and the password generated at startup) are unexpected because I've declared to disable basic authn via http.httpBasic().disable().
Why would this response come? Is there a better way to override the default basic auth configs? Is there an ordering on these configurations as suggested in this post? Where is this documented?
...env values
#Bean
public SecurityWebFilterChain webFilterChain(ServerHttpSecurity http) {
http.oauth2Client()
.and()
.oauth2Login()
.and()
.httpBasic()
.disable()
.formLogin()
.disable()
.csrf()
.disable()
.authorizeExchange()
.pathMatchers("/actuator/health").permitAll()
.anyExchange().authenticated();
return http.build();
}
#Bean
ReactiveClientRegistrationRepository getClientRegistrationRepository() {
ClientRegistration google =
ClientRegistration.withRegistrationId("google")
.scope("openid", "profile", "email")
.clientId(clientId)
.clientSecret(clientSecret)
.authorizationUri(authUri)
.tokenUri(tokenUri)
.userInfoUri(userInfoUri)
.redirectUri(redirectUri)
.jwkSetUri(jwksUri)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.userNameAttributeName("name")
.build();
return new InMemoryReactiveClientRegistrationRepository(google);
}
Project on github: https://github.com/segevmalool/spring-samples/blob/main/spring-security-webflux-postgres
httpBasic().authenticationEntryPoint(new HttpStatusServerEntryPoint(HttpStatus.UNAUTHORIZED))
Solution
Related
My Spring Webflux application provides multiple authentication methods for the APIs, the user either presents a JWT token or he presents a userid and password. I understand that each authentication method is a separate SecurityWebFilterChain. In my security config I defined 2 Beans, one for basic auth and one for JWT. Setting up each one for different endpoints works fine using a SecurityMatcher, but how do I setup both for the same endpoint. I want either basic auth or JWT token to authenticate for a specific endpoint. All my attempts result in the first authentication method failing and returning a 401 unauthorized without attempting to try the second method. How do I get it not to fail but to try the second SecurityWebFilterChain bean?
Here is the code from my security config
#Configuration
#EnableWebFluxSecurity
#EnableReactiveMethodSecurity
public class SecurityConfig {
#Autowired private SecurityContextRepository securityContextRepository;
#Bean
SecurityWebFilterChain webHttpSecurity(
ServerHttpSecurity http, BasicAuthenticationManager authenticationManager) {
http.securityMatcher(new PathPatternParserServerWebExchangeMatcher("/api/something/**"))
.authenticationManager(authenticationManager)
.authorizeExchange((exchanges) -> exchanges.anyExchange().authenticated())
.httpBasic()
.and()
.csrf()
.disable();
return http.build();
}
#Bean
SecurityWebFilterChain springWebFilterChain(
ServerHttpSecurity http, AuthenticationManager authenticationManager) {
String[] patterns =
new String[] {
"/v2/api-docs",
"/configuration/ui",
"/swagger-resources/**",
"/configuration/**",
"/swagger-ui/**",
"/swagger-ui.html",
"/v3/api-docs/**",
"/webjars/**",
};
return http.cors()
.disable()
.exceptionHandling()
.authenticationEntryPoint(
(swe, e) ->
Mono.fromRunnable(() -> swe.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED)))
.accessDeniedHandler(
(swe, e) ->
Mono.fromRunnable(() -> swe.getResponse().setStatusCode(HttpStatus.FORBIDDEN)))
.and()
.csrf()
.disable()
.authenticationManager(authenticationManager)
.securityContextRepository(securityContextRepository)
.authorizeExchange()
.pathMatchers(patterns)
.permitAll()
.pathMatchers(HttpMethod.OPTIONS)
.permitAll()
.anyExchange()
.authenticated()
.and()
.build();
}
The first Bean sets up basic auth for one specific endpoint using a custom authentication manager which veruifies the userid and password, the second bean sets up JWT auth for all other endpoints (with a custom AuthenticationManager that verifies the token etc.) except those that are excluded. Lets say I have the following endpoints
/api/something
/api/whatever
.....
endpoint 1 I want to authenticate with either basic auth or JWT
endpoint 2,3 ,n I want only JWT
As I have it now endpoint 1 is using only basicAuth and all other endpoints use JWT. How can I add JWT to endpoint 1 as well?
I'm trying to create form login with spring boot webflux. I can login and after login I'm redirectored successfully. But when I browse to a page that requires authentication, I'm getting error. If I remove the page from security config and get principal from ReactiveSecurityContextHolder I'm getting the user details.
Here is my security config:
public class SecurityConfig {
#Autowired
private UserService userService;
#Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
return http
.csrf().disable()
.authorizeExchange()
.pathMatchers("/user/account")
.authenticated()
.anyExchange().permitAll()
.and()
.formLogin()
.loginPage("/user/login")
.authenticationSuccessHandler(new RedirectServerAuthenticationSuccessHandler("/"))
.authenticationManager(reactiveAuthenticationManager())
.and()
.logout()
.and()
.build();
}
#Bean
public ReactiveAuthenticationManager reactiveAuthenticationManager() {
return authentication -> userService.loginUser(authentication)
.switchIfEmpty(Mono.error(new UsernameNotFoundException(authentication.getName())))
.map(user -> new UsernamePasswordAuthenticationToken(user, null));
}
}
Do I need to do anything else in the ReactiveAuthenticationManager? Is that even required?
In this repository : https://github.com/mohamedanouarbencheikh/dashboard-auth-microservice
you have a complete example of spring security implementation with jwt in microservice architecture using spring cloud routing (gateway) which is based on reactive programming and Netty as application server, and angular as frontend
Answering to my own question so that anyone facing same problem can get some help:
The issue was resolved when I've changed the UsernamePasswordAuthenticationToken constructor and passed the authority parameter as null. This is really ridiculous. Here is the updated code:
#Bean
public ReactiveAuthenticationManager reactiveAuthenticationManager() {
return authentication -> userService.loginUser(authentication)
.switchIfEmpty(Mono.error(new UsernameNotFoundException(authentication.getName())))
.map(user -> new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities()));
}
I've also simplified the config by removing authenticationSuccessHandler and authenticationManager from the config. Spring automatically redirects to /. For authenticationManager it automatically checks for a ReactiveAuthenticationManager bean and uses if found. Here is my updated config:
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
return http
.csrf().disable()
.authorizeExchange()
.pathMatchers("/user/account")
.authenticated()
.anyExchange().permitAll()
.and()
.formLogin()
.loginPage("/user/login")
.and()
.logout()
.logoutUrl("/user/logout")
.logoutSuccessHandler(logoutSuccessHandler("/user/bye"))
.and()
.build();
}
I am having different group of endpoints like API, login and open access.
Wants to trigger oauth only for login endpoint. 401 error for api endpoint without valid session.
I was trying to put multiple SecurityWebFilterChain definitions. But it is not working. It is giving 401 for all urls.
#Bean
#Order(1)
public SecurityWebFilterChain openAccess(ServerHttpSecurity http) {
http.authorizeExchange(
exchanges ->
exchanges
.pathMatchers("/", "/ping", "/view/home", "/actuator/**")
.permitAll())
.httpBasic()
.disable()
.formLogin()
.disable()
.csrf()
.csrfTokenRepository(csrfTokenRepository());
return http.build();
}
#Bean
#Order(2)
public SecurityWebFilterChain apiSecurity(ServerHttpSecurity http) {
http.authorizeExchange(
exchanges ->
exchanges
.pathMatchers("/api/user/")
.hasAuthority(
PRODUCT
.getAccessName()
.toLowerCase()))
.httpBasic()
.disable()
.formLogin()
.disable()
.csrf()
.csrfTokenRepository(csrfTokenRepository());
return http.build();
}
#Bean
#Order(3)
public SecurityWebFilterChain oauthAccess(ServerHttpSecurity http) {
http.authorizeExchange(
exchanges ->
exchanges
.pathMatchers("/kp/oauth2/**")
.permitAll()
.pathMatchers("/login")
.authenticated())
.httpBasic()
.disable()
.formLogin()
.disable()
.csrf()
.csrfTokenRepository(csrfTokenRepository())
.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessHandler(
logoutSuccessHandler("/logout.com/logout?service=" + casServerUrl))
.and()
.oauth2Login();
return http.build();
}
Here are the application startup logs:
https://gist.github.com/rajeevprasanna/122b4e42f048b2c07eb80f60cd423f34
When I enter URL http://localhost:8080/login in the browser, it is giving a 401 error. want it to trigger OAuth flow as per my configuration
https://gist.github.com/rajeevprasanna/aa0d586fc86cbd31e103c5c3f8a3fc06
UPDATE: i have tried modifying the code and added securityMatcher. but got into different issue with oauth authentication. issue posted
separately on so:
Spring security not redirecting to given OAuth authentication URL
The official Spring Github Repo's Readme reads:
The application is almost finished functionally. The last thing we
need to do is implement the logout feature that we sketched in the
home page. If the user is authenticated then we show a "logout" link
and hook it to a logout() function in the AppComponent. Remember, it
sends an HTTP POST to "/logout" which we now need to implement on the
server. This is straightforward because it is added for us already by
Spring Security (i.e. we don’t need to do anything for this simple use
case). For more control over the behaviour of logout you could use the
HttpSecurity callbacks in your WebSecurityAdapter to, for instance
execute some business logic after logout.
Taken from: https://github.com/spring-guides/tut-spring-security-and-angular-js/tree/master/single
However, I am using basic authentication and testing it with Postman app. The POST on '/logout' gives me a 403 Forbidden like so:
{
"timestamp": "2018-07-30T07:42:48.172+0000",
"status": 403,
"error": "Forbidden",
"message": "Forbidden",
"path": "/logout"
}
My Security Configurations are:
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/user/save")
.permitAll()
.and()
.authorizeRequests()
.antMatchers("/user/**")
.hasRole("USER")
.and()
.authorizeRequests()
.antMatchers("/admin/**")
.hasRole("ADMIN")
.and()
.httpBasic()
.and()
.logout()
.permitAll()
.and()
.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
}
I want the session to be invalidated, all the cookies to be deleted, such that when I query again on the endpoint /user with wrong credentials, I should get a 403. However, even after POST on /logout (which gives 403 anyway), the application accepts the GET on /user from the previous session and shows me the details of the user.
The endpoint is:
#GetMapping
public Principal user(Principal user){
return user;
}
I'm using Spring Security with Thymeleaf and want to create a login and a register form on different sites that make both use of CSRF protection. Protecting the login site is easy, as with the folloing WebSecurity configuration
#Override
protected void configure(final HttpSecurity http) throws Exception {
http
.formLogin()
.loginPage("/login")
.permitAll()
.and()
.requestMatchers()
.antMatchers("/login", "/oauth/authorize", "/oauth/confirm_access")
.and()
.authorizeRequests()
.anyRequest()
.authenticated();
}
Spring supports adding CSRF protection in general by the Security Filter Chain that is build in the configure method. This Filter Chain contains a CSRFFilter that adds/evaluates the CSRF token. This Filter Chain is then used for all matches defined in the above configuration. The mechanism of getting the Filters that are applied to a request can be found here in the method
doFilterInternal(ServletRequest, ServletResponse, FilterChain)
The problem is, if I add the "/register" site to this configuration, the user is redirected to the "/login" site first. If I don't add it to the above config, the mentioned FilterChain is not applied (and so not the CsrfFilter).
So what I want is to reuse the CsrfFilter in the Filter Chain of the "/register" site, but I don't know how to do that.
I'd prefer this approach to other ideas like writing a custom CSRF filter as suggested here or here.
From all of this i understood the problem is that you want people to access /register without needing to login first. Which is a simple fix:
#Override
protected void configure(final HttpSecurity http) throws Exception {
http
.formLogin()
.loginPage("/login")
.permitAll()
.and()
.requestMatchers()
// add this line
.antMatchers("/register").permitAll().and
//
.antMatchers("/login", "/oauth/authorize", "/oauth/confirm_access")
.and()
.authorizeRequests()
.anyRequest()
.authenticated();
}
Turned out that the Spring Security Filter chain is applied to all endpoints mentioned in the list provided to requestMatchers().antMatchers().
So to use CSRF protection for a site that is not the login site, I just had to add it to this list and then permit all access to it, so there is no redirect to the login page. My final config looks like this
#Override
protected void configure(final HttpSecurity http) throws Exception {
http.requestMatchers()
// consider these patterns for this config and the Security Filter Chain
.antMatchers("/login", "/register", "/oauth/authorize", "/oauth/confirm_access", "/oauth/token_key",
"/oauth/check_token", "/oauth/error")
.and()
// define authorization behaviour
.authorizeRequests()
// /register is allowed by anyone
.antMatchers("/register").permitAll()
// /oauth/authorize needs authentication; enables redirect to /login
.antMatchers("/oauth/authorize").authenticated()
// any other request in the patterns above are forbidden
.anyRequest().denyAll()
.and()
.formLogin()
// we have a custom login page at /login
// that is permitted to everyone
.loginPage("/login").permitAll();
}