How to config ip address dynamically with spring-security? - spring-security

I'm new to spring-security, I need to setup a authorization system to secure a REST service.
In my case, my "users" are some servers of different departments and companies.So I tried to config the servers as MyUser which is sub class of UserDetails.
But I got an issue when I was asked to authorize the ip address of the servers. I saw there're ip address authorizations in WebSecurityConfigurerAdapter.configure(HttpSecurity http), and I can retrieve the configuration with configure(AuthenticationManagerBuilder auth){auth.userDetailsService(myUserDetailsService);}. But it seems that the configure(HttpSecurity http) method only runs once when the system boots.
So, what should I do? Is there any way to add a customized checker or something to verify the ip address?
These are my code:
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
// #formatter:off
http.httpBasic().and()
.authorizeRequests()
.antMatchers("/order/**").hasAuthority("read_order") //(1)
.antMatchers("/order/**").hasIpAddress("192.168.1.45") //(2)
.anyRequest().denyAll();
// #formatter:on
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService);
}
}
With customized userService which implements the UserDetailsService, I can replace the (1) line with configuration in database. Which means when I change the configuration, Spring Security will load it from database. I don't need to restart the system.
How do I do the similar thing to the (2) line?

I found a solution. I'm not sure is it the best way, but it can work. This is the solution.
First, we can define a class which implements AccessDecisionManager:
#Service
public class ResourceAccessDecisionManager implements AccessDecisionManager {
#Override
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes)
throws
AccessDeniedException, InsufficientAuthenticationException {
//...
HttpServletRequest request = ((FilterInvocation) object).getHttpRequest();
String ip = request.getRemoteHost();
Object principal = authentication.getPrincipal();
User user;
if (principal instanceof User){
user=(User)principal;
if (!ip.equals(user.getIpConfig())){
throw new AccessDeniedException("wrong ip");
}
}
//...
}
//...
}
Surely, you had to store the ip config within the User class.
Then, we define a class extends AbstractSecurityInterceptor implements Filter:
#Service
public class ResourceFilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {
//...
#Autowired
public void setMyAccessDecisionManager(ResourceAccessDecisionManager resourceAccessDecisionManager) {
super.setAccessDecisionManager(resourceAccessDecisionManager);
}
#Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
throws IOException, ServletException {
FilterInvocation fi = new FilterInvocation(servletRequest, servletResponse, filterChain);
invoke(fi);
}
private void invoke(FilterInvocation fi) throws IOException, ServletException {
InterceptorStatusToken token = super.beforeInvocation(fi);
try {
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
} finally {
super.afterInvocation(token, null);
}
}
//...
}
And add the filter into security config class:
#Configuration
#EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
private final ResourceService resourceService;
private final UserService userService;
private final HttpServletRequest request;
private final ResourceFilterSecurityInterceptor resourceFilterSecurityInterceptor;
private final EnvironmentConfig environmentConfig;
#Autowired
public SecurityConfiguration(ResourceService resourceService, UserService userService,
HttpServletRequest request,
ResourceFilterSecurityInterceptor resourceFilterSecurityInterceptor,
EnvironmentConfig environmentConfig) {
this.resourceService = resourceService;
this.userService = userService;
this.request = request;
this.resourceFilterSecurityInterceptor = resourceFilterSecurityInterceptor;
this.environmentConfig = environmentConfig;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
// #formatter:off
http
.httpBasic().and()
.authorizeRequests()
.antMatchers("/order/**").authenticated()
.anyRequest().authenticated()
.and().csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
;
http.addFilterBefore(resourceFilterSecurityInterceptor,FilterSecurityInterceptor.class);
// #formatter:on
}
//...
}
Now it can work, but any suggestion is welcome and admirable to improve the solution.

Related

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

I can not return access token in JWT SPRINGBOOT V2.2.0 OAuth2

I have a hard time on a personal study project. Difficulty implementing oauth2, whenever I try to get access token it returns me
2019-11-19 22:01:35.398 ERROR 4705 --- [nio-8080-exec-2] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Handler dispatch failed; nested exception is java.lang.StackOverflowError] with root cause
java.lang.StackOverflowError: null
at org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter$UserDetailsServiceDelegator.loadUserByUsername(WebSecurityConfigurerAdapter.java:448) ~[spring-security-config-5.2.0.RELEASE.jar:5.2.0.RELEASE]
at org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter$UserDetailsServiceDelegator.loadUserByUsername(WebSecurityConfigurerAdapter.java:449) ~[spring-security-config-5.2.0.RELEASE.jar:5.2.0.RELEASE]
2019-11-19 22:01:35.426 WARN 4705 --- [nio-8080-exec-2] o.s.web.servlet.PageNotFound : No mapping for POST /error
my project uses Version Spring Boot v2.2.0.RELEASE and Java 1.8
Application.propeties
spring.datasource.url=jdbc:mysql://localhost:3306/test
spring.datasource.username=test
spring.datasource.password=test123
spring.jpa.hibernate.ddl-auto=update
spring.datasource.testWhileIdle = true
spring.datasource.timeBetweenEvictionRunsMillis = 3600000
spring.datasource.validationQuery = SELECT 1
spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration
spring.jpa.show-sql=true
AuthorizationServerConfig
#Configuration
#EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
#Autowired
private AuthenticationManager authenticationManager;
#Autowired
private PasswordEncoder passwordEncoder;
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("angular")
.secret(passwordEncoder.encode("angular"))
.scopes("read", "write")
.authorizedGrantTypes("password", "refresh_token")
.accessTokenValiditySeconds(1800)
.refreshTokenValiditySeconds(3600 * 24)
.and()
.withClient("admin")
.secret("admin")
.scopes("read")
.authorizedGrantTypes("password", "refresh_token")
.accessTokenValiditySeconds(1800)
.refreshTokenValiditySeconds(3600 * 24);
}
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.tokenStore(tokenStore())
.accessTokenConverter(accessTokenConverter())
.reuseRefreshTokens(false)
.authenticationManager(authenticationManager);
}
#Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter();
accessTokenConverter.setSigningKey("financas");
return accessTokenConverter;
}
#Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
}
ResourceServerConfig
#Configuration
#EnableWebSecurity
#EnableResourceServer
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
#Autowired
private UserDetailsService userDetailsService;
#Autowired
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
#Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/pagamento").permitAll()
.anyRequest().authenticated()
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.csrf().disable();
}
#Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.stateless(true);
}
#Bean
public static PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
#Bean
public MethodSecurityExpressionHandler createExpressionHandler() {
return new OAuth2MethodSecurityExpressionHandler();
}
}
SecurityConfig
#EnableWebSecurity
#EnableAuthorizationServer
#EnableResourceServer
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Bean
#Override
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
#Bean
#Override
public UserDetailsService userDetailsService() {
return super.userDetailsService();
}
}
now my refresh token classes
RefreshTokenPostProcessor
#ControllerAdvice
public class RefreshTokenPostProcessor implements ResponseBodyAdvice<OAuth2AccessToken> {
#Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
return returnType.getMethod().getName().equals("postAccessToken");
}
#Override
public OAuth2AccessToken beforeBodyWrite(OAuth2AccessToken body,
MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass,
ServerHttpRequest request, ServerHttpResponse response) {
HttpServletRequest req = ((ServletServerHttpRequest)request).getServletRequest();
HttpServletResponse resp = ((ServletServerHttpResponse)response).getServletResponse();
DefaultOAuth2AccessToken token = (DefaultOAuth2AccessToken) body;
String refreshToken = body.getRefreshToken().getValue();
adicionaRefreshTokenNoCookie(refreshToken, req, resp);
removerRefreshTokenDoBody(token);
return body;
}
private void removerRefreshTokenDoBody(DefaultOAuth2AccessToken token) {
token.setRefreshToken(null);
}
private void adicionaRefreshTokenNoCookie(String refreshToken, HttpServletRequest req, HttpServletResponse resp) {
Cookie refreshTokenCookie = new Cookie("refreshToken", refreshToken);
refreshTokenCookie.setHttpOnly(true);
refreshTokenCookie.setSecure(false); //TODO: change in production
refreshTokenCookie.setPath(req.getContextPath() + "/oauth/token");
refreshTokenCookie.setMaxAge(2592000);
resp.addCookie(refreshTokenCookie);
}
}
RefreshTokenPreProcessorFilter
#Component
#Order(Ordered.HIGHEST_PRECEDENCE)
public class RefreshTokenPreProcessorFilter implements Filter {
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
if ("/oauth/token".equalsIgnoreCase(req.getRequestURI())
&& "refresh_token".equals(req.getParameter("grant_type"))
&& req.getCookies() != null) {
for (Cookie cookie : req.getCookies()) {
if (cookie.getName().equals("refreshToken")) {
String refreshToken = cookie.getValue();
req = new MyServletRequestWrapper(req, refreshToken);
}
}
}
chain.doFilter(req, response);
}
#Override
public void destroy() {
}
#Override
public void init(FilterConfig filterConfig) throws ServletException {
}
static class MyServletRequestWrapper extends HttpServletRequestWrapper {
private String refreshToken;
public MyServletRequestWrapper(HttpServletRequest request, String refreshToken) {
super(request);
}
#Override
public Map<String, String[]> getParameterMap() {
ParameterMap<String, String[]> map = new ParameterMap<>(getRequest().getParameterMap());
map.put("refresh_token", new String[] {refreshToken});
map.setLocked(true);
return map;
}
}
}
SpringBootApplication
#SpringBootApplication
#WebAppConfiguration
public class FinancaApplication {
public static void main(String[] args) {
SpringApplication.run(FinancaApplication.class, args);
}
}
Remembering that my authentication is in the database and I am using PasswordEncoder on the database password and authentication passwords, I would like some help setting up the server and where my error is because I cannot generate an access token, the error returned is just this one in the console. thankful
I like your code Felipe - it's a very nicely written API - the problem is that it attempts to issue tokens also, which an API should not do.
I'd recommend walking through my tutorial, to understand the roles of API, UI and Authorization Server:
https://authguidance.com/2017/09/24/basicspa-overview/
Once you've done that I think you'll be able to adapt your API and fix your own problems - feel free to post back any follow up questions though.
In the real world almost all companies use a third party (cloud) system as the Authorization Server:
https://authguidance.com/2019/09/15/developer-domain-setup/
You will learn OAuth much faster this way - and it is free.
You then only need to focus on integrating your APIs and UIs with standards compliant endpoints like this:
https://authguidance.com/2019/03/24/java-spring-boot-api-oauth-coding/
Happy to answer follow up questions if it helps ..

Unable to set a custom authentication failure handler in 4.0 spring security

I want to set up custom AuthenticationFailureHandler in my project. Even though I configure my authenticationFailureHandler as below , it is not properly picked up when the sign-in fails.
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
..................
#Inject
private AjaxAuthenticationFailureHandler ajaxAuthenticationFailureHandler;
..................
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/resources/**").permitAll()
.anyRequest().authenticated().and()
.formLogin().loginPage("/signin").failureHandler(ajaxAuthenticationFailureHandler)
.permitAll()
.failureUrl("/signin")
.defaultSuccessUrl("/search").and()
.logout()
.logoutRequestMatcher(new AntPathRequestMatcher("/signout"))
.logoutSuccessUrl("/signin")
.permitAll().and().csrf();
}
My custom AuthenticationFailureHandler Class
#Component
public class AjaxAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
#Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
// Custom code
}
}
When I debug the application, the method onAuthenticationFailure is called from SimpleUrlAuthenticationFailureHandler, but not in my custom AjaxAuthenticationFailureHandler even though I extend the same SimpleUrlAuthenticationFailureHandler.
What could be the mistake or any missed configuration to resolve the problem?
In this case, when we set the custom AjaxAuthenticationFailureHandler and then we configure failureUrl("/signin"),the configuration failureUrl("/signin") will overwrite the already configured AjaxAuthenticationFailureHandler with a new SimpleUrlAuthenticationFailureHandler.
This is the implementation of failureUrl and failureHandler methods as in AbstractAuthenticationFilterConfigurer.
public abstract class AbstractAuthenticationFilterConfigurer<B extends HttpSecurityBuilder<B>, T extends AbstractAuthenticationFilterConfigurer<B, T, F>, F extends AbstractAuthenticationProcessingFilter> extends AbstractHttpConfigurer<T, B> {
...........
private AuthenticationFailureHandler failureHandler;
private String failureUrl;
..........
public final T failureUrl(String authenticationFailureUrl) {
T result = failureHandler(new SimpleUrlAuthenticationFailureHandler(authenticationFailureUrl));
this.failureUrl = authenticationFailureUrl;
return result;
}
public final T failureHandler(AuthenticationFailureHandler authenticationFailureHandler) {
this.failureUrl = null;
this.failureHandler = authenticationFailureHandler;
return getSelf();
}
}
If we set only the failurehandler as below, our custom AjaxAuthenticationFailureHandler will be set.
.formLogin().loginPage("/signin")
.permitAll()
.failureHandler(ajaxAuthenticationFailureHandler)
.defaultSuccessUrl("/search").and()
If we want to set the failure URL we can set it in the custom AjaxAuthenticationFailureHandler using setDefaultFailureUrl(String defaultFailureUrl) which is derived from it's parent class SimpleUrlAuthenticationFailureHandler.
You can create a constructor in your AjaxAuthenticationFailureHandler class and pass in the defaultFailureUrl parameter down to the parent clss you extend (SimpleUrlAuthenticationFailureHandler). Your AjaxAuthenticationFailureHandler class would look like: -
#Component
public class AjaxAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
public AjaxAuthenticationFailureHandler (String defaultFailureUrl) {
super(defaultFailureUrl);
}
#Override
public void onAuthenticationFailure(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
// Custom code
}
}
Your configure block would then look like this: -
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/resources/**").permitAll()
.anyRequest().authenticated().and()
.formLogin()
.loginPage("/signin")
.failureHandler(new AjaxAuthenticationFailureHandler("/signin?auth=failure") // or whatever is a sensible url
.permitAll()
.defaultSuccessUrl("/search").and()
...
Note, it's really important that the "/signin?auth=failure" is added to an authorizeRequests() section otherwise the controller won't pick up the auth parameter e.g.
.and()
.authorizeRequests()
.antMatchers(
"/css/**",
"/js/**",
"/images/**",
"/signin**" // REALLY IMPORTANT !!!
).permitAll()
See https://stackoverflow.com/a/39618113/1692179 for more information on that.
Your controller can now check for the auth parameter e.g.
#Controller
public class SigninController
#RequestMapping(value="/login", method = RequestMethod.GET)
public String handleError (Model model,
#RequestParam(name = "auth", required = false) String auth) {
if ("error".equals(auth)) {
model.addAttribute("error", "invalid username/password");
}
return "login";
}
}
Hope this helps! :-)

How to set a custom invalid session strategy in Spring Security

I'm developing a web application, based on Spring-Boot - 1.1.6, Spring -Security -3.2.5 and more.
I'm using Java based configuration:
#Configuration
#EnableWebMvcSecurity
public class SecurityCtxConfig extends WebSecurityConfigurerAdapter {
#Bean
DelegatingAuthenticationEntryPoint delegatingAuthenticationEntryPoint() {
LinkedHashMap<RequestMatcher, AuthenticationEntryPoint> map = new LinkedHashMap<RequestMatcher, AuthenticationEntryPoint>();
Http403ForbiddenEntryPoint defaultEntryPoint = new Http403ForbiddenEntryPoint();
map.put(AnyRequestMatcher.INSTANCE, defaultEntryPoint);
DelegatingAuthenticationEntryPoint retVal = new DelegatingAuthenticationEntryPoint(map);
retVal.setDefaultEntryPoint(defaultEntryPoint);
return retVal;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
ExceptionHandlingConfigurer<HttpSecurity> exceptionHandling = http.exceptionHandling();
exceptionHandling.authenticationEntryPoint(delegatingAuthenticationEntryPoint());
http.logout().logoutSuccessHandler(new LogoutSuccessHandler() {
#Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication arg2)
throws IOException, ServletException {
response.setStatus(HttpServletResponse.SC_OK);
}
});
}
}
The requirement is to return Http status 401 in case that the session cookie is invalid or missing(no matter the reason)
I see the InvalidSessionStrategy but I don't find a way to set it on the SessionManagementFilter.
Can some one please instract me how to implement my plan or another one that will fulfill the requirement
Using SpringBoot this works for me:
#Configuration
#EnableWebSecurity
public class UISecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
...
http.addFilterAfter(expiredSessionFilter(), SessionManagementFilter.class);
...
}
private Filter expiredSessionFilter() {
SessionManagementFilter smf = new SessionManagementFilter(new HttpSessionSecurityContextRepository());
smf.setInvalidSessionStrategy((request, response) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Session go BOOM!"));
return smf;
}
}
We had the exact same problem and I did this hack to solve it (yes I know, this is a hack, therefore the name...).
I create a BeanPostProcessor and search for the SessionManagementFilter to reconfigure it...
#Bean
public HackyBeanPostProcessor myBeanPostProcessor() {
return new HackyBeanPostProcessor();
}
protected static class HackyBeanPostProcessor implements BeanPostProcessor {
#Override
public Object postProcessBeforeInitialization(Object bean, String beanName) {
// FIXME check if a new spring-security version allows this in an
// other way (current: 3.2.5.RELEASE)
if (bean instanceof SessionManagementFilter) {
SessionManagementFilter filter = (SessionManagementFilter) bean;
filter.setInvalidSessionStrategy(new InvalidSessionStrategy() {
#Override
public void onInvalidSessionDetected(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
}
});
}
return bean;
}
#Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
return bean;
}
}
Since I'm using AspectJ (I mean, compile time weaving and not Spring AOP), it was quite easy to hack the SessionManagementFilter creation by setting my custom InvalidSessionStrategy after the SessionManagementFilter is constructed:
#Aspect
public class SessionManagementAspect {
private static final Log logger = LogFactory.getLog();
#AfterReturning("execution( org.springframework.security.web.session.SessionManagementFilter.new(..))&&this(smf)")
public void creation(JoinPoint pjp, SessionManagementFilter smf) throws Throwable {
logger.debug("Adding/Replacing the invalid session detection policy to return 401 in case of an invalid session");
smf.setInvalidSessionStrategy(new InvalidSessionStrategy() {
#Override
public void onInvalidSessionDetected(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
logInvalidSession(request, "invalid cookie");
if (!response.isCommitted())
response.sendError(HttpStatus.UNAUTHORIZED.value());
}
});
}
}
If you are not using AspectJ, try adding #Component and add this Aspect to your context, it might work if the SessionManagementFilter is a bean (Since Spring-AOP applias only on spring beans)

Spring Security with Java Configuration: How to handle BadCredentialsException from a custom provider

I need to authenticate some rest services using a token id in the url (or maybe in the request header - but this is not important for now). I am trying to use java configuration to set this up using as a guide this post. My problem is that I do not know how to handle "BadCredentialsException" that is thrown when the authentication fails from the provider. Here is my Security Config:
public static class SecurityConfigForRS extends
WebSecurityConfigurerAdapter {
#Autowired
TokenAuthenticationProvider tokenAuthenticationProvider;
#Override
protected void configure(AuthenticationManagerBuilder auth)
throws Exception {
auth.authenticationProvider(tokenAuthenticationProvider);
}
#Bean
#Override
public AuthenticationManager authenticationManagerBean()
throws Exception {
return super.authenticationManagerBean();
}
#Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
http.regexMatcher("^/rest.*")
.addFilterBefore(
new TokenAuthenticationFilter(
authenticationManagerBean()),
AbstractPreAuthenticatedProcessingFilter.class)
.and().csrf().disable();
}
}
For now I skip the other implementations - if it helps I will post them later.
When the token is missing or is invalid, the TokenAuthernticationProvider throws a BadCredentialsException. I need to catch this and send back an 401-Unauthorized. Is it possible to do this?
The first Filter I created was a subclass of GenericFilterBean and it did not have support for authentication failure handler or success handler. However AbstractAuthenticationProcessingFilter supports success and failure handlers. My filter is as simple as that:
public class TokenAuthenticationProcessingFilter extends
AbstractAuthenticationProcessingFilter {
public TokenAuthenticationProcessingFilter(
RequestMatcher requiresAuthenticationRequestMatcher) {
super(requiresAuthenticationRequestMatcher);
}
#Override
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException,
IOException, ServletException {
Authentication auth = new TokenAuthentication("-1");
try {
Map<String, String[]> params = request.getParameterMap();
if (!params.isEmpty() && params.containsKey("auth_token")) {
String token = params.get("auth_token")[0];
if (token != null) {
auth = new TokenAuthentication(token);
}
}
return this.getAuthenticationManager().authenticate(auth);
} catch (AuthenticationException ae) {
unsuccessfulAuthentication(request, response, ae);
}
return auth;
}}
and my http security is:
public static class SecurityConfigForRS extends
WebSecurityConfigurerAdapter {
#Autowired
TokenAuthenticationProvider tokenAuthenticationProvider;
#Override
protected void configure(AuthenticationManagerBuilder auth)
throws Exception {
auth.authenticationProvider(tokenAuthenticationProvider);
}
#Bean
#Override
public AuthenticationManager authenticationManagerBean()
throws Exception {
return super.authenticationManagerBean();
}
#Bean
protected AbstractAuthenticationProcessingFilter getTokenAuthFilter()
throws Exception {
TokenAuthenticationProcessingFilter tapf = new TokenAuthenticationProcessingFilter(
new RegexRequestMatcher("^/rest.*", null));
tapf.setAuthenticationManager(authenticationManagerBean());
return tapf;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
http.regexMatcher("^/rest.*")
.addFilterAfter(getTokenAuthFilter(),
BasicAuthenticationFilter.class).csrf().disable();
}
}
The filter chain order does matter! I placed it after BasicAuthenticationFilter and it works fine. Of course there might be a better solution but for now this works!
May be you can try with Global Exception handling with ControllerAdvice
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
#ControllerAdvice
public class ExceptionControllerAdvice {
#ExceptionHandler // when Invalid Credentials
public ResponseEntity<ErrorMessage> handleInvalidCredentialsException(
BadCredentialsException ex) {
return new ResponseEntity<ErrorMessage>(
new ErrorMessage(ex.getMessage()), HttpStatus.UNAUTHORIZED);
}
#Getter
#Setter
#AllArgsConstructor
class ErrorMessage {
private String error;
}
}

Resources