I am writing a unit test for spring security and JWT validation. There are 3 basic cases I want to start the test with:
When no token -> expect 401
When token but wrong scope -> expect 403
When token and scope -> expect 200
I tested my code using postman and they return expected responses. In my unit test I have this:
#SpringBootTest
#ContextConfiguration(classes = SecurityConfig.class)
#AutoConfigureMockMvc
#EnableAutoConfiguration
#Import(DataImporterControllerConfig.class)
public class SecurityConfigTest {
#Autowired
private MockMvc mockMvc;
#Test
void doImportExpect200() throws Exception {
mockMvc.perform(put(URI).with(jwt().authorities(new SimpleGrantedAuthority(
"SCOPE_data:write"))).contentType(APPLICATION_JSON_VALUE).accept(APPLICATION_JSON)
.content(BODY)).andExpect(status().isOk());
}
It does pass validation part and try to return some value from the controller:
#RestController
#RequestMapping(value = "${data.uriPrefix}")
#Loggable(value = INFO, trim = false)
public class DataImporterController {
private final DataImporterService dataImporterService;
#PutMapping(path = "/someurl", produces = APPLICATION_JSON_VALUE, consumes = APPLICATION_JSON_VALUE)
public dataImportResponse doImport(#RequestHeader(name = ACCEPT, required = true) final String acceptHeader,
final #RequestBody #NotBlank String body) {
return new DataImportResponse(dataImporterService.doImport(body));
}
The logic inside dataImporterService.doImport(body) require some db operation, so ideally, I want to mock it and make it return some value (something like when(dataImporterServiceMock.doImport(body)).thenReturn(something).
However, when I try it, it doesn't work. I think it is because I am not creating a controller with mocked service. I tried to create one, but due to configuration for SecurityConfig class, it is not that easy. Here is SecurityConfig class:
#EnableWebSecurity
public class SecurityConfig {
#Value("${auth0.audience}")
private String audience;
#Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}")
private String issuer;
#Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
/*
This is where we configure the security required for our endpoints and setup our app to serve as
an OAuth2 Resource Server, using JWT validation.
*/
http
.csrf().disable()
.authorizeRequests()
.antMatchers(HttpMethod.GET, "/actuator/**").permitAll()
.antMatchers(HttpMethod.PUT, "/dataimporter/**").hasAuthority("SCOPE_data:write")
.anyRequest().authenticated()
.and().cors()
.and().oauth2ResourceServer().jwt();
return http.build();
}
#Bean
JwtDecoder jwtDecoder() {
/*
By default, Spring Security does not validate the "aud" claim of the token, to ensure that this token is
indeed intended for our app. Adding our own validator is easy to do:
*/
NimbusJwtDecoder jwtDecoder = (NimbusJwtDecoder)
JwtDecoders.fromOidcIssuerLocation(issuer);
OAuth2TokenValidator<Jwt> audienceValidator = new AudienceValidator(audience);
OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer(issuer);
OAuth2TokenValidator<Jwt> withAudience = new DelegatingOAuth2TokenValidator<>(withIssuer, audienceValidator);
jwtDecoder.setJwtValidator(withAudience);
return jwtDecoder;
}
}
I got this code from Auth0 and just modified antMatcher portion. How can I test it so it returns 200 (mock service or something)?
As already stated in the answer to your other question "How to write unit test for SecurityConfig for spring security", for unit-testing a #Controller, use #WebMvcTest (with mocked dependencies), not #SpringBootTest which is intended for integration testing, loads much more config, and instantiate & autowire actual components (and, as a consequence, is slower and less focused)
#WebMvcTest(controllers = DataImporterController.class)
public class DataImporterControllerUnitTest {
#MockBean
DataImporterService dataImporterService;
#Autowired
private MockMvc mockMvc;
#BeforeEach
public void setUp() {
when(dataImporterService.doImport(body)).thenReturn(something);
}
#Test
void doImportExpect200() throws Exception {
mockMvc.perform(put(URI).with(jwt().authorities(new SimpleGrantedAuthority(
"SCOPE_data:write"))).contentType(APPLICATION_JSON_VALUE).accept(APPLICATION_JSON)
.content(BODY)).andExpect(status().isOk());
}
}
If you had followed the link I already provided, you'd have find that in the main README, in the many samples, and in the tutorials
Notes
I am writing a unit test for spring security and JWT validation.
This are completely separate concerns and is to be done separately:
spring security access-control is unit-tested with secured components: #Controller of course, but also #Service, #Repository, etc. where method-security is used (refer to your previous question for instructions and to my repo for samples)
Authorization header is not considered when building #WebMvcTest or #WebfluxTest security-context. Authentication instances are built based on MockMvc request post-processors, WebTestClient mutators or annotations and this does not involve any token decoding or validation. JWT validation has to be tested in a JwtDecoder unit-test (only if you keep your own, which you shouldn't, see below)
Spring Security does not validate the "aud" claim of the token
This is wrong with spring-boot since 2.7: spring.security.oauth2.resourceserver.jwt.audiences property does just that. There is no need for you to override the JwtDecoder provided by spring-boot (actually, you shouldn't), and, as a consequence to unit-test your own.
When no token -> expect 401
When token but wrong scope -> expect 403
When token and scope -> expect 200
To be exact, the phrasing would better be:
"anonymous" rather than "no token"
"authenticated" rather than just "token"
"authorities" rather than "scope"
The reason for that are:
the OAuth2 token is validated and then turned into an Authentication instance: by default AnonymousAuthenticationToken if token is missing or validation fails, and whatever the configured authentication-converter returns if validation is successful
spring-security Role-Based Access-Control is completely generic (nothing specific to OAuth2) and relies on GrantedAuthority, not scope. This common confusion is due to the fact that there is nothing related to RBAC in OAuth2 nor OpenID standards and the default authentication-converter had to choose a claim that is always there to map authorities from. scope claim was picked as default, adding the SCOPE_ prefix. You should refer to how RBAC is implemented by your own authorization server and provide an authentication-converter bean to map authorities from the right claim(s).
Auth0 uses roles and permissions claims when RBAC is enabled, Keycloak uses realm_access.roles and resource_access.{client-id}.roles, etc., reason for me implementing a configurable authorities-converter, which picks the claims that should be used as authorities source from application properties (and how to map it: prefix and case transformation).
Last, your configuration is still risky (enabled sessions with disabled CSRF protection and poor CORS config). You should really consider using "my" starters, or follow the tutorial I wrote for configuring a resource-server with just spring-boot-oauth2-resource-server.
Related
I am new to Spring Boot Security. I am performing validation of licenseKey in every end-point in REST call. It is working fine.
I want to do it in a common way like SecurityConfig extends WebSecurityConfigurerAdapter {} class so that I should not pass extra parameter in methods for validation. It means there should be common validation if the licenseKey exists then the REST calls should be authorized to go otherwise, it should throw error. Currently I am passing HttpServletRequest which contains licenseKey, the methods are working fine. But our requirement is to perform only in one place in SecurityConfig so that all the requests can be validated.
#GetMapping(path="some/path")
public ResponseEntity<> viewDetails(HttpServletRequest httpRequest, MappingVO mappingVO) {
String licenseUser = userDetailsService.getLicenseUser(httpRequest).getUser().getEmailAddress();
....
....
}
#DeleteMapping(path="some/path")
public ResponseEntity<> deletePart(HttpServletRequest httpRequest, Part part) {
String licenseUser = userDetailsService.getLicenseUser(httpRequest).getUser().getEmailAddress();
....
....
}
In class CustomUserDetails, it has been written like this.
public CustomUserDetails getLicenseUser(HttpServletRequest httpRequest) {
String userId = httpRequest.getHeader("licenseKey");
CustomUserDetails ud = (CustomUserDetails) loadUserByUsername(userId);
return ud;
}
You should add a custom filter in the filter chain in your security config that executes before each request.
Just create a Custom Filter implementing OncePerRequestFilter.java and do the license Key validation inside of that filter. The implementation logic inside your Custom filter will run once before each request that is made on your spring boot server.
Refer
https://www.baeldung.com/spring-onceperrequestfilter
https://www.javadevjournal.com/spring-boot/spring-boot-add-filter/
If you are using Spring Security and want to filter requests to verify that it has valid token passed to it (if you're not already doing this), refer to the official documentation of your respective version of spring security to see where should you add your filter inside the filterChain.
Check Filter Ordering in:
https://docs.spring.io/spring-security/site/docs/3.1.4.RELEASE/reference/security-filter-chain.html
The token validation filter should ideally be exeucted before UsernamePasswordAuthenticationFilter.
I am pretty new in Spring Security and I am working on a Spring Boot project that uses Basic Authentication in order to protect some APIs. I am starting from an existing tutorial code (a Udemy course) trying to adapt it to my own use cases.
In this project I have this SecurityConfiguration used to configure the basic authentication.
#Configuration
#EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter
{
private static String REALM = "REAME";
private static final String[] USER_MATCHER = { "/api/utenti/cerca/**"};
private static final String[] ADMIN_MATCHER = { "/api/utenti/inserisci/**", "/api/utenti/elimina/**" };
#Override
protected void configure(HttpSecurity http) throws Exception
{
http.csrf().disable()
.authorizeRequests()
.antMatchers(USER_MATCHER).hasAnyRole("USER")
.antMatchers(ADMIN_MATCHER).hasAnyRole("ADMIN")
.anyRequest().authenticated()
.and()
.httpBasic().realmName(REALM).authenticationEntryPoint(getBasicAuthEntryPoint()).and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
#Bean
public AuthEntryPoint getBasicAuthEntryPoint()
{
return new AuthEntryPoint();
}
/* To allow Pre-flight [OPTIONS] request from browser */
#Override
public void configure(WebSecurity web)
{
web.ignoring().antMatchers(HttpMethod.OPTIONS, "/**");
}
#Bean
public BCryptPasswordEncoder passwordEncoder()
{
return new BCryptPasswordEncoder();
};
#Bean
#Override
public UserDetailsService userDetailsService()
{
UserBuilder users = User.builder();
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(users
.username("ReadUser")
.password(new BCryptPasswordEncoder().encode("BimBumBam_2018"))
.roles("USER").build());
manager.createUser(users
.username("Admin")
.password(new BCryptPasswordEncoder().encode("MagicaBula_2018"))
.roles("USER", "ADMIN").build());
return manager;
}
}
So from what I have understand:
Here it id defined the list of API that can be accessed by a nornmal user and the list of API that can be accessed by and admin user:
private static final String[] USER_MATCHER = { "/api/utenti/cerca/**"};
private static final String[] ADMIN_MATCHER = { "/api/utenti/inserisci/**", "/api/utenti/elimina/**" };
Into the previous configure() method basically it is stating that the API URL matching with the USER_MATCHER are accessible by logged user having role USER while API having URL matching ADMIN_MATCHER are accessible by logged user having role ADMIN. Is this interpretation correct?
Finnally the UserDetailsService bean simply define two users: one belonging to the USER "group" and the other one belonging to both the USER and ADMIN "group".
So, if I well understood, the first one will be aple only to access to the API having enpoint URL /api/utenti/cerca/** while the second one will be able to access also to the APIs having endpoint URLs /api/utenti/inserisci/** and /api/utenti/elimina/**
Is it my reasoning correct?
And now my doubt: into a controller class of this project I defined this method:
#RestController
#RequestMapping("api/users")
#Log
public class UserController {
#Autowired
UserService userService;
//#Autowired
//private BCryptPasswordEncoder passwordEncoder;
//#Autowired
//private ResourceBundleMessageSource errMessage;
#GetMapping(value = "/test", produces = "application/json")
public ResponseEntity<String> getTest() throws NotFoundException {
log.info(String.format("****** getTest() START *******"));
return new ResponseEntity<String>("TEST", HttpStatus.OK);
}
..............................................................................................................
..............................................................................................................
..............................................................................................................
}
As you can see this method handling a GET request toward the localhost:8019/api/users/test endpoint.
This endpoint URL is not in any of the previous two list related the protected endpoint (it is not into the USER_MATCHER list neither into the ADMIN_MATCHER list. So I expected that simply this endpoint was not protected and accessible to everyone. But performing the previous request using PostMan, I obtain this error message:
HTTP Status 401 : Full authentication is required to access this resource
So basically it seems to me that also if this endpoint not belong to any protected endpoint list it is in some way protected anyway (it seems to me that at least the user must be authenticated (infact trying both the previous user I can obtain the expected output, so it should mean that the endpoint is not protected by the user rule but it is protected againts not authenticated access).
Why? Maybe it depende by the previous configure() method settings, in particular this line?
.anyRequest().authenticated()
In case is it possible to disable in some way to implement something like this:
If a called endpoint belong to one of the previous two lists (USER_MATCHER and ADMIN_MATCHER) --> the user must be authenticated and need to have the correct role.
If a called endpoint not belong to one of the previous lists --> everybody can access, also not authenticated user.
This approach make sense or am I loosing something?
I take this occasion to ask you also another information: do you think that it is possible to configure Spring security of this specific project in order to protect some specific endpoints using the basic authentication and some other specific endpoints using the JWT authentication.
Sone further notes to explain why this last question. This project is a microservice that at the moment is used by another microservice (used to generate JWT token) in order to obtain user information. (the other microservice call an API of this project in order to receive user information so it can generate a JWT token that will be used in my application. The comunication between these 2 microservice must use basic authentication).
Since this project contains all the entity classes used to map the tables related to the users on my DB, my idea was to use this project also for generic user management, so it could include functionality like: add a brand new user, changes information of an existing user, obtain the list of all the users, search a specific user, and so on.
These new APIs will be protected by JWT token because each API can be called from a specific user type having different privileges on the system.
So I am asking if in a situation like this I can add without problem 2 different types of authentication (basic authentication for the API that retrieve a user so the other microservice can obtain this info) and JWT authentication for all the other APIs. It make sense or is it better to create a brand new project for a new user management microservice?
So, if I well understood, the first one will be aple only to access to the API having enpoint URL /api/utenti/cerca/** while the second one will be able to access also to the APIs having endpoint URLs /api/utenti/inserisci/** and /api/utenti/elimina/**
Yes.
Why? Maybe it depende by the previous configure() method settings, in particular this line?
Yes, when using .anyRequest().authenticated(), any requests that have not been matched will have to be authenticated.
If a called endpoint not belong to one of the previous lists --> everybody can access, also not authenticated user.
You can achieve this by doing anyRequest().permitAll(). But this is not so secure because you are allowing access to every other endpoints, instead you should stay with anyRequest().authenticated() and allow access to specific endpoints manually, like so:
http
.authorizeRequests()
.antMatchers(USER_MATCHER).hasAnyRole("USER")
.antMatchers(ADMIN_MATCHER).hasAnyRole("ADMIN")
.antMatchers("/api/users/test").permitAll()
.anyRequest().authenticated()
...
I am trying to familiarize myself with Spring Security, in particular migrating from Spring Security OAuth to Soring Security (as in the following example/guide https://github.com/spring-projects/spring-security/wiki/OAuth-2.0-Migration-Guide).
However, I am seeming to only get 403 Forbidden errors. I am accessing from Postman and am using my company's existing OAuth server. I am able to get a token from the auth server, so I know I have those credentials correct and I have verified what roles the OAuth user has.
I am using the following dependencies:
implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'
implementation 'org.springframework.boot:spring-boot-starter-web'
This is the simple endpoint I am attempting to access:
#RestController
public class AppController
{
#GetMapping("/hello")
public String hello()
{
return "hello";
}
}
This is my application.yml file:
spring:
security:
oauth2:
resourceserver:
jwt:
jwk-set-uri: <<company-website-here>>/uaa/oauth/token_keys
And this is my security configuration class:
#Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter
{
#Override
protected void configure(HttpSecurity http) throws Exception
{
http.authorizeRequests()
.antMatchers(HttpMethod.GET, "/hello").hasRole("MY_ROLE")
.anyRequest().authenticated()
.and()
.oauth2ResourceServer().jwt();
}
}
I can't seem to figure out why I seem to only get 403 errors. I have also tried adding #EnableWebSecurity to the security config class, but that didn't make a difference. Adding the auth server URL explicitly to the server and/or manually creating a JwtDecoder didn't do the trick either; it appears the url is being automatically picked up from the yml file, based on its property name.
I am trying to move away from using the org.springframework.security.oauth.boot dependency and ResourceServerConfigurerAdapter.
I had to add my own converter like so:
private static class JwtAuthenticationConverter implements Converter<Jwt, AbstractAuthenticationToken>
{
private final Converter<Jwt, Collection<GrantedAuthority>> jwtGrantedAuthoritiesConverter;
public JwtAuthenticationConverter()
{
this.jwtGrantedAuthoritiesConverter = jwt -> jwt
.getClaimAsStringList("authorities")
.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
}
#Override
public final AbstractAuthenticationToken convert(#NonNull Jwt jwt)
{
Collection<GrantedAuthority> authorities = jwtGrantedAuthoritiesConverter.convert(jwt);
return new JwtAuthenticationToken(jwt, authorities, jwt.getClaimAsString("client_id"));
}
}
Then had to add this to the main security config:
.jwtAuthenticationConverter(new JwtAuthenticationConverter());
There may be a couple of things happening.
As you're migrating to Spring Security 5, you may need to extract your authorities manually. Check this post and it's correct answer.
You are using hasRole function and this will append "ROLE_" before your authority/role. So if the role on your JWT token is not ROLE_JWT_ROLE you should use
hasTransaction.
I have a Spring boot REST service (spring-boot-starter-parent:1.3.2) that exposes some endpoints using RestController defined methods. I am also using Spring security. Everything works fine until I try to define a controller method that is mapped to "/images". When I try to access this api path I get the following error. By debugging I can see that my controller handler is being mapped, but the preauthorize filter is not being called (it is called properly for other mappings). I have set the following properties, but with no change. How do I fix this so that I can use "/images"?
spring.resources.add-mappings=false
spring.mvc.static-path-pattern=/hide-me/**
Error:
"exception": "org.springframework.security.authentication.AuthenticationCredentialsNotFoundException",
"message": "An Authentication object was not found in the SecurityContext",
Code:
#RestController
#PreAuthorize(value = "hasAnyAuthority('SOMEUSER')")
public class ImageController {
...
#RequestMapping(value = { "/images/{imageId}" }, method = RequestMethod.GET)
#ResponseBody
public Image getImage(#PathVariable UUID imageId) {
return imageDataService.getImage(imageId);
}
...
If I change the mapping to the following then it works just fine.
#RequestMapping(value = { "/image/{imageId}" }, method = RequestMethod.GET)
#ResponseBody
public Image getImage(#PathVariable UUID imageId) {
return imageDataService.getImage(imageId);
}
I'm thinking that the config for static resources has a default entry that tells Spring security to ignore the "/images" path for the preauth filter. I'm debugging around trying to figure out where that might be overridden.
SpringBoot by default use some paths
private static final String[] CLASSPATH_RESOURCE_LOCATIONS = {
"classpath:/META-INF/resources/", "classpath:/resources/",
"classpath:/static/", "classpath:/public/" };
https://spring.io/blog/2013/12/19/serving-static-web-content-with-spring-boot
And one of this paths is /images
Java Web Application. Spring Boot. Locating Images
Also you have the following restrictions when usind SpringSecurity
The basic features you get out of the box in a web application are:
An AuthenticationManager bean with in-memory store and a single user
(see SecurityProperties.User for the properties of the user). Ignored
(insecure) paths for common static resource locations (/css/,
/js/, /images/, /webjars/ and **/favicon.ico). HTTP Basic
security for all other endpoints. Security events published to
Spring’s ApplicationEventPublisher (successful and unsuccessful
authentication and access denied).
http://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/
Common low-level features (HSTS, XSS, CSRF, caching) provided by Spring Security are on by default.
You need to ensure, that security is done for every request. This can be done using the following SecurityConfiguration:
#Configuration
#EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().authenticated();
}
}
I've got java Spring Security app (built with jhipster) and I'm trying to add some unit tests that test logic based on the current authenticated user.
To do that i've configured my MockMvc object to use a web application context and spring security as so:
#Test
#Transactional
public void getAllContacts() throws Exception {
restContactMockMvc = MockMvcBuilders
.webAppContextSetup(context)
.apply(springSecurity())
.build();
restContactMockMvc.perform(get("/api/contacts")).andExpect(status().isOk());
}
The problem is that I've also configured the app to require HTTPS, and whenever I run this unit test I get a 302 redirect response since it appears to be trying to send an HTTP request instead of HTTPS.
The way I've enforced HTTPS is with the following line in SecurityConfiguration.configure(HttpSecurity http):
if (env.acceptsProfiles("!dev"))http.requiresChannel().anyRequest().requiresSecure();
So, my question is how can I get my unit test to run correctly? Probably either by sending a mock HTTPS request, or by disabling HTTPS in the case of running a unit test?
Using Springs MockMvc stuff you can also specify that you want to send a mock HTTPS request using the secure(true) method as follows:
restContactMockMvc.perform( get("/api/contacts").secure( true ) ).andExpect( status().isOk() )
I'm answering my own question with the fix that i've started using, in hopes that this can help others. I'm still open to other better solutions.
In SecurityConfiguration.configure(HttpSecurity http) method, i've modified the if statement for enforcing https to skip it if we are running an integrationTest:
if (env.acceptsProfiles("!dev") && !((StandardEnvironment) env).getPropertySources().contains("integrationTest")) {
http.requiresChannel().anyRequest().requiresSecure();
}
In addition to the correct answer of #rhinds you can also add this code to your #Before method to have all calls be secure by default:
import static org.springframework.test.web.servlet.setup.MockMvcBuilders.standaloneSetup;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.testSecurityContext;
public class YourTestClass {
#Autowired
private Filter springSecurityFilterChain;
#Autowired
private SecurityTestController securityTestController;
private MockMvc mockMvc;
#Before
public void setup() {
mockMvc = standaloneSetup(securityTestController)
.addFilters(springSecurityFilterChain)
.defaultRequest(get("/").secure(true).with(testSecurityContext()))
.build();
}
// Your Tests Here
#Test
public void httpsRedirect() throws Exception {
mockMvc.perform(get("/").secure(false))
.andExpect(status().isFound())
.andExpect(result -> StringUtils.isNotEmpty(result.getResponse().getHeader("Location")))
.andExpect(result -> result.getResponse().getHeader("Location").startsWith("https://"));
}
}
With a bad configuration, Spring Security can change response status to "302 FOUND". Double check your security config.
#Bean
#Throws(Exception::class)
fun authTokenFilterBean(): CustomAuthTokenFilter {
val authTokenFilter = CustomAuthTokenFilter()
// if you don't have these lines, add them:
authTokenFilter.setAuthenticationSuccessHandler(MyAuthSuccessHandler())
authTokenFilter.setAuthenticationFailureHandler(MyAuthFailureHandler())
return authTokenFilter
}
where
CustomAuthTokenFilter is your custom implementation of AbstractAuthenticationProcessingFilter;
MyAuthSuccessHandler and MyAuthFailureHandler are handlers provided by you. They can be empty, or they can contain your custom logic.