There are lots of guidelines, sample codes that show how to secure REST API with Spring Security, but most of them assume a web client and talk about login page, redirection
You're right, it isn't easy and there aren't many good examples out there. Examples i saw made it so you couldn't use other spring security stuff side by side. I did something similar recently, here's what i did.
You need a custom token to hold your header value
public class CustomToken extends AbstractAuthenticationToken {
private final String value;
//Getters and Constructor. Make sure getAutheticated returns false at first.
//I made mine "immutable" via:
@Override
public void setAuthenticated(boolean isAuthenticated) {
//It doesn't make sense to let just anyone set this token to authenticated, so we block it
//Similar precautions are taken in other spring framework tokens, EG: UsernamePasswordAuthenticationToken
if (isAuthenticated) {
throw new IllegalArgumentException(MESSAGE_CANNOT_SET_AUTHENTICATED);
}
super.setAuthenticated(false);
}
}
You need a spring security filter to extract the header and ask the manager to authenticate it, something like thisemphasized text
public class CustomFilter extends AbstractAuthenticationProcessingFilter {
public CustomFilter(RequestMatcher requestMatcher) {
super(requestMatcher);
this.setAuthenticationSuccessHandler((request, response, authentication) -> {
/*
* On success the desired action is to chain through the remaining filters.
* Chaining is not possible through the success handlers, because the chain is not accessible in this method.
* As such, this success handler implementation does nothing, and chaining is accomplished by overriding the successfulAuthentication method as per:
* http://docs.spring.io/autorepo/docs/spring-security/3.2.4.RELEASE/apidocs/org/springframework/security/web/authentication/AbstractAuthenticationProcessingFilter.html#successfulAuthentication(javax.servlet.http.HttpServletRequest,%20javax.servlet.http.HttpServletResponse,%20javax.servlet.FilterChain,%20org.springframework.security.core.Authentication)
* "Subclasses can override this method to continue the FilterChain after successful authentication."
*/
});
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException, IOException, ServletException {
String tokenValue = request.getHeader("SOMEHEADER");
if(StringUtils.isEmpty(tokenValue)) {
//Doing this check is kinda dumb because we check for it up above in doFilter
//..but this is a public method and we can't do much if we don't have the header
//also we can't do the check only here because we don't have the chain available
return null;
}
CustomToken token = new CustomToken(tokenValue);
token.setDetails(authenticationDetailsSource.buildDetails(request));
return this.getAuthenticationManager().authenticate(token);
}
/*
* Overriding this method to maintain the chaining on authentication success.
* http://docs.spring.io/autorepo/docs/spring-security/3.2.4.RELEASE/apidocs/org/springframework/security/web/authentication/AbstractAuthenticationProcessingFilter.html#successfulAuthentication(javax.servlet.http.HttpServletRequest,%20javax.servlet.http.HttpServletResponse,%20javax.servlet.FilterChain,%20org.springframework.security.core.Authentication)
* "Subclasses can override this method to continue the FilterChain after successful authentication."
*/
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
//if this isn't called, then no auth is set in the security context holder
//and subsequent security filters can still execute.
//so in SOME cases you might want to conditionally call this
super.successfulAuthentication(request, response, chain, authResult);
//Continue the chain
chain.doFilter(request, response);
}
}
Register your custom filter in spring security chain
@Configuration
public static class ResourceEndpointsSecurityConfig extends WebSecurityConfigurerAdapter {
//Note, we don't register this as a bean as we don't want it to be added to the main Filter chain, just the spring security filter chain
protected AbstractAuthenticationProcessingFilter createCustomFilter() throws Exception {
CustomFilter filter = new CustomFilter( new RegexRequestMatcher("^/.*", null));
filter.setAuthenticationManager(this.authenticationManagerBean());
return filter;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
//fyi: This adds it to the spring security proxy filter chain
.addFilterBefore(createCustomFilter(), AnonymousAuthenticationFilter.class)
}
}
A custom auth provider to validate that token extracted with the filter.
public class CustomAuthenticationProvider implements AuthenticationProvider {
@Override
public Authentication authenticate(Authentication auth)
throws AuthenticationException {
CustomToken token = (CustomToken)auth;
try{
//Authenticate token against redis or whatever you want
//This i found weird, you need a Principal in your Token...I use User
//I found this to be very redundant in spring security, but Controller param resolving will break if you don't do this...anoying
org.springframework.security.core.userdetails.User principal = new User(...);
//Our token resolved to a username so i went with this token...you could make your CustomToken take the principal. getCredentials returns "NO_PASSWORD"..it gets cleared out anyways. also the getAuthenticated for the thing you return should return true now
return new UsernamePasswordAuthenticationToken(principal, auth.getCredentials(), principal.getAuthorities());
} catch(Expection e){
//TODO throw appropriate AuthenticationException types
throw new BadCredentialsException(MESSAGE_AUTHENTICATION_FAILURE, e);
}
}
@Override
public boolean supports(Class<?> authentication) {
return CustomToken.class.isAssignableFrom(authentication);
}
}
Finally, register your provider as a bean so the authentication manager finds it in some @Configuration class. You probably could just @Component it too, i prefer this method
@Bean
public AuthenticationProvider createCustomAuthenticationProvider(injectedDependencies) {
return new CustomAuthenticationProvider(injectedDependencies);
}
Another Example Project which uses JWT - Jhipster
Try Generating a Microservice application using JHipster. It generates a template with out of the box integration between Spring Security and JWT.
https://jhipster.github.io/security/
My sample app does exactly this - securing REST endpoints using Spring Security in a stateless scenario. Individual REST calls are authenticated using an HTTP header. Authentication information is stored on the server side in an in-memory cache and provides the same semantics as those offered by the HTTP session in a typical web application. The app uses the full Spring Security infrastructure with very minimum custom code. No bare filters, no code outside of the Spring Security infrastructure.
The basic idea is to implement the following four Spring Security components:
org.springframework.security.web.AuthenticationEntryPoint
to trap REST calls requiring authentication but missing the required authentication token and thereby deny the requests.org.springframework.security.core.Authentication
to hold the authentication information required for the REST API.org.springframework.security.authentication.AuthenticationProvider
to perform the actual authentication (against a database, an LDAP server, a web service, etc.).org.springframework.security.web.context.SecurityContextRepository
to hold the authentication token in between HTTP requests. In the sample, the implementation saves the token in an EHCACHE instance.The sample uses XML configuration but you can easily come up with the equivalent Java config.
I recommend JSON Web Tokens http://jwt.io/ , it's stateless and scalable.
Here is an example project, https://github.com/brahalla/Cerberus
The code secure all endpoints - but I'm sure that you can play with that :). The token is stored in Redis using Spring Boot Starter Security and you have to define our own UserDetailsService
which you pass into AuthenticationManagerBuilder
.
Long story short - copy paste EmbeddedRedisConfiguration
and SecurityConfig
and replace AuthenticationManagerBuilder
to your logic.
HTTP:
Requesting token - sending basic HTTP auth content in a request header. A token is given back in a response header.
http --print=hH -a user:password localhost:8080/v1/users
GET /v1/users HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate
Authorization: Basic dXNlcjpwYXNzd29yZA==
Connection: keep-alive
Host: localhost:8080
User-Agent: HTTPie/0.9.3
HTTP/1.1 200 OK
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Content-Length: 4
Content-Type: text/plain;charset=UTF-8
Date: Fri, 06 May 2016 09:44:23 GMT
Expires: 0
Pragma: no-cache
Server: Apache-Coyote/1.1
X-Application-Context: application
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block
x-auth-token: cacf4a97-75fe-464d-b499-fcfacb31c8af
Same request but using token:
http --print=hH localhost:8080/v1/users 'x-auth-token: cacf4a97-75fe-464d-b499-fcfacb31c8af'
GET /v1/users HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate
Connection: keep-alive
Host: localhost:8080
User-Agent: HTTPie/0.9.3
x-auth-token: cacf4a97-75fe-464d-b499-fcfacb31c8af
HTTP/1.1 200 OK
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Content-Length: 4
Content-Type: text/plain;charset=UTF-8
Date: Fri, 06 May 2016 09:44:58 GMT
Expires: 0
Pragma: no-cache
Server: Apache-Coyote/1.1
X-Application-Context: application
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block
If you pass wrong username/password or token you get 401.
JAVA
I added those dependencies into build.gradle
compile("org.springframework.session:spring-session-data-redis:1.0.1.RELEASE")
compile("org.springframework.boot:spring-boot-starter-security")
compile("org.springframework.boot:spring-boot-starter-web")
compile("com.github.kstyrc:embedded-redis:0.6")
Then Redis configration
@Configuration
@EnableRedisHttpSession
public class EmbeddedRedisConfiguration {
private static RedisServer redisServer;
@Bean
public JedisConnectionFactory connectionFactory() throws IOException {
redisServer = new RedisServer(Protocol.DEFAULT_PORT);
redisServer.start();
return new JedisConnectionFactory();
}
@PreDestroy
public void destroy() {
redisServer.stop();
}
}
Security config:
@Configuration
@EnableWebSecurity
@Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
UserService userService;
@Override
protected void configure(AuthenticationManagerBuilder builder) throws Exception {
builder.userDetailsService(userService);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.requestCache()
.requestCache(new NullRequestCache())
.and()
.httpBasic();
}
@Bean
public HttpSessionStrategy httpSessionStrategy() {
return new HeaderHttpSessionStrategy();
}
}
Usually in tutorials you find AuthenticationManagerBuilder
using inMemoryAuthentication
but there is a lot more choices (LDAP, ...) Just take a look into class definition. I'm using userDetailsService
which requires UserDetailsService
object.
And finally my user service using CrudRepository
.
@Service
public class UserService implements UserDetailsService {
@Autowired
UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserAccount userAccount = userRepository.findByEmail(username);
if (userAccount == null) {
return null;
}
return new User(username, userAccount.getPassword(), AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER"));
}
}