I'm using Spring #RolesAllowed to secure my APIs (methods), but I'd like to change what happens when a method is called from an unauthorized user. The current behavior is that Spring throws an HTTP 403 error. This is great, but I would just like to add an additional error code in the body of the 403 response to be able to distinguish between access denied errors in different scenarios.
I'm having a hard time figuring out where the implementation of the #RolesAllowed annotation is located. Has anyone come across it? Or attempted to modify its behavior?
The methods in my controller currently look like the following:
#RolesAllowed({"ROLE_DEFENDANT", "ROLE_ADMIN"})
#RequestMapping(method = RequestMethod.POST, value = "/{caseId}/owner")
public ResponseEntity<?> assignOwner(#PathVariable String caseId) {
// method implementation
}
Another way to do this is with an exception handler class and the #ExceptionHandler annotation.
#ControllerAdvice
public class GlobalExceptionHandler {
#ExceptionHandler(AccessDeniedException.class)
public ResponseEntity<?> handleAccessDenied(HttpServletRequest request, AccessDeniedException ex) {
// exception handling logic
if (request.getUserPrincipal() == null) {
// user is not logged in
} else {
// user is logged in but doesn't have permission to the requested resource
}
// return whatever response you'd like
return new ResponseEntity<>(HttpStatus.UNAUTHORIZED);
}
}
What you are trying to do, can be done without having to modify the annotation.
In your Spring config, you can specify an AccessDeniedHandler bean which will be called when Spring Security determines that your user is not allowed to perform the action that they've tried to perform.
The access denied handler is really simple:
public class CustomDefaultAccessDeniedHandler implements AccessDeniedHandler {
#Override
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException, ServletException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
}
}
An example of an AuthenticationProvider that gives you a bit more information about what failed would be:
public class CustomAuthenticationProvider implements AuthenticationProvider {
#Autowired
private UserService userService;
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
UsernamePasswordAuthenticationToken auth = (UsernamePasswordAuthenticationToken) authentication;
String username = String.valueOf(auth.getPrincipal().toString());
String password = String.valueOf(auth.getCredentials());
if(username.isEmpty() || password.isEmpty()){
throw new UsernameNotFoundException("You pudding, there is no username or password");
} else {
SystemUser user = userService.findByUsername(username);
if(user == null){
throw new UsernameNotFoundException("No user exists, stop hacking");
}
//Do more stuff here to actually apply roles to the AuthToken etc
return new UsernamePasswordAuthenticationToken(username, null, authorities);
}
}
}
Related
I'm working with RedisHttpSession and my basic goal is to save the staff object in the session object on successful login, retrieve it wherever I need and destroy the session on logout.
On successful login, this is what I'm doing:
Staff staff = staffService.getEmailInstance(body.getEmailId());
request.getSession(true).setAttribute("staff", staff);
And Logout is simply this:
request.getSession().invalidate();
In a different controller, I am calling this utility method that checks if the staff is logged in: util.isStaffLoggedIn(request, response, StaffRole.EDITOR); If the staff is logged in, the API proceeds, else the user is redirected to the login page.
#Service
public class Util {
public boolean isStaffLoggedIn(HttpServletRequest request, HttpServletResponse response, StaffRole staffRole)
throws PaperTrueInvalidCredentialsException, PaperTrueJavaException {
Staff staff = (Staff) request.getSession().getAttribute("staff");
if (!isObjectNull(staff) && staff.getStaffRole().equals(staffRole)) {
return true;
}
invalidateSessionAndRedirect(request, response);
return false;
}
public void invalidateSessionAndRedirect(HttpServletRequest request, HttpServletResponse response)
throws PaperTrueJavaException, PaperTrueInvalidCredentialsException {
request.getSession().invalidate();
try {
response.sendRedirect(ProjectConfigurations.configMap.get("staff_logout_path"));
} catch (IOException e) {
throw new PaperTrueJavaException(e.getMessage());
}
throw new PaperTrueInvalidCredentialsException("Staff not loggedIn");
}
}
Now while the app is running, the get-jobs API is called immidiately after successful login. Most of the times the request.getSession().getAttribute("staff") method works fine and returns the 'staff' object but, once in a while, it returns null. This doesn't happen often, but it does. I printed the session Id to see if they are different after logout, and they were. After each logout I had a new session Id. I even checked if the staff object I retrieved from the database was null, but it wasn't.
The staff object was successfully saved in the sessions but I wasn't able to retrieve it in othe APIs. This is how my session config looks:
#EnableRedisHttpSession(maxInactiveIntervalInSeconds = 10800)
public class SessionConfig {
HashMap<String, String> configMap = ProjectConfigurations.configMap;
#Bean
public LettuceConnectionFactory connectionFactory() {
int redisPort = Integer.parseInt(configMap.get("redis_port"));
RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration(
configMap.get("redis_host"), redisPort);
redisStandaloneConfiguration.setPassword(configMap.get("redis_password"));
return new LettuceConnectionFactory(redisStandaloneConfiguration);
}
#Bean
public CookieSerializer cookieSerializer() {
DefaultCookieSerializer serializer = new DefaultCookieSerializer();
serializer.setCookieName("PTSESSIONID");
serializer.setSameSite("none");
serializer.setUseSecureCookie(!configMap.get("staff_logout_path").contains("localhost"));
return serializer;
}
}
Please let me know if I missed out anything. Thanks in advance.
Update 1
I'm not invalidating the session anymore and I've replaced request.getSession(true).setAttribute("staff", staff); to request.getSession().setAttribute("staff", staff);
I'm setting the 'staff' in StaffController and getting it in EditorController. Here's how I'm setting it:
#RestController
#RequestMapping(path = { "/staff" }, produces = "application/json")
public class StaffApiController {
private final HttpServletRequest request;
private final HttpSession httpSession;
#Autowired
private StaffService staffService;
#Autowired
StaffApiController(HttpServletRequest request, HttpServletResponse response, HttpSession session) {
this.request = request;
this.httpSession = session;
}
#PostMapping("/login")
public ResponseEntity<StaffLoginResponse> login(#Valid #RequestBody StaffLoginBody body) {
StaffLoginResponse staffLoginResponse = new StaffLoginResponse();
try {
if (!staffService.isValidLogin(body.getEmailId(), body.getPassword())) {
throw new PaperTrueInvalidCredentialsException("Invalid Credentials");
}
Staff staff = staffService.getEmailInstance(body.getEmailId());
httpSession.setAttribute("staff", staff);
staffLoginResponse.setEmail(staff.getEmail()).setRole(staff.getStaffRole().getValue())
.setStaffID(staff.getId()).setStatus(new Status("Staff Login Successful"));
} catch (PaperTrueException e) {
httpSession.removeAttribute("staff");
staffLoginResponse.setStatus(new Status(e.getCode(), e.getMessage()));
}
return ResponseEntity.ok(staffLoginResponse);
}
#PostMapping("/logout")
public ResponseEntity<Status> logout() {
httpSession.removeAttribute("staff");
return ResponseEntity.ok(new Status("Staff Logged Out Successfully"));
}
}
If you are using Spring Security, you can create a custom "/login" endpoint that authenticates the user by setting the SecurityContext.
You can use the default logout behaviour provided by Spring Security.
If you do not need to supply the credentials in the body, you can use the default login behaviour provided by Spring Security and omit this Controller altogether.
This is intended as a starting point.
It does not offer comprehensive security, for example it may be vulnerable session fixation attacks.
#RestController
public class LoginController {
private AuthenticationManager authenticationManager;
public LoginController(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
#PostMapping("/login")
public void login(#RequestBody StaffLoginBody body, HttpServletRequest request) {
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(body.getUsername(), body.getPassword());
Authentication auth = authenticationManager.authenticate(token);
SecurityContextHolder.getContext().setAuthentication(auth);
HttpSession session = request.getSession();
session.setAttribute("staff", "staff_value");
}
#GetMapping("/jobs")
public String getStaffJobs(HttpServletRequest request) {
return request.getSession().getAttribute("staff").toString();
}
}
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// expose AuthenticationManager bean to be used in Controller
#Override
#Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests(authorize -> authorize
.antMatchers("/login").permitAll()
.anyRequest().authenticated()
)
// use built in logout
.logout(logout -> logout
.deleteCookies("PTSESSIONID")
);
}
}
You will need to add the Spring Security dependency to use this code org.springframework.boot:spring-boot-starter-security.
I am trying a develop a spring boot rest API with JWT authorization using spring security. I want all of my request to go through the filter to validate the JWT token except for the /authenticate request which should generate the jwt token. But with the below code, the /authenticate request is also getting intercepted by the filter due to which its failing with 401. Please let me know what am I missing in the below code.
JwtTokenFilter class
#Component
public class JwtTokenFilter extends OncePerRequestFilter
{
#Autowired
private UserService jwtUserDetailsService;
#Autowired
private JwtTokenUtil jwtTokenUtil;
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException
{
final String requestTokenHeader = request.getHeader("Authorization");
String username = null;
String jwtToken = null;
// JWT Token is in the form "Bearer token". Remove Bearer word and get
// only the Token
if (requestTokenHeader != null && requestTokenHeader.startsWith("Bearer "))
{
jwtToken = requestTokenHeader.substring(7);
try
{
username = jwtTokenUtil.getUsernameFromToken(jwtToken);
}
catch (IllegalArgumentException e)
{
System.out.println("Unable to get JWT Token");
}
catch (ExpiredJwtException e)
{
System.out.println("JWT Token has expired");
}
}
else
{
logger.warn("JWT Token does not begin with Bearer String");
}
// Once we get the token validate it.
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null)
{
UserDetails userDetails = this.jwtUserDetailsService.loadUserByUsername(username);
// if token is valid configure Spring Security to manually set
// authentication
if (jwtTokenUtil.validateToken(jwtToken, userDetails))
{
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
usernamePasswordAuthenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
// After setting the Authentication in the context, we specify
// that the current user is authenticated. So it passes the
// Spring Security Configurations successfully.
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
}
}
chain.doFilter(request, response);
}
}
JwtConfig class
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class JwtConfigurer extends WebSecurityConfigurerAdapter
{
#Autowired
private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
#Autowired
private UserService jwtUserDetailsService;
#Autowired
private JwtTokenFilter jwtRequestFilter;
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception
{
// configure AuthenticationManager so that it knows from where to load
// user for matching credentials
// Use BCryptPasswordEncoder
auth.userDetailsService(jwtUserDetailsService).passwordEncoder(passwordEncoder());
}
#Bean
public PasswordEncoder passwordEncoder()
{
return new BCryptPasswordEncoder();
}
#Bean
#Override
public AuthenticationManager authenticationManagerBean() throws Exception
{
return super.authenticationManagerBean();
}
#Override
protected void configure(HttpSecurity httpSecurity) throws Exception
{
// We don't need CSRF for this example
httpSecurity.csrf().disable().
// dont authenticate this particular request
authorizeRequests().antMatchers("/authenticate").permitAll().
// all other requests need to be authenticated
anyRequest().authenticated().and().
// make sure we use stateless session; session won't be used to
// store user's state.
exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint).and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
// Add a filter to validate the tokens with every request
httpSecurity.addFilterAfter(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
}
}
Controller class
#RestController
#CrossOrigin
public class JwtAuthenticationController
{
#Autowired
private AuthenticationManager authenticationManager;
#Autowired
private JwtTokenUtil jwtTokenUtil;
#Autowired
private UserService userDetailsService;
#RequestMapping(value = "/authenticate", method = RequestMethod.POST)
public ResponseEntity<?> createAuthenticationToken(#RequestBody User authenticationRequest) throws Exception
{
authenticate(authenticationRequest.getUsername(), authenticationRequest.getPassword());
final UserDetails userDetails = userDetailsService.loadUserByUsername(authenticationRequest.getUsername());
final String token = jwtTokenUtil.generateToken(userDetails);
User u = new User();
u.setUsername(authenticationRequest.getUsername());
u.setToken(token);
return ResponseEntity.ok(u);
}
private void authenticate(String username, String password) throws Exception
{
try
{
authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
}
catch (DisabledException e)
{
throw new Exception("USER_DISABLED", e);
}
catch (BadCredentialsException e)
{
throw new Exception("INVALID_CREDENTIALS", e);
}
}
}
I struggled with this for two days and the best solution was the Tom answer combined with this setup on my SecurityConfig:
override fun configure(http: HttpSecurity?) {
// Disable CORS
http!!.cors().disable()
// Disable CSRF
http.csrf().disable()
// Set session management to stateless
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
//Add JwtTokenFilter
http.addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter::class.java)
}
Basically, OncePerRequestFilter works in that way only. Not sure if this can be avoided. Quoting the documentation :
Filter base class that aims to guarantee a single execution per
request dispatch, on any servlet container.
You can try adding the method type as well to skip teh authentication on the endpoint.
.antMatchers(HttpMethod.GET, "/authenticate").permitAll()
As already pointed by Mohit, even i couldn't see any mistakes in your configuration.
If you understand below explanation, it will help you to resolve.
Even though /authenticate request is permitAll configured the request should pass through your JWT Filter. But FilterSecurityInterceptor is the last filter it will check for configured antMatchers and associated restrictions/permissions based on that it will decide whether request should be permitted or denied.
For /authenticate method it should pass through filter and requestTokenHeader, username should be null and make sure chain.doFilter(request, response); is reaching without any exceptions.
And when it reaches FilterSecurityInterceptor and If you have set log level to debug) logs similar as given below should be printed.
DEBUG - /app/admin/app-config at position 12 of 12 in additional filter chain; firing Filter: 'FilterSecurityInterceptor'
DEBUG - Checking match of request : '/app/admin/app-config'; against '/resources/**'
DEBUG - Checking match of request : '/app/admin/app-config'; against '/'
DEBUG - Checking match of request : '/app/admin/app-config'; against '/login'
DEBUG - Checking match of request : '/app/admin/app-config'; against '/api/**'
DEBUG - Checking match of request : '/app/admin/app-config'; against '/app/admin/app-config'
DEBUG - Secure object: FilterInvocation: URL: /app/admin/app-config; Attributes: [permitAll]
DEBUG - Previously Authenticated: org.springframework.security.authentication.AnonymousAuthenticationToken#511cd205: Principal: anonymousUser; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails#2cd90: RemoteIpAddress: 0:0:0:0:0:0:0:1; SessionId: 696171A944493ACA1A0F7D560D93D42B; Granted Authorities: ROLE_ANONYMOUS
DEBUG - Voter: org.springframework.security.web.access.expression.WebExpressionVoter#6df827bf, returned: 1
DEBUG - Authorization successful
Attach those logs, so that then problem can be predicted.
Write a configuration class that implements org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter and override the configur method like so:
#Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
// dont authenticate this particular request. you can use a wild card here. e.g /unprotected/**
httpSecurity.csrf().disable().authorizeRequests().antMatchers("/authenticate").permitAll().
//authenticate everything else
anyRequest().authenticated().and().exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint).and().sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
// Add a filter to validate the tokens with every request
httpSecurity.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
}
I had similar problem, and I overcome it by comparing request path to the path I do not want to filter.
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//To skip OncePerRequestFilter for authenticate endpoint
if(request.getServletPath().equals("/authenticate")){
filterChain.doFilter(request, response);
return;
}
// filter logic continue..
I'm trying to implement a custom authentication logic with latest version of Spring Boot, Web and Security, but I'm struggling with some issues. I was trying out many solutions in similar questions/tutorials without success or understanding what actually happens.
I'm creating a REST application with stateless authentication, i.e. there is a REST endpoint (/web/auth/login) that expects username and password and returns a string token, which is then used in all the other REST endpoints (/api/**) to identify the user. I need to implement a custom solution as authentication will become more complex in the future and I would like to understand the basics of Spring Security.
To achieve the token authentication, I'm creating a customized filter and provider:
The filter:
public class TokenAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public TokenAuthenticationFilter() {
super(new AntPathRequestMatcher("/api/**", "GET"));
}
#Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
String token = request.getParameter("token");
if (token == null || token.length() == 0) {
throw new BadCredentialsException("Missing token");
}
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(token, null);
return getAuthenticationManager().authenticate(authenticationToken);
}
}
The provider:
#Component
public class TokenAuthenticationProvider implements AuthenticationProvider {
#Autowired
private AuthenticationTokenManager tokenManager;
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String token = (String)authentication.getPrincipal();
return tokenManager.getAuthenticationByToken(token);
}
#Override
public boolean supports(Class<?> authentication) {
return UsernamePasswordAuthenticationToken.class.equals(authentication);
}
}
The config:
#EnableWebSecurity
#Order(1)
public class TokenAuthenticationSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private TokenAuthenticationProvider authProvider;
#Override
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher("/api/**")
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and().addFilterBefore(authenticationFilter(), BasicAuthenticationFilter.class);
}
#Bean
public TokenAuthenticationFilter authenticationFilter() throws Exception {
TokenAuthenticationFilter tokenProcessingFilter = new TokenAuthenticationFilter();
tokenProcessingFilter.setAuthenticationManager(authenticationManager());
return tokenProcessingFilter;
}
#Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authProvider);
}
}
The AuthenticationTokenManager used in the provider (and also in the login process):
#Component
public class AuthenticationTokenManager {
private Map<String, AuthenticationToken> tokens;
public AuthenticationTokenManager() {
tokens = new HashMap<>();
}
private String generateToken(AuthenticationToken authentication) {
return UUID.randomUUID().toString();
}
public String addAuthentication(AuthenticationToken authentication) {
String token = generateToken(authentication);
tokens.put(token, authentication);
return token;
}
public AuthenticationToken getAuthenticationByToken(String token) {
return tokens.get(token);
}
}
What happens:
I'm appending a valid token in the request to "/api/bla" (which is a REST controller returning some Json). The filter and provider both get invoked. The problem is, the browser is redirected to "/" instead of invoking the REST controller's requested method. This seems to happen in SavedRequestAwareAuthenticationSuccessHandler, but why is this handler being used?
I tried
to implement an empty success handler, resulting in a 200 status code and still not invoking the controller
to do authentication in a simple GenericFilterBean and setting the authentication object via SecurityContextHolder.getContext().setAuthentication(authentication) which results in a "Bad credentials" error page.
I would like to understand why my controller is not being called after I authenticated the token. Besides that, is there a "Spring" way to store the token instead of storing it in a Map, like a custom implementation of SecurityContextRepository?
I really appreciate any hint!
Might be a little late but I was having the same problem and adding:
#Override
protected void successfulAuthentication(
final HttpServletRequest request, final HttpServletResponse response,
final FilterChain chain, final Authentication authResult)
throws IOException, ServletException {
chain.doFilter(request, response);
}
to my AbstractAuthenticationProcessingFilter implementation did the trick.
Use setContinueChainBeforeSuccessfulAuthentication(true) in constructor
I am using Spring Security with SpringMVC to create a web application (I will refer to this as the WebApp for clarity) that speaks to an existing application (I will refer to this as BackendApp).
I want to delegate authentication responsibilities to the BackendApp (so that I don't need to synchronise the two applications).
To implement this, I would like the WebApp (running spring security) to communicate to the BackendApp via REST with the username and password provided by the user in a form and authenticate based on whether the BackendApp's response is 200 OK or 401 Unauthorised.
I understand I will need to write a custom Authentication Manager to do this however I am very new to spring and can't find any information on how to implement it.
I believe I will need to do something like this:
public class CustomAuthenticationManager implements AuthenticationManager{
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = authentication.getName();
String pw = authentication.getCredentials().toString();
// Code to make rest call here and check for OK or Unauthorised.
// What do I return?
}
}
Do I set authentication.setAuthenticated(true) if successful and false if otherwise and thats it?
Once this is written, how do I configure spring security to use this authentication manager using a java configuration file?
Thanks in advance for any assistance.
Take a look at my sample below. You have to return an UsernamePasswordAuthenticationToken. It contains the principal and the GrantedAuthorities. Hope I could help :)
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = authentication.getPrincipal() + "";
String password = authentication.getCredentials() + "";
User user = userRepo.findOne(username);
if (user == null) {
throw new BadCredentialsException("1000");
}
if (!encoder.matches(password, user.getPassword())) {
throw new BadCredentialsException("1000");
}
if (user.isDisabled()) {
throw new DisabledException("1001");
}
List<Right> userRights = rightRepo.getUserRights(username);
return new UsernamePasswordAuthenticationToken(username, null, userRights.stream().map(x -> new SimpleGrantedAuthority(x.getName())).collect(Collectors.toList()));
}
PS: userRepo and rightRepo are Spring-Data-JPA Repositories which access my custom User-DB
SpringSecurity JavaConfig:
#Configuration
#EnableWebMvcSecurity
public class MySecurityConfiguration extends WebSecurityConfigurerAdapter {
public MySecurityConfiguration() {
super(false);
}
#Override
protected AuthenticationManager authenticationManager() throws Exception {
return new ProviderManager(Arrays.asList((AuthenticationProvider) new AuthProvider()));
}
}
In its most simplest:
#Override
public Authentication authenticate(Authentication auth) throws AuthenticationException {
String username = auth.getName();
String password = auth.getCredentials().toString();
// to add more logic
List<GrantedAuthority> grantedAuths = new ArrayList<>();
grantedAuths.add(new SimpleGrantedAuthority("ROLE_USER"));
return new UsernamePasswordAuthenticationToken(username, password, grantedAuths);
}
My solution is almost the same as the first answer:
1) You need a class which implements the Authentication Provider
#Service
#Configurable
public class CustomAuthenticationProvider implements AuthenticationProvider {
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
// Your code of custom Authentication
}
}
2) Opposite to the first answer you don't need to have following code in your WebSecurityConfiguration if you have only this custom provider.
#Override
protected AuthenticationManager authenticationManager() throws Exception {
return new ProviderManager(Arrays.asList((AuthenticationProvider) new AuthProvider()));
}
The issue is that Spring looks for available providers and use the default if nothing else is found. But as you have the implementation of the AuthenticationProvider - your implementation will be used.
First you must configure Spring security to use your custom AuthenticationProvider.
So, in your spring-security.xml (or equivalent config file) you must define wich class is implementing this feature. For example:
<authentication-manager alias="authenticationManager">
<authentication-provider ref="myAuthenticationProvider" />
</authentication-manager>
<!-- Bean implementing AuthenticationProvider of Spring Security -->
<beans:bean id="myAuthenticationProvider" class="com.teimas.MyAutenticationProvider">
</beans:bean>
Secondly you must implement AuthenticationProvider as in your example. Specially the method authenticate(Authentication authentication) in which your rest call must be. For example:
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
User user = null;
try {
//use a rest service to find the user.
//Spring security provides user login name in authentication.getPrincipal()
user = userRestService.loadUserByUsername(authentication.getPrincipal().toString());
} catch (Exception e) {
log.error("Error loading user, not found: " + e.getMessage(), e);
}
if (user == null) {
throw new UsernameNotFoundException(String.format("Invalid credentials", authentication.getPrincipal()));
} else if (!user.isEnabled()) {
throw new UsernameNotFoundException(String.format("Not found enabled user for username ", user.getUsername()));
}
//check user password stored in authentication.getCredentials() against stored password hash
if (StringUtils.isBlank(authentication.getCredentials().toString())
|| !passwordEncoder.isPasswordValid(user.getPasswordHash(), authentication.getCredentials().toString()) {
throw new BadCredentialsException("Invalid credentials");
}
//doLogin makes whatever is necesary when login is made (put info in session, load other data etc..)
return doLogin(user);
}
This is how I did using component-based configuration (SecurityFilterChain) and new authorizeHttpRequests
#Bean
protected SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.authorizeHttpRequests(auth -> auth
.antMatchers(UNPROTECTED_URLS).permitAll()
.oauth2ResourceServer()
.accessDeniedHandler(restAccessDeniedHandler)
.authenticationEntryPoint(authenticationEntryPoint)
.jwt()
.authenticationManager(new ProviderManager(authenticationProvider)); // this is custom authenticationProvider
return httpSecurity.build();
}
I'm working with Spring Security 2.0.7. It had been implemented the UserDetailsService with a preAuthenticatedUserDetailsService bean.
It's working fine. Now I want to add a new custom error messages.
In the method loadUserByUsername I want to add some custom bussines logic.
For ex. based on some attribute, I don't want the user to log in so I throw a UsernameNotFoundException with a custom message.
Spring is the one who handdle the exception and set it to the session, but when I retrive the exception from the session with "SPRING_SECURITY_LAST_EXCEPTION" key I get a "Bad credentials" message.
At the moment is fixed with a nasty workarround, mostly I want to understand what happend!
Ideas?
Ps. I read a lot of this issue here in SO but mostly all with Spring security 3.0
Just to close it. As #M. Deinum mentioned in the comments. Spring handle this way for security purposes. The implementation can be seen in the ExceptionTranslationFilter class.
That problem was nagging me for a while before finding an efficient & easy solution.
The issue was resolved by using a custom AuthenticationFailureHandler (that is added to HttpSecurity configuration) to get the custom error message from UserDetailsService before sending it back to login page as error parameter
The custom AuthenticationFailureHandler is defined like following:
#Component("CustomAuthenticationFailureHandler")
public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler {
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
#Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
String encodedErrorMessage = null;
// Get AuthenticationException that was thrown in UserDetailsService, retrieve the error message and attach it as encoded error parameter of the error login page
if (exception != null) {
String errorMessage = exception.getMessage();
encodedErrorMessage = Base64.getUrlEncoder().encodeToString(errorMessage.getBytes());
}
redirectStrategy.sendRedirect(request, response, "/login?error=" + encodedErrorMessage);
}
}
Then the CustomAuthenticationFailureHandler is configured in WebSecurityConfigurerAdapter (also the configureGlobal is updated) as following:
#Configuration
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private UserDetailsService userDetailsService;
#Autowired
CustomAuthenticationFailureHandler customAuthenticationFailureHandler;
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().authenticated()
.and().formLogin().loginPage("/login").permitAll()
.failureHandler(customAuthenticationFailureHandler) // Add failure handler class
// ...
// ...
}
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authProvider());
}
#Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
public AuthenticationProvider authProvider() {
DaoAuthenticationProvider impl = new DaoAuthenticationProvider();
impl.setUserDetailsService(userDetailsService);
impl.setPasswordEncoder(bCryptPasswordEncoder());
// setHideUserNotFoundExceptions is set to false in order to get the exceptions CustomAuthenticationFailureHandler
impl.setHideUserNotFoundExceptions(false);
return impl;
}
}
The exceptions related to user account (UsernameNotFoundException) are always thrown in UserDetailsService interface:
#Service
public class UserDetailsServiceImpl implements UserDetailsService{
private UserDetails getUserByUsername(String username) {
UserDTO userDTO = userService.findByUsername(username);
if (userDTO == null) {
// Custom error message when no account was found by the given username
throw new UsernameNotFoundException("No user account was found by the username [" + username + "]");
}
Date expirationDate = userDTO.getExpirationDate();
if(expirationDate != null) {
if(expirationDate.before(new Date())) {
// Custom error message when the account is expired
throw new UsernameNotFoundException("The user account [" + username + "] is expired");
}
}
// Can add more UsernameNotFoundException with custom messages based on functional requirements
// ...
List<GrantedAuthority> grantedAuthorities = getGrantedAutorities(userDTO);
return new org.springframework.security.core.userdetails.User(userDTO.getUsername(), userDTO.getPassword(), grantedAuthorities);
}
}
The exceptions are handled in CustomAuthenticationFailureHandler in order to send the custom error messages (encoded using Base64) like following :
#Component("CustomAuthenticationFailureHandler")
public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler {
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
#Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
String encodedErrorMessage = null;
// Get AuthenticationException that was thrown in UserDetailsService, retrieve the error message and attach it as encoded error parameter of the error login page
if (exception != null) {
String errorMessage = exception.getMessage();
encodedErrorMessage = Base64.getUrlEncoder().encodeToString(errorMessage.getBytes());
}
redirectStrategy.sendRedirect(request, response, "/login?error=" + encodedErrorMessage);
}
}
Then the error message is retrieved in the Controller like following:
#GetMapping(value = {"/login", "/"})
public String login(Model model, #RequestParam(name="error", required = false) String error) {
if (error != null) {
byte[] decodedBytes = Base64.getDecoder().decode(error);
String decodedString = new String(decodedBytes);
model.addAttribute("error", decodedString);
}
return "login";
}