Spring Security Limiting URL access by roles - java

I am trying to implement SpringSecurity mechanism on this little project, that will limit interactions with the URL of the request by roles.
I have two roles USER and ADMIN, USER can see the items, but not add or delete them, while ADMIN can do both.
Now the problem, the requests from USER role and even unauthenticated users to create/delete/read an item are allowed somehow. It seems to me that my application is not configured correctly somewhere.
SecurityConfig:
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
#Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("user").password("{noop}12345").roles("USER").and()
.withUser("admin").password("{noop}12345").roles("ADMIN");
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.httpBasic().and().authorizeRequests()
.antMatchers("api/**").hasRole("ADMIN")
.antMatchers("api/items", "api/items/").hasRole("USER")
.anyRequest().authenticated()
.and().csrf().disable().headers().frameOptions().disable();
}
}
Controller:
#RestController
public class ItemController {
#Autowired
private ItemService itemService;
#GetMapping("/api/items")
public List<Item> getItems() {
return itemService.getAllItems();
}
#PostMapping(value = "/api/addItem",consumes = {"application/json"},produces = {"application/json"})
#ResponseBody
public Item addItem(#RequestBody Item item) {
itemService.addItem(item);
return item;
}
#DeleteMapping(value = "api/deleteItem/{id}")
#ResponseBody
public String deleteItem(#PathVariable int id) {
itemService.deleteItem(id);
return "Deleted";
}
}
I am sending requests to the following URL's:
http://localhost:8080/api/items // GET
http://localhost:8080/api/addItem // POST
http://localhost:8080/api/deleteItem/4 // DELETE

Have you tried adding slashes to your antmatcher patterns, such as:
antMatchers("/api/**").hasRole("ADMIN")
The Spring documentation mentions:
Note: a pattern and a path must both be absolute or must both be relative in order for the two to match. Therefore it is recommended that users of this implementation to sanitize patterns in order to prefix them with "/" as it makes sense in the context in which they're used.
Furthermore, Spring security uses the first match of all the matching rules expressed. I would recommend reordering the matchers from most-specific to less-specific as otherwise a call to api/items will be matched by the api/** matcher instead of being matched by the api/items matcher.
.antMatchers("/api/items", "api/items/").hasRole("USER")
.antMatchers("/api/**").hasRole("ADMIN")

On top of #GetMapping or #PostMapping you can add following annotation to manage Role based access
#PreAuthorize("hasAnyRole('ROLE_ADMIN')")
#PostMapping(value = "/api/addItem",consumes = {"application/json"},produces = {"application/json"})
#ResponseBody
public Item addItem(#RequestBody Item item) {
itemService.addItem(item);
return item;
}

Related

Specify different (basic) authentication log-in per endpoint Spring MVC

Imagine the following (hypothetical) data structure
endpoint | username | password
users admin 123
info george awd
data magnus e4
this means that every endpoint requires different credentials and no one username/password combo can log in to every endpoint. I am looking for a way to make this scalable in our Spring MVC project when adding more endpoints. We could use roles and hardcore this into the config class but the endpoints and login combinations vary for every customer installation
Given the following SecurityConfiguration with LookupAuthenticationService being the class that looks up the username/password data in the database
#EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
private static final String[] ENDPOINT_LIST = {
"/rest/**"
};
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers(ENDPOINT_LIST)
.authenticated()
.and()
.httpBasic();
}
#Autowired
protected void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authenticationProvider());
}
#Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
authenticationProvider.setUserDetailsService(userDetailsService());
authenticationProvider.setPasswordEncoder(passwordEncoder());
return authenticationProvider;
}
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
#Override
protected UserDetailsService userDetailsService() {
return new LookupAuthenticationService(passwordEncoder());
}
}
The ideal situation would be if LookupAuthenticationService has access to the request so we know which endpoint to fetch but I guess this is only possible when working with individual Filters
The possibilities I've found so far are:
Add a WebSecurityConfigurerAdapter and multiple UserDetailsServer specific per endpoint -> lots of code
Add a HandlerInterceptor per endpoint -> lots of code
AuthenticationManagerResolver returning a different AuthenticationManager based on pathInfo?
Any input how to best resolve this issue would be appreciated
You can have a table where you map endpoints to rules, like so:
pattern
authority
/users/**
ROLE_ADMIN
/info/**
ROLE_USER
/another/**
ROLE_ANOTHER
And instead of assigning a user to an endpoint, you assign a role to the users. With this in place, you can create an AuthorizationManager which is going to protect your endpoints based on the request path.
#Component
public class AccessRuleAuthorizationManager implements AuthorizationManager<RequestAuthorizationContext> {
private final AccessRuleRepository rules;
private RequestMatcherDelegatingAuthorizationManager delegate;
public AccessRuleAuthorizationManager(AccessRuleRepository rules) {
this.rules = rules;
}
#Override
public AuthorizationDecision check(Supplier<Authentication> authentication, RequestAuthorizationContext object) {
return this.delegate.check(authentication, object.getRequest());
}
#EventListener
void applyRules(ApplicationReadyEvent event) {
Builder builder = builder();
for (AccessRule rule : this.rules.findAll()) {
builder.add(
new AntPathRequestMatcher(rule.getPattern()),
AuthorityAuthorizationManager.hasAuthority(rule.getAuthority())
);
}
this.delegate = builder.build();
}
}
And, in your SecurityConfiguration you simply do this:
#Autowired
private AccessRuleAuthorizationManager access;
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authz) ->
authz.anyRequest().access(this.access)
)
.httpBasic(Customizer.withDefaults());
}
I recommend you to take a look at this repository and watch the presentation from the repository's description. The last steps of the presentation was adding the custom AuthorizationManager, and there's a great explanation about it.

Map OAuth2 user attributes to Object prior to controller method call

What I have currently
I'm currently implementing an OIDC Resource Provider for my company. They use their intern OIDC servers, which I managed to work with by following this example: https://github.com/jgrandja/oauth2login-demo/tree/linkedin
I'm now able to retrieve user information from the Authorization Server, like that:
#RestController
#RequestMapping("/some/route")
public class SomeController {
#GetMapping("/some/route")
public ResponseEntity<?> getSomething(#RegisteredOAuth2AuthorizedClient OAuth2AuthorizedClient authorizedClient) {
String userInfoEndpointUri = authorizedClient.getClientRegistration()
.getProviderDetails().getUserInfoEndpoint().getUri();
Map userAttributes = this.webClient
.get()
.uri(userInfoEndpointUri)
.attributes(oauth2AuthorizedClient(authorizedClient))
.retrieve()
.bodyToMono(Map.class)
.block();
String firstName = (String) userAttributes.get("first_name");
String lastName = (String) userAttributes.get("last_name");
...
}
}
What I'd like
I am now searching for a solution to map the userAttributes to an Object prior to
the controller method call, so that I get e.g.:
#GetMapping("/some/route")
public ResponseEntity<?> getSomething(MyCostumUserBean user) {
String firstName = user.getFirstName();
String lastName = user.getLastName();
...
}
I read something about the ChannelInterceptor and HandlerInterceptor and also the PrincipalExtractor and AuthoritiesExtractor.
The problem is, that I am just learning the Spring framework and these possibilities are overwhelming me.
It would be a plus if that method would allow some validation and would immediately respond with Error codes if the validation fails.
After that is achieved, I would like to add additional information to MyCostumUserBean from another server, which I send the identity of the current session's user to and receive e.g. Role/Permissions of that user.
I tried to put it in a picture:
Question
What is the proper / by the Spring Framework intended way to deal with that? How do I achieve that?
Extra: Is it secure to rely on OAuth2AuthorizedClient.getPrincipalName()? Or can that be faked by an user, by faking the Cookie/Token?
I think you are asking the way to configure the success handler, or a filter which can check the user attributes.
If this is what you are asking, There are many ways to do it.
For examples:
Use User scope check:(need to assign the scope to the user in advance.)
#ResponseBody
#GetMapping("/some/route")
public String getSomeThing(#RegisteredOAuth2AuthorizedClient("custom") OAuth2AuthorizedClient authorizedClient) {
Set<String> scopes = authorizedClient.getAccessToken()
.getScopes();
if (scopes.contains("users:read")) {
} else if (scopes.contains("users:read")) {
return " page 1";
} else {
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Forbidden.");
}
}
You can put some logic in the successHandler:
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/**")
.permitAll().and()
.formLogin()
.successHandler(successHandler());
}
#Bean
public CustomSuccessHandler successHandler() {
return new CustomSuccessHandler();
}
If you want to apply a filter for your Security Chians:
#Configuration
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
...
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
...
.and()
.addFilterBefore(getBeforeAuthenticationFilter(), CustomBeforeAuthenticationFilter.class)
.formLogin()
.loginPage()
.permitAll()
...
}
public UsernamePasswordAuthenticationFilter getBeforeAuthenticationFilter() throws Exception {
CustomBeforeAuthenticationFilter filter = new CustomBeforeAuthenticationFilter();
....
return filter;
}
}
You can also achieve the same purpose by using a Customizing Filter Chains, by give the different order and the relative login in it.
#Configuration
#Order(SecurityProperties.BASIC_AUTH_ORDER - 10)
public class ApplicationConfigurerAdapterForUserGroup1 extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
...;
}
}
#Configuration
#Order(SecurityProperties.BASIC_AUTH_ORDER - 10)
public class ApplicationConfigurerAdapterForUserGroup2 extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
...;
}
}

Spring Security allow each user to see their own profile but none else

In Spring MVC with Spring Security, is it possible to achieve this?
#Override WebSecurityConfigurerAdapter.configure(HttpSecurity)
#Override
protected void configure(HttpSecurity http) throws Exception
{
http
.authorizeRequests()
.mvcMatchers("/users/{authentication.principal.username}").hasAnyRole(ADMIN, MANAGER)
.antMatchers("/users/**").hasRole(ADMIN)
.anyRequest().authorized()
...
}
/users/** is a restricted area and should be accessible by admins only. But managers should still be able to see their own profile (/users/user_with_manager_role), and only their own profile, not those of any other users (regardless of their role).
Solution
I've found a solution in Andrew's answer. My Code now looks like this:
WebSecurityConfigurerAdapter
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true) // added this annotation
public class SecurityConfig extends WebSecurityConfigurerAdapter
#Override WebSecurityConfigurerAdapter.configure(HttpSecurity)
#Override
protected void configure(HttpSecurity http) throws Exception
{
http
.authorizeRequests()
// removed /users handling
.anyRequest().authorized()
...
}
UsersController
#Controller
#RequestMapping("/users")
public class UsersController
{
#GetMapping("{username}")
#PreAuthorize("authentication.principal.username == #username) || hasRole('ADMIN')")
public String usersGet(#PathVariable("username") String username)
{
// do something with username, for example get a User object from a JPA repository
return "user";
}
}
I'm afraid it's not possible: when this configuration is being set up, it has no info about {authentication.principal.username} which will be resolved at some point in future.
But Spring gives you a bunch of built-in method security expressions you can annotate your methods with.
Starting from a simple expression like #PreAuthorize("hasRole('ADMIN')"), you might end up with a custom one:
#XMapping(path = "/users/{username}")
#PreAuthorize("#yourSecurityService.isMyPage(authentication.principal, #username)")
public void yourControllerMethod(#PathVariable String username);
#yourSecurityService.isMyPage(authentication.principal, #username) refers to your #Service method public boolean isMyPage(Principal, String).
How about something like this:
#Override
protected void configure(HttpSecurity http) throws Exception
{
http
.authorizeRequests()
.antMatchers(HttpMethod.GET, "/myself").hasAnyRole(ADMIN, MANAGER)
.antMatchers("/users/**").hasRole(ADMIN)
.anyRequest().hasAnyRole(ADMIN, MANAGER)
...
}
#RequestMapping(value = "/myself", method = RequestMethod.GET)
public Profile getMyself() {
// return the profile of the loged in user
}
With this manager and admins can get their own profile and admins can also request other profiles with /users/{username}.

Can not get the #Secured Method Security annotations working in Spring Security

I have done a lot of Research and to me everything looks right... but I cannot get this to work! Anyone has any idea?
No matter what I do, the relevant mapping remains public to anyone (anonymous or logged in, no matter what Role they have).
Ideally I would like to have ALL requests to be Public, except those which are annotated by #Secured() - obviously only the users with the specific roles would be allowed access to these mappings.
Is that possible?
FYI as a workaround I currently built a method "hasRole(String role)" which checks the role of the logged-in user, and throws a NotAuthorizedException (custom made) if the method returns false.
UserDetails
#Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<GrantedAuthority> grantedAuthorities = null;
System.out.print("Account role... ");
System.out.println(account.getRole());
if (account.getRole().equals("USER")) {
GrantedAuthority grantedAuthority = new SimpleGrantedAuthority("ROLE_USER");
grantedAuthorities = Arrays.asList(grantedAuthority);
}
if (account.getRole().equals("ADMIN")) {
GrantedAuthority grantedAuthorityUser = new SimpleGrantedAuthority("ROLE_USER");
GrantedAuthority grantedAuthorityAdmin = new SimpleGrantedAuthority("ROLE_ADMIN");
grantedAuthorities = Arrays.asList(grantedAuthorityUser, grantedAuthorityAdmin);
}
return grantedAuthorities;
}
SecurityConfig
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private AuthFailure authFailure;
#Autowired
private AuthSuccess authSuccess;
#Autowired
private EntryPointUnauthorizedHandler unauthorizedHandler;
#Autowired
private UserDetailsServiceImpl userDetailsService;
/*#Autowired
public void configAuthBuilder(AuthenticationManagerBuilder builder) throws Exception {
builder.userDetailsService(userDetailsService);
}*/
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Autowired
#Override
public void configure(AuthenticationManagerBuilder builder) throws Exception {
builder.userDetailsService(userDetailsService);
}
private CsrfTokenRepository csrfTokenRepository() {
HttpSessionCsrfTokenRepository repository = new HttpSessionCsrfTokenRepository();
repository.setHeaderName("X-XSRF-TOKEN");
return repository;
}
#Override
public void configure(HttpSecurity http) throws Exception {
http.csrf().csrfTokenRepository(csrfTokenRepository())
.and().exceptionHandling().authenticationEntryPoint(unauthorizedHandler)
.and().formLogin().loginPage("/login").successHandler(authSuccess).failureHandler(authFailure)
//.and().authorizeRequests().antMatchers("/rest/**").authenticated()
//.and().authorizeRequests().antMatchers("/**").permitAll()
.and().addFilterAfter(new CsrfHeaderFilter(), CsrfFilter.class);;
}
AccountController
#Secured("ROLE_USER")
#RequestMapping(method = RequestMethod.GET)
public List<Account> getAllAccounts(#RequestParam(value = "mail", required = false) String mail) {
Thanks!
You can make use of Controller scoped Security with Spring HttpSecurity. Try add this to your configure Method:
.antMatchers("rest/accounts*").hasRole("ADMIN")
And if you wish ANY Request to be public (really?):
.anyRequest().permitAll()
You can additionally secure your Methodinvocation for Example in your UserDetailsService when you access it from anywhere:
#Secured("ROLE_USER")
public getAllAccounts(...){...}
Only then you have to annotate your SecurityConfig with:
#EnableGlobalMethodSecurity(securedEnabled = true)
In practice we recommend that you use method security at your service
layer, to control access to your application, and do not rely entirely
on the use of security constraints defined at the web-application
level. URLs change and it is difficult to take account of all the
possible URLs that an application might support and how requests might
be manipulated. You should try and restrict yourself to using a few
simple ant paths which are simple to understand. Always try to use
a"deny-by-default" approach where you have a catch-all wildcard ( / or
) defined last and denying access. Security defined at the service
layer is much more robust and harder to bypass, so you should always
take advantage of Spring Security’s method security options.
see: http://docs.spring.io/autorepo/docs/spring-security/4.0.0.CI-SNAPSHOT/reference/htmlsingle/#request-matching
Here, I would like to add something based on the above right answer from sven.kwiotek. If in the ROLE table you still want to use "USER", "ADMIN"... the solution is also easy:
When fetch the role from database, do not forget to add "ROLE_" prefix manully, for example,
List<GrantedAuthority> authorities = user.getRoles().stream().map(role ->
new SimpleGrantedAuthority("ROLE_" + role.getRole()))
.collect(Collectors.toList());
and then you could use annotation #Secured("ROLE_USER") in the controller method with safety.
The reason is that in the org.springframework.security.access.vote.RoleVoter class all roles should start with ROLE_ prefix.

Combine Dynamic datasource routing with spring-data-rest

I'm using Dynamic datasource routing as indicated in this blog post:
http://spring.io/blog/2007/01/23/dynamic-datasource-routing/
This works fine, but when I combine it with spring-data-rest and browsing of my generated repositories I (rightfully) get an exception that my lookup-key is not defined (I do not set a default).
How and where can I hook into the Spring data rest request handling to set the lookup-key based on 'x' (user authorizations, path prefix, or other), before any connection is made to the database?
Code-wise my datasource configuration just mostly matches the blogpost at the top, with some basic entity classes, generated repositories and Spring Boot to wrap everything together. If need I could post some code, but there's nothing much to see there.
My first idea is to leverage Spring Security's authentication object to set current datasource based on authorities attached to the authentication.
Of course, you can put the lookup key in a custom UserDetails object or even a custom Authentication object, too. For sake of brevity I`ll concentrate on a solution based on authorities.
This solution requires a valid authentication object (anonymous user can have a valid authentication, too). Depending on your Spring Security configuration changing authority/datasource can be accomplished on a per request or session basis.
My second idea is to work with a javax.servlet.Filter to set lookup key in a thread local variable before Spring Data Rest kicks in. This solution is framework independent and can be used on a per request or session basis.
Datasource routing with Spring Security
Use SecurityContextHolder to access current authentication's authorities. Based on the authorities decide which datasource to use.
Just as your code I'm not setting a defaultTargetDataSource on my AbstractRoutingDataSource.
public class CustomRoutingDataSource extends AbstractRoutingDataSource {
#Override
protected Object determineCurrentLookupKey() {
Set<String> authorities = getAuthoritiesOfCurrentUser();
if(authorities.contains("ROLE_TENANT1")) {
return "TENANT1";
}
return "TENANT2";
}
private Set<String> getAuthoritiesOfCurrentUser() {
if(SecurityContextHolder.getContext().getAuthentication() == null) {
return Collections.emptySet();
}
Collection<? extends GrantedAuthority> authorities = SecurityContextHolder.getContext().getAuthentication().getAuthorities();
return AuthorityUtils.authorityListToSet(authorities);
}
}
In your code you must replace the in memory UserDetailsService (inMemoryAuthentication) with a UserDetailsService that serves your need.
It shows you that there are two different users with different roles TENANT1 and TENANT2 used for the datasource routing.
#Configuration
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication()
.withUser("user1").password("user1").roles("USER", "TENANT1")
.and()
.withUser("user2").password("user2").roles("USER", "TENANT2");
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher("/**")
.authorizeRequests()
.antMatchers("/**").hasRole("USER")
.and()
.httpBasic()
.and().csrf().disable();
}
}
Here is a complete example: https://github.com/ksokol/spring-sandbox/tree/sdr-routing-datasource-spring-security/spring-data
Datasource routing with javax.servlet.Filter
Create a new filter class and add it to your web.xml or register it with the AbstractAnnotationConfigDispatcherServletInitializer, respectively.
public class TenantFilter implements Filter {
private final Pattern pattern = Pattern.compile(";\\s*tenant\\s*=\\s*(\\w+)");
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String tenant = matchTenantSystemIDToken(httpRequest.getRequestURI());
Tenant.setCurrentTenant(tenant);
try {
chain.doFilter(request, response);
} finally {
Tenant.clearCurrentTenant();
}
}
private String matchTenantSystemIDToken(final String uri) {
final Matcher matcher = pattern.matcher(uri);
if (matcher.find()) {
return matcher.group(1);
}
return null;
}
}
Tenant class is a simple wrapper around a static ThreadLocal.
public class Tenant {
private static final ThreadLocal<String> TENANT = new ThreadLocal<>();
public static void setCurrentTenant(String tenant) { TENANT.set(tenant); }
public static String getCurrentTenant() { return TENANT.get(); }
public static void clearCurrentTenant() { TENANT.remove(); }
}
Just as your code I`m not setting a defaultTargetDataSource on my AbstractRoutingDataSource.
public class CustomRoutingDataSource extends AbstractRoutingDataSource {
#Override
protected Object determineCurrentLookupKey() {
if(Tenant.getCurrentTenant() == null) {
return "TENANT1";
}
return Tenant.getCurrentTenant().toUpperCase();
}
}
Now you can switch datasource with http://localhost:8080/sandbox/myEntities;tenant=tenant1. Beware that tenant has to be set on every request. Alternatively, you can store the tenant in the HttpSession for subsequent requests.
Here is a complete example: https://github.com/ksokol/spring-sandbox/tree/sdr-routing-datasource-url/spring-data

Categories

Resources