Spring web and security. Add /api prefix to login route - spring

I am using spring security. I need to add the /api prefix to the login route. I am extending the UsernamePasswordAuthenticationFilter class
JWTAuthenticationFiler.class
public class JWTAuthenticationFilter extends
UsernamePasswordAuthenticationFilter{
private AuthenticationManager authenticationManager;
public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
#Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
try {
User creds = new ObjectMapper()
.readValue(request.getInputStream(), User.class);
return authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
creds.getUsername(),
creds.getPassword(),
new ArrayList<>())
);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
#Override
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain,
Authentication authResult) throws IOException, ServletException {
String token = Jwts.builder()
.setSubject(((org.springframework.security.core.userdetails.User) authResult.getPrincipal()).getUsername())
.setExpiration(new Date(System.currentTimeMillis() + SecurityConstants.EXPIRATION_TIME))
.signWith(SignatureAlgorithm.HS512, SecurityConstants.SECRET.getBytes())
.compact();
response.addHeader(SecurityConstants.HEADER_STRING, token);
}
}
The base class contains in constructor instantiation of the login route:
public class UsernamePasswordAuthenticationFilter extends
AbstractAuthenticationProcessingFilter {
// ~ Static fields/initializers
// =====================================================================================
public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;
private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;
private boolean postOnly = true;
// ~ Constructors
// ===================================================================================================
public UsernamePasswordAuthenticationFilter() {
super(new AntPathRequestMatcher("/login", "POST"));
}
...
I have to override somehow the "/login" to "/api/login". How to do that?
Thank you!

To change the default value for filterProcessesUrl in your custom authentication filter extending UsernamePasswordAuthenticationFilter you have to use setRequiresAuthenticationRequestMatcher, see AbstractAuthenticationProcessingFilter:
This filter will intercept a request and attempt to perform authentication from that request if the request matches the setRequiresAuthenticationRequestMatcher(RequestMatcher).
with a RequestMatcher that matches your custom URL.
You can call the setter in your constructor, in your factory method or in XML configuration.

I suppose you use the WebSecurityConfigurerAdapter. Therefore you have to use the formLogin in order to set the login endpoint.
public class SecurityConfigurationAdapter extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/apilogin*").permitAll()
.anyRequest().authenticated()
.and().formLogin().loginPage("/apilogin")
.and().httpBasic();
}
}

I believe you need to call setRequiresAuthenticationRequestMatcher on the filter when defining the bean in your configuration class.
code.setRequiresAuthenticationRequestMatcher(
new AntPathRequestMatcher("/api/login","POST"));
#Bean
public UsernamePasswordAuthenticationFilter authenticationFilter() {
UsernamePasswordAuthenticationFilter authFilter = new UsernamePasswordAuthenticationFilter();
authFilter.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/login","POST"));
return authFilter;
}

Related

spring boot security custom successHandler with rest not working

not sure if my question is good..
Perhaps I was looking very badly for information about the spring security
In general, I hope it will not be difficult for you to answer.
The question is, I use spring security with my login page. The login page is just in the public templates folder. I do not create a separate Controller for it that would return the view page (would it be correct to create a controller for it that would return the view login page?). In any case, my code works even without this page view controller. But only my custom SuccessHandler does not work (which, after login, checks by roles and would redirect to another page).
Should I redirect by role to the appropriate pages using a different approach? (I mean if ADMIN_ROLE after login is redirected to the admin-panel.html)
my security
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(securedEnabled = true)
public class CustomWebSecurityConfiguration extends WebSecurityConfigurerAdapter {
#Autowired
private UserServiceImpl userServiceImpl;
#Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and()
.authorizeRequests()
.antMatchers("/", "/templates/sign-up.html").permitAll()
.antMatchers("/api/users", "/api/users/login").permitAll()
.antMatchers("/templates/admin-panel.html").hasRole("ADMIN")
.antMatchers("/all-users").hasRole("ADMIN")
.antMatchers("/news").hasRole("USER")
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/templates/login.html")
.defaultSuccessUrl("/")
.permitAll()
.successHandler(myAuthenticationSuccessHandler())
.and()
.logout()
.permitAll()
.logoutSuccessUrl("/index.html");
http.csrf().disable();
}
#Override
public void configure(WebSecurity web) {
web
.ignoring()
.antMatchers("/css/**")
.antMatchers("/js/**")
.antMatchers("/static/**")
.antMatchers("/resources/**");
}
#Autowired
protected void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userServiceImpl).passwordEncoder(bCryptPasswordEncoder());
}
#Bean
public AuthenticationSuccessHandler myAuthenticationSuccessHandler(){
return new CustomAuthenticationSuccessHandler();
}
#Bean
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
my custom success handler
public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
protected final Log logger = LogFactory.getLog(this.getClass());
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
public CustomAuthenticationSuccessHandler() {
super();
}
// API
#Override
public void onAuthenticationSuccess(final HttpServletRequest request, final HttpServletResponse response, final Authentication authentication) throws IOException {
handle(request, response, authentication);
clearAuthenticationAttributes(request);
}
// IMPL
protected void handle(final HttpServletRequest request, final HttpServletResponse response, final Authentication authentication) throws IOException {
final String targetUrl = determineTargetUrl(authentication);
if (response.isCommitted()) {
logger.debug("Response has already been committed. Unable to redirect to " + targetUrl);
return;
}
redirectStrategy.sendRedirect(request, response, targetUrl);
}
protected String determineTargetUrl(final Authentication authentication) {
Map<String, String> roleTargetUrlMap = new HashMap<>();
roleTargetUrlMap.put("ROLE_USER", "/index.html");
roleTargetUrlMap.put("ROLE_ADMIN", "/templates/admin-panel.html");
final Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
for (final GrantedAuthority grantedAuthority : authorities) {
String authorityName = grantedAuthority.getAuthority();
if(roleTargetUrlMap.containsKey(authorityName)) {
return roleTargetUrlMap.get(authorityName);
}
}
throw new IllegalStateException();
}
/**
* Removes temporary authentication-related data which may have been stored in the session
* during the authentication process.
*/
protected final void clearAuthenticationAttributes(final HttpServletRequest request) {
final HttpSession session = request.getSession(false);
if (session == null) {
return;
}
session.removeAttribute(WebAttributes.AUTHENTICATION_EXCEPTION);
}
}
my controller
#CrossOrigin
#RestController
#RequestMapping("/api/users")
public class UserController {
private final UserServiceImpl userService;
private AuthenticationManager authenticationManager;
public UserController(UserServiceImpl userService, AuthenticationManager authenticationManager) {
this.userService = userService;
this.authenticationManager = authenticationManager;
}
#PostMapping
public ResponseEntity<?> register(#RequestBody UserDTO user) {
try {
userService.register(user);
return new ResponseEntity<>("User added", HttpStatus.OK);
} catch (Exception e) {
return new ResponseEntity<>(e, HttpStatus.BAD_REQUEST);
}
}
#PostMapping(value = "/login")
public ResponseEntity<?> login(#RequestBody UserDTO user, HttpServletResponse response) {
try {
Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword()));
boolean isAuthenticated = isAuthenticated(authentication);
if (isAuthenticated) {
SecurityContextHolder.getContext().setAuthentication(authentication);
// response.sendRedirect("/templates/admin-panel.html");
// my pathetic attempt to create a redirect to another page
}
return new ResponseEntity<>("user authenticated", HttpStatus.OK);
} catch (Exception e) {
return new ResponseEntity<>(e, HttpStatus.FORBIDDEN);
}
}
private boolean isAuthenticated(Authentication authentication) {
return authentication != null && !(authentication instanceof AnonymousAuthenticationToken) && authentication.isAuthenticated();
}
my static files
enter image description here
My Guess, as you didn't post your login page itself:
You don't need a controller listening to POST /login this normally automatically registered by Spring Security with all security related authentication stuff. No need to try it by yourself as in UserController.login(). I guess by regsitering this endpoint you override / deactivate the regular spring security behaviour.
Normally you just need a login page with a form that posts correctly to /login. The handling on the backend side is done by spring security itself.
See https://spring.io/guides/gs/securing-web/ for a minimal worling setup.

SecurityContextHolder returns null after I call setAuthentication with the custom authentication token

I have a small Spring Boot project which has username/password authentication. I use spring-security sign-in with our LDAP connection. What I want, and I did manage to do in several projects, is extending AbstractAuthenticationToken class in order to add my own fields.
In my custom GenericFilterBean class, I want to create my own authentication object and set into SecurityContextHolder as below:
KfsMsgToken kfsMsgToken = new KfsMsgToken(
kfsInMessageInfo.getObjId(),
new ArrayList<>());
SecurityContextHolder.getContext().setAuthentication(kfsMsgToken);
And, here is my custom Authentication class:
public class KfsMsgToken extends AbstractAuthenticationToken {
String kfsInMsgOid;
public KfsMsgToken(String kfsInMsgOid, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.kfsInMsgOid = kfsInMsgOid;
}
/**
*
* #return
*/
#Override
public Object getCredentials() {
return null;
}
/**
*
* #return
*/
#Override
public Object getPrincipal() {
return null;
}
public String getKfsInMsgOid() {
return kfsInMsgOid;
}
public void setKfsInMsgOid(String kfsInMsgOid) {
this.kfsInMsgOid = kfsInMsgOid;
}
}
The problem is, after login successfully, I see UsernamePasswordAuthenticationToken which has been already setted. I reset authentication field using my custom token object, returns null in service layer. I have no idea what is the reason.
All advises are appreciated!
My security configuration:
#Configuration
#Order(SecurityProperties.BASIC_AUTH_ORDER)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Value("${ldap.urls}")
private String ldapUrl;
#Value("${ldap.domain}")
private String ldapDomain;
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/css/**", "/fonts/**", "/img/**", "/js/**", "/pdf/**").permitAll()
.and().formLogin().defaultSuccessUrl("/index", true).loginProcessingUrl("/login").permitAll().and()
.authorizeRequests()
.anyRequest().authenticated()
.and()
.csrf().disable();
}
#Bean
public ActiveDirectoryLdapAuthenticationProvider activeDirectoryLdapAuthenticationProvider() {
ActiveDirectoryLdapAuthenticationProvider activeDirectoryLdapAuthenticationProvider = new
ActiveDirectoryLdapAuthenticationProvider(ldapDomain, ldapUrl);
return activeDirectoryLdapAuthenticationProvider;
}
}
How I register my filter:
#Bean
public FilterRegistrationBean jwtFilter() {
final FilterRegistrationBean registrationBean = new FilterRegistrationBean();
registrationBean.setFilter(new KfsInMsgFilter(kfsInMessageService, kfsMsgToken, messageChannelService));
registrationBean.setOrder(0);
registrationBean.addUrlPatterns(SECURE);
registrationBean.setDispatcherTypes(DispatcherType.REQUEST);
return registrationBean;
}
This is what I do in my filter:
public class KfsInMsgFilter extends GenericFilterBean {
#Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
...
// at this point, Authentication holds UserNamePasswordAuthenticationToken
KfsMsgToken kfsMsgToken = new KfsMsgToken(
kfsInMessageInfo.getObjId(),
new ArrayList<>());
SecurityContextHolder.getContext().setAuthentication(kfsMsgToken);
...
}
}
You need to extend AbstractAuthenticationProcessingFilter and have your custom LDAP authentication inside there and register and finally use
http.addFilterAfter(
new CustomFilter(), UsernamePasswordAuthenticationProcessingFilter.class);
Setting authenticated field over my custom token,
kfsMsgToken.setAuthenticated(Boolean.TRUE.booleanValue());
solved my problem.
But,
I think there is a better way. Using scoped beans is more efficient and practical.
I changed my custom filter to a request-scoped bean:
#Data
#AllArgsConstructor
#NoArgsConstructor
#Component
#Scope(value="request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class KfsMsgToken {
String kfsInMsgOid;
}
By autowiring, I can get and read the value that I need.

Access Deny and Allow Functionality using Spring and Spring security

Currently I am trying to implement authentication example using spring MVC and spring boot with spring security. In my sample application what I am trying to do is - I am sending one authentication token in header of one URL. I need to take this authentication token from URL and decode. If username and password is matching , then only need to transfer the control to end point "api/getStudent/v1" or something like this. Otherwise from there only need to give the response that denying.
For this Currently I tried with authentication provider from spring security. But it is not suitable for taking the token from header of request. Here my confusion is that , from spring security which method I have to implement here ? Can anyone suggest a standard way of implementation ? Or Any documentation for this type of implementation?
All you need to do is create a custom security filter and plug this filter before spring security BasicAuthenticationFilter. Sample code -
public class CustomAuthenticationFilter extends OncePerRequestFilter {
#Override
protected void doFilterInternal(final HttpServletRequest request, final HttpServletResponse response, final FilterChain filterChain) throws ServletException, IOException {
String authHeader = request.getHeaders("Authorization");
//Decode the authHeader
//Validate the authHeader with your username & password
if(invalid) {
//throw exception and abort processing
}
filterChain.doFilter(request, response);
}
}
Now either you can create the bean OR make this as #component so that spring picks it up and creates bean for you.
In your security configuration, add following -
#Configuration
public class CustomWebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.addFilterAfter(new CustomAuthenticationFilter(), BasicAuthenticationFilter.class);
}
}
You can try out the following. I have used JWT authentication here. And as per your problem you can preauthorize your end point "api/getStudent/v1" with spring's #Preauthorize annotation.
Following is the end point where user will be directed upon the signin.
#PostMapping("/signin")
public ResponseEntity<?> authenticateUser(#Valid #RequestBody LoginForm loginRequest) {
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(loginRequest.getEmail(), loginRequest.getPassword()));
SecurityContextHolder.getContext().setAuthentication(authentication);
String jwt = jwtProvider.generateJwtToken(authentication);
UserPrinciple userPrinciple = (UserPrinciple) authentication.getPrincipal();
String name = userRepo.findById(userPrinciple.getId()).get().getName();
return ResponseEntity.ok(new JwtResponse(jwt, userPrinciple.getUsername(),
userPrinciple.getAuthorities(),name,userPrinciple.getGender()));
}
Following is the WebSecurityConfig class
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(
prePostEnabled = true
)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
UserDetailsServiceImpl userDetailsService;
#Autowired
private JwtAuthEntryPoint unauthorizedHandler;
#Bean
public JwtAuthTokenFilter authenticationJwtTokenFilter() {
return new JwtAuthTokenFilter();
}
#Override
public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
authenticationManagerBuilder
.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder());
}
#Bean
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Bean
public AuthorizationRequestRepository<OAuth2AuthorizationRequest> customAuthorizationRequestRepository() {
return new HttpSessionOAuth2AuthorizationRequestRepository();
}
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.cors().and()
.csrf()
.disable()
.authorizeRequests()
.antMatchers("/api/auth/**").permitAll()
.anyRequest().authenticated()
.and()
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler)
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class);
}
}
Following JWTProvider class includes the method to generate the JWT token.(note: I have set the email of each user as the username. You can do it according to your wish)
#Component
public class JwtProvider {
#Autowired
UserRepository userRepo;
private static final Logger logger = LoggerFactory.getLogger(JwtProvider.class);
public String generateJwtToken(Authentication authentication) {
UserPrinciple userPrincipal = (UserPrinciple) authentication.getPrincipal();
String name = userRepo.findById(userPrincipal.getId()).get().getName();
return Jwts.builder()
.setSubject((userPrincipal.getUsername())) //getUsername returns the email
.claim("id",userPrincipal.getId() )
.claim("name",name)
.setIssuedAt(new Date())
.setExpiration(new Date((new Date()).getTime() + EXPIRATION_TIME))
.signWith(SignatureAlgorithm.HS512, SECRET)
.compact();
}
public String generateJwtToken(UserPrinciple userPrincipal) {
String name = userRepo.findById(userPrincipal.getId()).get().getName();
return Jwts.builder()
.setSubject((userPrincipal.getUsername())) //getUsername returns the email
.claim("id",userPrincipal.getId() )
.claim("name",name)
.setIssuedAt(new Date())
.setExpiration(new Date((new Date()).getTime() + EXPIRATION_TIME))
.signWith(SignatureAlgorithm.HS512, SECRET)
.compact();
}
public boolean validateJwtToken(String authToken) {
try {
Jwts.parser().setSigningKey(SECRET).parseClaimsJws(authToken);
return true;
} catch (SignatureException e) {
logger.error("Invalid JWT signature -> Message: {} ", e);
} catch (MalformedJwtException e) {
logger.error("Invalid JWT token -> Message: {}", e);
} catch (ExpiredJwtException e) {
logger.error("Expired JWT token -> Message: {}", e);
} catch (UnsupportedJwtException e) {
logger.error("Unsupported JWT token -> Message: {}", e);
} catch (IllegalArgumentException e) {
logger.error("JWT claims string is empty -> Message: {}", e);
}
return false;
}
public String getUserNameFromJwtToken(String token) {
return Jwts.parser()
.setSigningKey(SECRET)
.parseClaimsJws(token)
.getBody().getSubject();
}
}
Following is the JWTAuthTokenFilter class which is initiated in WebSecurityConfig class. Here is where it decodes the token from the rquest and checks whether the token is valid or not
public class JwtAuthTokenFilter extends OncePerRequestFilter {
#Autowired
private JwtProvider tokenProvider;
#Autowired
private UserDetailsServiceImpl userDetailsService;
private static final Logger logger = LoggerFactory.getLogger(JwtAuthTokenFilter.class);
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
try {
String jwt = getJwt(request);
if (jwt != null && tokenProvider.validateJwtToken(jwt)) {
String email = tokenProvider.getUserNameFromJwtToken(jwt);//returns the email instead of username
UserDetails userDetails = userDetailsService.loadUserByUsername(email);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception e) {
logger.error("Can NOT set user authentication -> Message: {}", e);
}
filterChain.doFilter(request, response);
}
private String getJwt(HttpServletRequest request) {
String authHeader = request.getHeader("Authorization");
if (authHeader != null && authHeader.startsWith("Bearer ")) {
return authHeader.replace("Bearer ", "");
}
return null;
}
}
Following is the JWTAuthEntryPoint . Check WebSecurityConfig class for the use of this class
#Component
public class JwtAuthEntryPoint implements AuthenticationEntryPoint {
private static final Logger logger = LoggerFactory.getLogger(JwtAuthEntryPoint.class);
#Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException e)
throws IOException, ServletException {
logger.error("Unauthorized error. Message - {}", e.getMessage());
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Error -> Unauthorized");
}
}
Following is the class I created for the constraints
public class SecurityConstraints {
public static final String SECRET = "********";//add any secret you want
public static final long EXPIRATION_TIME = 864_000_000L;
}
Seem like you are working with REST API, you can use JWT and Custom Filter similar to this (https://medium.com/#hantsy/protect-rest-apis-with-spring-security-and-jwt-5fbc90305cc5)
I am sending one authentication token in header of one URL. I need to
take this authentication token from URL and decode. If username and
password is matching...
Usually, the goal of using tokens for authentication is to get rid of username and password check.
Basic HTTP authentication that is supported by Spring Security out of the box assumes passing base64 encoded username and password in the HTTP header: e.g. Authorization: Basic QWxhZGRpbjpPcGVuU2VzYW1l (base64 encoded Aladdin:OpenSesame).
#Configuration
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/public").permitAll()
.anyRequest().authenticated()
.and()
.httpBasic();
}
}
If you still need to extract username and password from a token in a different way, consider the following example.
Considering you have the following REST controller:
#RestController
public class TestRestController {
#GetMapping("/api/getStudent/v1")
public String helloWorld() {
return "Hello, World!";
}
#GetMapping("/info")
public String test() {
return "Test";
}
}
In order to make endpoint /api/getStudent/v1 protected and /info public, and extract principal and credentials from the HTTP request header you need to implement custom AbstractAuthenticationProcessingFilter:
public class HeaderUsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public HeaderUsernamePasswordAuthenticationFilter(RequestMatcher requiresAuthenticationRequestMatcher) {
super(requiresAuthenticationRequestMatcher);
setAuthenticationSuccessHandler((request, response, authentication) -> {
});
setAuthenticationFailureHandler((request, response, exception) ->
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, exception.getMessage()));
}
#Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
String token = request.getHeader("token");
String username = token; //get username from token
String password = token; //get password from token
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(username, password);
return getAuthenticationManager().authenticate(authenticationToken);
}
#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);
}
}
This filter must extract principal and credentials from the token passed in header and attempt an authentication with Spring Security.
Next, you have to create an instance of this custom filter and configure Spring Security to add the filter in the security filter chain (.addFilterBefore(authenticationFilter(), UsernamePasswordAuthenticationFilter.class)):
#Configuration
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Bean
public HeaderUsernamePasswordAuthenticationFilter authenticationFilter() throws Exception {
HeaderUsernamePasswordAuthenticationFilter authenticationFilter =
new HeaderUsernamePasswordAuthenticationFilter(new AntPathRequestMatcher("/api/**"));
authenticationFilter.setAuthenticationManager(authenticationManagerBean());
return authenticationFilter;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.csrf().disable()
.addFilterBefore(
authenticationFilter(),
UsernamePasswordAuthenticationFilter.class);
}
//...
}
It is important to make the filter aware of the Spring Security authenticationManagerBean: authenticationFilter.setAuthenticationManager(authenticationManagerBean());.
You can configure what endpoints to protect with aunthentication by passing a RequestMatcher: e.g. new AntPathRequestMatcher("/api/**").
For testing, you can create in-memory UserDetailsService and test user with username test, password test and authority admin:
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
//...
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("test")
.password(passwordEncoder().encode("test"))
.authorities("admin");
}
}
Run the application and try to access the public endpoint without an authentication:
curl -i http://localhost:8080/info
HTTP/1.1 200
Test
the protected endpoint without an authentication:
curl -i http://localhost:8080/api/getStudent/v1
HTTP/1.1 401
the protected endpoint without an invalid token:
curl -i http://localhost:8080/api/getStudent/v1 -H 'token: not_valid'
HTTP/1.1 401
and finally the protected endpoint with a valid token:
curl -i http://localhost:8080/api/getStudent/v1 -H 'token: test'
HTTP/1.1 200
Hello, World!

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.

Spring security manually authentication not working

i'm changing an existing app with spring boot, this app not use spring security for authentication, the authentication is a method in a controller, so i want use spring security and i'm trying to use manually authentication in spring security but not working, below you can see the code:
Controller:
#Autowired
#Qualifier(BeanIds.AUTHENTICATION_MANAGER)
private AuthenticationManager authenticationManager;
#PostMapping(value = "/authenticate")
public ResponseEntity<UsuarioRequest> login(#RequestBody UsuarioRequest request, HttpServletRequest servletRequest)
throws AppException {
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(request.getUsulog(), request.getUsupass());
Authentication authentication = authenticationManager
.authenticate(authToken);
SecurityContext context = SecurityContextHolder.getContext();
context.setAuthentication(authentication);
UsuarioRequest usuario = usuarioFacadeAPI.findByUsername(request.getUsulog());
return new ResponseEntity<UsuarioRequest>(usuario, HttpStatus.OK);
}
Security Config:
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private SiscoAuthenticationProvider siscoAuthenticationProvider;
#Autowired
public SecurityConfig(SiscoAuthenticationProvider siscoAuthenticationProvider) {
super();
this.siscoAuthenticationProvider = siscoAuthenticationProvider;
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(siscoAuthenticationProvider);
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin().disable();
http.csrf().disable();
http.authenticationProvider(siscoAuthenticationProvider).authorizeRequests()
.antMatchers("/login/api/**", "/zona/api/**", "/rol/api/**").permitAll()
.anyRequest().authenticated();
}
#Bean(name = BeanIds.AUTHENTICATION_MANAGER)
#Override
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
}
CustomAuthenticationProvider:
#Component
public class SiscoAuthenticationProvider implements AuthenticationProvider{
private static final String ROLE = "ROLE_";
#Autowired
private UsuarioServiceAPI usuarioServiceAPI;
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
UsernamePasswordAuthenticationToken token = null;
try {
UsuarioRequest request = usuarioServiceAPI.authenticate(authentication.getPrincipal().toString(), authentication.getCredentials().toString());
List<RolRequest> rols = request.getRoles();
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
for (RolRequest rol : rols) {
authorities.add(new SimpleGrantedAuthority(ROLE+rol.getRolnom()));
}
token = new UsernamePasswordAuthenticationToken(authentication.getPrincipal(), authentication.getCredentials(), authorities);
} catch (AppException e) {
String message = BundleLoader.getMessage(e.getDetails().getBundle(), e.getDetails().getKey(),
LocaleContextHolder.getLocale());
throw new UsernameNotFoundException(message, e);
}
return token;
}
#Override
public boolean supports(Class<?> authentication) {
return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
}
}
For the permitAll config no problem occurred, but any other request returns 403 error code even after authentication is success, i suspect that in the controller the SecurityContextHolder not update the authentication, by this the user is always anonymous.
i found a solution for the problem, i changed the Spring Security Config class, specifically the method configure(HttpSecurity http) code below:
#Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin().disable();
http.csrf().disable();
http.authenticationProvider(siscoAuthenticationProvider).authorizeRequests()
.antMatchers("/login/api/**", "/zona/api/**", "/rol/api/**").not().authenticated()
.anyRequest().not().anonymous();
}
the prev config was have problems, with permitAll method and with authenticated method for anyRequest, changing this config for not().authenticated() and not().anonymous() in that order, i get the expected result.

Resources