Standalone Spring OAuth2 JWT Authorization Server + CORS - spring-security

So I have the following Authorization Server condensed from this example from Dave Syer
#SpringBootApplication
public class AuthserverApplication {
public static void main(String[] args) {
SpringApplication.run(AuthserverApplication.class, args);
}
/* added later
#Configuration
#Order(Ordered.HIGHEST_PRECEDENCE)
protected static class MyWebSecurity extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http //.csrf().disable()
.authorizeRequests()
.antMatchers(HttpMethod.OPTIONS, "/oauth/token").permitAll();
}
}*/
#Configuration
#EnableAuthorizationServer
protected static class OAuth2AuthorizationConfig extends
AuthorizationServerConfigurerAdapter {
#Autowired
private AuthenticationManager authenticationManager;
#Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
KeyPair keyPair = new KeyStoreKeyFactory(
new ClassPathResource("keystore.jks"), "foobar".toCharArray())
.getKeyPair("test");
converter.setKeyPair(keyPair);
return converter;
}
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("acme")
//.secret("acmesecret")
.authorizedGrantTypes(//"authorization_code", "refresh_token",
"password").scopes("openid");
}
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints)
throws Exception {
endpoints.authenticationManager(authenticationManager).accessTokenConverter(
jwtAccessTokenConverter());
}
#Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer)
throws Exception {
oauthServer.tokenKeyAccess("permitAll()").checkTokenAccess(
"isAuthenticated()");
}
}
}
when I run it and test it with curl
curl acme#localhost:8110/oauth/token -d grant_type=password -d client_id=acme -d username=user -d password=password
I get a JWT as respons, but as soon as I try to access the AuthServer from my Frontend (Angular JS on a different port) I get CORS error. Not becauce of missing Headers, but because the OPTION request is rejected and is missing the credentials.
Request URL:http://localhost:8110/oauth/token
Request Method:OPTIONS
Status Code:401 Unauthorized
WWW-Authenticate:Bearer realm="oauth", error="unauthorized", error_description="Full authentication is required to access this resource"
I already knew that I have to add a CorsFilter and additionally found this post where I used the the snippet for the first Answer to let the OPTIONS request access /oauth/token without credentials:
#Order(-1)
public class MyWebSecurity extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers(HttpMethod.OPTIONS, "/oauth/token").permitAll();
}
}
After that I got with curl the following error:
{"timestamp":1433370068120,"status":403,"error":"Forbidden","message":"Expected CSRF token not found. Has your session expired?","path":"/oauth/token"}
So to make it simple I just added http.csrf().disable() to the configure method of MyWebSecurity class, which solves the Problem with the OPTION request, but therefore the POST request isn't working anymore and I get There is no client authentication. Try adding an appropriate authentication filter. (also with curl).
I tried to find out if I have to somehow connect MyWebSecurity class and the AuthServer, but without any luck. The original example (link in the beginning) injects as well the authenticationManager, but this changed nothing for me.

Found the reason for my Problem!
I just needed to end the filterchain and return the result immediatly if a OPTIONS request is processed by the CorsFilter!
SimpleCorsFilter.java
#Component
#Order(Ordered.HIGHEST_PRECEDENCE)
public class SimpleCorsFilter implements Filter {
public SimpleCorsFilter() {
}
#Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletResponse response = (HttpServletResponse) res;
HttpServletRequest request = (HttpServletRequest) req;
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
response.setHeader("Access-Control-Max-Age", "3600");
response.setHeader("Access-Control-Allow-Headers", "x-requested-with, authorization");
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
response.setStatus(HttpServletResponse.SC_OK);
} else {
chain.doFilter(req, res);
}
}
#Override
public void init(FilterConfig filterConfig) {
}
#Override
public void destroy() {
}
}
After that I could ignore the OPTIONS preflight request in my AuthServer =D
So the Server works as in the snipped above and you can ignore the block comment with MyWebSecurity class in the beginning.

I found a solution using the solution for the question. But I have another way to describe the solution:
#Configuration
public class WebSecurityGlobalConfig extends WebSecurityConfigurerAdapter {
....
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring()
.antMatchers(HttpMethod.OPTIONS);
}
...
}

I came across similar issue using following
Backend Spring Boot 1.5.8.RELEASE
Spring OAuth2 Spring OAuth 2.2.0.RELEASE w
Vuejs app using axios ajax request library
With postman everything works! When I started making request from Vuejs app then I got the following errors
OPTIONS http://localhost:8080/springboot/oauth/token 401 ()
and
XMLHttpRequest cannot load http://localhost:8080/springboot/oauth/token. Response for preflight has invalid HTTP status code 401
After reading a bit, I found out that I can instruct my Spring OAuth to ignore the OPTIONS request by overriding configure in my WebSecurityConfigurerAdapter implementation class as follow
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers(HttpMethod.OPTIONS);
}
Addition of the above helped but then, I came across the CORS specific error
OPTIONS http://localhost:8080/springboot/oauth/token 403 ()
and
XMLHttpRequest cannot load http://localhost:8080/springboot/oauth/token. Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:8000' is therefore not allowed access. The response had HTTP status code 403.
And solved the above issue with the help of a CorsConfig as shown below
#Configuration
public class CorsConfig {
#Bean
public FilterRegistrationBean corsFilterRegistrationBean() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.applyPermitDefaultValues();
config.setAllowCredentials(true);
config.setAllowedOrigins(Arrays.asList("*"));
config.setAllowedHeaders(Arrays.asList("*"));
config.setAllowedMethods(Arrays.asList("*"));
config.setExposedHeaders(Arrays.asList("content-length"));
config.setMaxAge(3600L);
source.registerCorsConfiguration("/**", config);
FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(source));
bean.setOrder(0);
return bean;
}
}
After addition of the above class, it works as expected. Before I go prod I will research consequences of using
web.ignoring().antMatchers(HttpMethod.OPTIONS);
as well as best practices for above Cors configuration. For now * does the job but, definitely not secure for production.
Cyril's answer helped me partially and then I came across the CorsConfig idea in this Github issue.

well, you're right! that's a solution, and it worked also for me (I had the same issue)
But let me sussgest to use a smarter CORS Filter implementation for Java:
http://software.dzhuvinov.com/cors-filter.html
This is very complete solution for Java applications.
Actually, you can see here how your point is resolved.

Using Spring Boot 2 here.
I had to do this in my AuthorizationServerConfigurerAdapter
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
Map<String, CorsConfiguration> corsConfigMap = new HashMap<>();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
//TODO: Make configurable
config.setAllowedOrigins(Collections.singletonList("*"));
config.setAllowedMethods(Collections.singletonList("*"));
config.setAllowedHeaders(Collections.singletonList("*"));
corsConfigMap.put("/oauth/token", config);
endpoints.getFrameworkEndpointHandlerMapping()
.setCorsConfigurations(corsConfigMap);
//additional settings...
}

I tried different things to solve this issue. I would say that the below was fixed this issue on my side (Using Spring Boot 2)
1-Add the below method to the below method class that extends WebSecurityConfigurerAdapter:
// CORS settings
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring()
.antMatchers(HttpMethod.OPTIONS);
}
2-Add the below to my class that extends AuthorizationServerConfigurerAdapter
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
// enable cors for "/oauth/token"
Map<String, CorsConfiguration> corsConfigMap = new HashMap<>();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.setAllowedOrigins(Collections.singletonList("*"));
config.setAllowedMethods(Collections.singletonList("*"));
config.setAllowedHeaders(Collections.singletonList("*"));
corsConfigMap.put("/oauth/token", config);
endpoints.getFrameworkEndpointHandlerMapping()
.setCorsConfigurations(corsConfigMap);
// add the other configuration
}

Related

Spring OAuth2.0 : Spring Authorization Server 1.0 and Resource server in the same boot application

I'm struggling to use the very same Spring Boot 3.0 application as both authentication server and resource server, but until now, I've not been able to make the whole thing working.
First, I defined a very simple RestController:
#RestController
#RequestMapping("api")
public class PublicAPI {
#GetMapping("/apitest")
public String test(Principal principal) {
return " This is a test ==>";
}
}
Then, essentially following the code found in a Sample project of Spring, I managed to setup my boot app as Spring Authorization Server. I'm able to use Postman to get the authentication token using Oauth2 flow: I'm redirected to Spring's standard login page, I log in with credentials, and I get the Token.
Problem is, if I try to GET http://localhost:9000/api/apitest` using provided token, I get a 401 response from Spring boot.
This is my Security Configuration:
#Bean
#Order(1)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http, CorsConfiguration configCors) throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class).oidc(Customizer.withDefaults());
http
.exceptionHandling((exceptions) -> exceptions
.authenticationEntryPoint(
new LoginUrlAuthenticationEntryPoint("/login"))
);
http.cors().configurationSource(request -> configCors);
return http.build();
}
#Bean
#Order(2)
SecurityFilterChain apiFilter(HttpSecurity http) throws Exception {
http
.securityMatcher("/api/**")
.authorizeHttpRequests()
.requestMatchers("/api/**").authenticated()
.and()
.oauth2ResourceServer()
.jwt();
return http.build();
}
#Bean
#Order(3)
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http, CorsConfiguration configCors) throws Exception {
http
.securityMatcher("/oauth2/**", "/login")
.authorizeHttpRequests()
.requestMatchers("/login", "/oauth2/**")
.authenticated()
.and()
.formLogin(Customizer.withDefaults());
http.cors().configurationSource(request -> configCors);
return http.build();
}
#Bean
public CorsConfiguration corsConfiguration() throws Exception {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowCredentials(true);
configuration.setAllowedOriginPatterns(List.of("*"));
configuration.setAllowedMethods(List.of("*"));
configuration.setAllowedHeaders(List.of("*"));
return configuration;
}
If I try to access another Spring API in a different Spring Boot application which uses the first one as Authentication Server I get no errors.
Pretty sure that there's something wrong my configuration... any hint will be greatly appreciated !
At the very end, it turned out that another filter has been configured:
#Component
#Order(Ordered.HIGHEST_PRECEDENCE)
public class LoopbackIpRedirectFilter extends OncePerRequestFilter {
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
if (request.getServerName().equals("localhost") && request.getHeader("host") != null) {
UriComponents uri = UriComponentsBuilder.fromHttpRequest(new ServletServerHttpRequest(request))
.host("127.0.0.1").build();
response.sendRedirect(uri.toUriString());
return;
}
filterChain.doFilter(request, response);
}
}
Removing the LoopbackIpRedirectFilter problem was fixed

Use several spring security configuration and apply them according to the calling url

I currently have a backend application that implements a very simple Spring security based on login / password that must be added in the http headers.
I also have a front end that uses OKTA as a provider and works with JWT tokens.
I now want to make the end points dedicated to the front end applications use the JWT token system and all the others use the current login/password system.
I can make my application work with an OKTA configuration or with a login / password configuration but I can't make both work together.
Looking at the different messages on stack overflow I have implemented a double configuration but it is always the first one that is applied. The second one is simply ignored and the endpoints of the perimeter are allowed without any token or login / password
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Configuration
#Order(1)
public static class OauthOktaConfigurationAdapter extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) throws Exception {
http.cors();
http.csrf().disable();
http
.authorizeRequests().antMatchers("/api/v1/end-point/**").authenticated()
.and().oauth2ResourceServer().jwt();
Okta.configureResourceServer401ResponseBody(http);
}
}
#Configuration
#Order(2)
public static class StandardSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter {
#Value("${http.auth-app-id-header-name}")
private String appIdRequestHeaderName;
#Value("${http.auth-api-key-header-name}")
private String apiKeyRequestHeaderName;
private final AuthenticationManager authenticationManager;
#Autowired
public StandardSecurityConfigurationAdapter(AuthenticationManager authenticationManager) {
super();
this.authenticationManager = authenticationManager;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.cors();
http.csrf().disable();
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and().addFilter(initAuthenticationFilter())
.antMatcher("/api/v1/tools/**")
.authorizeRequests().anyRequest().authenticated();
}
private RequestHeaderAuthenticationFilter initAuthenticationFilter() {
RequestHeaderAuthenticationFilter requestHeaderAuthenticationFilter = new RequestHeaderAuthenticationFilter(appIdRequestHeaderName,
apiKeyRequestHeaderName);
requestHeaderAuthenticationFilter.setContinueFilterChainOnUnsuccessfulAuthentication(false);
requestHeaderAuthenticationFilter.setAuthenticationManager(authenticationManager);
return requestHeaderAuthenticationFilter;
}
}
#Override
#Bean
#Primary
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
In this code, configuration 2 is never used even if I make a call to /api/v1/tools
If I remove configuration 1, configuration 2 is applied.
Can you help me to understand what I am doing wrong?
EDIT 1 :
With the help and suggestion of Eleftheria Stein-Kousathana, i change my configuration (and i add Swagger white list configuration)
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private static final String[] AUTH_WHITELIST = {
"/v2/api-docs",
"/swagger-resources/configuration/ui",
"/swagger-resources",
"/swagger-resources/configuration/security",
"/swagger-ui.html",
"/webjars/**"
};
#Configuration
#Order(1)
public static class SwaggerConfigurationAdapter extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) throws Exception {
System.out.println("Loading configuration 1");
http.cors();
http.csrf().disable();
http
.requestMatchers(matchers -> matchers.antMatchers(AUTH_WHITELIST))
.authorizeRequests(authz -> {
authz.anyRequest().permitAll();
});
}
}
#Configuration
#Order(2)
public static class OauthOktaConfigurationAdapter extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) throws Exception {
System.out.println("Loading configuration 2");
http.cors();
http.csrf().disable();
http
.requestMatchers(matchers -> matchers.antMatchers("/api/v1/end-point/**"))
.authorizeRequests(authz -> {
try {
authz.anyRequest().authenticated().and().oauth2ResourceServer().jwt();
} catch (Exception e) {
e.printStackTrace();
}
});
Okta.configureResourceServer401ResponseBody(http);
}
}
#Configuration
#Order(3)
public static class StandardSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter {
#Value("${algo.http.auth-app-id-header-name}")
private String appIdRequestHeaderName;
#Value("${algo.http.auth-api-key-header-name}")
private String apiKeyRequestHeaderName;
private final AuthenticationManager authenticationManager;
#Autowired
public StandardSecurityConfigurationAdapter(AuthenticationManager authenticationManager) {
super();
this.authenticationManager = authenticationManager;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
System.out.println("Loading configuration 3");
http.cors();
http.csrf().disable();
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and().addFilter(initAuthenticationFilter())
.requestMatchers(matchers -> matchers.antMatchers("/api/**"))
.authorizeRequests(authz -> {
try {
authz.anyRequest().authenticated();
} catch (Exception e) {
e.printStackTrace();
}
});
}
private RequestHeaderAuthenticationFilter initAuthenticationFilter() {
RequestHeaderAuthenticationFilter requestHeaderAuthenticationFilter = new RequestHeaderAuthenticationFilter(appIdRequestHeaderName,
apiKeyRequestHeaderName);
requestHeaderAuthenticationFilter.setContinueFilterChainOnUnsuccessfulAuthentication(false);
requestHeaderAuthenticationFilter.setAuthenticationManager(authenticationManager);
return requestHeaderAuthenticationFilter;
}
}
#Override
#Bean
#Primary
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
I feel that I am very close to succeeding
Swaggers is accessible when not authenticated
The routes corresponding to "/api/v1/end-point/**" need a JWT token otherwise I get a 401 error
The routes corresponding to "/api/** " need a login / password otherwise I get a 401 error
But now I have the following error:
Every time I request a page under swagger or make a call to my api, my web browser asks me for a login / password.
If I cancel I can still navigate on Swagger UI and make call to "/api/v1/end-point/**".
Every Login / password are rejected even they are valid in configuration 3.
If I don't fill the login / password and make a call to any route of "/api/**" i got the following error :
2021-07-23 14:49:16.642 [http-nio-8081-exec-9] INFO c.c.a.a.c.CorrelationIdLoggingAspect - Calling api.controller.endpoint.getActivities executed in 197ms.
2021-07-23 14:49:22.247 [http-nio-8081-exec-1] ERROR o.a.c.c.C.[.[.[.[dispatcherServlet] - Servlet.service() for servlet [dispatcherServlet] in context with path [/secret] threw exception [Filter execution threw an exception] with root cause
java.lang.StackOverflowError: null
at java.base/java.lang.reflect.Method.invoke(Method.java:566)
at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:344)
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:205)
at com.sun.proxy.$Proxy236.authenticate(Unknown Source)
at org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:195)
at org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter$AuthenticationManagerDelegator.authenticate(WebSecurityConfigurerAdapter.java:501)
at jdk.internal.reflect.GeneratedMethodAccessor220.invoke(Unknown Source)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:566)
at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:344)
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:205)
at com.sun.proxy.$Proxy236.authenticate(Unknown Source)
at org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:195)
at org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter$AuthenticationManagerDelegator.authenticate(WebSecurityConfigurerAdapter.java:501)
If I'm understanding your program sketch and description correctly, let me attempt to summarize. Your application seeks to support the following:
Serve up swagger UI to the public and allow browsing of API definitions.
Use authenticated API endpoints (prefixed by /api/v1/end-point) with an Okta-provided JWT from another client (not swagger).
Use authenticated API endpoints (prefixed by /api, but not /api/v1/end-point) via swagger with username/password as headers.
Note: I'm not going to cover how to configure Okta as a provider here, nor configuring swagger. If those steps are not done correctly, you may still have issues.
As far as Spring Security, I think your main issue is due to the fact that you don't appear to have configured an authentication provider for your header-based configuration. This is typically done via a UserDetailsService (see section on UserDetailsService):
#Bean
public UserDetailsService userDetailsService() {
// #formatter:off
UserDetails userDetails = User.builder()
.username("api-client")
.password("{noop}my-api-key")
.roles("USER")
.build();
// #formatter:on
return new InMemoryUserDetailsManager(userDetails);
}
This is obviously an example not meant for production. But the important point is that you have to provide a way for Spring Security to determine that the credentials are valid. Whether it's a user's username/password, or an API client's appId/apiKey, the principal (see Authentication) is looked up through a UserDetailsService, and then the credentials are validated by the AuthenticationProvider.
Unfortunately, the built-in RequestHeaderAuthenticationFilter is built on top of a different type of provider that assumes you are pre-authenticated, and is therefore incompatible with username/password authentication. While you could work around this by adapting one type of provider to another, it's more straight forward (at least for example purposes) to adapt the UsernamePasswordAuthenticationFilter to your use case. For example:
private UsernamePasswordAuthenticationFilter usernamePasswordAuthenticationFilter() throws Exception {
UsernamePasswordAuthenticationFilter usernamePasswordAuthenticationFilter = new UsernamePasswordAuthenticationFilter() {
#Override
protected String obtainUsername(HttpServletRequest request) {
return request.getHeader(getUsernameParameter());
}
#Override
protected String obtainPassword(HttpServletRequest request) {
return request.getHeader(getPasswordParameter());
}
#Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
super.successfulAuthentication(request, response, chain, authResult);
chain.doFilter(request, response);
}
};
usernamePasswordAuthenticationFilter.setAuthenticationManager(authenticationManager());
usernamePasswordAuthenticationFilter.setUsernameParameter(appIdRequestHeaderName);
usernamePasswordAuthenticationFilter.setPasswordParameter(apiKeyRequestHeaderName);
usernamePasswordAuthenticationFilter.setRequiresAuthenticationRequestMatcher(AnyRequestMatcher.INSTANCE);
usernamePasswordAuthenticationFilter.setPostOnly(false);
usernamePasswordAuthenticationFilter.setAuthenticationSuccessHandler((request, response, authentication) -> {
// Do nothing
});
return usernamePasswordAuthenticationFilter;
}
If you're interested in making this feel more built-in, check out the section of the docs on custom DSLs.
I would also suggest you override the configure(WebSecurity web) method in WebSecurityConfigurerAdapter to perform your permitAll and condense the configuration down to two, as well as eliminating the /api/** pattern so your entire application is secure by default. Here's a full example (omitting any Okta-specific code) that also demonstrates correct usage of the Spring Security lambda DSL:
#Configuration
public class SecurityConfiguration {
private static final String[] AUTH_WHITELIST = {
"/v2/api-docs",
"/swagger-resources/configuration/ui",
"/swagger-resources",
"/swagger-resources/configuration/security",
"/swagger-ui.html",
"/webjars/**"
};
#Order(1)
#EnableWebSecurity
public static class OauthOktaConfigurationAdapter extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
// #formatter:off
http
.antMatcher("/api/v1/end-point/**")
.authorizeRequests((authorizeRequests) ->
authorizeRequests
.anyRequest().authenticated()
)
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)
.sessionManagement((sessionManagement) ->
sessionManagement
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.cors(withDefaults())
.csrf(CsrfConfigurer::disable);
// #formatter:on
}
}
#Order(2)
#EnableWebSecurity
public static class StandardSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter {
#Value("${algo.http.auth-app-id-header-name}")
private String appIdRequestHeaderName;
#Value("${algo.http.auth-api-key-header-name}")
private String apiKeyRequestHeaderName;
#Override
public void configure(WebSecurity web) {
web.ignoring().antMatchers(AUTH_WHITELIST);
}
#Override
protected void configure(HttpSecurity http) throws Exception {
// #formatter:off
http
.addFilterAt(usernamePasswordAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
.authorizeRequests((authorizeRequests) ->
authorizeRequests
.anyRequest().authenticated()
)
.sessionManagement((sessionManagement) ->
sessionManagement
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.cors(withDefaults())
.csrf(CsrfConfigurer::disable);
// #formatter:on
}
private UsernamePasswordAuthenticationFilter usernamePasswordAuthenticationFilter() throws Exception {
UsernamePasswordAuthenticationFilter usernamePasswordAuthenticationFilter = new UsernamePasswordAuthenticationFilter() {
#Override
protected String obtainUsername(HttpServletRequest request) {
return request.getHeader(getUsernameParameter());
}
#Override
protected String obtainPassword(HttpServletRequest request) {
return request.getHeader(getPasswordParameter());
}
#Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
super.successfulAuthentication(request, response, chain, authResult);
chain.doFilter(request, response);
}
};
usernamePasswordAuthenticationFilter.setAuthenticationManager(authenticationManager());
usernamePasswordAuthenticationFilter.setUsernameParameter(appIdRequestHeaderName);
usernamePasswordAuthenticationFilter.setPasswordParameter(apiKeyRequestHeaderName);
usernamePasswordAuthenticationFilter.setRequiresAuthenticationRequestMatcher(AnyRequestMatcher.INSTANCE);
usernamePasswordAuthenticationFilter.setPostOnly(false);
usernamePasswordAuthenticationFilter.setAuthenticationSuccessHandler((request, response, authentication) -> {
// Do nothing
});
return usernamePasswordAuthenticationFilter;
}
#Bean
public UserDetailsService userDetailsService() {
// #formatter:off
UserDetails userDetails = User.builder()
.username("api-client")
.password("{noop}my-api-key")
.roles("USER")
.build();
// #formatter:on
return new InMemoryUserDetailsManager(userDetails);
}
}
}
Final note: One caveat is that I included disabling CSRF, which you have done. This is only a reasonable thing to do if you don't intend to use this application in a web browser with sessions. Since I marked both configurations as stateless (your Okta+JWT example was not), this seems reasonable. Most of the time, however, you really don't want to disable CSRF protection, especially if the reason is "I can't figure out how to make my UI application work with CSRF enabled."
First of all, thank you very much for your help.
I took the time to respond because I wanted to understand your answer.
You are right about the description of the sketch I am trying to implement.
With your configuration I can now access Swagger without any login/password.
The first configuration (OKTA) works fine and I think the last one (login / password) does too.
I now face one last error when I try to access the routes
protected by login and password.
I am facing an issue where Spring throws an "org.springframework.security.authentication.ProviderNotFoundException: No AuthenticationProvider found for org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken" exception.
I am looking to resolve this issue and I think everything will work after that.
Let me modestly point out that the setter methods :
requestHeaderAuthenticationFilter.setPrincipalRequestHeader(appIdRequestHeaderName);
requestHeaderAuthenticationFilter.setCredentialsRequestHeader(apiKeyRequestHeaderName);
are not accessible and I keep setting them by constructor.
RequestHeaderAuthenticationFilter requestHeaderAuthenticationFilter = new RequestHeaderAuthenticationFilter(appIdRequestHeaderName, apiKeyRequestHeaderName)
Thank you very much for all the answers.
We found the solution thanks your help.
Here the final code for helping everyone who needs to do the same things as us. ​
Security configuration :
#Configuration
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private static final String[] AUTH_WHITELIST = {
"/v2/api-docs",
"/swagger-resources/configuration/ui",
"/swagger-resources",
"/swagger-resources/configuration/security",
"/swagger-ui.html",
"/webjars/**"
};
#Order(1)
#Configuration
public static class OauthOktaConfigurationAdapter extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) throws Exception {
http
.antMatcher("/api/v1/end-point/**")
.authorizeRequests((authz) -> authz.anyRequest().authenticated())
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)
.sessionManagement((sessionManagement) ->
sessionManagement
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.cors(withDefaults())
.csrf(CsrfConfigurer::disable);
Okta.configureResourceServer401ResponseBody(http);
}
}
#Order(2)
#Configuration
public static class StandardSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter {
#Value("${http.app-id-header-name}")
private String appIdRequestHeaderName;
#Value("${http.api-key-header-name}")
private String apiKeyRequestHeaderName;
private final AuthenticationManager authenticationManager;
#Autowired
public StandardSecurityConfigurationAdapter(AuthenticationManager authenticationManager) {
super();
this.authenticationManager = authenticationManager;
}
#Override
public void configure(WebSecurity web) {
web.ignoring().antMatchers(AUTH_WHITELIST);
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.addFilterAt(initAuthenticationFilter(), UsernameRequestHeaderAuthenticationFilter.class)
.authorizeRequests((authorizeRequests) ->
authorizeRequests
.anyRequest().authenticated()
)
.sessionManagement((sessionManagement) ->
sessionManagement
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.cors(withDefaults())
.csrf(CsrfConfigurer::disable);
}
private UsernameRequestHeaderAuthenticationFilter initAuthenticationFilter() throws Exception {
UsernameRequestHeaderAuthenticationFilter usernameRequestHeaderAuthenticationFilter = new UsernameRequestHeaderAuthenticationFilter();
usernameRequestHeaderAuthenticationFilter.setAuthenticationManager(authenticationManager);
usernameRequestHeaderAuthenticationFilter.setUsernameParameter(appIdRequestHeaderName);
usernameRequestHeaderAuthenticationFilter.setPasswordParameter(apiKeyRequestHeaderName);
usernameRequestHeaderAuthenticationFilter.setRequiresAuthenticationRequestMatcher(AnyRequestMatcher.INSTANCE);
usernameRequestHeaderAuthenticationFilter.setPostOnly(false);
usernameRequestHeaderAuthenticationFilter.setAuthenticationSuccessHandler((request, response, authentication) -> {
// Do nothing
});
return usernameRequestHeaderAuthenticationFilter;
}
}
}
Authentication filter :
public class UsernameRequestHeaderAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
#Override
protected String obtainUsername(HttpServletRequest request) {
return request.getHeader(getUsernameParameter());
}
#Override
protected String obtainPassword(HttpServletRequest request) {
return request.getHeader(getPasswordParameter());
}
#Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
super.successfulAuthentication(request, response, chain, authResult);
chain.doFilter(request, response);
}
#Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
super.unsuccessfulAuthentication(request, response, failed);
}
}
We have also adapted our AuthenticationManager to use UserAuthorities
Thanks again to all

Remove HTTP Strict Transport Security (HSTS) response header in spring oauth2 token API

I am using Spring Security and Spring Oauth2 and JWT in my API project
The default API in order to login which Spring oauth 2 provided, is /oauth/token
This API always adds "Strict-Transport-Security: max-age=31536000 ; includeSubDomains" header to the response.
But I don't want this in my situation. And I have removed HSTS with the below source code.
#EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
// ...
.headers()
.httpStrictTransportSecurity().disable();
}
}
With above code, APIs I defined is removed HSTS in header. But the default API /oauth/token still return HSTS in header.
Is there any way to do this ?
Please help.
Thanks,
Tin
I just ran into the same issue.
The best solution I found is writing a filter that prevents others from setting the HSTS header in general.
#Component
#Order(value = Ordered.HIGHEST_PRECEDENCE)
public class HstsHeaderPreventionFilter implements Filter {
#Override
public void init(FilterConfig filterConfig) throws ServletException {
}
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
chain.doFilter(request, new HttpServletResponseWrapper((HttpServletResponse) response) {
public void setHeader(String name, String value) {
if (!name.equalsIgnoreCase("Strict-Transport-Security")) {
super.setHeader(name, value);
}
}
});
}
#Override
public void destroy() {
}
}

spring-boot, spring-security and dropwizard metrics

I have a spring-boot application with spring-security and dropwizard metrics. It uses Angularjs as a frontend. Authentication is done using separate login.html page with angularjs controller posting credentials to '/login' and after seccessful response routing to index.html (separate angularjs app). This all works quite well until I try to access dropwizard metrics. In this case I get a spring-security exception saying that user is anonymous (all other urls work fine).
My spring-security config:
#Configuration
#Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
#EnableWebSecurity
public class FormLoginSecurityConfigurer extends WebSecurityConfigurerAdapter {
private class AuthSuccessHandler implements AuthenticationSuccessHandler {
#Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response, Authentication authentication)
throws IOException, ServletException {
response.setStatus(HttpServletResponse.SC_OK);
}
}
private class AuthFailureHandler implements AuthenticationFailureHandler {
#Override
public void onAuthenticationFailure(HttpServletRequest request,
HttpServletResponse response, AuthenticationException exception)
throws IOException, ServletException {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
}
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/login.html", "/scripts/login/**", "/libs/**", "/styles/**", "/images/**").permitAll()
.anyRequest().authenticated()
.and()
.formLogin().loginPage("/login.html").loginProcessingUrl("/login")
.usernameParameter("username").passwordParameter("password")
.successHandler(new AuthSuccessHandler())
.failureHandler(new AuthFailureHandler())
.and().logout().logoutUrl("/logout").logoutSuccessUrl("/login.html")
.and().addFilterAfter(new CsrfHeaderFilter(), CsrfFilter.class)
.csrf().csrfTokenRepository(CsrfHeaderFilter.csrfTokenRepository());
}
#Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().withUser("user").password("password").roles("USER");
}
}
The metrics servlet is registered in the ServletContextInitilizer:
/**
* Configuration of web application with Servlet 3.0 APIs.
*/
#Configuration
public class WebConfigurer implements ServletContextInitializer {
#Override
public void onStartup(ServletContext servletContext) throws ServletException {
initMetrics(servletContext,
EnumSet.of(DispatcherType.REQUEST, DispatcherType.FORWARD, DispatcherType.ASYNC));
}
/**
* Initializes Metrics.
*/
private void initMetrics(ServletContext servletContext, EnumSet<DispatcherType> disps) {
log.debug("Initializing Metrics registries");
servletContext.setAttribute(InstrumentedFilter.REGISTRY_ATTRIBUTE,
metricRegistry);
servletContext.setAttribute(MetricsServlet.METRICS_REGISTRY,
metricRegistry);
log.debug("Registering Metrics Filter");
FilterRegistration.Dynamic metricsFilter = servletContext.addFilter("webappMetricsFilter",
new InstrumentedFilter());
metricsFilter.addMappingForUrlPatterns(disps, true, "/*");
metricsFilter.setAsyncSupported(true);
log.debug("Registering Metrics Servlet");
ServletRegistration.Dynamic metricsAdminServlet =
servletContext.addServlet("metricsServlet", new MetricsServlet());
metricsAdminServlet.addMapping("/metrics/metrics/*");
metricsAdminServlet.setAsyncSupported(true);
metricsAdminServlet.setLoadOnStartup(2);
}
}
However when I access anything under /metrics/metrics the browser prompts for basic authentication. The response has the following header WWW-Authenticate:"Basic realm="Spring"". Other resources are downloaded fine.
I'm new to this kind of applications and getting a bit frustrated :) Any help is appreciated.
Seems its all in the docs if one knows what to look for - link
The Actuator security features can be modified using external properties (management.security.*). To override the application access rules add a #Bean of type WebSecurityConfigurerAdapter and use #Order(SecurityProperties.ACCESS_OVERRIDE_ORDER) if you don’t want to override the actuator access rules, or #Order(ManagementServerProperties.ACCESS_OVERRIDE_ORDER) if you do want to override the actuator access rules.
Changed the order to ManagementServerProperties.ACCESS_OVERRIDE_ORDER and now it works.

In Spring-Security with Java Config, why does httpBasic POST want csrf token?

I am using Spring-Security 3.2.0.RC2 with Java config.
I set up a simple HttpSecurity config that asks for basic auth on /v1/**.
GET requests work but POST requests fail with:
HTTP Status 403 - Invalid CSRF Token 'null' was found on the request parameter '_csrf' or header 'X-CSRF-TOKEN'.
My security config looks like this:
#Configuration
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Resource
private MyUserDetailsService userDetailsService;
#Autowired
//public void configureGlobal(AuthenticationManagerBuilder auth)
public void configure(AuthenticationManagerBuilder auth)
throws Exception {
StandardPasswordEncoder encoder = new StandardPasswordEncoder();
auth.userDetailsService(userDetailsService).passwordEncoder(encoder);
}
#Configuration
#Order(1)
public static class RestSecurityConfig
extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.antMatcher("/v1/**").authorizeRequests()
.antMatchers("/v1/**").authenticated()
.and().httpBasic();
}
}
}
Any help on this greatly appreciated.
CSRF protection is enabled by default with Java configuration. To disable it:
#Configuration
public class RestSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
...;
}
}
You can also disable the CSRF check only on some requests or methods, using a configuration like the following for the http object:
http
.csrf().requireCsrfProtectionMatcher(new RequestMatcher() {
private Pattern allowedMethods =
Pattern.compile("^(GET|HEAD|TRACE|OPTIONS)$");
private RegexRequestMatcher apiMatcher =
new RegexRequestMatcher("/v[0-9]*/.*", null);
#Override
public boolean matches(HttpServletRequest request) {
// CSRF disabled on allowedMethod
if(allowedMethods.matcher(request.getMethod()).matches())
return false;
// CSRF disabled on api calls
if(apiMatcher.matches(request))
return false;
// CSRF enables for other requests
return true;
}
});
You can see more here:
http://blog.netgloo.com/2014/09/28/spring-boot-enable-the-csrf-check-selectively-only-for-some-requests/

Resources