Spring Security Custom Authentication Filter and Authorization - spring

I've implemented a custom authentication filter, and it works great. I use an external identity provider and redirect to my originally requested URL after setting my session and adding my authentication object to my security context.
Security Config
#EnableWebSecurity(debug = true)
#Configuration
class SecurityConfig extends WebSecurityConfigurerAdapter {
// this is needed to pass the authentication manager into our custom security filter
#Bean
#Override
AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean()
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
//.antMatchers("/admin/test").hasRole("METADATA_CURATORZ")
.antMatchers("/**").permitAll()
.anyRequest().authenticated()
.and()
.addFilterBefore(new CustomSecurityFilter(authenticationManagerBean()), UsernamePasswordAuthenticationFilter.class)
}
}
Filter logic
For now, my custom filter (once identity is confirmed) simply hard codes a role:
SimpleGrantedAuthority myrole = new SimpleGrantedAuthority("METADATA_CURATORZ")
return new PreAuthenticatedAuthenticationToken(securityUser, null, [myrole])
That authentication object (returned above) is then added to my SecurityContext before redirecting to the desired endpoint:
SecurityContextHolder.getContext().setAuthentication(authentication)
Controller Endpoint
#RequestMapping(path = '/admin/test', method = GET, produces = 'text/plain')
String test(HttpServletRequest request) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication()
String roles = auth.getAuthorities()
return "roles: ${roles}"
}
This endpoint then yields a response in the browser of:
"roles: [METADATA_CURATORZ]"
Great. So my authentication and applying a role to my user is working great.
Now, if I uncomment this line from the security config:
//.antMatchers("/admin/test").hasRole("METADATA_CURATORZ")
I can no longer access that resource and get a 403 -- even though we've already proven the role was set.
This seems totally nonsensical and broken to me, but I'm no Spring Security expert.
I'm probably missing something very simple. Any ideas?
Some questions I have:
Does my custom filter need to be placed before a specific built-in filter to ensure the authorization step occurs after that filter is executed?
When in the request cycle is the antMatcher/hasRole check taking place?
Do I need to change the order of what I am calling in my security configure chain, and how should I understand the config as I've currently written it? It's obviously not doing what I think it should be.

Does my custom filter need to be placed before a specific built-in filter to ensure the authorization step occurs after that filter is executed?
Your filter MUST come before FilterSecurityInterceptor, because that is where authorization and authentication take place. This filter is one of the last to be invoked.
Now as to where the best place for your filter might be, that really depends. For example, you really want your filter to come before AnonymousAuthenticationFilter because if not, unauthenticated users will always be "authenticated" with an AnonymousAuthenticationToken by the time your filter is invoked.
You can check out the default order of filters in FilterComparator. The AbstractPreAuthenticatedProcessingFilter pretty much corresponds to what it is you're doing - and its placement in the order of filters gives you an idea of where you could put yours. In any case, there should be no issue with your filter's order.
When in the request cycle is the antMatcher/hasRole check taking place?
All of this happens in FilterSecurityInterceptor, and more precisely, in its parent AbstractSecurityInterceptor:
protected InterceptorStatusToken beforeInvocation(Object object) {
Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource()
.getAttributes(object);
if (attributes == null || attributes.isEmpty()) {
...
}
...
Authentication authenticated = authenticateIfRequired();
// Attempt authorization
try {
this.accessDecisionManager.decide(authenticated, object, attributes);
}
catch (AccessDeniedException accessDeniedException) {
...
throw accessDeniedException;
}
Extra information:
In essence, the FilterSecurityInterceptor has a ExpressionBasedFilterInvocationSecurityMetadataSource that contains a Map<RequestMatcher, Collection<ConfigAttribute>>. At runtime, your request is checked against the Map to see if any RequestMatcher key is a match. If it is, a Collection<ConfigAttribute> is passed to the AccessDecisionManager, which ultimately either grants or denies access. The default AccessDecisionManager is AffirmativeBased and contains objects (usually a WebExpressionVoter) that process the collection of ConfigAttribute and via reflection invokes the SpelExpression that corresponds to your "hasRole('METADATA_CURATORZ')" against a SecurityExpressionRoot object that was initialized with your Authentication.
Do I need to change the order of what I am calling in my security configure chain, and how should I understand the config as I've currently written it? It's obviously not doing what I think it should be.
No, there shouldn't be any issue with your filters. Just as a side note, in addition to what you have in your configure(HttpSecurity http) methods, the WebSecurityConfigurerAdapter you extend from has some defaults:
http
.csrf().and()
.addFilter(new WebAsyncManagerIntegrationFilter())
.exceptionHandling().and()
.headers().and()
.sessionManagement().and()
.securityContext().and()
.requestCache().and()
.anonymous().and()
.servletApi().and()
.apply(new DefaultLoginPageConfigurer<>()).and()
.logout();
You can take a look at HttpSecurity if you want to see exactly what these do and what filters they add.
THE PROBLEM
When you do the following:
.authorizeRequests()
.antMatchers("/admin/test").hasRole("METADATA_CURATORZ")
... the role that is searched for is "ROLE_METADATA_CURATORZ". Why?
ExpressionUrlAuthorizationConfigurer's static hasRole(String role) method ends up processing "METADATA_CURATORZ":
if (role.startsWith("ROLE_")) {
throw new IllegalArgumentException(
"role should not start with 'ROLE_' since it is automatically inserted. Got '"
+ role + "'");
}
return "hasRole('ROLE_" + role + "')";
}
So your authorization expression becomes "hasRole('ROLE_METADATA_CURATORZ'" and this ends up calling the method hasRole('ROLE_METADATA_CURATORZ') on SecurityExpressionRoot, which in turn searches for the role ROLE_METADATA_CURATORZ in the Authentication's authorities.
THE SOLUTION
Change
SimpleGrantedAuthority myrole = new SimpleGrantedAuthority("METADATA_CURATORZ");
to:
SimpleGrantedAuthority myrole = new SimpleGrantedAuthority("ROLE_METADATA_CURATORZ");

Related

Using a request header value in #PreAuthorize

Is it possible to use a request header value in #PreAuthorize?
In my app, all requests have a custom header included which I need to use in conjunction with the user role to determine whether or not they should be allowed to access the controller.
It's ok if someone manually specifies a header as that won't be a security issue, as ultimately the role will control this. But I will need to use it to cut down on checking for that manually in each controller method.
Thank you,
Matt
1 - This may be the fastest method if you will only use it in a few places.
#GetMapping(value = "/private-api-method")
#PreAuthorize("#request.getHeader('header-name') == 'localhost:8080'")
public ResponseEntity<String> privateApiMethod(HttpServletRequest request) {
return ResponseEntity.ok("OK!");
}
OR
#GetMapping(value = "/private-api-method")
#PreAuthorize("#header == 'localhost:8080'")
public ResponseEntity<String> privateApiMethod(#RequestHeader("header-name") String header) {
return ResponseEntity.ok("OK!");
}
2 - This may be the best method if you will use it in many places. (In the SecurityServise, you can add multiple different methods of checking.)
#GetMapping(value = "/private-api-method")
#PreAuthorize("#securityService.checkHeader(#request)")
public ResponseEntity<String> privateApiMethod(HttpServletRequest request) {
return ResponseEntity.ok("OK!");
}
3 - You can choose a special method for yourself
A Custom Security Expression with Spring Security
Since you intend to check for a particular header/cookie/request-attribute for every controller methods, you should opt for a Filter as this would be a standard and you can have a guarantee for it be executed for each and every method and that too only once by extending from OncePerRequestFilter
Having said that, there would be 2 way you can achieve this:
By extending AbstractAuthenticationProcessingFilter or OncePerRequestFilter
For this you may refer the spring-security jwt token validation flow which all would advocate for:
Add method security at your desired controller method as #PreAuthorize("hasAuthority('USER_ROLE')")
Intercept the request before UsernamePasswordAuthenticationFilter, extract the Authentication header or cookies from the request and validate the token value for claims.
public class CustomHeaderAuthFilter extends AbstractAuthenticationProcessingFilter{
#Override
public Authentication attemptAuthentication(
HttpServletRequest request, HttpServletResponse response){
// Get all the headers from request, throw exception if your header not found
Enumeration<String> reqHeaders = request.getHeaderNames();
Assert.notNull(reqHeaders, "No headers found. Abort operation!");
Collections.list(reqHeaders)
.stream()
.filter(header_ -> header_.equals("TARGET_HEADER_NAME"))
.findAny().ifPresent(header_ -> {
// header found, would go for success-andler
});
// Here it means request has no target header
SecurityContextHolder.clearContext();
failureHandler.onAuthenticationFailure(request, response, new CustomException(""));
}
}
Going by this way, you need to register your filter with WebSecurityConfigurerAdapter and you may also provide your AuthenticationProvider if you extend from AbstractAuthenticationProcessingFilter.
By accessing HTTP Headers in rest controllers using #RequestHeader as dm-tr has mentioned.
Maybe you can try this
#PreAuthorize("hasAuthority('ROLE_SOMETHING')")
#RequestMapping("PATH")
public void checkIt(#RequestHeader("header-name") String header) {
if (null != header /* && header meets certain condition*/) {
// stuff
} else throw new ResponseStatusException(HttpStatus.FORBIDDEN); // PERMISSION NOT GRANTED, 403 ERROR
}

antMatchers allow ADMIN all routes while other roles are restricted

Specific user groups should have access to different api-routes. Given the following HttpSecurity we, for example, allow CUSTOMER to access GET /invoices/*. Also, I want to allow ADMIN to access any route /**.
#Override
protected void configure(HttpSecurity http) throws Exception {
http = http
.cors().and()
.csrf().disable(); // REST only
// ### Authentication
// ...
// ### Authorization
// anonymous (and all other roles)
e = e
.antMatchers(HttpMethod.POST,
"/user/account/create",
"/user/account/confirm/*",
"/feedback")
.permitAll()
.antMatchers(HttpMethod.GET,
"/" + StorageController.STORAGE_RELATIVE_PATH, // public files
"/translations/*")
.permitAll();
// role CUSTOMER
e = e
.antMatchers(HttpMethod.GET,
"/invoices",
"/invoices/*")
.hasRole(Role.CUSTOMER.toString())
.antMatchers(HttpMethod.PUT,
"/profile",
"/contracts/billingAddress")
.hasRole(Role.CUSTOMER.toString())
.antMatchers(HttpMethod.POST,
"/contracts",
"/profile/logo")
.hasRole(Role.CUSTOMER.toString());
// only ADMIN
e = e.antMatchers("/**").hasRole(Role.ADMIN.toString());
}
While ADMIN is allowed to use all non-mentioned /admin/.../ routes, the role get's a 403 on, for example, /invoices/* - why? From what I understand the specified configuration depends on the order and hence requires /invoices/* to have CUSTOMER role and hence ADMIN is not enough - correct?
If I add the following (section CUSTOMER or ADMIN), it works, but it is so cumbersome to always list ADMIN role. I just want ADMINs to be able to access everything.
#Override
protected void configure(HttpSecurity http) throws Exception {
// ...
// role CUSTOMER
e = e
// ... as above ...
// CUSTOMER or ADMIN
e = e
.antMatchers(HttpMethod.GET,
"/invoices/*") // AGAIN
.hasAnyRole(Role.CUSTOMER.toString(),
Role.ADMIN.toString());
// only ADMIN
e = e.antMatchers("/**").hasRole(Role.ADMIN.toString());
}
Also, I cannot put the ADMIN rule above the others, because it would exclude the other roles from accessing anything. Is there a way to specify what I want to do in a more elegant/easier way?
As you indicated in your question, this behaviour occurs because each matcher is considered in the order it was declared.
A request to /invoices will reach the below matcher first and since it is a match, it will apply the associated rule.
.antMatchers("/invoices").hasRole("CUSTOMER")
If the user making the request only has the role "ADMIN" then they will be denied access, because an "ADMIN" is not a "CUSTOMER".
Option 1
To get the desired behaviour you can explicitly list out the roles that have access to the endpoint.
As you mentioned, this is verbose, however, the advantage is that it clearly indicates the allowed roles in one place.
.antMatchers("/invoices").hasAnyRole("CUSTOMER", "ADMIN")
Option 2
If an "ADMIN" should be able to do anything a "CUSTOMER" can do, then you can declare a RoleHierarchy that states that any "ADMIN" is also a "CUSTOMER".
#Bean
public RoleHierarchy roleHierarchy() {
RoleHierarchyImpl hierarchy = new RoleHierarchyImpl();
hierarchy.setHierarchy("ROLE_ADMIN > ROLE_CUSTOMER");
return hierarchy;
}
The > symbol can be thought of as meaning "includes".
Then, any endpoints protected by .hasRole("CUSTOMER") can also be accessed by users with the role "ADMIN", since having the role "ADMIN" implies that they also have the role "CUSTOMER".

How to set up cache-control on a 304 reply with Spring-security [duplicate]

I am trying to filter some url pattern to caching.
What I have attempted is put some codes into WebSecurityConfigurerAdapter implementation.
#Override
protected void configure(HttpSecurity http) throws Exception {
initSecurityConfigService();
// For cache
http.headers().defaultsDisabled()
.cacheControl()
.and().frameOptions();
securityConfigService.configure(http,this);
}
However this code will effect all of the web application. How can I apply this to certain URL or Content-Type like images.
I have already tried with RegexRequestMatcher, but it does not work for me.
// For cache
http.requestMatcher(new RegexRequestMatcher("/page/", "GET"))
.headers().defaultsDisabled()
.cacheControl()
.and().frameOptions();
I read this article : SpringSecurityResponseHeaders, but there is no sample for this case.
Thanks.
P.S. In short, I want to remove SpringSecurity defaults for certain url and resources.
What about having multiple WebSecurityConfigurerAdapters? One adapter could have cache controls for certain URLs and another one will not have cache control enabled for those URLs.
I solved this with Filter.
Below is part of my implementation of AbstractAnnotationConfigDispatcherServletInitializer. In onStartup method override.
FilterRegistration.Dynamic springSecurityFilterChain = servletContext.addFilter("springSecurityFilterChain", new DelegatingFilterProxy());
if(springSecurityFilterChain != null){
springSecurityFilterChain.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), true, "/render/*", "/service/*");
// I removed pattern url "/image/*" :)
}
What I have done is remove /image/* from MappingUrlPatterns.
Thanks for your answers!

spring security - how to remove cache control in certain url pattern

I am trying to filter some url pattern to caching.
What I have attempted is put some codes into WebSecurityConfigurerAdapter implementation.
#Override
protected void configure(HttpSecurity http) throws Exception {
initSecurityConfigService();
// For cache
http.headers().defaultsDisabled()
.cacheControl()
.and().frameOptions();
securityConfigService.configure(http,this);
}
However this code will effect all of the web application. How can I apply this to certain URL or Content-Type like images.
I have already tried with RegexRequestMatcher, but it does not work for me.
// For cache
http.requestMatcher(new RegexRequestMatcher("/page/", "GET"))
.headers().defaultsDisabled()
.cacheControl()
.and().frameOptions();
I read this article : SpringSecurityResponseHeaders, but there is no sample for this case.
Thanks.
P.S. In short, I want to remove SpringSecurity defaults for certain url and resources.
What about having multiple WebSecurityConfigurerAdapters? One adapter could have cache controls for certain URLs and another one will not have cache control enabled for those URLs.
I solved this with Filter.
Below is part of my implementation of AbstractAnnotationConfigDispatcherServletInitializer. In onStartup method override.
FilterRegistration.Dynamic springSecurityFilterChain = servletContext.addFilter("springSecurityFilterChain", new DelegatingFilterProxy());
if(springSecurityFilterChain != null){
springSecurityFilterChain.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), true, "/render/*", "/service/*");
// I removed pattern url "/image/*" :)
}
What I have done is remove /image/* from MappingUrlPatterns.
Thanks for your answers!

Wildfly Database Module Authentication : How to record logins [duplicate]

Given an authentication mechanism of type FORM defined for a Java web app, how do you capture the login performed event before being redirected to requested resource? Is there any kind of listener where I can put my code to be executed when a user logs in?
I feel like defining a filter is not the best solution, as the filter is linked to the resource and would be invoked even when the user is already authenticated and asking for a resource. I'm wondering if there's some class/method triggered only by login event.
There's no such event in Java EE. Yet. As part of JSR375, container managed security will be totally reworked as it's currently scattered across different container implemantations and is not cross-container compatible. This is outlined in this Java EE 8 Security API presentation.
There's already a reference implementation of Security API in progress, Soteria, developed by among others my fellow Arjan Tijms. With the new Security API, CDI will be used to fire authentication events which you can just #Observes. Discussion on the specification took place in this mailing list thread. It's not yet concretely implemented in Soteria.
Until then, assuming FORM based authentication whereby the user principal is internally stored in the session, your best bet is manually checking in a servlet filter if there's an user principal present in the request while your representation of the logged-in user is absent in the HTTP session.
#Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) {
HttpServletRequest request = (HttpServletRequest) req;
String username = request.getRemoteUser();
if (username != null && request.getSession().getAttribute("user") == null) {
// First-time login. You can do your thing here.
User user = yourUserService.find(username);
request.getSession().setAttribute("user", user);
}
chain.doFilter(req, res);
}
Do note that registering a filter on /j_security_check is not guaranteed to work as a decent container will handle it internally before the first filters are hit, for obvious security reasons (user-provided filters could manipulate the request in a bad way, either accidentally or awarely).
If you however happen to use a Java EE server uses the Undertow servletcontainer, such as WildFly, then there's a more clean way to hook on its internal notification events and then fire custom CDI events. This is fleshed out in this blog of Arjan Tijms. As shown in the blog, you can ultimately end up with a CDI bean like this:
#SessionScoped
public class SessionAuthListener implements Serializable {
private static final long serialVersionUID = 1L;
public void onAuthenticated(#Observes AuthenticatedEvent event) {
String username = event.getUserPrincipal().getName();
// Do something with name, e.g. audit,
// load User instance into session, etc
}
public void onLoggedOut(#Observes LoggedOutEvent event) {
// take some action, e.g. audit, null out User, etc
}
}
You can use Servlet filter on the j_security_check URI. This filter will not be invoke on every request, but only on the login request.
Check the following page - Developing servlet filters for form login processing - this works in WebSphere App Server, and WebSphere Liberty profile.
Having such filter:
#WebFilter("/j_security_check")
public class LoginFilter implements Filter {
...
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
System.out.println("Filter called 1: " +((HttpServletRequest)request).getUserPrincipal());
chain.doFilter(request, response);
System.out.println("Filter called 2: " + ((HttpServletRequest)request).getUserPrincipal());
}
gives the following output:
// on incorrect login
Filter called 1: null
[AUDIT ] CWWKS1100A: Authentication did not succeed for user ID user1. An invalid user ID or password was specified.
Filter called 2: null
// on correct login
Filter called 1: null
Filter called 2: WSPrincipal:user1
UPDATE
Other possible way to do it is to use your own servlet for login, change the action in your login page to that servlet and use request.login() method. This is servlet API so should work even in Wildfly and you have full control over login. You just need to find out how wildfly passes the originally requested resource URL (WebSphere does it via cookie).
Servlet pseudo code:
public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String user = request.getParameter("j_username");
String password = request.getParameter("j_password");
try {
request.login(user, password);
// redirect to requested resource
} catch (Exception e) {
// login failed - redirect to error login page
}

Resources