I currently authenticate my requests against the JWK of my authorization server.
I'm using the spring-boot-starter-oauth2-resource-server package on spring-boot 2.3.
The JWT is taken out from the Authorization: Bearer <token> header and validated against the JWK endpoint.
My security config looks like this:
SecurityConfig.java
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
protected Environment env;
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http.authorizeRequests().antMatchers("/logs").permitAll();
http.authorizeRequests().antMatchers("/", "/api/**").authenticated();
http.oauth2ResourceServer().jwt();
}
}
application.properties
spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://auth.work.com/v1/jwk
External users don't have the Authorization: ... header in their requests, but instead have the following 2 cookies that make up the JWT:
auth.token_type = Bearer
auth.access_token = <token>
Is there a way to extract the JWT from the cookies and validate against the auth server if the the Authorization: ... header is missing?
Could I extract the cookies and add a header to the request before it tries to authorize?
Or it could even be a second method in the authentication chain.
I found out about custom token resolvers and ended up creating one to validate both the header and the cookie. I'm not sure if this is the cleanest solution, but it works.
The only issue I have with this is that it doesn't automatically validate the Authorization: Bearer ... header anymore, so I had to add the code for it too.
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http.authorizeRequests().antMatchers("/logs").permitAll();
http.authorizeRequests().antMatchers("/", "/api/**").authenticated();
http.oauth2ResourceServer().jwt().and().bearerTokenResolver(this::tokenExtractor);
}
public String tokenExtractor(HttpServletRequest request) {
String header = request.getHeader(HttpHeaders.AUTHORIZATION);
if (header != null)
return header.replace("Bearer ", "");
Cookie cookie = WebUtils.getCookie(request, "auth.access_token");
if (cookie != null)
return cookie.getValue();
return null;
}
}
I think you may use Custom Filter, intercept request and fetch cookies from HttpServletRequest.
#Override
public void doFilter(
ServletRequest request,
ServletResponse response,
FilterChain chain) throws IOException, ServletException {
Cookie[] cookies = request.getCookies();
// Your validation logic
}
Just add it to Spring Security filters chain.
More details how to do it here: https://www.baeldung.com/spring-security-custom-filter
Related
I am using okta to do authentication. Our company's okta disabled the 'default' authorization server. So right now I cannot use 'okta-spring-security-starter' to simple do this to verify token passed from url headers:
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
#Configuration
public class OktaOAuth2WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/health").permitAll()
.anyRequest().authenticated()
.and()
.oauth2ResourceServer().jwt();
http.cors();
Okta.configureResourceServer401ResponseBody(http);
}
}
So I need to hit okta introspect endpoint (https://developer.okta.com/docs/reference/api/oidc/#introspect) to verify. So I am wondering can I integrate this procedure within the config of WebSecurityConfigurerAdapter. maybe something like this???:
import com.okta.spring.boot.oauth.Okta;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
#Configuration
public class OktaOAuth2WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/health").permitAll()
.anyRequest().authenticated()
.and()
/*add something there*/
http.cors();
}
}
I saw something like override AuthenticationProvider(Custom Authentication provider with Spring Security and Java Config), and use httpbasic auth. Can I do similiar thing if I use .oauth2ResourceServer().jwt().
My idea is override the authentication provider and in the provider, hit the okta introspect endpoint, will this work???
Spring Security 5.2 ships with support for introspection endpoints. Please take a look at the Opaque Token sample in the GitHub repo.
To answer briefly here, though, you can do:
http
.authorizeRequests(authz -> authz
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.opaqueToken(opaque -> opaque
.introspectionUri("the-endpoint")
.introspectionClientCredentials("client-id", "client-password")
)
);
If you are using Spring Boot, then it's a bit simpler. You can provide those properties in your application.yml:
spring:
security:
oauth2:
resourceserver:
opaquetoken:
introspection-uri: ...
client-id: ...
client-secret: ...
And then your DSL can just specify opaqueToken:
http
.authorizeRequests(authz -> authz
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.opaqueToken(opaque -> {})
);
I don't use Okta thus I don't know how exactly it works. But I have 2 assumptions:
Every request contains an accessToken in the Authorization header
You make a POST request to ${baseUrl}/v1/introspect and it will answer you with true or false to indicate that accessToken is valid or not
With these 2 assumptions in mind, if I have to manually implement custom security logic authentication, I would do following steps:
Register and implement a CustomAuthenticationProvider
Add a filter to extract access token from request
Registering custom authentication provider:
// In OktaOAuth2WebSecurityConfig.java
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(customAuthenticationProvider());
}
#Bean
CustomAuthenticationProvider customAuthenticationProvider(){
return new CustomAuthenticationProvider();
}
CustomAuthenticationProvider:
public class CustomAuthenticationProvider implements AuthenticationProvider {
private static final Logger logger = LoggerFactory.getLogger(CustomAuthenticationProvider.class);
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
logger.debug("Authenticating authenticationToken");
OktaTokenAuthenticationToken auth = (OktaTokenAuthenticationToken) authentication;
String accessToken = auth.getToken();
// You should make a POST request to ${oktaBaseUrl}/v1/introspect
// to determine if the access token is good or bad
// I just put a dummy if here
if ("ThanhLoyal".equals(accessToken)){
List<GrantedAuthority> authorities = Collections.singletonList(new SimpleGrantedAuthority("USER"));
logger.debug("Good access token");
return new UsernamePasswordAuthenticationToken(auth.getPrincipal(), "[ProtectedPassword]", authorities);
}
logger.debug("Bad access token");
return null;
}
#Override
public boolean supports(Class<?> clazz) {
return clazz == OktaTokenAuthenticationToken.class;
}
}
To register the filter to extract accessToken from request:
// Still in OktaOAuth2WebSecurityConfig.java
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.addFilterAfter(accessTokenExtractorFilter(), UsernamePasswordAuthenticationFilter.class)
.authorizeRequests().anyRequest().authenticated();
// And other configurations
}
#Bean
AccessTokenExtractorFilter accessTokenExtractorFilter(){
return new AccessTokenExtractorFilter();
}
And the filter it self:
public class AccessTokenExtractorFilter extends OncePerRequestFilter {
private static final Logger logger = LoggerFactory.getLogger(AccessTokenExtractorFilter.class);
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
logger.debug("Filtering request");
Authentication authentication = getAuthentication(request);
if (authentication == null){
logger.debug("Continuing filtering process without an authentication");
filterChain.doFilter(request, response);
} else {
logger.debug("Now set authentication on the request");
SecurityContextHolder.getContext().setAuthentication(authentication);
filterChain.doFilter(request, response);
}
}
private Authentication getAuthentication(HttpServletRequest request) {
String accessToken = request.getHeader("Authorization");
if (accessToken != null){
logger.debug("An access token found in request header");
List<GrantedAuthority> authorities = Collections.singletonList(new SimpleGrantedAuthority("USER"));
return new OktaTokenAuthenticationToken(accessToken, authorities);
}
logger.debug("No access token found in request header");
return null;
}
}
I have uploaded a simple project here for your easy reference: https://github.com/MrLoyal/spring-security-custom-authentication
How it works:
The AccessTokenExtractorFilter is placed right after the UsernamePasswordAuthenticationFilter, which is a default filter by Spring Security
A request arrives, the above filter extracts accessToken from it and place it in the SecurityContext
Later, the AuthenticationManager calls the AuthenticationProvider(s) to authenticate request. This case, the CustomAuthenticationProvider is invoked
BTW, your question should contain spring-security tag.
Update 1: About AuthenticationEntryPoint
An AuthenticationEntryPoint declares what to do when an unauthenticated request arrives ( in our case, what to do when the request does not contain a valid "Authorization" header).
In my REST API, I simply response 401 HTTP status code to client.
// CustomAuthenticationEntryPoint
#Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
response.reset();
response.setStatus(401);
// A utility method to add CORS headers to the response
SecUtil.writeCorsHeaders(request, response);
}
Spring's LoginUrlAuthenticationEntryPoint redirects user to login page if one is configured.
So if you want to redirect unauthenticated requests to Okta's login page, you may use a AuthenticationEntryPoint.
I'm using Spring for a webapplication's backend, and will be using Angular for frontend. I'm trying to use CSRF protection for the login only, following
this guide with some modification. What I'm trying to achieve is, that Angular first ajaxing to "/init" which will set the CSRF TOKEN cookie, then it can call the login, which is CSRF protected.
When I'm calling the /init method, it gives back a CSRF token, however after that when I'm calling /login I keep getting 403 forbidden.
Here's the request (using Postman for testing purposes):
POST /login HTTP/1.1
Host: localhost:8080
Accept: application/json
Content-Type: application/json
Cache-Control: no-cache
{"username":"test","password":"test"}
Here's the java code for the csrf:
private RequestMatcher csrfRequestMatcher = new RequestMatcher() {
private RegexRequestMatcher requestMatcher =
new RegexRequestMatcher("/login", null);
#Override
public boolean matches(HttpServletRequest request) {
return requestMatcher.matches(request);
}
};
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf()
.requireCsrfProtectionMatcher(csrfRequestMatcher)
.and()
.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint)
.and().authorizeRequests().antMatchers("/test/**").authenticated()
.and().addFilterAt(MyUsernamePasswordAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
.formLogin().permitAll()
.and().logout().logoutSuccessHandler(MyLogoutHandler())
.and().addFilterAfter(new CsrfGrantingFilter(), SessionManagementFilter.class);
}
public class CsrfGrantingFilter implements Filter {
#Override
public void init(FilterConfig filterConfig) throws ServletException {}
#Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
throws IOException, ServletException {
CsrfToken csrf = (CsrfToken) servletRequest.getAttribute(CsrfToken.class.getName());
String token = csrf.getToken();
if (token != null && isInit(servletRequest)) {
HttpServletResponse response = (HttpServletResponse) servletResponse;
Cookie cookie = new Cookie("XSRF-TOKEN", token);
cookie.setPath("/");
response.addCookie(cookie);
}
filterChain.doFilter(servletRequest, servletResponse);
}
private boolean isInit(ServletRequest servletRequest) {
HttpServletRequest request = (HttpServletRequest) servletRequest;
return request.getRequestURI().equals("/init");
}
#Override
public void destroy() {}
}
Am I missing something obvious? Or is just my request bad?
CSRF is enabled by default in Spring, so why is there has to be a requestMatcher, Filter etc? Angular has built in support for CSRF - "XSRF" based on cookies - https://angular.io/guide/http#security-xsrf-protection
To quote a guide: "Angular wants the cookie name to be "XSRF-TOKEN" and Spring Security provides it as a request attribute by default, so we just need to transfer the value from a request attribute to a cookie."
So to make it work with Angular, in the confgiure() method, configure the csrf() as
http.csrf()
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
Just call /login and after successful login, Angular (assuming Angular 2+) will pick up the token from the cookie and send it
Everything was fine, but in the header i had to put the token under "X-CSRF-TOKEN" key, not XSRF-TOKEN or CSRF-TOKEN.
A REST Spring Security /user service in a Spring Boot application is failing to immediately update the XSRF-TOKEN cookie when a user authenticates. This is causing the next request for /any-other-REST-service-url to return an Invalid CSRF certificate error, until the /user service is called again. How can this problem be resolved so that the REST /user service properly updates the XSRF-TOKEN cookie in the same request/response transaction in whichit first authenticates the user?
The backend REST /user service is called three times by a front end app, but the /user service only returns matched JSESSIONID/XSRF-TOKEN cookies on the first and third call, NOT on the second call.
In the first request to the server, no credentials (no username or password) are sent to the / url pattern, which I think calls the /user service, and the server responds with a JSESSIONID and XSRF-TOKEN that it associated with an anonymous user. The Network tab of the FireFox developer tools shows these cookies as:
Response cookies:
JSESSIONID:"D89FF3AD2ACA7007D927872C11007BCF"
path:"/"
httpOnly:true
XSRF-TOKEN:"67acdc7f-5127-4ea2-9a7b-831e95957789"
path:"/"
The user then makes various requests for publicly accessible resources without error, and the Network tab of the FireFox developer tools shows these same cookie values.
The second request to the /user service is done though a login form, which sends a valid username and password, which the/user service uses to authenticate the user. But the /user service only returns an updated jsessionid cookie, and does not update the xsrf-token cookie in this step. Here are the cookies shown in the Network tab of the FireFox developer tools at this point:
The 200 GET user included the following cookies in the Network tab of FireFox:
Response cookies:
JSESSIONID:"5D3B51A03B9AE218586591E67C53FB89"
path:"/"
httpOnly:true
AUTH1:"yes"
Request cookies:
JSESSIONID:"D89FF3AD2ACA7007D927872C11007BCF"
XSRF-TOKEN:"67acdc7f-5127-4ea2-9a7b-831e95957789"
Note that the response included a new JSESSIONID, but did not include a new XSRF-TOKEN. This results in a mismatch causing a 403 error (due to invalid csrf token) in the subsequent requests to other rest services, until this is resolved by a third call to the /user service. is there a way that we can force the preceding 200 get user to return the new XSRF-TOKEN also?
The third call to the backend REST /user service uses the very same username and password credentials that were used in the second request shown above, but this third call to /user results in the XSRF_TOKEN cookie being updated properly, while the same correct JSESSIONID is retained. Here is what the Network tab of the FireFox developer tools shows at this point:
The 200 GET user shows that the mismatched request forces an update of the XSRF-TOKEN in the response:
Response cookies:
XSRF-TOKEN:"ca6e869c-6be2-42df-b7f3-c1dcfbdb0ac7"
path:"/"
AUTH1:"yes"
Request cookies:
JSESSIONID:"5D3B51A03B9AE218586591E67C53FB89"
XSRF-TOKEN:"67acdc7f-5127-4ea2-9a7b-831e95957789"
The updated xsrf-token now matches the jsessionid, and thus subsequent requests to other backend rest services can now succeed.
What specific changes can be made to the code below to force an update of both the XSRF-TOKEN and the JSESSIONID cookies the first time the /user service is called with proper username and password by the login form? Do we make specific changes in the code for the backend /user method in Spring? Or is the change made in the Security Configuration classes? What can we try to fix this problem?
The code for the backend /user service and the Security Config are in the main application class of the Spring Boot backend app, which is in UiApplication.java as follows:
#SpringBootApplication
#Controller
#EnableJpaRepositories(basePackages = "demo", considerNestedRepositories = true)
public class UiApplication extends WebMvcConfigurerAdapter {
#Autowired
private Users users;
#RequestMapping(value = "/{[path:[^\\.]*}")
public String redirect() {
// Forward to home page so that route is preserved.
return "forward:/";
}
#RequestMapping("/user")
#ResponseBody
public Principal user(HttpServletResponse response, HttpSession session, Principal user) {
response.addCookie(new Cookie("AUTH1", "yes"));
return user;
}
public static void main(String[] args) {
SpringApplication.run(UiApplication.class, args);
}
#Bean
public LocaleResolver localeResolver() {
SessionLocaleResolver slr = new SessionLocaleResolver();
slr.setDefaultLocale(Locale.US);
return slr;
}
#Bean
public LocaleChangeInterceptor localeChangeInterceptor() {
LocaleChangeInterceptor lci = new LocaleChangeInterceptor();
lci.setParamName("lang");
return lci;
}
#Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/login").setViewName("login");
}
#Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(localeChangeInterceptor());
}
#Order(Ordered.HIGHEST_PRECEDENCE)
#Configuration
protected static class AuthenticationSecurity extends GlobalAuthenticationConfigurerAdapter {
#Autowired
private Users users;
#Override
public void init(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(users);
}
}
#SuppressWarnings("deprecation")
#Configuration
#Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
#EnableWebMvcSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
protected static class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.httpBasic().and().authorizeRequests()
.antMatchers("/registration-form").permitAll()
.antMatchers("/confirm-email**").permitAll()
.antMatchers("/submit-phone").permitAll()
.antMatchers("/check-pin").permitAll()
.antMatchers("/send-pin").permitAll()
.antMatchers("/index.html", "/", "/login", "/message", "/home", "/public*", "/confirm*", "/register*")
.permitAll().anyRequest().authenticated().and().csrf()
.csrfTokenRepository(csrfTokenRepository()).and()
.addFilterAfter(csrfHeaderFilter(), CsrfFilter.class);
}
private Filter csrfHeaderFilter() {
return new OncePerRequestFilter() {
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
CsrfToken csrf = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
if (csrf != null) {
Cookie cookie = WebUtils.getCookie(request, "XSRF-TOKEN");
String token = csrf.getToken();
if (cookie == null || token != null && !token.equals(cookie.getValue())) {
cookie = new Cookie("XSRF-TOKEN", token);
cookie.setPath("/");
response.addCookie(cookie);
}
}
filterChain.doFilter(request, response);
}
};
}
private CsrfTokenRepository csrfTokenRepository() {
HttpSessionCsrfTokenRepository repository = new HttpSessionCsrfTokenRepository();
repository.setHeaderName("X-XSRF-TOKEN");
return repository;
}
}
}
The relevant segment of the server logs showing the CSRF error is:
2016-01-20 02:02:06.811 DEBUG 3995 --- [nio-9000-exec-5] o.s.s.w.header.writers.HstsHeaderWriter : Not injecting HSTS header since it did not match the requestMatcher org.springframework.security.web.header.writers.HstsHeaderWriter$SecureRequestMatcher#70b8c8bb
2016-01-20 02:02:06.813 DEBUG 3995 --- [nio-9000-exec-5] o.s.security.web.FilterChainProxy : /send-pin at position 4 of 13 in additional filter chain; firing Filter: 'CsrfFilter'
2016-01-20 02:02:06.813 DEBUG 3995 --- [nio-9000-exec-5] o.s.security.web.csrf.CsrfFilter : Invalid CSRF token found for http://localhost:9000/send-pin
What specific changes do I need to make to the code above to resolve this CSRF error?
How do I force an immediate update of the XSRF cookie upon whenever the backend /user service changes a user's status (login, logout, etc.)?
Note: I am guessing (based on my research) that the solution to this problem will involve changing the configuration of some combination of the following Spring Security classes, all of which are defined in the UiApplication.java shown below:
the WebSecurityConfigurerAdapter,
the OncePerRequestFilter,
the CsrfTokenRepository,
the GlobalAuthenticationConfigurerAdapter and/or
the Principal returned by the /user service.
But what specific changes need to be made to solve the problem?
Updated Answer
The reason you are getting a 401 is because a basic authentication header is found in the request when the user is registering. This means Spring Security tries to validate the credentials but the user is not yet present so it responds with a 401.
You should
Make the /register endpoint public and provide a controller that registers the user
Do not include the username/password for registration form in the Authorization header as this will cause Spring Security to try to validate the credentials. Instead include the parameters as JSON or form encoded parameters that your /register controller process
Original Answer
After authenticating, Spring Security uses CsrfAuthenticationStrategy to invalidate any CsrfToken's (to ensure that a session fixation attack is not possible). This is what triggers a new CsrfToken to be used.
However, the problem is that csrfTokenRepository is invoked before authentication is performed. This means that when csrfTokenRepository checks to see if the token has changed the result if false (it has not changed yet).
To resolve the issue, you can inject a custom AuthenticationSuccessHandler. For example:
public class MyAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response, Authentication authentication)
throws ServletException, IOException {
CsrfToken csrf = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
if (csrf != null) {
Cookie cookie = WebUtils.getCookie(request, "XSRF-TOKEN");
String token = csrf.getToken();
if (cookie == null || token != null && !token.equals(cookie.getValue())) {
cookie = new Cookie("XSRF-TOKEN", token);
cookie.setPath("/");
response.addCookie(cookie);
}
}
super.onAuthenticationSuccess(request,response,authentication);
}
}
Then you can configure it:
protected void configure(HttpSecurity http) throws Exception {
http
.formLogin()
.successHandler(new MyAuthenticationSuccessHandler())
.and()
.httpBasic().and()
.authorizeRequests()
.antMatchers("/registration-form").permitAll()
.antMatchers("/confirm-email**").permitAll()
.antMatchers("/submit-phone").permitAll()
.antMatchers("/check-pin").permitAll()
.antMatchers("/send-pin").permitAll()
.antMatchers("/index.html", "/", "/login", "/message", "/home", "/public*", "/confirm*", "/register*").permitAll()
.anyRequest().authenticated()
.and()
.csrf()
.csrfTokenRepository(csrfTokenRepository())
.and()
.addFilterAfter(csrfHeaderFilter(), CsrfFilter.class);
}
I have POST request with something like this in my Spring project:
{"clientKey":"XXX", "accessKey":"ZZZ", ... }
My backend works in very simple paradigm: get clientKey (login) and accessKey (password) params from POST body, check their persistence in DB and then do some business logic.
I need to implement minimal security check logic using Spring Security for each incoming request (without sessions and tokens).
SecurityConfig.java
#Configuration
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter
{
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/**").hasRole("USER")
.and().csrf().disable();
http.addFilterBefore(new ApiAuthorizationFilter(), UsernamePasswordAuthenticationFilter.class);
}
}
ApiAuthorizationFilter.java
public class ApiAuthorizationFilter extends UsernamePasswordAuthenticationFilter {
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
//
// always prints "{}", why?
//
Logger.getLogger("test").log(Level.INFO, request.getParameterMap().toString());
//
// Ok, I will make some manual auth operations for testing purposes.
// Seems what it isn't work too..
//
Set<SimpleGrantedAuthority> authorities = new HashSet<>(1);
authorities.add(new SimpleGrantedAuthority("USER"));
Authentication auth = new UsernamePasswordAuthenticationToken(
"94fc97a7b3fd2175472ec4a41bcb3b14",
"746b2aa32fe90f0ba53e6efe7a8d1f1f",
authorities);
SecurityContextHolder.getContext().setAuthentication(auth);
chain.doFilter(request, response);
}
}
What I doing wrong? Does UsernamePasswordAuthenticationFilter work only with login forms when submitted or I need another filter in security chain?
I'm using Spring 4 with Spring Security, custom GenericFilterBean and AuthenticationProvider implementations. I have mostly secured URLs with the exception of a URL to create new session: /v2/session (e.g. login based on the username and password and returns Auth Token to be used in the subsequent requests that require authentication) configured as follows:
#Configuration
#ComponentScan(basePackages={"com.api.security"})
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private ApiAuthenticationProvider apiAuthenticationProvider;
#Autowired
private AuthTokenHeaderAuthenticationFilter authTokenHeaderAuthenticationFilter;
#Autowired
private AuthenticationEntryPoint apiAuthenticationEntryPoint;
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) {
auth.authenticationProvider(apiAuthenticationProvider);
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.addFilterBefore(authTokenHeaderAuthenticationFilter, BasicAuthenticationFilter.class) // Main auth filter
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.authorizeRequests()
.antMatchers(HttpMethod.POST, "/v2/session").permitAll()
.anyRequest().authenticated();
http.exceptionHandling()
.authenticationEntryPoint(apiAuthenticationEntryPoint);
}
}
The authTokenHeaderAuthenticationFilter runs on every request and gets Token from the request header:
/**
* Main Auth Filter. Always sets Security Context if the Auth token Header is not empty
*/
#Component
public class AuthTokenHeaderAuthenticationFilter extends GenericFilterBean {
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
final String token = ((HttpServletRequest) request).getHeader(RequestHeaders.AUTH_TOKEN_HEADER);
if (StringUtils.isEmpty(token)) {
chain.doFilter(request, response);
return;
}
try {
AuthenticationToken authRequest = new AuthenticationToken(token);
SecurityContextHolder.getContext().setAuthentication(authRequest);
}
} catch (AuthenticationException failed) {
SecurityContextHolder.clearContext();
return;
}
chain.doFilter(request, response); // continue down the chain
}
}
The custom apiAuthenticationProvider will try to authenticate all requests based on the token provided in the header and if authentication is unsuccessful - throws AccessException and client will receive HTTP 401 response:
#Component
public class ApiAuthenticationProvider implements AuthenticationProvider {
#Autowired
private remoteAuthService remoteAuthService;
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
AuthenticationToken authRequest = (AuthenticationToken) authentication;
String identity = null;
try {
identity = remoteAuthService.getUserIdentityFromToken(authRequest.getToken());
} catch (AccessException e) {
throw new InvalidAuthTokenException("Cannot get user identity from the token", e);
}
return new AuthenticationToken(identity, authRequest.getToken(), getGrantedAuthorites());
}
}
This works perfectly fine for the requests that require authentication. This works fine for the /v2/session request without the Authentication Header in it. However, for the /v2/session request that has an expired Auth Token in the header (or in the cookie - not shown in the code samples; this may happen sometimes if the client didn't clear the headers or continues sending cookies with requests) the security context will be initialized and apiAuthenticationProvider will throw an exception and respond with HTTP 401 to the client.
Since /v2/session has been configured as
http.authorizeRequests()
.antMatchers(HttpMethod.POST, "/v2/session").permitAll()
I would expect Spring Security to determine that before calling ApiAuthenticationProvider.authenticate(). What should be the way for the filter or auth provider to ignore/not throw the exception for the URLs configured as permitAll()?
Spring security filters get triggered before the request authorisation checks are performed. For the authorisation checks to work, it is assumed that the request has been through the filters and the Spring security context has been set (or not, depending on whether authentication credentials have been passed in).
In your filter you have check that continues with the filter chain processing if the token is not there. Unfortunately, if it is, then it will be passed to your provider for authentication, which throws an exception because the token has expired thus you're getting the 401.
Your best bet is to bypass filter execution for the URLs that you consider public. You can either do this in the filter itself or in your configuration class. Add the following method to your SecurityConfig class:
#Override
public void configure(WebSecurity webSecurity) {
webSecurity.ignoring().antMatchers(HttpMethod.POST, "/v2/session");
}
What this will do, is bypass your AuthTokenHeaderAuthenticationFilter completely for POST /v2/sessions URL.