Custom spring security filter not called at runtime - spring

I'm trying to enable spring security in a spring boot rest services project and I'm getting some problems.
I configured it with this code
#Configuration
#EnableWebSecurity
public class WebSecurity extends WebSecurityConfigurerAdapter {
#Autowired
private LdapAuthenticationProvider ldapAuthenticationProvider;
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().authorizeRequests()
.anyRequest().authenticated()
.and()
.httpBasic().and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(ldapAuthenticationProvider);
}
#Bean
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
and implemented a custom authentication provider in order to login to LDAP (which has a non standard configuration so I wasn't able to make the default ldap provider works)
#Component
public class LdapAuthenticationProvider implements AuthenticationProvider {
#Override
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
String email = authentication.getName();
String password = authentication.getCredentials().toString();
LdapConnection ldap = new LdapConnection();
String uid = ldap.getUserUID(email);
if(uid == null || uid == ""){
throw new BadCredentialsException("User " + email + " not found");
}
if(ldap.login(uid, password)){
return new UsernamePasswordAuthenticationToken(uid, null, new ArrayList<>());
}else{
throw new BadCredentialsException("Bad credentials");
}
}
#Override
public boolean supports(Class<?> authentication) {
return true;
//To indicate that this authenticationprovider can handle the auth request. since there's currently only one way of logging in, always return true
}
}
This code is working fine, in the sense that calling my services with a basic authorization header it is able to correctly login and return the service called. The problems started when I tried to insert a different authorization/authentication. Instead of using the basic authentication I would like to pass the credential from a form in my react front end, so I would like to pass them as a json body in a POST call. (the idea is then to generate a jwt token and use that for the following communication).
So I changed the configure method to this:
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().authorizeRequests()
.anyRequest().authenticated()
.and()
.addFilter(new JWTAuthenticationFilter(authenticationManager()))
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
and defined a custom authentication filter:
public class JWTAuthenticationFilter extends
UsernamePasswordAuthenticationFilter {
#Autowired
private AuthenticationManager authenticationManager;
public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
#Override
public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res) throws AuthenticationException{
String requestBody;
try{
requestBody = IOUtils.toString(req.getReader());
JsonParser jsonParser = JsonParserFactory.getJsonParser();
Map<String, Object> requestMap = jsonParser.parseMap(requestBody);
return authenticationManager.authenticate( new UsernamePasswordAuthenticationToken(requestMap.get("email"), requestMap.get("password"), new ArrayList<>()));
}catch(IOException e){
throw new InternalAuthenticationServiceException("Something goes wrong parsing the request body",e );
}
}
#Override
protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse res, FilterChain chain, Authentication auth) throws IOException{
JwtTokenProvider tokenProvider = new JwtTokenProvider();
String token = tokenProvider.generateToken(auth.getPrincipal().toString());
Cookie cookie = new Cookie("jwt",token);
cookie.setHttpOnly(true);
cookie.setSecure(true);
res.addCookie(cookie);
}
}
Problem is, whatever I'm doing, the runtime doesn't seems to enter in this filter at all. What am I missing? I guess is something big and stupid but I can't figure it out...
UPDATE: the problem seems to be that the UsernamePassWordAuthenticationFilter can be called only through a form. I then change my code to extend AbstractAuthenticationProcessingFilter instead.
The modified filter:
public class JWTAuthenticationFilter extends
AbstractAuthenticationProcessingFilter {
private AuthenticationManager authenticationManager;
public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
super("/api/secureLogin");
this.authenticationManager = authenticationManager;
}
#Override
public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res) throws AuthenticationException{
String requestBody;
try{
requestBody = IOUtils.toString(req.getReader());
JsonParser jsonParser = JsonParserFactory.getJsonParser();
Map<String, Object> requestMap = jsonParser.parseMap(requestBody);
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(requestMap.get("email"), requestMap.get("password"), new ArrayList<>());
return authenticationManager.authenticate(token);
}catch(IOException e){
throw new InternalAuthenticationServiceException("Something goes wrong parsing the request body",e );
}
}
}
and the modified configure method:
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().authorizeRequests()
.antMatchers(HttpMethod.POST, "/api/secureLogin").permitAll()
.antMatchers(HttpMethod.GET, "/api").permitAll()
.antMatchers("/api/**").authenticated()
.and()
.addFilterBefore(new JWTAuthenticationFilter(authenticationManager()), UsernamePasswordAuthenticationFilter.class)
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}

You expect the filter to be triggered by accessing the path api/secureLogin. By default UsernamePasswordAuthenticationFilter is triggered only by accessing /login.
If you add following line in the constructor of the JWTAuthenticationFilter which extends UsernamePasswordAuthenticationFilter it should work:
this.setFilterProcessesUrl("/api/secureLogin");

Hi #MikyjpegYou can use UsernamePassWordAuthenticationFilter. It does not have to be called from a form as you mentioned.As long as it is called via a POST method with the url /login (instead of your api/secureLogin url), it will be executed.
The constructor uses a request matcher that only allows this url & request method:
public UsernamePasswordAuthenticationFilter() {
super(new AntPathRequestMatcher("/login", "POST"));
}

I see your adding the configuration while overriding the configure method, try adding the filter mapping in your web.xml. Something like this under the 'web-app' node:
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter>
<filter-name>JWTAuthenticationFilter</filter-name>
<filter-class>com.yourProject.JWTAuthenticationFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<filter-mapping>
<filter-name>JWTAuthenticationFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

Answer by ADAM has worked for me. Those who are working with annotation based configuration can add following
#Override protected Filter[] getServletFilters() {
return new Filter[]{
new JwtAuthenticationFilter()
};
}
Into your AppInitialiser extends AbstractAnnotationConfigDispatcherServletInitializer

public class BeforeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public BeforeAuthenticationFilter() {
super(new AntPathRequestMatcher("api/secureLogin", "POST"));
super.setFilterProcessesUrl("api/secureLogin");
}

A bit late, however, I've given this answer to a similar question, so it can also be useful here: Your custom implementation extends the UsernamePasswordAuthenticationFilter (which in its turn extends the AbstractAuthenticationProcessingFilter). The UsernamePasswordAuthenticationFilter, by default, is used for .formLogin authentication, handling the default AntRequestMatcher "/login". If you use a different protected endpoint, the filter's attemptAuthentication() method never gets action. So, if you want to use a different matcher (a different protected endpoint), you have to override the default AntRequestMatcher. For instance, you can do so within your custom filter constructor, by using something like that:
super.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/auth/signin", "GET"));

Related

How to configure two security configs with two filters in spring boot correctly?

I've implmemented security in my spring boot microservices project, the requirment is to have
two types of configurations, one for user request (from angular) and one from other services.
The design is to use JWT token for user request and API key for system calls.
Here is the config file (one file) but have also try to split it to two files with no impact:
#Configuration
#EnableWebSecurity
public class SecurityConfig {
#Configuration
#Order(1)
public static class APISecurityConfig extends WebSecurityConfigurerAdapter {
#Value("${my.api.key.header}")
private String principalRequestHeader;
#Value("${my.api.key.token}")
private String principalRequestValue;
#Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.cors().disable().csrf().disable();
httpSecurity
.antMatcher("/api/users/**")
.authorizeRequests() //
.anyRequest().authenticated()
.and()
.addFilterBefore(new APIKeyAuthFilter(principalRequestHeader, principalRequestValue), UsernamePasswordAuthenticationFilter.class);
}
}
#Order(2)
#Configuration
public static class MySecurityConfiguration extends WebSecurityConfigurerAdapter {
#Autowired
UserDetailsService userDetailsService;
#Bean
public AuthTokenFilter authenticationJwtTokenFilter() {
return new AuthTokenFilter();
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
#Bean
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/api/users/**");
}
#Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.cors().disable().csrf().disable();
httpSecurity
.authorizeRequests()
.antMatchers("/users/UserEmailExist", "/users/User/Add", "/users/Authenticate",
"/users/User/ChangePassword")
.permitAll()
.and()
.authorizeRequests()
.antMatchers("/users/**").hasAnyRole(ROLE_ADMIN_USER, ROLE_MANAGER_USER)
.anyRequest().authenticated()
.and()
.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class);
}
}
}
Each config has a filter attached to it, here the api one:
public class APIKeyAuthFilter extends GenericFilterBean {
private String principalRequestHeader;
private String principalRequestValue;
public APIKeyAuthFilter(String principalRequestHeader, String principalRequestValue) {
super();
this.principalRequestHeader = principalRequestHeader;
this.principalRequestValue = principalRequestValue;
}
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
if(request instanceof HttpServletRequest && response instanceof HttpServletResponse) {
String apiKey = getApiKey((HttpServletRequest) request);
if(apiKey != null) {
if(apiKey.equals(principalRequestValue)) {
ApiKeyAuthenticationToken apiToken = new ApiKeyAuthenticationToken(apiKey, AuthorityUtils.NO_AUTHORITIES);
SecurityContextHolder.getContext().setAuthentication(apiToken);
} else {
HttpServletResponse httpResponse = (HttpServletResponse) response;
httpResponse.setStatus(401);
httpResponse.getWriter().write("Invalid API Key");
return;
}
}
}
chain.doFilter(request, response);
}
}
Here is the filter for jwt (normal user from angular):
public class AuthTokenFilter extends OncePerRequestFilter {
#Autowired
private JwtUtils jwtUtils;
#Autowired
private MyUserDetailsService userDetailsService;
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
try {
String jwt = parseJwt(request);
if (jwt != null && jwtUtils.validateJwtToken(jwt)) {
String username = jwtUtils.getUserNameFromJwtToken(jwt);
MSUserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception e) {
logger.error("Cannot set user authentication: {}", e);
}
filterChain.doFilter(request, response);
}
}
I've created two different controllers, one with prefix /api/users and second /users.
Here is what happen in two different scenarios:
The user login from Angular, get jwt token and process request which end up in the Jwt filter,
this scenarion looking good with no issues as the user is able to process request as long
he is authenticate.
Microservice send a request with api-key to url with /api/users prefix, it ended up on the same
filter the normal user ended which is not correct and without JWT token he is actually
able to proceed to the controller and process the request without going
to the correct filter.
The only solution I have is to have only one filter and process the header
for api-key and jwt but it doesn't seem right.
I've looked online and try to figure out what I'm doing wrong but no clue as of now.
An update on this issue so I hope it will help to the community.
Firstly, I removed the following code and this mainly fix the problem:
// #Override
// public void configure(WebSecurity web) throws Exception {
// web.ignoring().antMatchers("/api/users/**");
// }
The way the solution work as a whole is that the first configuration #Order(1) you
define .antMatcher which means the configuration will work only for urls that match
the prefix.
So now, scenario 1. User from Angular go the the JWT filter only.
scenario 2. API user will lend in the API filter first! But once it's done (After succesfull authentication) it still
continue to the JWT filter but becuase it doesn't have JWT the filter not doing anything.
I would like to avoid to other filter in case of API call but the solution work,
problem solved.
I must say that security in spring boot is the most complex I came across so far from other features.
Because the AuthTokenFilter is instantiated with #Bean, which causes the filter to be added to the ApplicationFilterChain, after the APIKeyAuthFilter is processed, it can also enter the AuthTokenFilter.

AbstractAuthenticationProcessingFilter triggered no matter what SecurityFilterChain it is a part of

I am experimenting with spring security and came across a strange behavior.
My idea is to create a security filter that authenticates requests based on JWT (or JWS) tokens:
public class JWTokenFilter extends AbstractAuthenticationProcessingFilter {
public JWTokenFilter(AuthenticationManager authenticationManager) {
super("/**"); //doesn't have any effect, every request still gets considered by this filter
setAuthenticationManager(authenticationManager);
}
#Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
String token = request.getHeader("Authorization");
if (!StringUtils.hasText(token)) {
throw new TokenException("Token is empty");
}
var authentication = determineAuthentication(token.replace("Bearer","").trim());
//the AbstractAuthenticationProcessingFilter fills the Security context
return this.getAuthenticationManager().authenticate(authentication);
}
#Override
protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) {
System.out.println("Asked for "+request.getRequestURI());
return request.getHeader("Authorization") != null;
}
private TokenAuthentication<UserInfo> determineAuthentication(String token) {
var split = token.split("\\.");
if (split.length < 2 || split.length > 3) {
throw new TokenException("Token malformed");
}
if (split.length == 2){
return new JWTAuthentication<>(token);
}else {
return new JWSAuthentication<>(token);
}
}
}
I have 3 #RestController classes which have their paths mapped:
#RequestMapping("/admin")
#RequestMapping("/all")
#RequestMapping("/anon")
Along with this, I have the following security configuration:
#Configuration
#Order(98)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.requestMatchers()
.antMatchers("/all/**","/anon/**")
.and()
.authorizeRequests().antMatchers("/all/**").permitAll()
.and()
.authorizeRequests().antMatchers("/anon/**").anonymous();
}
#Override
public void configure(WebSecurity web) {
web.ignoring().mvcMatchers("/webjars/**", "/css/**");
}
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
#Configuration
#Order(99)
public static class TokenSecurityConfig extends WebSecurityConfigurerAdapter{
#Lazy
#Autowired
private JWTokenFilter tokenFilter;
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests() //having /admin/** or /** makes no difference
.anyRequest().authenticated()
.and().addFilterBefore(tokenFilter,ExceptionTranslationFilter.class);//put this filter near the end of the chain
}
#Bean
public JWTokenFilter tokenFilter(JWTokenAuthenticationProvider jwTokenAuthenticationProvider,JWSTokenAuthenticationProvider jwsTokenAuthenticationProvider){
var list = new ArrayList<AuthenticationProvider>();
list.add(jwsTokenAuthenticationProvider);
list.add(jwTokenAuthenticationProvider);
ProviderManager manager = new ProviderManager(list);
return new JWTokenFilter(manager);
}
}
}
From this configuration here we can see that there are 2 SecurityFilterChans (not counting the /webjars and /css ones):
That matches all requests for "/all/**" and "/anon/**" REST routes
That matches any request
Since the 1. chain has lower #Order(98), than 2. #Order(99), that means that the 1. chain will be considered first which is shown by the debugger,
and matched if the incoming request looks like:
curl --request GET \
--url http://localhost:8080/all/hello \
Now what I am experiencing is that the JWTokenFilter method boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) is always called no matter the request path !
And in the console output, I can find Asked for /all/hello.
Edit:
My spring boot version is 2.3.6.RELEASE
My question is:
Why is the JWTokenFIlter even asked if it should authenticate requests with paths that are not matched by the SecurityFilterChain it is a part of?
I believe I have a better answer, but I wanted to answer your original question as well. I split this into two sections.
Improved Answer
I realize this doesn't answer the original question, but I think you may be better off using the built in support for JWT based authentication. I'd check out the OAuth 2.0 Resource Server section of the reference documentation.
Answer to Original Question
Spring Boot will automatically register any Filter exposed as a #Bean for every request directly with the Servlet Container.
You have two options that I see. The first is to avoid exposing the JwtTokenFilter as a #Bean.
#Configuration
#Order(99)
public static class TokenSecurityConfig extends WebSecurityConfigurerAdapter{
#Autowired
JWTokenAuthenticationProvider jwTokenAuthenticationProvider;
#Autowired
JWSTokenAuthenticationProvider jwsTokenAuthenticationProvider;
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests() //having /admin/** or /** makes no difference
.anyRequest().authenticated()
.and().addFilterBefore(tokenFilter(),ExceptionTranslationFilter.class);//put this filter near the end of the chain
}
public JWTokenFilter tokenFilter(){
var list = new ArrayList<AuthenticationProvider>();
list.add(jwsTokenAuthenticationProvider);
list.add(jwTokenAuthenticationProvider);
ProviderManager manager = new ProviderManager(list);
return new JWTokenFilter(manager);
}
}
Alternatively, you can continue exposing JwtTokenFilter as a #Bean and create a FilterRegistrationBean that disables registration.
#Bean
public FilterRegistrationBean registration(JWTokenFilter filter) {
FilterRegistrationBean registration = new FilterRegistrationBean(filter);
registration.setEnabled(false);
return registration;
}

How does Spring Security Filter Work With Custom Authentication and How To Combine It with Servlet Filter?

So I have a question regarding Spring Security. So I want to check authentication using custom header which then I want to check the token given in the custom header to redis value and set the data object as credentials at custom implementation of abstract authentication token.
I have already followed the tutorial in this web: https://shout.setfive.com/2015/11/02/spring-boot-authentication-with-custom-http-header/, but I can't update the authentication interface in SecurityContextHolder.getContext() (I set the credentials in my implementation of Authentication Interface, but when I get it in the service, the credentials is null).
I also found other problems, I actually want to order the filter like this:
ExceptionHandlerFilter (to catch exception error in the filter) -> Other filter or CustomWebSecurityConfigurerAdapter.
But when the url matches the antMatcher, I found that ExceptionHandlerFilter was skipped by the application.
I was so confused by this and could not find better tutorial in implementing custom authentication using Spring Security. So I want to ask whether you guys can tell me how Spring Security works and how to combine it with Filter?
Here is my first filter to catch exception
#Component
#Order(0)
public class ExceptionHandlerFilter extends OncePerRequestFilter {
private JaminExceptionHandler exceptionHandler;
private ObjectMapper objectMapper = new ObjectMapper();
#Autowired
public ExceptionHandlerFilter(JaminExceptionHandler exceptionHandler) {
this.exceptionHandler = exceptionHandler;
}
#Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
try {
filterChain.doFilter(request, response);
} catch (Throwable exception) {
ResponseEntity<?> responseEntity = this.exceptionHandler.handleException(exception, request);
response.setStatus(responseEntity.getStatusCode().value());
response.setHeader("Content-Type", "application/json");
response.getWriter().write(this.objectMapper.writeValueAsString(responseEntity.getBody()));
}
}
}
Here is my Auth Filter
#Component
public class AuthFilter extends OncePerRequestFilter {
#Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String token = request.getHeader("J-Auth");
if (token != null) {
Authentication auth = new JaminAuthenticationToken(token);
SecurityContextHolder.getContext().setAuthentication(auth);
filterChain.doFilter(request, response);
} else {
throw new JaminException("Not authorized", JaminExceptionType.NOT_AUTHORIZED, HttpStatus.UNAUTHORIZED);
}
}
}
Authentication Provider
#Component
public class JaminAuthenticationProvider implements AuthenticationProvider {
private RedisTemplate<String, String> authRedis;
private ObjectMapper objectMapper = new ObjectMapper();
#Autowired
public JaminAuthenticationProvider(#Qualifier("authRedis") RedisTemplate<String, String> authRedis) {
this.authRedis = authRedis;
}
private UserDTO getUserDTO(String token) throws IOException {
String userData = this.authRedis.opsForValue().get(token);
if (userData == null) {
throw new JaminException("Not authorized", JaminExceptionType.NOT_AUTHORIZED, HttpStatus.UNAUTHORIZED);
}
return this.objectMapper.readValue(userData, UserDTO.class);
}
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
JaminAuthenticationToken auth = (JaminAuthenticationToken) authentication;
try {
UserDTO userDTO = this.getUserDTO(auth.getToken());
auth.setCredentials(userDTO);
return auth;
} catch (IOException e) {
e.printStackTrace();
}
throw new JaminException("Not authorized", JaminExceptionType.NOT_AUTHORIZED, HttpStatus.UNAUTHORIZED);
}
#Override
public boolean supports(Class<?> authentication) {
return JaminAuthenticationToken.class.isAssignableFrom(authentication);
}
}
WebSecurityConfigurerAdapter
#Configuration
#EnableWebSecurity
#Order(1)
public class JaminSecurityAdapter extends WebSecurityConfigurerAdapter {
#Autowired
private JaminAuthenticationProvider jaminAuthenticationProvider;
private void disableDefaultSecurity(HttpSecurity http) throws Exception {
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.csrf().disable();
http.formLogin().disable();
http.logout().disable();
http.httpBasic().disable();
}
#Override
protected void configure(HttpSecurity http) throws Exception {
this.disableDefaultSecurity(http);
http.antMatcher("/auth/check")
.authorizeRequests()
.anyRequest().authenticated()
.and()
.addFilterBefore(new AuthFilter(), BasicAuthenticationFilter.class);
// http.authorizeRequests().anyRequest().permitAll();
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(jaminAuthenticationProvider);
}
}
Spring Security has some "before and after" steps. There are a few Handlers that can help. I don't know your code, but if you can get your authentication ok, maybe you just have to extend a SuccessHandler and set the authentication there, like i did in my blog project:
if(checkEmail(authentication)) {
val adminRole = SimpleGrantedAuthority("ROLE_ADMIN")
val oldAuthorities = SecurityContextHolder.getContext().getAuthentication().getAuthorities()
val updateAuthorities = mutableListOf<GrantedAuthority>()
updateAuthorities.add(adminRole)
updateAuthorities.addAll(oldAuthorities)
SecurityContextHolder.getContext().setAuthentication(UsernamePasswordAuthenticationToken(authentication.getPrincipal(),
authentication.getCredentials(),
updateAuthorities))
}
And about the filters, maybe you can find your answer here. I don't like using filters and interceptors, but sometimes they are really necessary.

Implement Spring Security for Rest Api

I use this code for Rest API authentication:
#Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
Optional<String> basicToken = Optional.ofNullable(request.getHeader(HttpHeaders.AUTHORIZATION))
.filter(v -> v.startsWith("Basic"))
.map(v -> v.split("\\s+")).filter(a -> a.length == 2).map(a -> a[1]);
if (!basicToken.isPresent()) {
return sendAuthError(response);
}
byte[] bytes = Base64Utils.decodeFromString(basicToken.get());
String namePassword = new String(bytes, StandardCharsets.UTF_8);
int i = namePassword.indexOf(':');
if (i < 0) {
return sendAuthError(response);
}
String name = namePassword.substring(0, i);
String password = namePassword.substring(i + 1);
// Optional<String> clientId = authenticationService.authenticate(name, password, request.getRemoteAddr());
Merchants merchant = authenticationService.authenticateMerchant(name, password, request.getRemoteAddr());
if (merchant == null) {
return sendAuthError(response);
}
request.setAttribute(CURRENT_CLIENT_ID_ATTRIBUTE, merchant.getId());
return true;
}
How I can rewrite the code with Spring Security in order to get the same result but for different links to have authentication? For example:
localhost:8080/v1/notification - requests should NOT be authenticated.
localhost:8080/v1/request - requests should be authenticated.
Here you can find a working project https://github.com/angeloimm/springbasicauth
I know in the pom.xml file there are a lot of useless dependencies but I started from an already existing project and I had no time to depure it
Basically you must:
configure spring security
configure spring mvc
implements your own authentication provider according to spring security. Note I used an inMemoryAuthentication. Please modify it according to yuor own wishes
Let me explain the code.
Spring MVC Configuration:
#Configuration
#EnableWebMvc
#ComponentScan(basePackages= {"it.olegna.test.basic"})
public class WebMvcConfig implements WebMvcConfigurer {
#Override
public void configureMessageConverters(final List<HttpMessageConverter<?>> converters) {
converters.add(new MappingJackson2HttpMessageConverter());
}
}
Here we don't do anything else that configuring spring MVC by telling it where to find controllers and so on and to use a single message converter; the MappingJackson2HttpMessageConverter in order to produce JSON responses
Spring Security Configuration:
#Configuration
#EnableWebSecurity
#Import(value= {WebMvcConfig.class})
public class WebSecConfig extends WebSecurityConfigurerAdapter {
#Autowired private RestAuthEntryPoint authenticationEntryPoint;
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication()
.withUser("test")
.password(passwordEncoder().encode("testpwd"))
.authorities("ROLE_USER");
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/securityNone")
.permitAll()
.anyRequest()
.authenticated()
.and()
.httpBasic()
.authenticationEntryPoint(authenticationEntryPoint);
}
#Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
}
Here we configure Spring Security in order to use HTTP Basic Authentication for all requests except the ones starting with securityNone. We use a NoOpPasswordEncoder in order to encode the provided password; this PasswrodEncoder does absolutly nothing... it leaves the passwrod as it is.
RestEntryPoint:
#Component
public class RestAuthEntryPoint implements AuthenticationEntryPoint {
#Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
}
}
This entrypoint disables all requests not containg the Authentication header
SimpleDto: a very simple DTO representing the JSON answer form a controller
public class SimpleDto implements Serializable {
private static final long serialVersionUID = 1616554176392794288L;
private String simpleDtoName;
public SimpleDto() {
super();
}
public SimpleDto(String simpleDtoName) {
super();
this.simpleDtoName = simpleDtoName;
}
public String getSimpleDtoName() {
return simpleDtoName;
}
public void setSimpleDtoName(String simpleDtoName) {
this.simpleDtoName = simpleDtoName;
}
}
TestBasicController: a very simple controller
#RestController
#RequestMapping(value= {"/rest"})
public class TestBasicController {
#RequestMapping(value= {"/simple"}, method= {RequestMethod.GET}, produces= {MediaType.APPLICATION_JSON_UTF8_VALUE})
public ResponseEntity<List<SimpleDto>> getSimpleAnswer()
{
List<SimpleDto> payload = new ArrayList<>();
for(int i= 0; i < 5; i++)
{
payload.add(new SimpleDto(UUID.randomUUID().toString()));
}
return ResponseEntity.ok().body(payload);
}
}
So if you try this project by using postman or any other tester you can have 2 scenarios:
authentication required
all ok
Let's suppose you want to invoke the URL http://localhost:8080/test_basic/rest/simple without passing the Authentication header. The HTTP Status code will be 401 Unauthorized
This means that the Authentication Header is required
By adding this header to the request Authorization Basic dGVzdDp0ZXN0cHdk all works pretty good
Note that the String dGVzdDp0ZXN0cHdk is the Base64 encoding of the string username:password; in our case is the Base64 encoding of test:testpwd defined in the inMemoryAuthentication
I hope this is usefull
Angelo
WEB SECURITY USER DATAIL SERVICE
In order to configure Spring security to retrieve user details from DB you must do the following:
create a org.springframework.security.core.userdetails.UserDetailsService implementation like this:
#Service
public class UserDetailsServiceImpl implements UserDetailsService {
#Autowired
private BasicService svc;
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
BasicUser result = svc.findByUsername(username);
if( result == null )
{
throw new UsernameNotFoundException("No user found with username "+username);
}
return result;
}
}
Inject it to the spring security configuration and use it like this:
public class WebSecConfig extends WebSecurityConfigurerAdapter {
#Autowired private RestAuthEntryPoint authenticationEntryPoint;
#Autowired
UserDetailsService userDetailsService;
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
// auth
// .inMemoryAuthentication()
// .withUser("test")
// .password(passwordEncoder().encode("testpwd"))
// .authorities("ROLE_USER");
auth.userDetailsService(userDetailsService);
auth.authenticationProvider(authenticationProvider());
}
#Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
authenticationProvider.setUserDetailsService(userDetailsService);
authenticationProvider.setPasswordEncoder(passwordEncoder());
return authenticationProvider;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/securityNone")
.permitAll()
.anyRequest()
.authenticated()
.and()
.httpBasic()
.authenticationEntryPoint(authenticationEntryPoint);
}
#Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
}
I pushed the code on the github link I provided. There you can find a full working example based on:
spring 5
spring security 5
hibernate
h2 DB
Feel free to adapt it to your own scenario
You can use a default spring-security configuration described on various websites, like baeldung.com or mkyong.com. The trick in your sample seems to be the call to get the Merchant. Depending on the complexity of the authenticationService and the Merchant object, you can either use the following code, or implement a facade to get similar behaviour.
#Autowired
public void authenticationManager(AuthenticationManagerBuilder auth) {
auth.authenticationProvider(new AuthenticationProvider() {
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Merchants merchant = authenticationService.authenticateMerchant(name, password, request.getRemoteAddr());
if(merchant == null) {
throw new AuthenticationException("No Merchant found.");
}
return new UsernamePasswordAuthenticationToken(name, password, merchant.getAuthorities());
}
#Override
public boolean supports(Class<?> authentication) {
return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication));
}
});
}
Setting the attribute on the request, if necessary could be done by a separate filter which takes the Principal from the SecurityContext and puts it on the request as an attribute.

Spring - Add a check for 3rd parameter during authentication

At a glance, I have API Back-end App written in Spring Boot which uses JWT for secured data transmission. I want to add 3rd parameter for authorization, so I should have login, password and storeID parameters. I am inspired by this answer How implement Spring security when login page having more field apart from user name and password? but when I followed proposed solution my 3rd parameter in not used. My impression is that I am missing something important in Security Config. Could you please point to my mistake?
SecurityConfig
#SuppressWarnings("SpringJavaAutowiringInspection")
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private JwtAuthenticationEntryPoint unauthorizedHandler;
#Autowired
private UserDetailsService userDetailsService;
#Autowired
private AuthenticationDetailsSource<HttpServletRequest, ?> webAuthenticationDetailsSourceImpl;
#Autowired
public void configureAuthentication(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
authenticationManagerBuilder
.authenticationProvider(myAuthProvider());
}
#Bean
public CustomUserDetailsAuthenticationProvider myAuthProvider() throws Exception {
CustomUserDetailsAuthenticationProvider provider = new CustomUserDetailsAuthenticationProvider();
provider.setPasswordEncoder(passwordEncoder());
provider.setUserDetailsService(userDetailsService);
return provider;
}
#Bean
public UsernamePasswordAuthenticationFilter usernamePasswordAuthenticationFilter() throws Exception {
UsernamePasswordAuthenticationFilter usernamePasswordAuthenticationFilter = new UsernamePasswordAuthenticationFilter();
usernamePasswordAuthenticationFilter.setAuthenticationManager(authenticationManager());
usernamePasswordAuthenticationFilter.setAuthenticationDetailsSource(webAuthenticationDetailsSourceImpl);
return usernamePasswordAuthenticationFilter;
}
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
#Bean
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Bean
public JwtAuthenticationTokenFilter authenticationTokenFilterBean() throws Exception {
JwtAuthenticationTokenFilter authenticationTokenFilter = new JwtAuthenticationTokenFilter();
authenticationTokenFilter.setAuthenticationManager(authenticationManagerBean());
return authenticationTokenFilter;
}
#Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
// we don't need CSRF because our token is invulnerable
.csrf().disable()
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
// don't create session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests()
// allow anonymous resource requests
.antMatchers(
HttpMethod.GET,
"/",
"/*.html",
"/favicon.ico",
"/**/*.html",
"/**/*.css",
"/**/*.js"
).permitAll()
.antMatchers("/auth/**").permitAll()
.anyRequest().authenticated();
// Custom JWT based security filter
httpSecurity
.addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class);
// disable page caching
httpSecurity.headers().cacheControl();
}
}
I was under impression I can check against storeID field in WebAuthenticationDetailsSourceImpl, but looks like it has never been executed because I don't see anything related in log.
WebAuthenticationDetailsSourceImpl:
#Component
public class WebAuthenticationDetailsSourceImpl implements AuthenticationDetailsSource<HttpServletRequest, JwtAuthenticationRequest> {
#Override
public JwtAuthenticationRequest buildDetails(HttpServletRequest context) {
System.out.println("___#####_____");
System.out.println(context);
System.out.println("___#####_____");
return new JwtAuthenticationRequest();
}
}
cuz you don't insert "your" usernamePasswordAuthenticationFilter that set webAuthenticationDetailsSourceImpl to Spring Security's authentication filter chain.
perhaps current your authentication filter chain is
~
JwtAuthenticationTokenFilter
(Spring Security's original)UsernamePasswordAuthenticationFilter
~
hence,if you want to retrieve your additional parameter in "your" usernamePasswordAuthenticationFilter add this filter too like a JwtAuthenticationTokenFilter
but , if you want to simply retrieve parameter at JwtAuthenticationTokenFilter
use setAuthenticationDetailsSource at there
#Bean
public JwtAuthenticationTokenFilter authenticationTokenFilterBean() throws Exception {
JwtAuthenticationTokenFilter authenticationTokenFilter = new JwtAuthenticationTokenFilter();
authenticationTokenFilter.setAuthenticationManager(authenticationManagerBean());
authenticationTokenFilter.setAuthenticationDetailsSource(webAuthenticationDetailsSourceImpl);
return authenticationTokenFilter;
}

Resources