I have a spring boot microservice that validates a JWT (issued by a different service) for authentication. It is working nicely, and I can access the JWT details in my controller like so:
// build.gradle
implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'
// MyController.java
#RestController
#RequestMapping("/")
public class MyController {
#GetMapping()
public String someControllerMethod(#AuthenticationPrincipal Jwt jwt) {
int userId = Integer.parseInt(jwt.getClaim("userid"));
...
}
}
That works great. I can extract what I need from the JWT and go on to talk to my database with the correct userid etc.
However I find it a bit tedious to have to use the Jwt type to get these values in each controller. Is there a way I can inject a different type as the #AuthenticationPrincipal?
E.g. my own class which has already extracted what is needed from the JWT, and exposes something like .getUserId() that returns an int?
That would also let me centralise the logic of parsing the claims or throwing exceptions if they are not as expected etc.
UPDATE
After more google spelunking, it seems I have two options
Option1: #ControllerAdvice and #ModelAttribute
As explained in this answer. I can do something like:
import com.whatever.CustomPrincipal; // a basic "data" class with some properties, getters, setters and constructor
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ModelAttribute;
#ControllerAdvice
public class SecurityControllerAdvice {
#ModelAttribute
public CustomPrincipal customPrincipal(Authentication auth) throws Exception {
CustomPrincipal customPrincipal;
if (auth != null && auth.getPrincipal() instanceof Jwt) {
Jwt jwt = (Jwt) auth.getPrincipal();
String sessionId = jwt.getClaimAsString("sessionid");
int userId = Integer.parseInt(jwt.getClaimAsString("userid"));
customPrincipal = new CustomPrincipal(userId, sessionId);
} else {
// log an error and throw an exception?
}
return customPrincipal;
}
}
and then
import com.whatever.CustomPrincipal;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RestController;
#RestController
#ControllerAdvice
public class HelloWorldController {
#GetMapping("/controlleradvice")
public String index(#ModelAttribute CustomPrincipal cp) {
log.info(cp.getUserId());
return "whatever";
}
}
This seems pretty succinct, and neat and tidy. 1 new class with #ControllerAdvice, and bob's your uncle!
Option2: Using jwtAuthenticationConverter()
This answer shows another way to do it, using a "converter", which seems to convert the default Principal from a JWT to a custom object (that extends AbstractAuthenticationToken) that contains the JWT (.getCredentials()) as well as a custom object like CustomPrincipal (or a User class or something).
#EnableWebSecurity
public class SecurityConfig {
#Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.cors().disable()
.csrf().disable()
.authorizeHttpRequests((authorize) -> authorize
.anyRequest().authenticated()
)
.oauth2ResourceServer().jwt(customizer -> customizer.jwtAuthenticationConverter((new MyPrincipalJwtConvertor())));
return http.build();
}
}
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.oauth2.jwt.Jwt;
public class MyPrincipalJwtConvertor implements Converter<Jwt, MyAuthenticationToken> {
#Override
public MyAuthenticationToken convert(Jwt jwt) {
var principal = new MyPrincipal(Integer.parseInt(jwt.getClaimAsString("userid")), jwt.getClaimAsString("sessionid"));
return new MyAuthenticationToken(jwt, principal);
}
}
#RestController
public class HelloWorldController {
#GetMapping("/converter")
public String converter(#AuthenticationPrincipal MyPrincipal myPrincipal) {
log.info("/converter triggered");
log.info("" + myPrincipal.getUserId());
return "woo";
}
}
import lombok.AllArgsConstructor;
import lombok.Data;
#Data
#AllArgsConstructor
public class MyPrincipal {
private int userId;
private String sessionId;
}
Option 1 is much simpler it seems.
But Option 2 is nice, as, I have Filter's that run to do additional validation (like validate the session id in the JWT). When that filter runs, when it calls SecurityContext.getContext().getAuthentication().getPrincipal(), it will get the MyPrincipal object, and not have to call Jwt.getClaimAsString() and cast it etc.
I guess I am asking, are there pros and cons to these two approaches I have not considered? Is one of them perhaps bastardising/abusing something in a way it is not meant to be?
Or is it much the same and I should select whichever I prefer?
I have rest backend wrote on Spring Boot and oauth2 (provided by Google) auto redirect on "/login". I want to make Firebase auth on the backend for mobile beside with oauth for web, like on the following algorithm:
User authorizes on mobile -> User sends request -> Backend gets request -> Backend checks if user openid exists in local database -> Backend returns response or exception page
The following code is my current WebSecurityConfiguration:
#Configuration
#EnableWebSecurity
#EnableOAuth2Sso
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().mvcMatchers("/","/static/**","/public/**","/assets/**","/api/sensors/**", "/emulator/**").permitAll()
.anyRequest().authenticated()
.and().logout().logoutSuccessUrl("/").permitAll()
.and()
.csrf().disable();
}
#Bean
public PrincipalExtractor principalExtractor(PersonRepository personRepository) {
return map -> {
String id = (String) map.get("sub");
Person person1 = personRepository.findById(id).orElseGet(() -> {
Person person = new Person();
person.setPersonId(id);
person.getDetails().setFirstName((String) map.get("given_name"));
person.getDetails().setLastName((String) map.get("family_name"));
person.getDetails().setEmail((String) map.get("email"));
person.getDetails().setPictureUrl((String) map.get("picture"));
person.getSettings().setLocale(new Locale((String) map.get("locale")));
person.setPersonRole(PersonRole.USER);
person.setStatus(PersonStatus.NORMAL);
person.newToken();
return person;
});
return personRepository.save(person1);
};
}
}
Add Firebase Configuration Bean of the form:
import javax.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.google.firebase.*;
#Configuration
public class FirebaseConfig {
#Bean
public DatabaseReference firebaseDatabse() {
DatabaseReference firebase = FirebaseDatabase.getInstance().getReference();
return firebase;
}
#Value("${firebase.database.url}")
private String databaseUrl;
#Value("${firebase.config.path}")
private String configPath;
#PostConstruct
public void init() {
/**
* https://firebase.google.com/docs/server/setup
*
* Create service account , download json
*/
InputStream inputStream = FirebaseConfig.class.getClassLoader().getResourceAsStream(configPath);
FirebaseOptions options = new FirebaseOptions.Builder().setServiceAccount(inputStream)
.setDatabaseUrl(databaseUrl).build();
FirebaseApp.initializeApp(options);
}
}
In your application.properties, add
firebase.config.path=Configuration.json
firebase.database.url=<firebase-database-path>
You can download your Configuration.json for your Firebase project by referring to this page
I have issues with using the Pre/Post Authorization Annotations from Spring Security and the Servlet API with Keycloak integration. I investigated a lot of articles, tutorials and the following questions without further luck:
Obtaining user roles in servlet application using keycloak
Spring Boot Keycloak - How to get a list of roles assigned to a user?
Using spring security annotations with keycloak
Spring Boot + Spring Security + Hierarchical Roles
How do I add method based security to a Spring Boot project?
Configure DefaultMethodSecurityExpressionHandler using Spring Security Java Config
SpringBoot + method based hierarchical roles security: ServletContext is required
All I want is removing the ROLES_ prefix, use hierarchical roles and a comfortable way to retrieve the users' roles.
As of now, I am able to retrieve a hierarchical role like this in a Controller but cannot use the annotations:
#Controller
class HomeController {
#Autowired
AccessToken token
#GetMapping('/')
def home(Authentication auth, HttpServletRequest request) {
// Role 'admin' is defined in Keycloak for this application
assert token.getResourceAccess('my-app').roles == ['admin']
// All effective roles are mapped
assert auth.authorities.collect { it.authority }.containsAll(['admin', 'author', 'user'])
// (!) But this won't work:
assert request.isUserInRole('admin')
}
// (!) Leads to a 403: Forbidden
#GetMapping('/sec')
#PreAuthorize("hasRole('admin')") {
return "Hello World"
}
}
I am guessing that the #PreAuthorize annotation does not work, because that Servlet method is not successful.
There are only three roles - admin, author, user - defined in Keycloak and Spring:
enum Role {
USER('user'),
AUTHOR('author'),
ADMIN('admin')
final String id
Role(String id) {
this.id = id
}
#Override
String toString() {
id
}
}
Keycloak Configuration
Upon removing the #EnableGlobalMethodSecurity annotation from this Web Security reveals an Error creating bean with name 'resourceHandlerMapping' caused by a No ServletContext set error - no clue, where that comes from!
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
/**
* Registers the KeycloakAuthenticationProvider with the authentication manager.
*/
#Autowired
void configureGlobal(AuthenticationManagerBuilder auth) {
auth.authenticationProvider(keycloakAuthenticationProvider().tap { provider ->
// Assigns the Roles via Keycloaks role mapping
provider.grantedAuthoritiesMapper = userAuthoritiesMapper
})
}
#Bean
RoleHierarchyImpl getRoleHierarchy() {
new RoleHierarchyImpl().tap {
hierarchy = "$Role.ADMIN > $Role.AUTHOR > $Role.USER"
}
}
#Bean
GrantedAuthoritiesMapper getUserAuthoritiesMapper() {
new RoleHierarchyAuthoritiesMapper(roleHierarchy)
}
SecurityExpressionHandler<FilterInvocation> expressionHandler() {
// Removes the prefix
new DefaultWebSecurityExpressionHandler().tap {
roleHierarchy = roleHierarchy
defaultRolePrefix = null
}
}
// ...
#Bean
#Scope(scopeName = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
AccessToken accessToken() {
def request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest()
def authToken = (KeycloakAuthenticationToken) request.userPrincipal
def securityContext = (KeycloakSecurityContext) authToken.credentials
return securityContext.token
}
#Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http)
http
.authorizeRequests()
.expressionHandler(expressionHandler())
// ...
}
}
Global Method Security Configuration
I needed to explicitly allow allow-bean-definition-overriding, because otherwise I got a bean with that name already defined error, which reveals that I completely lost control over this whole situation and don't know what's goin on.
#Configuration
#EnableGlobalMethodSecurity(prePostEnabled = true)
class GlobalMethodSecurityConfig extends GlobalMethodSecurityConfiguration {
#Autowired
RoleHierarchy roleHierarchy
#Override
protected MethodSecurityExpressionHandler createExpressionHandler() {
((DefaultMethodSecurityExpressionHandler)super.createExpressionHandler()).tap {
roleHierarchy = roleHierarchy
defaultRolePrefix = null
}
}
}
Any further configurations that could be important? Thanks a lot for your help!
As M. Deinum pointed out, one must remove the defaultRolePrefix in multiple places with a BeanPostProcessor, which is explained in (docs.spring.io) Disable ROLE_ Prefixing.
This approach seemed not very clean to me and so I wrote a custom AuthoritiesMapper to achieve mapping hierarchical roles from Keycloak without the need to rename them to the ROLE_ Spring standard. First of all, the Roles enumeration was modified to conform that standard inside the application scope:
enum Role {
USER('ROLE_USER'),
AUTHOR('ROLE_AUTHOR'),
ADMIN('ROLE_ADMIN')
// ...
}
Secondly, I replaced the RoleHierarchyAuthoritiesMapper with a prefixing hierarchical implementation:
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
// ..
// Replaces the RoleHierarchyAuthoritiesMapper
#Bean
GrantedAuthoritiesMapper getUserAuthoritiesMapper() {
new PrefixingRoleHierarchyAuthoritiesMapper(roleHierarchy)
}
}
class PrefixingRoleHierarchyAuthoritiesMapper extends RoleHierarchyAuthoritiesMapper {
String prefix = 'ROLE_'
PrefixingRoleHierarchyAuthoritiesMapper(RoleHierarchy roleHierarchy) {
super(roleHierarchy)
}
#Override
Collection<? extends GrantedAuthority> mapAuthorities(Collection<? extends GrantedAuthority> authorities) {
def prefixedAuthorities = authorities.collect { GrantedAuthority originalAuthority ->
new GrantedAuthority() {
String authority = "${prefix}${originalAuthority.authority}".toUpperCase()
}
}
super.mapAuthorities(prefixedAuthorities)
}
}
And lastly, I got rid of the GlobalMethodSecurityConfig.
Apart from suggestions provided in (docs.spring.io) Disable ROLE_ Prefixing, and suggestion provided by M. Deinum, one more modification is needed while using KeycloakWebSecurityConfigurerAdapter.
In configureGlobal method, grantedAuthoritiesMapper bean is set in the bean keycloakAuthenticationProvider. And in grantedAuthoritiesMapper, prefix can be set to anything you want, where the default value is "ROLE_".
The code goes as follows:
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider();
SimpleAuthorityMapper grantedAuthoritiesMapper = new SimpleAuthorityMapper();
grantedAuthoritiesMapper.setPrefix("");
keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(grantedAuthoritiesMapper);
auth.authenticationProvider(keycloakAuthenticationProvider);
}
This solution works for me.
In spring boot, upon configuring a Resource server we have the option to set the security.oauth2.resource.jwk.key-set-uri property if the access tokens will be JWTs and the issuer provides an endpoint for clients to acquire the public RSA key for verification in JWK format.
What is the expected behavior to initiate a keystore from this JWK? The property is being loaded in the ResourceServerProperties.JWK but then what. Should spring boot call this URI and fetch the jwks then create a store for me to use in verification?
I am following this tutorial to setup the configuration of the keystore http://www.baeldung.com/spring-security-oauth-jwt
#Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
Resource resource = new ClassPathResource("public.txt");
String publicKey = null;
try {
publicKey = IOUtils.toString(resource.getInputStream());
} catch (final IOException e) {
throw new RuntimeException(e);
}
converter.setVerifierKey(publicKey);
return converter;
}
But instead of loading a .pem public key I think I want to load it from a jwk.
If you want to use JWKS, use JwkTokenStore in place of JwtTokenStore.
spring-security-oauth2/jwk internally implements key loading and management according to the auth0 spec
You can also see docs on auto-configuration of the same, however i feel configuring it in quite straight-forward (see below).
We don't have to do any verification as JwkTokenStore sets up the verification with JwkDefinitionSource JwkVerifyingJwtAccessTokenConverter using JWKS exposed at #Value("{jsecurity.oauth2.resource.jwk.key-set-uri}")
However, the spring-security-oauth2/jwk classes from spring don't have any public constructors, we often need and can perform any custom steps in AccessTokenConversion, like a common need is to extract jwt content to auth context, we can always inject a custom converter to JwkTokenStore
import org.springframework.security.oauth2.provider.token.store.jwk.*;
import org.springframework.security.oauth2.provider.token.store.*
import org.springframework.security.oauth2.provider.token.*;
import java.utl.*;
#Configuration
class JwtConfiguration {
#Bean
public DefaultTokenServices tokenServices(final TokenStore tokenStore) {
final DefaultTokenServices dts = new DefaultTokenServices();
dts.setTokenStore(tokenStore);
dts.setSupportRefreshToken(true);
return dts;
}
#Bean
public TokenStore tokenStore(
#Value("{jsecurity.oauth2.resource.jwk.key-set-uri}") final String jwksUrl,
final JwtAccessTokenConverter jwtAccessTokenConverter) {
return new JwkTokenStore(jwksUrl, jwtAccessTokenConverter, null);
}
#Bean
public JwtAccessTokenConverter createJwtAccessTokenConverter() {
final JwtAccessTokenConverter converter;
converter.setAccessTokenConverter(new DefaultAccessTokenConverter() {
#Override
public OAuth2Authentication extractAuthentication(Map<String, ?> map) {
final OAuth2Authentication auth = super.extractAuthentication(map);
auth.setDetails(map); //this will get spring to copy JWT content into
return auth;
}
}
return conveter;
}
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.TokenStore;
#Configuration
#EnableResourceServer
class ResourceServerConfig extends ResourceServerConfigurerAdapter {
private String resourceId;
private TokenStore tokenStore;
public ResourceServerConfig(
#Value("\${jwt.reourceId}") private String resourceId,
private TokenStore tokenStore) {
this.resourceId = resourceId;
this.tokenStore = tokenStore;
}
/**
* Ensures request to all endpoints ore a
#Override
public void configure(final HttpSecurity http) {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/**").authenticated();
}
/**
* Configure resources
* Spring OAuth expects "aud" claim in JWT token. That claim's value should match to the resourceId value
* (if not specified it defaults to "oauth2-resource").
*/
#Override
public void configure(final ResourceServerSecurityConfigurer resources) {
resources.resourceId(resourceId).tokenStore(tokenStore);
}
}
The main goal of this implementation would be to verify a JWT locally using the corresponding JWK(JSON WEB TOKEN KEY SET). The JWK used for verification is matched using the kid header parameter of the JWT and the kid attribute of the JWK.
The server can validate this token locally without making any network requests, talking to a database, etc. This can potentially make session management faster because instead of needing to load the user from a database (or cache) on every request, you just need to run a small bit of local code. This is probably the single biggest reason people like using JWTs: they are stateless.
I was trying to figure out how to unit test if my the URLs of my controllers are properly secured. Just in case someone changes things around and accidentally removes security settings.
My controller method looks like this:
#RequestMapping("/api/v1/resource/test")
#Secured("ROLE_USER")
public #ResonseBody String test() {
return "test";
}
I set up a WebTestEnvironment like so:
import javax.annotation.Resource;
import javax.naming.NamingException;
import javax.sql.DataSource;
import org.junit.Before;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.FilterChainProxy;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
#RunWith(SpringJUnit4ClassRunner.class)
#WebAppConfiguration
#ContextConfiguration({
"file:src/main/webapp/WEB-INF/spring/security.xml",
"file:src/main/webapp/WEB-INF/spring/applicationContext.xml",
"file:src/main/webapp/WEB-INF/spring/servlet-context.xml" })
public class WebappTestEnvironment2 {
#Resource
private FilterChainProxy springSecurityFilterChain;
#Autowired
#Qualifier("databaseUserService")
protected UserDetailsService userDetailsService;
#Autowired
private WebApplicationContext wac;
#Autowired
protected DataSource dataSource;
protected MockMvc mockMvc;
protected final Logger logger = LoggerFactory.getLogger(this.getClass());
protected UsernamePasswordAuthenticationToken getPrincipal(String username) {
UserDetails user = this.userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
user,
user.getPassword(),
user.getAuthorities());
return authentication;
}
#Before
public void setupMockMvc() throws NamingException {
// setup mock MVC
this.mockMvc = MockMvcBuilders
.webAppContextSetup(this.wac)
.addFilters(this.springSecurityFilterChain)
.build();
}
}
In my actual test I tried to do something like this:
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.Test;
import org.springframework.mock.web.MockHttpSession;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
import eu.ubicon.webapp.test.WebappTestEnvironment;
public class CopyOfClaimTest extends WebappTestEnvironment {
#Test
public void signedIn() throws Exception {
UsernamePasswordAuthenticationToken principal =
this.getPrincipal("test1");
SecurityContextHolder.getContext().setAuthentication(principal);
super.mockMvc
.perform(
get("/api/v1/resource/test")
// .principal(principal)
.session(session))
.andExpect(status().isOk());
}
}
I picked this up here:
http://java.dzone.com/articles/spring-test-mvc-junit-testing here:
http://techdive.in/solutions/how-mock-securitycontextholder-perfrom-junit-tests-spring-controller or here:
How to JUnit tests a #PreAuthorize annotation and its spring EL specified by a spring MVC Controller?
Yet if one looks closely this only helps when not sending actual requests to URLs, but only when testing services on a function level. In my case an "access denied" exception was thrown:
org.springframework.security.access.AccessDeniedException: Access is denied
at org.springframework.security.access.vote.AffirmativeBased.decide(AffirmativeBased.java:83) ~[spring-security-core-3.1.3.RELEASE.jar:3.1.3.RELEASE]
at org.springframework.security.access.intercept.AbstractSecurityInterceptor.beforeInvocation(AbstractSecurityInterceptor.java:206) ~[spring-security-core-3.1.3.RELEASE.jar:3.1.3.RELEASE]
at org.springframework.security.access.intercept.aopalliance.MethodSecurityInterceptor.invoke(MethodSecurityInterceptor.java:60) ~[spring-security-core-3.1.3.RELEASE.jar:3.1.3.RELEASE]
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172) ~[spring-aop-3.2.1.RELEASE.jar:3.2.1.RELEASE]
...
The following two log messages are noteworthy basically saying that no user was authenticated indicating that setting the Principal did not work, or that it was overwritten.
14:20:34.454 [main] DEBUG o.s.s.a.i.a.MethodSecurityInterceptor - Secure object: ReflectiveMethodInvocation: public java.util.List test.TestController.test(); target is of class [test.TestController]; Attributes: [ROLE_USER]
14:20:34.454 [main] DEBUG o.s.s.a.i.a.MethodSecurityInterceptor - Previously Authenticated: org.springframework.security.authentication.AnonymousAuthenticationToken#9055e4a6: Principal: anonymousUser; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails#957e: RemoteIpAddress: 127.0.0.1; SessionId: null; Granted Authorities: ROLE_ANONYMOUS
Seaching for answer I couldn't find any to be easy and flexible at the same time, then I found the Spring Security Reference and I realized there are near to perfect solutions. AOP solutions often are the greatest ones for testing, and Spring provides it with #WithMockUser, #WithUserDetails and #WithSecurityContext, in this artifact:
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<version>4.2.2.RELEASE</version>
<scope>test</scope>
</dependency>
In most cases, #WithUserDetails gathers the flexibility and power I need.
How #WithUserDetails works?
Basically you just need to create a custom UserDetailsService with all the possible users profiles you want to test. E.g
#TestConfiguration
public class SpringSecurityWebAuxTestConfig {
#Bean
#Primary
public UserDetailsService userDetailsService() {
User basicUser = new UserImpl("Basic User", "user#company.com", "password");
UserActive basicActiveUser = new UserActive(basicUser, Arrays.asList(
new SimpleGrantedAuthority("ROLE_USER"),
new SimpleGrantedAuthority("PERM_FOO_READ")
));
User managerUser = new UserImpl("Manager User", "manager#company.com", "password");
UserActive managerActiveUser = new UserActive(managerUser, Arrays.asList(
new SimpleGrantedAuthority("ROLE_MANAGER"),
new SimpleGrantedAuthority("PERM_FOO_READ"),
new SimpleGrantedAuthority("PERM_FOO_WRITE"),
new SimpleGrantedAuthority("PERM_FOO_MANAGE")
));
return new InMemoryUserDetailsManager(Arrays.asList(
basicActiveUser, managerActiveUser
));
}
}
Now we have our users ready, so imagine we want to test the access control to this controller function:
#RestController
#RequestMapping("/foo")
public class FooController {
#Secured("ROLE_MANAGER")
#GetMapping("/salute")
public String saluteYourManager(#AuthenticationPrincipal User activeUser)
{
return String.format("Hi %s. Foo salutes you!", activeUser.getUsername());
}
}
Here we have a get mapped function to the route /foo/salute and we are testing a role based security with the #Secured annotation, although you can test #PreAuthorize and #PostAuthorize as well.
Let's create two tests, one to check if a valid user can see this salute response and the other to check if it's actually forbidden.
#RunWith(SpringRunner.class)
#SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
classes = SpringSecurityWebAuxTestConfig.class
)
#AutoConfigureMockMvc
public class WebApplicationSecurityTest {
#Autowired
private MockMvc mockMvc;
#Test
#WithUserDetails("manager#company.com")
public void givenManagerUser_whenGetFooSalute_thenOk() throws Exception
{
mockMvc.perform(MockMvcRequestBuilders.get("/foo/salute")
.accept(MediaType.ALL))
.andExpect(status().isOk())
.andExpect(content().string(containsString("manager#company.com")));
}
#Test
#WithUserDetails("user#company.com")
public void givenBasicUser_whenGetFooSalute_thenForbidden() throws Exception
{
mockMvc.perform(MockMvcRequestBuilders.get("/foo/salute")
.accept(MediaType.ALL))
.andExpect(status().isForbidden());
}
}
As you see we imported SpringSecurityWebAuxTestConfig to provide our users for testing. Each one used on its corresponding test case just by using a straightforward annotation, reducing code and complexity.
Better use #WithMockUser for simpler Role Based Security
As you see #WithUserDetails has all the flexibility you need for most of your applications. It allows you to use custom users with any GrantedAuthority, like roles or permissions. But if you are just working with roles, testing can be even easier and you could avoid constructing a custom UserDetailsService. In such cases, specify a simple combination of user, password and roles with #WithMockUser.
#Target({ElementType.METHOD, ElementType.TYPE})
#Retention(RetentionPolicy.RUNTIME)
#Inherited
#Documented
#WithSecurityContext(
factory = WithMockUserSecurityContextFactory.class
)
public #interface WithMockUser {
String value() default "user";
String username() default "";
String[] roles() default {"USER"};
String password() default "password";
}
The annotation defines default values for a very basic user. As in our case the route we are testing just requires that the authenticated user be a manager, we can quit using SpringSecurityWebAuxTestConfig and do this.
#Test
#WithMockUser(roles = "MANAGER")
public void givenManagerUser_whenGetFooSalute_thenOk() throws Exception
{
mockMvc.perform(MockMvcRequestBuilders.get("/foo/salute")
.accept(MediaType.ALL))
.andExpect(status().isOk())
.andExpect(content().string(containsString("user")));
}
Notice that now instead of the user manager#company.com we are getting the default provided by #WithMockUser: user; yet it won't matter because what we really care about is his role: ROLE_MANAGER.
Conclusions
As you see with annotations like #WithUserDetails and #WithMockUser we can switch between different authenticated users scenarios without building classes alienated from our architecture just for making simple tests. Its also recommended you to see how #WithSecurityContext works for even more flexibility.
Since Spring 4.0+, the best solution is to annotate the test method with #WithMockUser
#Test
#WithMockUser(username = "user1", password = "pwd", roles = "USER")
public void mytest1() throws Exception {
mockMvc.perform(get("/someApi"))
.andExpect(status().isOk());
}
Remember to add the following dependency to your project
'org.springframework.security:spring-security-test:4.2.3.RELEASE'
It turned out that the SecurityContextPersistenceFilter, which is part of the Spring Security filter chain, always resets my SecurityContext, which I set calling SecurityContextHolder.getContext().setAuthentication(principal) (or by using the .principal(principal) method). This filter sets the SecurityContext in the SecurityContextHolder with a SecurityContext from a SecurityContextRepository OVERWRITING the one I set earlier. The repository is a HttpSessionSecurityContextRepository by default. The HttpSessionSecurityContextRepository inspects the given HttpRequest and tries to access the corresponding HttpSession. If it exists, it will try to read the SecurityContext from the HttpSession. If this fails, the repository generates an empty SecurityContext.
Thus, my solution is to pass a HttpSession along with the request, which holds the SecurityContext:
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.Test;
import org.springframework.mock.web.MockHttpSession;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
import eu.ubicon.webapp.test.WebappTestEnvironment;
public class Test extends WebappTestEnvironment {
public static class MockSecurityContext implements SecurityContext {
private static final long serialVersionUID = -1386535243513362694L;
private Authentication authentication;
public MockSecurityContext(Authentication authentication) {
this.authentication = authentication;
}
#Override
public Authentication getAuthentication() {
return this.authentication;
}
#Override
public void setAuthentication(Authentication authentication) {
this.authentication = authentication;
}
}
#Test
public void signedIn() throws Exception {
UsernamePasswordAuthenticationToken principal =
this.getPrincipal("test1");
MockHttpSession session = new MockHttpSession();
session.setAttribute(
HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY,
new MockSecurityContext(principal));
super.mockMvc
.perform(
get("/api/v1/resource/test")
.session(session))
.andExpect(status().isOk());
}
}
Add in pom.xml:
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<version>4.0.0.RC2</version>
</dependency>
and use org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors for authorization request.
See the sample usage at https://github.com/rwinch/spring-security-test-blog
(https://jira.spring.io/browse/SEC-2592).
Update:
4.0.0.RC2 works for spring-security 3.x.
For spring-security 4 spring-security-test become part of spring-security (http://docs.spring.io/spring-security/site/docs/4.0.x/reference/htmlsingle/#test, version is the same).
Setting Up is changed: http://docs.spring.io/spring-security/site/docs/4.0.x/reference/htmlsingle/#test-mockmvc
public void setup() {
mvc = MockMvcBuilders
.webAppContextSetup(context)
.apply(springSecurity())
.build();
}
Sample for basic-authentication: http://docs.spring.io/spring-security/site/docs/4.0.x/reference/htmlsingle/#testing-http-basic-authentication.
Here is an example for those who want to Test Spring MockMvc Security Config using Base64 basic authentication.
String basicDigestHeaderValue = "Basic " + new String(Base64.encodeBase64(("<username>:<password>").getBytes()));
this.mockMvc.perform(get("</get/url>").header("Authorization", basicDigestHeaderValue).accept(MediaType.APPLICATION_JSON)).andExpect(status().isOk());
Maven Dependency
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.3</version>
</dependency>
Short answer:
#Autowired
private WebApplicationContext webApplicationContext;
#Autowired
private Filter springSecurityFilterChain;
#Before
public void setUp() throws Exception {
final MockHttpServletRequestBuilder defaultRequestBuilder = get("/dummy-path");
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.webApplicationContext)
.defaultRequest(defaultRequestBuilder)
.alwaysDo(result -> setSessionBackOnRequestBuilder(defaultRequestBuilder, result.getRequest()))
.apply(springSecurity(springSecurityFilterChain))
.build();
}
private MockHttpServletRequest setSessionBackOnRequestBuilder(final MockHttpServletRequestBuilder requestBuilder,
final MockHttpServletRequest request) {
requestBuilder.session((MockHttpSession) request.getSession());
return request;
}
After perform formLogin from spring security test each of your requests will be automatically called as logged in user.
Long answer:
Check this solution (the answer is for spring 4): How to login a user with spring 3.2 new mvc testing
Options to avoid using SecurityContextHolder in tests:
Option 1: use mocks - I mean mock SecurityContextHolder using some mock library - EasyMock for example
Option 2: wrap call SecurityContextHolder.get... in your code in some service - for example in SecurityServiceImpl with method getCurrentPrincipal that implements SecurityService interface and then in your tests you can simply create mock implementation of this interface that returns the desired principal without access to SecurityContextHolder.
Pretty Late answer though. But This has worked for me , and could be useful.
While Using Spring Security ans mockMvc, all you need to is use #WithMockUser annotation like others are mentioned.
Spring security also provides another annotation called #WithAnonymousUser for testing unauthenticated requests. However you should be careful here. You would be expecting 401, but I got 403 Forbidden Error by default. In actual scenarios, when you are running actual service, It is redirected and you end up getting the correct 401 response code.Use this annotation for anonymous requests.
You may also think of ommitting the annotaions and simply keep it unauthorized. But this usually raises the correct exceptions(like AuthenticationException), but you will get correct status code if it is handled correctly(If you are using custom handler). I used to get 500 for this. So look for the exceptions raised in the debugger, and check if it is handled rightly and returns the correct status code.
Create a class TestUserDetailsImpl on your test package:
#Service
#Primary
#Profile("test")
public class TestUserDetailsImpl implements UserDetailsService {
public static final String API_USER = "apiuser#example.com";
private User getAdminUser() {
User user = new User();
user.setUsername(API_USER);
SimpleGrantedAuthority role = new SimpleGrantedAuthority("ROLE_API_USER");
user.setAuthorities(Collections.singletonList(role));
return user;
}
#Override
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
if (Objects.equals(username, ADMIN_USERNAME))
return getAdminUser();
throw new UsernameNotFoundException(username);
}
}
Rest endpoint:
#GetMapping("/invoice")
#Secured("ROLE_API_USER")
public Page<InvoiceDTO> getInvoices(){
...
}
Test endpoint:
#Test
#WithUserDetails("apiuser#example.com")
public void testApi() throws Exception {
...
}
When using MockMvcBuilders.webAppContextSetup(wac).addFilters(...) than springSecurityFilterChain (more specifically SecurityContextPersistenceFilter) will take over and will remove the SecurityContext prepared by #WithMockUser (pretty silly); this happens because SecurityContextPersistenceFilter tries to "restore" the SecurityContext from the HttpSession where finds none. Well, use this simple AutoStoreSecurityContextHttpFilter defined below which will take care of putting #WithMockUser's preppared SecurityContext into the HttpSession such that later SecurityContextPersistenceFilter will be able to find it.
#ContextConfiguration(...) // the issue doesn't occur when using #SpringBootTest
public class SomeTest {
#Autowired
private Filter springSecurityFilterChain;
private MockMvc mockMvc;
#BeforeEach
void setup(WebApplicationContext wac) {
this.mockMvc = MockMvcBuilders.webAppContextSetup(wac)
.addFilters(new AutoStoreSecurityContextHttpFilter(), springSecurityFilterChain).build();
}
#WithMockUser
#Test
void allowAccessToAuthenticated() {
...
}
}
// don't use this Filter in production because it's only intended for tests, to solve the
// #WithMockUser & springSecurityFilterChain (more specifically SecurityContextPersistenceFilter) "misunderstandings"
public class AutoStoreSecurityContextHttpFilter extends HttpFilter {
protected void doFilter(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws IOException, ServletException {
req.getSession().setAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, SecurityContextHolder.getContext());
super.doFilter(req, res, chain);
}
}