I have added JWT Authentication using Auth0 to my Spring Boot REST API following this example.
Now, as expected, my previously working Controller unit tests give a response code of401 Unauthorized rather than 200 OK as I am not passing any JWT in the tests.
How can I mock the JWT/Authentication part of my REST Controller tests?
Unit test class
#AutoConfigureMockMvc
public class UserRoundsControllerTest extends AbstractUnitTests {
private static String STUB_USER_ID = "user3";
private static String STUB_ROUND_ID = "7e3b270222252b2dadd547fb";
#Autowired
private MockMvc mockMvc;
private Round round;
private ObjectId objectId;
#BeforeEach
public void setUp() {
initMocks(this);
round = Mocks.roundOne();
objectId = Mocks.objectId();
}
#Test
public void shouldGetAllRoundsByUserId() throws Exception {
// setup
given(userRoundService.getAllRoundsByUserId(STUB_USER_ID)).willReturn(
Collections.singletonList(round));
// mock the rounds/userId request
RequestBuilder requestBuilder = Requests.getAllRoundsByUserId(STUB_USER_ID);
// perform the requests
MockHttpServletResponse response = mockMvc.perform(requestBuilder)
.andReturn()
.getResponse();
// asserts
assertNotNull(response);
assertEquals(HttpStatus.OK.value(), response.getStatus());
}
//other tests
}
Requests class (used above)
public class Requests {
private Requests() {}
public static RequestBuilder getAllRoundsByUserId(String userId) {
return MockMvcRequestBuilders
.get("/users/" + userId + "/rounds/")
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON);
}
}
Spring Security Config
/**
* Configures our application with Spring Security to restrict access to our API endpoints.
*/
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Value("${auth0.audience}")
private String audience;
#Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}")
private String issuer;
#Override
public void configure(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.cors().and().csrf().disable().sessionManagement().
sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeRequests()
.mvcMatchers(HttpMethod.GET, "/users/**").authenticated()
.mvcMatchers(HttpMethod.POST, "/users/**").authenticated()
.mvcMatchers(HttpMethod.DELETE, "/users/**").authenticated()
.mvcMatchers(HttpMethod.PUT, "/users/**").authenticated()
.and()
.oauth2ResourceServer().jwt();
}
#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;
}
#Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("*"));
configuration.setAllowedMethods(Arrays.asList("*"));
configuration.setAllowedHeaders(Arrays.asList("*"));
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
Abstract Unit test class
#ExtendWith(SpringExtension.class)
#SpringBootTest(
classes = PokerStatApplication.class,
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT
)
public abstract class AbstractUnitTests {
// mock objects etc
}
If I understand correctly your case there is one of the solutions.
In most cases, JwtDecoder bean performs token parsing and validation if the token exists in the request headers.
Example from your configuration:
#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;
}
So for the tests, you need to add stub of this bean and also for replacing this bean in spring context, you need the test configuration with it.
It can be some things like this:
#TestConfiguration
public class TestSecurityConfig {
static final String AUTH0_TOKEN = "token";
static final String SUB = "sub";
static final String AUTH0ID = "sms|12345678";
#Bean
public JwtDecoder jwtDecoder() {
// This anonymous class needs for the possibility of using SpyBean in test methods
// Lambda cannot be a spy with spring #SpyBean annotation
return new JwtDecoder() {
#Override
public Jwt decode(String token) {
return jwt();
}
};
}
public Jwt jwt() {
// This is a place to add general and maybe custom claims which should be available after parsing token in the live system
Map<String, Object> claims = Map.of(
SUB, USER_AUTH0ID
);
//This is an object that represents contents of jwt token after parsing
return new Jwt(
AUTH0_TOKEN,
Instant.now(),
Instant.now().plusSeconds(30),
Map.of("alg", "none"),
claims
);
}
}
For using this configuration in tests just pick up this test security config:
#SpringBootTest(classes = TestSecurityConfig.class)
Also in the test request should be authorization header with a token like Bearer .. something.
Here is an example regarding your configuration:
public static RequestBuilder getAllRoundsByUserId(String userId) {
return MockMvcRequestBuilders
.get("/users/" + userId + "/rounds/")
.accept(MediaType.APPLICATION_JSON)
.header(HttpHeaders.AUTHORIZATION, "Bearer token"))
.contentType(MediaType.APPLICATION_JSON);
}
For me, I made it pretty simple.
I don't want to actually check for the JWT token, this can also be mocked.
Have a look at this security config.
#Override
public void configure(HttpSecurity http) throws Exception {
//#formatter:off
http
.cors()
.and()
.authorizeRequests()
.antMatchers("/api/v1/orders/**")
.authenticated()
.and()
.authorizeRequests()
.anyRequest()
.denyAll()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.oauth2ResourceServer()
.jwt();
Then in my test, I make use of two thing
Provide a mock bean for the jwtDecoder
Use the SecurityMockMvcRequestPostProcessors to mock the JWT in the request. This is available in the following dependency
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
And Here is how it's done.
#SpringBootTest
#AutoConfigureMockMvc
public class OrderApiControllerIT {
#Autowired
protected MockMvc mockMvc;
#MockBean
private JwtDecoder jwtDecoder;
#Test
void testEndpoint() {
MvcResult mvcResult = mockMvc.perform(post("/api/v1/orders")
.with(SecurityMockMvcRequestPostProcessors.jwt())
.content(jsonString)
.contentType(MediaType.APPLICATION_JSON)
)
.andDo(print())
.andExpect(status().is2xxSuccessful())
.andReturn();
}
That's it and it should work.
For others like me, who after gathering information from what seems like a gazillion StackOverlow answers on how to do this, here is the summary of what ultimately worked for me (using Kotlin syntax, but it is applicable to Java as well):
Step 1 - Define a custom JWT decoder to be used in tests
Notice the JwtClaimNames.SUB entry - this is the user name which will ultimately be accessible via authentication.getName() field.
val jwtDecoder = JwtDecoder {
Jwt(
"token",
Instant.now(),
Instant.MAX,
mapOf(
"alg" to "none"
),
mapOf(
JwtClaimNames.SUB to "testUser"
)
)
}
Step 2 - Define a TestConfiguration
This class goes in your test folder. We do this to replace real implementation with a stub one which always treats the user as authenticated.
Note that we are not done yet, check Step 3 as well.
#TestConfiguration
class TestAppConfiguration {
#Bean // important
fun jwtDecoder() {
// Initialize JWT decoder as described in step 1
// ...
return jwtDecoder
}
}
Step 3 - Update your primary configuration to avoid bean conflict
Without this change your test and production beans would clash, resulting in a conflict. Adding this line delays the resolution of the bean and lets Spring prioritise test bean over production one.
There is a caveat, however, as this change effectively removes bean conflict protection in production builds for JwtDecoder instances.
#Configuration
class AppConfiguration {
#Bean
#ConditionalOnMissingBean // important
fun jwtDecoder() {
// Provide decoder as you would usually do
}
}
Step 4 - Import TestAppConfiguration in your test
This makes sure that your test actually takes TestConfiguration into account.
#SpringBootTest
#Import(TestAppConfiguration::class)
class MyTest {
// Your tests
}
Step 5 - Add #WithMockUser annotation to your test
You do not really need to provide any arguments to the annotation.
#Test
#WithMockUser
fun myTest() {
// Test body
}
Step 6 - Provide Authentication header during the test
mockMvc
.perform(
post("/endpointUnderTest")
.header(HttpHeaders.AUTHORIZATION, "Bearer token") // important
)
.andExpect(status().isOk)
SecurityConfig bean can be loaded conditionally as,
#Configuration
#EnableWebSecurity
public class SecurityConfig {
#Bean
#Profile("!test")
public WebSecurityConfigurerAdapter securityEnabled() {
return new WebSecurityConfigurerAdapter() {
#Override
protected void configure(HttpSecurity http) throws Exception {
// your code goes here
}
};
}
#Bean
#Profile("test")
public WebSecurityConfigurerAdapter securityDisabled() {
return new WebSecurityConfigurerAdapter() {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().permitAll();
}
};
}
}
So this bean won't be initialized in case of test profile. It means now security is disabled and all endpoints are accessible without any authorization header.
Now "test" profile needs to be active in case of running the tests, this can be done as,
#RunWith(SpringRunner.class)
#ActiveProfiles("test")
#WebMvcTest(UserRoundsController.class)
public class UserRoundsControllerTest extends AbstractUnitTests {
// your code goes here
}
Now this test is going to run with profile "test".
Further if you want to have any properties related to this test, that can be put under src/test/resources/application-test.properties.
Hope this helps! please let me know otherwise.
Update:
Basic idea is to disable security for test profile. In previous code, even after having profile specific bean, default security was getting enabled.
You can get the Bearer token and pass it on as a HTTP Header. Below is a sample snippet of the Test Method for your reference,
#Test
public void existentUserCanGetTokenAndAuthentication() throws Exception {
String username = "existentuser";
String password = "password";
String body = "{\"username\":\"" + username + "\", \"password\":\"
+ password + "\"}";
MvcResult result = mvc.perform(MockMvcRequestBuilders.post("/token")
.content(body))
.andExpect(status().isOk()).andReturn();
String response = result.getResponse().getContentAsString();
response = response.replace("{\"access_token\": \"", "");
String token = response.replace("\"}", "");
mvc.perform(MockMvcRequestBuilders.get("/users/" + userId + "/rounds")
.header("Authorization", "Bearer " + token))
.andExpect(status().isOk());
}
I am using the JwtAuthenticationToken from the Security Context. The #WithMockUser annotation is creating a Username-based Authentication Token.
I wrote my own implementation of #WithMockJwt:
#Target({ElementType.METHOD, ElementType.TYPE})
#Retention(RetentionPolicy.RUNTIME)
#Inherited
#Documented
#WithSecurityContext(factory = WithMockJwtSecurityContextFactory.class)
public #interface WithMockJwt {
long value() default 1L;
String[] roles() default {};
String email() default "ex#example.org";
}
And the related factory:
public class WithMockJwtSecurityContextFactory implements WithSecurityContextFactory<WithMockJwt> {
#Override
public SecurityContext createSecurityContext(WithMockJwt annotation) {
val jwt = Jwt.withTokenValue("token")
.header("alg", "none")
.claim("sub", annotation.value())
.claim("user", Map.of("email", annotation.email()))
.build();
val authorities = AuthorityUtils.createAuthorityList(annotation.roles());
val token = new JwtAuthenticationToken(jwt, authorities);
SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(token);
return context;
}
}
And now I can annotate test with:
#Test
#WithMockJwt
void test() {
...omissis...
create application.properties in test/resources (it will override main but for test stage only)
turnoff security by specifyig:
security.ignored=/**
security.basic.enable= false
spring.autoconfigure.exclude= org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration
Related
So I have multiple Controller classes exposing a number of endpoints in my service. One of these controllers is exposing endpoints used for webhooks. I want to expose all the endpoints in the webhooks Controller class and secure them with one WebSecurityConfigurerAdapter configuration and then all the other endpoints which stretch across a few other Controller classes be configured with a separate WebSecurityConfigurerAdapter configuration. The webhooks will be accessed by third party vendors and want them using a separate auth0 audience from the other endpoints which will be accessed internally. I have it setup now as follows but it doesn't seem to be working. In order for the two configurations to co-exist they need to have an #Order annotation assigned to their configure methods and these int values must be unique. But what is happening is all requests coming in seem to go to the Order(1) configuration first but are never making it to Order(2) if they are of the pattern described in my Order(2) config. Maybe that's not how it's supposed to work or maybe my implementation is incorrect. But when I pass in a request in a "/webhooks/" endpoint it always gives me a 401 error and then when I change that configuration to Order(1) it starts to work. And this behavior happens the other way as well. When I make the "/webhooks/" patter the first order I get a 401 for all the other requests because they are not making it to the next Order. Here is my code...
#EnableWebSecurity
public class SecurityConfig {
#Configuration
public static class ApiSecurityConfig extends WebSecurityConfigurerAdapter {
#Value("${auth0.api-audience}")
private String audience;
#Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}")
private String issuer;
#Override
#Order(1)
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
// in dev if you want to bypass auth you can change /ping to /*
.mvcMatchers(HttpMethod.GET, "/actuator/**", "/test").permitAll()
.mvcMatchers("/api/**")
.authenticated()
.and()
.cors()
.configurationSource(corsConfigurationSource())
.and()
.oauth2ResourceServer()
.jwt()
.decoder(jwtDecoder(audience, issuer));
// disable cors and csrf when running locally
if (getApplicationContext().getEnvironment().acceptsProfiles(Profiles.of("local"))) {
http.cors().and().csrf().disable();
}
}
static JwtDecoder jwtDecoder(String audience, String issuer) {
OAuth2TokenValidator<Jwt> withAudience = new AudienceValidator(audience);
OAuth2TokenValidator<Jwt> withBrand = new BrandValidator(audience);
OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer(issuer);
OAuth2TokenValidator<Jwt> validator = new DelegatingOAuth2TokenValidator<>(withAudience, withBrand, withIssuer);
NimbusJwtDecoder jwtDecoder = JwtDecoders.fromOidcIssuerLocation(issuer);
jwtDecoder.setJwtValidator(validator);
return jwtDecoder;
}
}
#Configuration
#Order(2)
public static class WebhookSecurityConfig extends WebSecurityConfigurerAdapter {
#Value("${auth0.webhook-audience}")
private String audience;
#Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}")
private String issuer;
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.mvcMatchers("/webhooks/**")
.authenticated()
.and()
.cors()
.configurationSource(corsConfigurationSource())
.and()
.oauth2ResourceServer()
.jwt()
.decoder(jwtDecoder(audience, issuer));
// disable cors and csrf when running locally
if (getApplicationContext().getEnvironment().acceptsProfiles(Profiles.of("local"))) {
http.cors().and().csrf().disable();
}
}
JwtDecoder jwtDecoder(String audience, String issuer) {
OAuth2TokenValidator<Jwt> withAudience = new AudienceValidator(audience);
OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer(issuer);
OAuth2TokenValidator<Jwt> validator = new DelegatingOAuth2TokenValidator<>(withAudience, withIssuer);
NimbusJwtDecoder jwtDecoder = JwtDecoders.fromOidcIssuerLocation(issuer);
jwtDecoder.setJwtValidator(validator);
return jwtDecoder;
}
}
static CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedMethods(List.of(
HttpMethod.GET.name(),
HttpMethod.PUT.name(),
HttpMethod.POST.name(),
HttpMethod.DELETE.name()
));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration.applyPermitDefaultValues());
return source;
}
}
In addition to the behavior I am seeing I am also seeing on startup of the Spring application...
2022-04-18 16:42:00,616 INFO org.springframework.security.web.DefaultSecurityFilterChain : Will not secure any request
2022-04-18 16:42:00,618 INFO org.springframework.security.web.DefaultSecurityFilterChain : Will not secure any request
I have a spring cloud gateway application which routes requests to another service. Another service defines contracts which are imported as stubs by spring cloud gateway application in tests.
Now I would like to have contract tests in my gateway that will consume the stubs of another service. The problem is that I do not know how to inject the StubRunnerPort as property/environment so it can be picked by my configuration class and configure the routes accordingly :
Api gateway routes configuration
#Configuration
class GatewayConfig {
#Value("${subscriptions.url}")
private String subscriptionsUrl;
#Autowired
private TokenRelayGatewayFilterFactory tokenFilterFactory;
#Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http.csrf(ServerHttpSecurity.CsrfSpec::disable);
return http.build();
}
#Bean
RouteLocator routeLocator(final RouteLocatorBuilder routeLocatorBuilder) {
return routeLocatorBuilder.routes()
.route("subscriptions", subscriptionsRoute())
.build();
}
private Function<PredicateSpec, Buildable<Route>> subscriptionsRoute() {
return spec -> spec
.path("/subscriptions/**")
.filters(s -> s.filter(tokenFilterFactory.apply()).prefixPath("/v1"))
.uri(subscriptionsUrl);
}
}
And the test class :
#SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = {PnApiGatewayApp.class})
#AutoConfigureStubRunner(ids = "io.mkrzywanski:subscription-app:+:stubs", stubsMode = StubRunnerProperties.StubsMode.CLASSPATH)
#ActiveProfiles("test")
class SubscriptionSpec {
private WebTestClient webClient;
#LocalServerPort
private int port;
#StubRunnerPort("io.mkrzywanski:subscription-app")
private int stubRunnerPort;
#Autowired
ConfigurableEnvironment environment;
#BeforeEach
void setup() {
String baseUri = "http://localhost:" + port;
this.webClient = WebTestClient.bindToServer()
.responseTimeout(Duration.ofSeconds(10))
.baseUrl(baseUri).build();
}
#Test
void test() {
String body = "{\"userId\":\"22e90bbd-7399-468a-9b76-cf050ff16c63\",\"itemSet\":[{\"value\":\" Rainbow Six\"}]}";
var response = webClient.post()
.uri("/subscriptions")
.header("Authorization", "Bearer xxx")
.header("Content-type", MediaType.APPLICATION_JSON_VALUE)
.bodyValue(body)
.exchange()
.expectStatus().isCreated()
.expectBody(String.class)
.value(Matchers.equalTo("{\"subscriptionId : \"6d692849-58fd-439b-bb2c-50a5d3669fa9\"\"}"));
}
Ideally I would like to have subscriptions.url property set after stub runner is configured but before my gateway configuration is picked by Spring so that url redirects will work.
I have already tried to use ApplicationContextInitializer but it seems that StubRunnerPort is not configured yet, when instance of initializer is launched.
So the question is - how to get stub runner port and use it to inject it into other services url, so that gateway would route the requests to the stub runner in tests?
Solution 1
This can be achieved by using application-test.yml file with defined url property that makes use of substitution. application-test.yml file. The approach is described here :
subscriptions:
url: http://localhost:${stubrunner.runningstubs.io.mkrzywanski.subscription-app.port}
Here stubrunner.runningstubs.io.mkrzywanski.subscription-app.port will be available as Stub port so it can be substituted. No configuration changes are reuqired.
Solution 2 (requires more code)
I made it work by creating a test configuration which extends configuration that contains url properties and RouteLocator configuration and has a dependency on batchStubRunner bean :
#DependsOn("batchStubRunner")
#EnableAutoConfiguration
#Import(LoggingFilter.class)
class GatewayTestConfig extends GatewayConfig implements InitializingBean {
#Autowired
ConfigurableEnvironment environment;
#Override
public void afterPropertiesSet() {
this.subscriptionsUrl = "http://localhost:" + environment.getProperty("stubrunner.runningstubs.io.mkrzywanski.subscription-app.port");
}
}
The key points here are :
the configuration is run only after batchStubRunner bean is available so the port of StrubRunner can be found in the environment
The configuration implements InitializingBean so I am able to override the subscriptionsUrl which is now protected in parent configuration
After subscriptionsUrl is overriden - it can be used to configure RouteLocator bean from parent configuration.
The test looks like this now :
#SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = {GatewayTestConfig.class})
#AutoConfigureStubRunner(ids = "io.mkrzywanski:subscription-app:+:stubs", stubsMode = StubRunnerProperties.StubsMode.CLASSPATH)
#ActiveProfiles("test")
class SubscriptionSpec {
private WebTestClient webClient;
#LocalServerPort
private int port;
#BeforeEach
void setup() {
String baseUri = "http://localhost:" + port;
this.webClient = WebTestClient.bindToServer()
.responseTimeout(Duration.ofSeconds(10))
.baseUrl(baseUri).build();
}
#Test
void shouldRouteToSubscriptions() {
String body = "{\"userId\":\"22e90bbd-7399-468a-9b76-cf050ff16c63\",\"itemSet\":[{\"value\":\"Rainbow Six\"}]}";
webClient.post()
.uri("/subscriptions")
.header("Accept", MediaType.APPLICATION_JSON_VALUE)
.header("Authorization", "Bearer xxx")
.header("Content-type", MediaType.APPLICATION_JSON_VALUE)
.bodyValue(body)
.exchange()
.expectStatus().isCreated()
.expectBody()
.jsonPath("$.subscriptionId").exists()
.jsonPath("$.subscriptionId").value(IsUUID.UUID());
}
}
I am using JWT based authentication in my Spring project and I was trying to write JUnit Test cases for the secured end-points though I am not able to test it and getting
org.opentest4j.AssertionFailedError:
Expected :201
Actual :403
My Security Config file is :
#Configuration
#EnableWebSecurity(debug = true)
#EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class ScopeMapping extends WebSecurityConfigurerAdapter {
#Autowired
XsuaaServiceConfiguration xsuaaServiceConfiguration;
private static final String VT_SCOPE = "VT";
private static final String DEVELOPER_SCOPE = "Developer";
private static final String USER_SCOPE = "User";
private static final String ADMIN_SCOPE = "Admin";
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http
.headers()
.contentSecurityPolicy("script-src 'self' https://trustedscripts.example.com; object-src https://trustedplugins.example.com; report-uri /csp-report-endpoint/");
http
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.mvcMatchers(HttpMethod.GET,"/health").permitAll()
.mvcMatchers("/v2/api-docs/**", "/swagger-ui.html", "/swagger-ui/**", "/swagger-resources/**").permitAll()
.mvcMatchers("/v1/**").hasAuthority(VT_SCOPE)
.mvcMatchers("/v2/**").permitAll()
.mvcMatchers("/user/**").hasAnyAuthority(VT_SCOPE, ADMIN_SCOPE, USER_SCOPE, DEVELOPER_SCOPE)
.mvcMatchers("/admin/**").hasAnyAuthority(VT_SCOPE, ADMIN_SCOPE, DEVELOPER_SCOPE)
.mvcMatchers("/vt/**").hasAnyAuthority(VT_SCOPE, DEVELOPER_SCOPE)
.anyRequest().denyAll()
.and()
.oauth2ResourceServer()
.jwt()
.jwtAuthenticationConverter(getJwtAuthenticationConverter());
}
Converter<Jwt, AbstractAuthenticationToken> getJwtAuthenticationConverter() {
TokenAuthenticationConverter converter = new TokenAuthenticationConverter(xsuaaServiceConfiguration);
converter.setLocalScopeAsAuthorities(true);
return converter;
}
}
Test-case
#WithUserDetails(value="ram",userDetailsServiceBeanName = "secureBean")
public void addForumTest() throws Exception {
// ForumQuery forumQuery = new ForumQuery("1","Query 1","Query 1 Desc","1",currentTime,replies,forumLikes);
ForumQueryDto forumQueryDto = new ForumQueryDto("1","Query 1","Query 1 Desc",userDto,currentTime,repliesDto);
Mockito.when(forumService.insertQuery(Mockito.any(ForumQueryDto.class))).thenReturn(forumQueryDto);
MvcResult result = mockMvc.perform(MockMvcRequestBuilders.post("/user/forum/add")
.contentType("application/json")
// .header(HttpHeaders.AUTHORIZATION,<JWT-TOKEN>)
.content(objectMapper.writeValueAsString(forumQueryDto)))
.andReturn();
MockHttpServletResponse response = result.getResponse();
assertEquals(HttpStatus.CREATED.value(), response.getStatus());
}
Custom-Bean
#Bean("secureBean")
public UserDetailsService userDetailsService() {
GrantedAuthority authority = new SimpleGrantedAuthority("VT_SCOPE");
GrantedAuthority authority1 = new SimpleGrantedAuthority("ADMIN_SCOPE");
GrantedAuthority authority2 = new SimpleGrantedAuthority("USER_SCOPE");
GrantedAuthority authority3 = new SimpleGrantedAuthority("DEVELOPER_SCOPE");
UserDetails userDetails = (UserDetails) new User("ram", "ram123", Arrays.asList(authority
, authority1, authority2, authority3));
return new InMemoryUserDetailsManager(Arrays.asList(userDetails));
}
Please take a look here. Spring security comes with proper testing support. I have shared link for section dedicated to test support for Security.
Example:
#Test
#WithMockUser(username="admin",roles={"USER","ADMIN"})
public void getMessageWithMockUserCustomUser() {
String message = messageService.getMessage();
...
}
This is how you apply test annotations on a class.
#RunWith(SpringJUnit4ClassRunner.class)
#ContextConfiguration
#WithMockUser(username="admin",roles={"USER","ADMIN"})
public class WithMockUserTests {
}
In the security configuration you have specified that any endpoints that match "/user/**" must have the authority "VT", "Admin", "User" or "Developer".
.mvcMatchers("/user/**").hasAnyAuthority(VT_SCOPE, ADMIN_SCOPE, USER_SCOPE, DEVELOPER_SCOPE)
However, in the test, the request is made with a user that has the authorities "VT_SCOPE", "ADMIN_SCOPE", "USER_SCOPE" and "DEVELOPER_SCOPE".
None of these authorities match the required authorities, which is why you see a 403 response.
Note that you can more robustly test JWT authentication, using the built-in jwt() RequestPostProcessor.
mvc
.perform(post("/user/forum/add")
.with(jwt().authorities(new SimpleGrantedAuthority("SCOPE_example"))));
You can find extensive details and examples in the Spring Security reference documentation.
I am trying to get role claims from an OAuth2AuthenticationToken to be detected as Spring Security authorities. There is a custom role defined on OIDC provider side (Azure AD in my case) that is nested inside the DefaultOidcUser, but not added automatically to the authorities:
I tried to extract them from the Jwt Token like this
However, when I do that, neither of the following methods is called (neither during login, nor later, even in the default configuration):
JwtGrantedAuthoritiesConverter.convert(Jwt)
JwtAuthenticationConverter.convert(Jwt)
JwtAuthenticationConverter.extractAuthorities(Jwt)
My current configuration is:
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
#Import(SecurityProblemSupport.class)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Override
public void configure(HttpSecurity http) throws Exception {
// #formatter:off
http
.csrf()
<some more config that has nothing to do with oauth/oidc>
.and()
.oauth2Login()
.and()
.oauth2ResourceServer()
.jwt()
.jwtAuthenticationConverter(jwtAuthenticationConverter())
.and()
.and()
.oauth2Client()
;
}
private JwtAuthenticationConverter jwtAuthenticationConverter() {
// create a custom JWT converter to map the roles from the token as granted authorities
JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName("roles");
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter);
return jwtAuthenticationConverter;
}
}
I also tried with a
CustomJwtAuthConverter implements Converter<Jwt, AbstractAuthenticationToken>
but to no avail.
Any help would be appreciated.
Managed to achieve it using an authorities mapper that also extracts claims from the oidcToken:
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
[...]
#Bean
public GrantedAuthoritiesMapper userAuthoritiesMapper() {
return authorities -> {
Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
authorities.forEach(
authority -> {
// Check for OidcUserAuthority because Spring Security 5.2 returns
// each scope as a GrantedAuthority, which we don't care about.
if (authority instanceof OidcUserAuthority) {
OidcUserAuthority oidcUserAuthority = (OidcUserAuthority) authority;
mappedAuthorities.addAll(SecurityUtils.extractAuthorityFromClaims(oidcUserAuthority.getUserInfo().getClaims()));
mappedAuthorities.addAll(SecurityUtils.extractAuthorityFromClaims(oidcUserAuthority.getIdToken().getClaims()));
}
}
);
return mappedAuthorities;
};
}
}
and inside SecurityUtils:
public static List<GrantedAuthority> extractAuthorityFromClaims(Map<String, Object> claims) {
return mapRolesToGrantedAuthorities(getRolesFromClaims(claims));
}
private static List<GrantedAuthority> mapRolesToGrantedAuthorities(Collection<String> roles) {
return roles.stream().filter(role -> role.startsWith("ROLE_")).map(SimpleGrantedAuthority::new).collect(Collectors.toList());
}
Afterwards the custom role should be present in mappedAuthorities and with it in the authorities of the token. Thus making the annotations "hasRole" and "hasAuthority" possible to use.
This is the solution I found when using keycloak.
#EnableGlobalMethodSecurity(securedEnabled = true)
#EnableWebSecurity(debug = true)
public class SecurityConfiguration {
private static final String REALM = "realm_access";
private static final String ROLES = "roles";
/**
* Configuration For oauth
*/
#Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests((authorize) -> authorize
.anyRequest().authenticated()
)
.oauth2ResourceServer()
.jwt()
.jwtAuthenticationConverter(source -> new CustomJwtConfigure().convert(source));
return http.build();
}
public static class CustomJwtConfigure implements Converter<Jwt, JwtAuthenticationToken> {
#Override
public JwtAuthenticationToken convert(Jwt jwt) {
var tokenAttributes = jwt.getClaims();
var jsonObject = (JSONObject) tokenAttributes.get(REALM);
var roles = (JSONArray) jsonObject.get(ROLES);
List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
roles.forEach(role -> grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_" + role)));
return new JwtAuthenticationToken(jwt, grantedAuthorities);
}
}
}
Link to example https://github.com/kesaven8/resourceServer-spring-boot
2022 update
I maintain a set of tutorials and samples to configure resource-servers security for:
both servlet and reactive applications
decoding JWTs and introspecting access-tokens
default or custom Authentication implementations
any OIDC authorization-server(s), including Keycloak of course (most samples support multiple realms / identity-providers)
The repo also contains a set of libs published on maven-central to:
mock OAuth2 identities during unit and integration tests (with authorities and any OpenID claim, including private ones)
configure resource-servers from properties file (including source claims for roles, roles prefix and case processing, CORS configuration, session-management, public routes and more)
Sample for a servlet with JWT decoder
#EnableMethodSecurity(prePostEnabled = true)
#Configuration
public class SecurityConfig {}
com.c4-soft.springaddons.security.issuers[0].location=https://localhost:8443/realms/master
com.c4-soft.springaddons.security.issuers[0].authorities.claims=realm_access.roles,resource_access.spring-addons-public.roles,resource_access.spring-addons-confidential.roles
com.c4-soft.springaddons.security.cors[0].path=/sample/**
<dependency>
<groupId>com.c4-soft.springaddons</groupId>
<artifactId>spring-addons-webmvc-jwt-resource-server</artifactId>
<version>6.0.3</version>
</dependency>
No, nothing more requried.
Unit-tests with mocked authentication
Secured #Component without http request (#Service, #Repository, etc.)
#Import({ SecurityConfig.class, SecretRepo.class })
#AutoConfigureAddonsSecurity
class SecretRepoTest {
// auto-wire tested component
#Autowired
SecretRepo secretRepo;
#Test
void whenNotAuthenticatedThenThrows() {
// call tested components methods directly (do not use MockMvc nor WebTestClient)
assertThrows(Exception.class, () -> secretRepo.findSecretByUsername("ch4mpy"));
}
#Test
#WithMockJwtAuth(claims = #OpenIdClaims(preferredUsername = "Tonton Pirate"))
void whenAuthenticatedAsSomeoneElseThenThrows() {
assertThrows(Exception.class, () -> secretRepo.findSecretByUsername("ch4mpy"));
}
#Test
#WithMockJwtAuth(claims = #OpenIdClaims(preferredUsername = "ch4mpy"))
void whenAuthenticatedWithSameUsernameThenReturns() {
assertEquals("Don't ever tell it", secretRepo.findSecretByUsername("ch4mpy"));
}
}
Secured #Controller (sample for #WebMvcTest but works for #WebfluxTest too)
#WebMvcTest(GreetingController.class) // Use WebFluxTest or WebMvcTest
#AutoConfigureAddonsWebSecurity // If your web-security depends on it, setup spring-addons security
#Import({ SecurityConfig.class }) // Import your web-security configuration
class GreetingControllerAnnotatedTest {
// Mock controller injected dependencies
#MockBean
private MessageService messageService;
#Autowired
MockMvcSupport api;
#BeforeEach
public void setUp() {
when(messageService.greet(any())).thenAnswer(invocation -> {
final JwtAuthenticationToken auth = invocation.getArgument(0, JwtAuthenticationToken.class);
return String.format("Hello %s! You are granted with %s.", auth.getName(), auth.getAuthorities());
});
when(messageService.getSecret()).thenReturn("Secret message");
}
#Test
void greetWitoutAuthentication() throws Exception {
api.get("/greet").andExpect(status().isUnauthorized());
}
#Test
#WithMockAuthentication(authType = JwtAuthenticationToken.class, principalType = Jwt.class, authorities = "ROLE_AUTHORIZED_PERSONNEL")
void greetWithDefaultMockAuthentication() throws Exception {
api.get("/greet").andExpect(content().string("Hello user! You are granted with [ROLE_AUTHORIZED_PERSONNEL]."));
}
}
Advanced use-cases
The most advanced tutorial demoes how to define a custom Authentication implementation to parse (and expose to java code) any private claim into things that are security related but not roles (in the sample it's grant delegation between users).
It also shows how to extend spring-security SpEL to build a DSL like:
#GetMapping("greet/on-behalf-of/{username}")
#PreAuthorize("is(#username) or isNice() or onBehalfOf(#username).can('greet')")
public String getGreetingFor(#PathVariable("username") String username) {
return ...;
}
I added spring security to the spring boot application and I have some api end points that needs to be called no matter user login or not.(I mean these are the rest end points where I need to retrieve data in my front side angular).
So,I config it as:
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(securedEnabled = true, proxyTargetClass = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private UserDetailsService customUserDetailsService;
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth
.userDetailsService(customUserDetailsService)
.passwordEncoder(passwordEncoder());
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().
disable()
.authorizeRequests()
.antMatchers(HttpMethod.OPTIONS, "/**")
.permitAll()
.antMatchers("/books").permitAll()
.antMatchers("/api/v1/search/**").permitAll()
.antMatchers("/admin/**").hasRole("ADMIN")
.anyRequest()
.authenticated()
.and()
.httpBasic();
}
}
I have all the api exposed from : http://localhost:8080/api/v1/ like:
http://localhost:8080/api/v1/books
http://localhost:8080/api/v1/bookcategory
I have configured using .antMatchers("/api/v1/search/**"),and my config for restendpoint is:
#RequestMapping("/api/v1")
#RestController
#CrossOrigin(origins ="http://localhost:4200")
public class BasicAuthController {
#GetMapping(path = "/basicauth")
public AuthenticationBean basicauth() {
System.out.println("hitted here");
return new AuthenticationBean("You are authenticated");
}
}
I allowed the csfr policy using:
#Configuration
public class RepositoryConfig implements RepositoryRestConfigurer{
#Autowired
private EntityManager entityManager;
#Override
public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) {
config.exposeIdsFor(entityManager.getMetamodel().getEntities().stream()
.map(Type::getJavaType).toArray(Class[]::new));
//to handle cross origin
config.getCorsRegistry().addMapping("/**").allowedOrigins("http://localhost:4200");
}
}
BookRepository.java
public interface BookRepository extends JpaRepository<Book,Long> {
#RestResource(path = "categoryid")
Page<Book> findByCategoryId(#Param("id") Long id,Pageable pageable);
//to get book by searching
#RestResource(path = "searchbykeyword")
Page<Book> findByNameContaining(#Param("xyz") String keyword,Pageable pageable);
}
front side I have angular 9 as:
auth.service.ts
import { Injectable } from '#angular/core';
import { HttpClient } from '#angular/common/http';
import { map } from 'rxjs/operators';
#Injectable({
providedIn: 'root'
})
export class AuthService {
// BASE_PATH: 'http://localhost:8080'
USER_NAME_SESSION_ATTRIBUTE_NAME = 'authenticatedUser';
public username: String;
public password: String;
constructor(private http: HttpClient) {
}
authenticationService(username: String, password: String) {
return this.http.get(`http://localhost:8080/api/v1/basicauth`,
{ headers: { authorization: this.createBasicAuthToken(username, password) } }).pipe(map((res) => {
this.username = username;
this.password = password;
this.registerSuccessfulLogin(username, password);
}));
}
createBasicAuthToken(username: String, password: String) {
return 'Basic ' + window.btoa(username + ":" + password)
}
}
//i didnot pasted all the codes.
So,I get error as when I goto link http://localhost:4200/books:
I have some projects using Angular+SpringBoot with security and I create a specific Bean to handle with CORS and I never have problem. If you can try, add this method bellow in your WebSecurityConfig class:
#Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("http://localhost:4200", "http://localhost:8080"));
configuration.setAllowedMethods(Arrays.asList("GET", "PUT", "POST","OPTIONS", "DELETE"));
configuration.setAllowedHeaders(Arrays.asList("authorization","content-type"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
The problem is CORS which is a security feature of your browser. It ensures that only resources form the same domain (and port!) can be accessed. Your Angular development server and the Tomcat run on a different port which causes the request to be declined. You have to configure CORS. However, you should know what you are doing because you are basically disabling a security feature. Usually it is not a problem tho. You can do this by adding the annotation #CrossOrigin to your controller methods or by using the Java configuration. For the second cause, I'm sure you'll easily find it on Google :)
CORS (Cross-Origin Resource Sharing) is a security feature of your browser that prevent authorized sites from using Resources from another origin
in nutshell it happens if your site on x:y origin and requesting resources from a:y, x:b or a:b origins (different port and/or domain)
what exactly happens in nutshell when this is the case
if you made a get (or post...) request from another origin, the browser will first make an option request to the same endpoint, if it's succeeded and has all the allowing headers it will make the get request, if not it will throw the error specifying why it was denied and don't make the original request
so we have two cases now, it's either the headers is returned only on the get request, but not the options one, or it's never returned