Exclude custom urls from ant matchers in Spring Security [duplicate] - java

I have an app that uses both Basic Auth and OAuth2.
Some URLs are authorized using Basic Auth and "/api/**" is authorized using OAuth2.
Currently, I have two Java config files (WebSecurityConfigurerAdapter and ResourceServerConfigurerAdapter)
Each of the config files define a public void configure(HttpSecurity http) method.
The trouble I'm having is that I need an elegant way to tell my app whether to use basic auth or oauth2 given the url request.
Currently I'm using requestMatchers to make this happen:
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter
{
#Override
protected void configure(HttpSecurity http) throws Exception
{
http
.csrf().disable()
.requestMatchers()
.antMatchers("/*", "/login/**", "/reviews/**")
.and()
.authorizeRequests()
.antMatchers("/*").permitAll()
.antMatchers("/js/**").permitAll()
.antMatchers("/img/**").permitAll()
.formLogin()
.loginPage("/login")
.successHandler(loginSuccessPostHandler)
.permitAll()
.and()
.logout()
.logoutSuccessUrl("/").permitAll()
.and()
.apply(getSpringSocialConfigurer());
}
}
#Configuration
public class OAuth2ServerConfig
{
#Configuration
#EnableResourceServer
protected static class Oauth2ServerConfig extends ResourceServerConfigurerAdapter
{
#Override
public void configure(HttpSecurity http) throws Exception
{
http.httpBasic().disable();
http.csrf().disable();
http.requestMatchers()
.antMatchers("/api/**")
.and()
.authorizeRequests()
.antMatchers("/api/v1/**").access("#oauth2.hasScope('read')");
}
}
}
The problem is that every time I add a new URL that's NOT "/api/**", I'll need to add it into my WebSecurityConfig's requestMatcher section... this could lead to silly bugs in the future.
Is there a way to have a requestMatcher search based on a negative lookahead regex? I tried this using the regex: ^(?!/api) but since it doesn't actually return a MATCH and only returns a "find == true", it doesn't seem to get the job done.
Any thoughts / suggestions?

You can use NegatedRequestMatcher:
A RequestMatcher that will negate the RequestMatcher passed in. For example, if the RequestMatcher passed in returns true, NegatedRequestMatcher will return false. If the RequestMatcher passed in returns false, NegatedRequestMatcher will return true.

You should use Order(...) annotation on #Configuration classes. Make your OAuth2ServerConfig config first and serving only http.requestMatchers().antMatchers("/api/**") and make your WebSecurityConfig second (#Order(2)) without http.requestMatchers() to serve all rest URLs!
See details on https://stackoverflow.com/a/44871933/173149

Related

InvalidTokenException for "unsecured" routes in Spring Security

Since I have updated Spring Boot Version to 2.6.7 I get logs that the way I define unsecured routes is not recommended anymore.
Log message:
You are asking Spring Security to ignore Ant [pattern='/actuator/**']. This is not recommended -- please use permitAll via HttpSecurity#authorizeHttpRequests instead.
The way I was describing the config that Spring Security has to ignore these patterns was done by defining a WebSecurityConfiguration and ignoring those routes. What happens in this case is, that the whole security chain is skipped and the above mentioned logs are written. It's okay for me, but not for Spring ;).
#Configuration
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
public void configure(WebSecurity web) throws Exception {
web.ignoring()
.antMatchers(
"/actuator/**"
);
}
}
When defining these route as part of the httpSecurity as mentioned in the log. The problem occurs that an expired/invalid token cause an error (401 unauthorized) as well for unsecured routes like /actuator/health.
#Configuration
#EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
#Getter
private String[] unsecuredPathList;
#PostConstruct
public void postConstruct() {
this.unsecuredPathList = new String[] {
"/actuator/**"};
}
#Bean
public JwtTokenStore jwtTokenStore() {
return new JwtTokenStore(new CustomJwtAccessTokenConverter(true));
}
#Override
public void configure(ResourceServerSecurityConfigurer resourceServer) throws Exception {
resourceServer.tokenStore(jwtTokenStore());
resourceServer.tokenExtractor(new SessionCookieTokenExtractor());
}
#Override
public void configure(HttpSecurity http) throws Exception {
http
.anonymous()
.authorities("ANONYMOUS")
.and()
.authorizeRequests()
.antMatchers(unsecuredPathList)
.permitAll()
.and()
.authorizeRequests()
.anyRequest()
.authenticated();
}
}
The goal I want to reach is:
For an unsecure resource the token will not be evaluated/the result is ignored and there is no 401-Unauthorized-Error.
Is there anything I can do in httpSecurity?
Or is there any other recommended way to reach that goal?
Thanks in advance for your help.
There is a group of methods in HttpSecurity class, which allows you to apply defined security rules only to specific paths, and thus create different security filter chains with different rules for different urls.
For example, you can exclude some urls like this:
// convert your String array into a List of RequestMatcher
List<RequestMatcher> excludedPathMatchers = Arrays.stream(unsecuredPathList)
.map(AntPathRequestMatcher::new)
.collect(Collectors.toList());
// configure HttpSecurity to apply filter chain only on paths, that don't match any of the excluded paths
http.requestMatcher(new NegatedRequestMatcher(new OrRequestMatcher(excludedPathMatchers)));
Or you can write something like this, if you have only 1 unsecured endpoint:
http.requestMatcher(new NegatedRequestMatcher(new AntPathRequestMatcher("/some_url/**")));

jhipster/spring keycloak integration - setting custom redirect_uri

I have a spring / jhipster application, with a button to login to keycloak. When I click the login button, it brings me to keycloak, and the URL would look something like
https://keycloak.mydomain.com/auth/realms/my-realm/protocol/openid-connect/auth?response_type=code&client_id=myclient&redirect_uri=http://localhost:8080/login/oauth2/code/oidc
So in the above, I want to change the redirect_uri=http://localhost:8080/login/oauth2/code/oidc to something else.
So I added the below code in src/main/java/../config/AuthorizationServerConfig.java:
#Configuration
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
#Bean
PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients
.inMemory()
.withClient("myclient")
.secret(passwordEncoder().encode("mysecret"))
.scopes("resource:read")
.authorizedGrantTypes("authorization_code")
.redirectUris("https://new-url-I-want.com");
}
}
But even with the above change, the redirect_uri doesn't change at all. Anyone know why this is?
And more information on why I made those changes:
With OIDC's authorization code flow, the service provider (in this case my website) provides the identity provider (Keycloak) with the URI to redirect the user back to after successful authentication. However, per the Spring docs, "With the default configuration, while the Authorization Code Flow is technically allowed, it is not completely configured":
https://docs.spring.io/spring-security-oauth2-boot/docs/current/reference/html5/#oauth2-boot-authorization-server-authorization-code-grant
The docs go on to state that "OAuth2 Boot does not support configuring a redirect URI as a property — say, alongside client-id and client-secret." so,
"To add a redirect URI, you need to specify the client by using either InMemoryClientDetailsService or JdbcClientDetailsService":
https://docs.spring.io/spring-security-oauth2-boot/docs/current/reference/html5/#registering-a-redirect-uri-with-the-client
add the follwing lines inside the method SecurityConfiguration.configure
.and()
.oauth2Login()
.defaultSuccessUrl("/url_to_redirect")
That is:
public class SecurityConfiguration extends WebSecurityConfigurerAdapter { ...
#Override
public void configure(WebSecurity web) { ... }
#Override
public void configure(HttpSecurity http) throws Exception {
...
.and()
.authorizeRequests()
.antMatchers("/api/auth-info").permitAll()
.antMatchers("/api/**").authenticated()
.antMatchers("/management/health").permitAll()
.antMatchers("/management/info").permitAll()
.antMatchers("/management/prometheus").permitAll()
.antMatchers("/management/**").hasAuthority(AuthoritiesConstants.ADMIN)
.and()
.oauth2Login()
.defaultSuccessUrl("/url_to_redirect") // <--- HERE
}
You can add another redirect uri by adding the following property and src/main/resources/config/application.yml
security:
oauth2:
client:
provider:
oidc:
issuer-uri: http://localhost:9080/auth/realms/jhipster
registration:
oidc:
client-id: web_app
client-secret: web_app
redirect-uri: http://localhost:8080/my-custom-redirect
Furthermore you have to configure the custom redirect uri for the oauth2 login in SecurityConfiguration as follows
...
.and()
.oauth2Login()
.redirectionEndpoint()
.baseUri("/my-custom-redirect")
.and()
.and()
.oauth2ResourceServer()
...

How to manage session with spring security based on request type?

I would like to configure web security layer based on my request type.
If the request starts with /rest then it should use Basic authentication with stateless session management and for login authentication then it should use CSRF with stateful session management.
I have tried below code.
#Override
protected void configure(HttpSecurity http) throws Exception
{
http
.authorizeRequests()
.antMatchers("/rest/**").hasRole("SUPER_ADMIN")
.anyRequest().fullyAuthenticated()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.httpBasic()
.authenticationEntryPoint(authenticationEntryPoint)
.and()
.formLogin().and().logout().permitAll();
}
It works with basic authentication but it doesn't work for the login request because the session is not stateful. Can anyone please help me to configure Spring security. I am new to Spring security.
You need
1. Rest API's to be authenticated by basic authentication
2. Your web application be authenticated by form login.
And authorization is other part in both cases that you can set it as per your requirement.
Let me explain what was wrong with your approach. By your approach you can achieve only one authentication entry point from one configuration. i.e, you can't achieve multiple authentication entry point.
Now coming to your first requirement of achieving multiple authentication entry point.
1. For Rest API resources -- authentication by HttpBasicAuthentication for antMatcher /rest/**
2. For WebApp resources -- authentication by Form Login for antMatcher other than /rest/**
To achieve this
1. You need to have implementation of WebSecurityConfigurerAdapter of different configuration order and different antMatcher patterns.
2. Order of each configuration is important.
- wildcard pattern(/**) should be placed last order
- non wildcard pattern or restricted pattern(/rest/**) should be placed first order
3. As those configuration classes are static and inner classes for a class which is annotated #EnableWebSecurity you should be careful while defining bean using #bean and autowiring using #Autowired.
Note:
Most of people makes mistake by not defining antmather for authorizeRequest()
If first configuration #Order(1) class is configured as below
http.authorizeRequests()
2nd configuration will become dead configuration because
http.authorizeRequests() => http.antMatcher("/**").authorizeRequests()
And all URL's will be configured only for first configuration only.
Refer code given below for better understanding.
#Configuration
#EnableWebSecurity
public class SpringSecurityConfiguration
{
#Bean
public PasswordEncoder passwordEncoder()
{
return new BCryptPasswordEncoder();
}
#Configuration
#Order(1)
public static class BasicAuthSecurityConfig extends WebSecurityConfigurerAdapter
{
#Autowired
private PasswordEncoder passwordEncoder;
#Autowired
public void configureInMemoryAuthentication(AuthenticationManagerBuilder auth) throws Exception
{
auth.inMemoryAuthentication()
.withUser("superadmin")
.password(passwordEncoder.encode("superadmin#123#"))
.roles("SUPER_ADMIN");
}
#Override
protected void configure(HttpSecurity http) throws Exception
{
http.csrf().disable()
.antMatcher("/rest/**")
.authorizeRequests()
.antMatchers("/rest/**").hasRole("SUPER_ADMIN")
.and().httpBasic();
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
}
#Configuration
#Order(2)
public static class LoginFormSecurityConfig extends WebSecurityConfigurerAdapter
{
#Autowired
private PasswordEncoder passwordEncoder;
#Autowired
public void configureInMemoryAuthentication(AuthenticationManagerBuilder auth) throws Exception
{
auth.inMemoryAuthentication()
.withUser("user")
.password(passwordEncoder.encode("user#123#"))
.roles("USER");
}
#Override
protected void configure(HttpSecurity http) throws Exception
{
http
.antMatcher("/**") //wild card i.e, allow all (But already /rest/** is filtered by 1st config)
.authorizeRequests()
.antMatchers("/resources/**").permitAll()
.antMatchers("/**").authenticated()
.and().formLogin()
.defaultSuccessUrl("/app/user/dashboard")
.and().exceptionHandling()
.accessDeniedPage("/403")
.and().logout()
.invalidateHttpSession(true);
http.sessionManagement().maximumSessions(1).expiredUrl("/login?expired");
}
}
}
This question has requirement of different sets of URL's(/rest/** and other than /rest/**) for different authentication filters. Here user's (for both basic auth and form login) may be authenticated against a single table (say user_details) or multiple tables (say api_users and web_users)
If you have requirement like there is no different set of URL's but two sets of users say customer and employees(staff) both are accessing same application but they needs to be authenticated against different tables(say users and customer table) in that case refer my another answer Spring Security user authentication against customers and employee table
You have to allow users to access login page without authentication and same you can do with static pages. See below configuration.
#Configuration
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/", "/home").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.permitAll()
.and()
.logout()
.permitAll();
}
#Bean
#Override
public UserDetailsService userDetailsService() {
UserDetails user =
User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("USER")
.build();
return new InMemoryUserDetailsManager(user);
}
}

Spring security antMatcher does not work

EDIT:
I further drilled down the problem and turns out issue persists even with single configuration. If I use single configuration and keep
http.antMatcher("/api/test/**")
urls don't get secured.
Removing the antMatcher and antMatchers immediately secures the url.
i.e if I use:
http.httpBasic()
.and()
.authorizeRequests()
.anyRequest()
.authenticated();
then only spring security is securing url. Why isn't antMatcher functioning?
(Updated the title to include actual issue.)
Original Post:
I have referred following stackoverflow questions:
Spring REST security - Secure different URLs differently
Using multiple WebSecurityConfigurerAdapter with different AuthenticationProviders (basic auth for API and LDAP for web app)
and spring security doc:
https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#multiple-httpsecurity
But I am not able to configure multiple http security elements.
When I follow the official spring doc, it works in my case only becuase of the fact that the second http security element is a catch-all, but as soon as I add a specific url, all the urls can be accessed without any authentication.
Here's my code:
#EnableWebSecurity
#Configuration
public class SecurityConfig {
#Bean
public UserDetailsService userDetailsService() throws Exception {
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withUsername("user").password("userPass").roles("USER").build());
manager.createUser(User.withUsername("admin").password("adminPass").roles("ADMIN").build());
return manager;
}
#Configuration
#Order(1)
public static class ApiWebSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter {
#Override
public void configure(AuthenticationManagerBuilder auth)
throws Exception {
auth.inMemoryAuthentication().withUser("user").password("user").roles("USER");
auth.inMemoryAuthentication().withUser("admin").password("admin").roles("ADMIN");
}
protected void configure(HttpSecurity http) throws Exception {
http
.antMatcher("/api/v1/**")
.authorizeRequests()
.antMatchers("/api/v1/**").authenticated()
.and()
.httpBasic();
}
}
#Configuration
#Order(2)
public static class FormLoginWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {
#Override
public void configure(AuthenticationManagerBuilder auth)
throws Exception {
auth.inMemoryAuthentication().withUser("user1").password("user").roles("USER");
auth.inMemoryAuthentication().withUser("admin1").password("admin").roles("ADMIN");
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.antMatcher("/api/test/**")
.authorizeRequests()
.antMatchers("/api/test/**").authenticated()
.and()
.formLogin();
}
}
}
Now any url can be accessed. If I remove antMatcher from second configuration, all the urls become secured.
The pattern must not contain the context path, see AntPathRequestMatcher:
Matcher which compares a pre-defined ant-style pattern against the URL ( servletPath + pathInfo) of an HttpServletRequest.
and HttpServletRequest.html#getServletPath:
Returns the part of this request's URL that calls the servlet. This path starts with a "/" character and includes either the servlet name or a path to the servlet, but does not include any extra path information or a query string. Same as the value of the CGI variable SCRIPT_NAME.
and HttpServletRequest.html#getContextPath:
Returns the portion of the request URI that indicates the context of the request. The context path always comes first in a request URI. The path starts with a "/" character but does not end with a "/" character. For servlets in the default (root) context, this method returns "". The container does not decode this string.
Your modified and simplified code:
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.antMatcher("/test/**")
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin();
}

Spring Security session/xsrf configuration by path

I have an existing Web application that uses spring security for authentication. It is also using session management to allow the user to be logged in for a predefined period of time, and XSRF tokens to prevent XSS attacks.
#Override
protected void configure(HttpSecurity http) throws Exception {
// #formatter:off
http
.exceptionHandling()
.authenticationEntryPoint(restEntryPoint())
.and()
.headers().addHeaderWriter(new StaticHeadersWriter("Server",""))
.and()
.httpBasic()
.authenticationEntryPoint(restEntryPoint())
.and()
.logout().addLogoutHandler(myLogoutHandler())
.logoutSuccessHandler(logoutSuccessHandler())
.and()
.authorizeRequests()
.antMatchers("/index.html", "/login", "/").permitAll()
.antMatchers(HttpMethod.OPTIONS).denyAll()
.antMatchers(HttpMethod.HEAD).denyAll()
.anyRequest().authenticated()
.and()
.authenticationProvider(myAuthenticationProvider)
.csrf()
.csrfTokenRepository(csrfTokenRepository())
.and()
.addFilterAfter(csrfHeaderFilter(), SessionManagementFilter.class);
// #formatter:on
}
This works great for the Web application. However, now I am requested to add a configuration that would let third party client applications to invoke my services via pure REST calls, i.e. they should be completely stateless and use http basic authentication - no session should be created and xsrf should be disabled (I think...).
I can define a shared URL path for all those client API calls. But how can I leverage my existing security configuration and server to support both requirements?
Answering my own question...
Spring security allows you use multiple configurations based on order. In the documentation, it gives the following example:
#EnableWebSecurity
public class MultiHttpSecurityConfig {
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) { 1
auth
.inMemoryAuthentication()
.withUser("user").password("password").roles("USER").and()
.withUser("admin").password("password").roles("USER", "ADMIN");
}
#Configuration
#Order(1) 2
public static class ApiWebSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) throws Exception {
http
.antMatcher("/api/**") 3
.authorizeRequests()
.anyRequest().hasRole("ADMIN")
.and()
.httpBasic();
}
}
#Configuration 4
public static class FormLoginWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin();
}
}
}
In the example above, /api would be permitted only for ADMIN roles, while other paths will be configured with the default FormLoginWebSecurityConfigurerAdapter.
See more at this URL:

Categories

Resources