I want to rewrite my Vaadin application to Vaadin 21.
With the Vaadin starter builder (https://vaadin.com/start) I created a simple app.
Currently my main struggle is to apply my simple CustomAuthenticationProvider to the Security manager to able to use the #RolesAllowed({ "user", "admin","USER"}) annotation.
Main problem that my AuthToken is generated somewhere else...
Its generate somewhere an empty Granted Authrities and ignore my custom AuthProvider code.
Question:
How to nicely handle role based access control?
Where I can use this annotation correctly:
#RolesAllowed({ "user", "admin","USER"})
public class ProfileView extends VerticalLayout {
Console after login:
UsernamePasswordAuthenticationToken [Principal=c.farkas, Credentials=[PROTECTED], Authenticated=false, Details=WebAuthenticationDetails [RemoteIpAddress=0:0:0:0:0:0:0:1, SessionId=DDE103F559B2F64B917753636B800564], Granted Authorities=[]]
xxx[USERcica, admin, USER]
??UsernamePasswordAuthenticationToken [Principal=c.farkas, Credentials=[PROTECTED], Authenticated=true, Details=null, Granted Authorities=[USERcica, admin, USER]]
SecurityConfiguration.java
#EnableWebSecurity
#Configuration
public class SecurityConfiguration extends VaadinWebSecurityConfigurerAdapter {
#Autowired
private RequestUtil requestUtil;
#Autowired
private VaadinDefaultRequestCache vaadinDefaultRequestCache;
#Autowired
private ViewAccessChecker viewAccessChecker;
#Autowired
CustomAuthenticationProvider customAuthenticationProvider;
public static final String LOGOUT_URL = "/";
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
#Override
protected void configure(HttpSecurity http) throws Exception {
// super.configure(http);
http.csrf().ignoringRequestMatchers(requestUtil::isFrameworkInternalRequest);
// nor with endpoints
http.csrf().ignoringRequestMatchers(requestUtil::isEndpointRequest);
// Ensure automated requests to e.g. closing push channels, service
// workers,
// endpoints are not counted as valid targets to redirect user to on
// login
http.requestCache().requestCache(vaadinDefaultRequestCache);
ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry urlRegistry = http
.authorizeRequests();
// Vaadin internal requests must always be allowed to allow public Flow
// pages
// and/or login page implemented using Flow.
urlRegistry.requestMatchers(requestUtil::isFrameworkInternalRequest).permitAll();
// Public endpoints are OK to access
urlRegistry.requestMatchers(requestUtil::isAnonymousEndpoint).permitAll();
// Public routes are OK to access
urlRegistry.requestMatchers(requestUtil::isAnonymousRoute).permitAll();
urlRegistry.requestMatchers(getDefaultHttpSecurityPermitMatcher()).permitAll();
// all other requests require authentication
urlRegistry.anyRequest().authenticated();
// Enable view access control
viewAccessChecker.enable();
setLoginView(http, LoginView.class, LOGOUT_URL);
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// Custom authentication provider - Order 1
auth.authenticationProvider(customAuthenticationProvider);
// Built-in authentication provider - Order 2
/* auth.inMemoryAuthentication().withUser("admin").password("{noop}admin#password")
// {noop} makes sure that the password encoder doesn't do anything
.roles("ADMIN") // Role of the user
.and().withUser("user").password("{noop}user#password").credentialsExpired(true).accountExpired(true)
.accountLocked(true).roles("USER");*/
}
#Override
public void configure(WebSecurity web) throws Exception {
super.configure(web);
web.ignoring().antMatchers("/images/*.png");
}
}
CustomAuthenticationProvider.java
#Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = authentication.getName();
String password = authentication.getCredentials().toString();
System.out.println(authentication);
try {
// LdapContext ldapContext =
ActiveDirectory.getConnection(username, password);
List<GrantedAuthority> authorityList = new ArrayList<GrantedAuthority>();
authorityList.add(new SimpleGrantedAuthority("USER" + "cica"));
authorityList.add(new SimpleGrantedAuthority("admin"));
authorityList.add(new SimpleGrantedAuthority("USER"));
System.out.println("xxx"+authorityList.toString());
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
username, password, authorityList);
System.out.println("??" + usernamePasswordAuthenticationToken);
String id = VaadinSession.getCurrent() != null ? VaadinSession.getCurrent().getSession().getId() : "";
return usernamePasswordAuthenticationToken;
} catch (NamingException e) {
// e.printStackTrace();
// throw new CortexException("Authentication failed");
throw new BadCredentialsException("Authentication failed");
}
}
#Override
public boolean supports(Class<?> aClass) {
return aClass.equals(UsernamePasswordAuthenticationToken.class);
}
}
You must add the ROLE_ prefix to tell Spring Security that the GrantedAuthority is of type role.
authorityList.add(new SimpleGrantedAuthority("ROLE_USER" + "cica"));
authorityList.add(new SimpleGrantedAuthority("ROLE_admin"));
authorityList.add(new SimpleGrantedAuthority("ROLE_USER"));
Related
I have an application which was working with (email/password) login process.
Now i have implemented LDAP auth.
A user enter in the authenticate process, if the database doesn't know him, it comes in the ldap process, and if it works, then we create his account in the database.
Next we receive the JWT token.
The problem is that if we retry to connect, new UsernamePasswordAuthenticationToken(email, password); is returning authenticated false. But credentials are OK, it should works...
I don't understand what is happening.. user is shown in the database with the password encrypted..
#Service
#RequiredArgsConstructor
public class OpenLdapAuthenticationProvider implements AuthenticationProvider {
#Value("${spring.ldap.enabled}")
private Boolean enableLDAP;
#Autowired
private LdapTemplate ldapTemplate;
private final UserService userService;
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String email = authentication.getName();
String password = authentication.getCredentials().toString();
// This method returns authenticated false !
Authentication authenticationWithDatabase = this.tryAuthWithDatabase(email, password);
if (authenticationWithDatabase.isAuthenticated() == false) {
if (enableLDAP == true) {
boolean authenticationWithLdap = this.tryAuthWithLdap(email, password);
if (authenticationWithLdap == true) {
UserDataDTO userDataDTO = this.retrieveUserInformationFromLDAP(email);
userDataDTO.setEmail(email);
userDataDTO.setPassword(password);
List<AppUserRole> userRoles = new ArrayList<>();
userRoles.add(AppUserRole.ROLE_DEV_VIEW);
userDataDTO.setAppUserRoles(userRoles);
boolean trySignup = userService.signup(userDataDTO);
if (trySignup == true) {
return this.tryAuthWithDatabase(email, password);
} else {
throw new InternalServerErrorException("Your account is not in database. Sign in with LDAP was OK, but registration failed. This should not happen.");
}
} else {
throw new UnAuthorizedErrorException(
"The connection has been refused (by the database and ldap). Check your credentials.");
}
} else {
throw new UnAuthorizedErrorException(
"The connection has been refused by the database and cannot be made by ldap because it has been disabled.");
}
} else {
return authenticationWithDatabase;
}
}
private Authentication tryAuthWithDatabase(String email, String password) {
return new UsernamePasswordAuthenticationToken(email, password);
}
private boolean tryAuthWithLdap(String email, String password) {
Filter filter = new EqualsFilter("mail", email); // mail = le champs dans l'arbre LDAP
return ldapTemplate.authenticate(LdapUtils.emptyLdapName(), filter.encode(), password);
}
public UserDataDTO retrieveUserInformationFromLDAP(String email) {
LdapQuery query = LdapQueryBuilder.query().where("objectClass").is("user").and("mail").is(email);
return ldapTemplate.search(query,
(AttributesMapper<UserDataDTO>) attributes ->
UserDataDTO.builder()
.name(attributes.get("givenName").get().toString())
.surname(attributes.get("sn").get().toString())
.username(attributes.get("displayName").get().toString())
.team(attributes.get("department").get().toString())
.build()).get(0);
}
#Override
public boolean supports(Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
}
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
#RequiredArgsConstructor
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private JwtTokenProvider jwtTokenProvider;
#Autowired
private OpenLdapAuthenticationProvider openLdapAuthenticationProvider;
public WebSecurityConfig(JwtTokenProvider jwtTokenProvider) {
this.jwtTokenProvider = jwtTokenProvider;
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(this.openLdapAuthenticationProvider);
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.addFilterBefore(corsFilter(), ChannelProcessingFilter.class);
http.csrf().disable();
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.authorizeRequests()
.antMatchers("/api/v3/authentificate").permitAll()
.antMatchers("/api/v3/signup").permitAll()
.anyRequest().authenticated();
http.apply(new JwtTokenFilterConfigurer(jwtTokenProvider));
}
#Override
public void configure(WebSecurity web) throws Exception {
// Allow swagger to be accessed without authentication
web.ignoring().antMatchers("/v3/api-docs/**")//
.antMatchers("/swagger-resources/**")//
.antMatchers("/swagger-ui/**")
.antMatchers("/swagger-ui.html");
}
#Override
#Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
#Configuration
public class GenericBeanConfig {
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12);
}
#Bean
public ModelMapper modelMapper() {
return new ModelMapper();
}
}
Thanks for any help, i really don't understand what's happening..
If you check UsernamePasswordAuthenticationToken.class, you will see 2 constructors:
/**
* This constructor can be safely used by any code that wishes to create a
* <code>UsernamePasswordAuthenticationToken</code>, as the {#link #isAuthenticated()}
* will return <code>false</code>.
*
*/
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
super(null);
this.principal = principal;
this.credentials = credentials;
setAuthenticated(false);
}
/**
* This constructor should only be used by <code>AuthenticationManager</code> or
* <code>AuthenticationProvider</code> implementations that are satisfied with
* producing a trusted (i.e. {#link #isAuthenticated()} = <code>true</code>)
* authentication token.
* #param principal
* #param credentials
* #param authorities
*/
public UsernamePasswordAuthenticationToken(Object principal, Object credentials,
Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
super.setAuthenticated(true); // must use super, as we override
}
The same can be seen in its documentation
Spring security's default implementation expects you to provide at least one granted authority to your user instance
That happens because Authentication provider receives an Authentication object not authenticated as input only username and password, by applying its logic (fetching from database, etc) if the result matches it returns an Authentication object username, password and its authorities if it doesnt match it returns that same input object
To solve your problem, you can try the following purely illustrative code:
// omitted code above
private Authentication tryAuthWithDatabase(String email, String password) {
// call your database or repository or service
// var fetchData receives the object from database
// perform token the matching against the fetch data
if( resultMatches ) {
return new UsernamePasswordAuthenticationToken(email, password, fetchData.getAuthorities() );
}
else {
return new UsernamePasswordAuthenticationToken( email, password );
}
}
// omitted code bellow
I hope i´ve helped. Let me know if you made it. Cheers!
The User login is working well but I want to add a Customer Module to the project. I know that I need to write a custom UserDetails class to get the customer Username but I want to ask if I need to write another Custom JWT filter for the Customer Login validation. Presently this is the Filter class that I have for User Login. I have added a username and password field to the Customer entity.
#Component
public class JwtRequestFilter extends OncePerRequestFilter {
#Autowired
private JwtTokenUtil jwtTokenUtil;
#Autowired
private UserAccountService myUserDetailsService;
#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;
if (requestTokenHeader != null) {
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");
}
}
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.myUserDetailsService.loadUserByUsername(username);
if (jwtTokenUtil.validateToken(jwtToken, userDetails)) {
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
String authorities = userDetails.getAuthorities().stream().map(GrantedAuthority::getAuthority)
.collect(Collectors.joining());
System.out.println("Authorities granted : " + authorities);
usernamePasswordAuthenticationToken
.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
}
else {
System.out.println("Not Valid Token");
}
}
chain.doFilter(request, response);
}
}
As you can see the Filter is using the custom UserDetails to verify the username . How do I add the Customer userdetails service to the filter ? This is my first multiple login project please be lenient with me.
Differentiate between user and customer while logging. Accordingly, call the different service to get user details. More can be found here.
Spring Security user authentication against customers and employee
How do I add the Customer userdetails service to the filter?: inject it as you did with UserAccountService. If you do this way, you're using 1 filter (and of course, this filter is in 1 SecurityFilterChain), you could basically implement your filter like: trying to validate your user by myUserDetailsService and if it's not successful, continue with myCustomerDetailsService.
For multiple login project. The second way you could do is using 2 SecurityFilterChain. UserJwtFilter for 1 SecurityFilterChain and CustomJwtFilter for 1 SecurityFilterChain for example. People usually do this way for different login mechanisms Basic, OAuth2, SAML2. E.g:
Basic Authentication:
#Configuration
#Order(2)
public class BasicAuthenticationFilterChain extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.requestMatchers()
.antMatchers("/login", "/logout")
.and()
OAuth2 Authentication:
#Configuration
#Order(3)
public class OAuth2AuthenticationFilterChain extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.requestMatchers()
.antMatchers("/oauth")
.and()
In this case when a request with "/login" it'll be directed to BasicAuthenticationFilterChain, and a request with "/oauth" will go to OAuth2AuthenticationFilterChain. About Order: the lower is the higher priority and once the request's processed with a SecurityFilterChain, it won't go to another SecurityFilterChain. You can implement your project this way.
Conclusion: There are a lot of ways you can implement your idea with spring security, it depends on your choice.
it looks to me like you already did.
#Autowired
private UserAccountService myUserDetailsService;
But I would suggest using a Constructor instead of #Autowired. Spring will fill in the constructor parameters just the same. This could be very slim when you use the lombok library as well.
Using a constructor also makes mocking this a bit easier for testing.
Updated as discussed in the comments:
#Log //another lombok thing
#RequiredArgsConstructor
#Component
public class JwtRequestFilter extends Filter{
private final JwtTokenUtil jwtTokenUtil;
private final UserAccountService myUserDetailsService;
private final CustomerAccountService myCustomerDetailsService;
private static final String AUTH_HEADER = "authorization";
#Override
protected void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws ServletException, IOException {
String tokenHeader = ((HttpServletRequest) request).getHeader(AUTH_HEADER);
if(hasValue(tokenHeader) && tokenHeader.toLowerCase().startsWith("bearer ")){
jwtToken = requestTokenHeader.substring(7);
String username;
String jwtToken;
try {
username = jwtTokenUtil.getUsernameFromToken(jwtToken);
if (uSecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = myUserDetailsService.loadUserByUsername(username);
if(isNull(userDetails)){
userDetails = myCustomerDetailsService.loadCustomerByUsername(username);
}
if (jwtTokenUtil.validateToken(jwtToken, userDetails)) {
var token = createSecurityToken(userDetails);
SecurityContextHolder.getContext().setAuthentication(token);
} else {
throw new RuntimeException("Not a Valid Token.");
}
} else {
log.info("Authorization already present");
}
} catch (IllegalArgumentException e) {
throw new("Unable to get JWT Token",e);
} catch (ExpiredJwtException e) {
throw new("JWT Token has expired",e);
}
} else {
throw new RuntimeException("No valid authorization header found.");
}
chain.doFilter(request, response);
}
private UsernamePasswordAuthenticationToken createSecurityToken(UserDetails userDetails){
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
log.info("Authorities granted : {}", userDetails.getAuthorities());
token.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
return token;
}
}
I'm trying to build a route, processed in Spring, to generate a token if the user/password submited in the body are valid. Only in that scenario it responds with the token.
The problem is that I'm Posting the correct username and password, exactly as stored in DB but I keep getting a "bad credentials" error.
This is the controller:
#RequestMapping(value = "/authenticate", method = RequestMethod.POST)
public ResponseEntity<?> createAuthenticationToken(#RequestBody JwtRequest authenticationRequest) throws Exception {
try {
LOGGER.info("Received a request to generate a token for user: "+authenticationRequest.getUsername()+"/"+authenticationRequest.getPassword());
final UserDetails userDetails = userDetailsService.loadUserByUsername(authenticationRequest.getUsername());
System.out.println(userDetails);
// Logs: org.springframework.security.core.userdetails.User#7ee29d27: Username: thisWasTheGoodUserName; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: Admin
// And also logs the hibernate query:
// select
// employee0_.id as id1_0_,
// employee0_.employee_name as employee_name2_0_,
// employee0_.pwd as pwd3_0_,
// employee0_.user_name as user_nam4_0_
// from
// employees employee0_
// where
// employee0_.user_name=?
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(userDetails, authenticationRequest.getPassword(), userDetails.getAuthorities());
// This step gets executed
authenticationManager.authenticate(usernamePasswordAuthenticationToken);
// And the same query to the DB is logged, one that I can run on dbeaver manually and get results from:
// select
// employee0_.id as id1_0_,
// employee0_.employee_name as employee_name2_0_,
// employee0_.pwd as pwd3_0_,
// employee0_.user_name as user_nam4_0_
// from
// employees employee0_
// where
// employee0_.user_name=?
// And throws exception in this authenticate
final String token = jwtTokenUtil.generateToken(userDetails);
TokenSucess tokenSuccessResponse = new TokenSucess(true, new JwtResponse(token));
LOGGER.info("Obtained token with success ");
return new ResponseEntity<TokenSucess>(tokenSuccessResponse, HttpStatus.OK);
} catch (Exception e) {
TokenError tokenErrorResp = new TokenError(false, "Error generating token.");
LOGGER.error("Error generating a token. Details: "+ e);
return new ResponseEntity<TokenError>(tokenErrorResp, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
This is the method I'm using in the service:
#Service
public class JwtUserDetailsService implements UserDetailsService {
#Autowired
private EmployeeRepository employeeRepository;
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Employee emp = employeeRepository.findByUserName(username);
if (emp == null ) {
throw new UsernameNotFoundException("Employee not found with username: " + username);
}
List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
authorities.add(new SimpleGrantedAuthority("Admin"));
return new org.springframework.security.core.userdetails.User(emp.getUserName(), emp.getPassword(), authorities);
}
}
The security config:
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
#Autowired
private UserDetailsService jwtUserDetailsService;
#Autowired
private JwtRequestFilter jwtRequestFilter;
private static final Logger LOGGER = LoggerFactory.getLogger(WebSecurityConfig.class);
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
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 {
try {
httpSecurity.csrf().disable()
// don't authenticate this particular request
.authorizeRequests().antMatchers("/authenticate").permitAll().
// all other requests need to be authenticated
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);
} catch (Exception e) {
LOGGER.error("Error handling http security configs. Details: "+e.getMessage());
}
}
}
And this is the curl example:
curl --location --request POST 'http://127.0.0.1:<port>/authenticate' \
--header 'Content-Type: application/json' \
--data-raw '{
"username": "thisWasTheGoodUserName",
"password": "123454321"
}'
Already tried with diferent scenarios in DB:
1. thisWasTheGoodUserName/123454321
2. thisWasTheGoodUserName/$2y$12$Mj0PRHipe14Wgm5c/GOuO.RyhjhuwRwoQYUnK8LcgsvHzQ4weYHGm (bcrypted 123454321)
Using several "step into" I found that the problem is happening in the following function - /Users/<user>/.m2/repository/org/springframework/security/spring-security-core/5.0.3.RELEASE/spring-security-core-5.0.3.RELEASE.jar!/org/springframework/security/authentication/dao/DaoAuthenticationProvider.class, even though the passwords are se same string:
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
if (authentication.getCredentials() == null) {
this.logger.debug("Authentication failed: no credentials provided");
throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
} else {
String presentedPassword = authentication.getCredentials().toString();
// presentedPassword is exactly the same as the one in userDetails.getPassword() but the matcher returns False...
if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
this.logger.debug("Authentication failed: password does not match stored value");
throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
}
}
Any idea what I could be doing wrong and causing the following Exception?
ERROR c.r.t.s.c.JwtAuthenticationController - Error generating a token. Details: org.springframework.security.authentication.BadCredentialsException: Bad credentials
Note: found the problem causing all this: the stored password in the DB was encrypted using an online bcrypt generator... The value does not match the generated bcrypt encoded password even though they are the same string.
I mean you can do a customized authenticate via AuthenticationProvider. You should comment configureGlobal(). The below is sample of WebSecurityConfig.
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
// #Autowired
// private UserDetailsService jwtUserDetailsService;
#Autowired
private JwtRequestFilter jwtRequestFilter;
private static final Logger LOGGER = LoggerFactory.getLogger(WebSecurityConfig.class);
//#Autowired
//public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
// auth.userDetailsService(jwtUserDetailsService).passwordEncoder(passwordEncoder());
//}
//#Bean
//public PasswordEncoder passwordEncoder() {
// return new BCryptPasswordEncoder();
//}
#Autowired
private HttpServletRequest request;
#Autowired
private EmployeeRepository employeeRepository;
#Bean
protected AuthenticationProvider authenticationProvider(){
return new AuthenticationProvider(){
#Override
public Authentication authenticate(Authentication auth) throws AuthenticationException {
String username= request.getParameter("username");
String password = request.getParameter("password");
Employee user= employeeRepository.findByUserName(username);
if (emp == null ) {
throw new UsernameNotFoundException("Employee not found with username: "
+ username);
}
// validate pwd by yourself.
// you should use same bcrypt generator to validate when saving pwd in DB
if(!UserUtils.validatePassword(password, user.getPassword())){
throw new BadCredentialsException("wrong password!");
}
List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
authorities.add(new SimpleGrantedAuthority("Admin"));
return new UsernamePasswordAuthenticationToken(username,
null, authorities);
}
#Override
public boolean supports(Class<?> authentication) {
return true;
}
};
}
#Bean
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
try {
httpSecurity.csrf().disable()
// don't authenticate this particular request
.authorizeRequests().antMatchers("/authenticate").permitAll().
// all other requests need to be authenticated
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);
} catch (Exception e) {
LOGGER.error("Error handling http security configs. Details: "+e.getMessage());
}
}
}
I have problem with validating user credentials. When I give correct credentials first time everything goes OK but giving invalid credentials first and then give correct ones I get invalid credentials error. I use Postman Basic
Auth.
My config class:
#Configuration
#EnableWebSecurity
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private UserService userService;
#Autowired
private CustomAuthenticationEntryPoint authenticationEntryPoint;
#Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable().authorizeRequests()
.antMatchers(HttpMethod.POST ,"/login").permitAll()
.antMatchers("/admin").hasAuthority("ADMIN")
.anyRequest().authenticated().and().exceptionHandling().authenticationEntryPoint(authenticationEntryPoint).and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.ALWAYS).and()
.logout()
.deleteCookies("remove")
.invalidateHttpSession(true);
http.rememberMe().disable();
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(this.userService)
.and().eraseCredentials(true);
}
#Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
And my controller class
#PostMapping
public ResponseEntity<?> loginButtonClicked(HttpServletRequest request) {
HttpSession session = request.getSession();
final String authorization = request.getHeader("Authorization");
String[] authorizationData=null;
if (authorization != null && authorization.startsWith("Basic")) {
// Authorization: Basic base64credentials
String base64Credentials = authorization.substring("Basic" .length()).trim();
String credentials = new String(Base64.getDecoder().decode(base64Credentials),
Charset.forName("UTF-8"));
// credentials = username:password
authorizationData = credentials.split(":", 2);
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(authorizationData[0], authorizationData[1],Arrays.asList(new SimpleGrantedAuthority("USER")));
User user = userService.findUserEntityByLogin(authorizationData[0]);
if(user != null && user.getFromWhenAcceptLoginAttempts() != null && (user.getFromWhenAcceptLoginAttempts()).isBefore(LocalDateTime.now())){
// Authenticate the user
Authentication authentication = authenticationManager.authenticate(authRequest);
SecurityContext securityContext = SecurityContextHolder.getContext();
securityContext.setAuthentication(authentication);
// Create a new session and add the security context.
session = request.getSession();
session.setAttribute("SPRING_SECURITY_CONTEXT", securityContext);
return new ResponseEntity<>(new LoginResponseObject(200,"ACCESS GRANTED. YOU HAVE BEEN AUTHENTICATED"), HttpStatus.OK);
}else{
session.getId();
SecurityContextHolder.clearContext();
if(session != null) {
session.invalidate();
}
return new ResponseEntity<>(new ErrorObject(403,"TOO MANY LOGIN REQUESTS","YOU HAVE ENTERED TOO MANY WRONG CREDENTIALS. YOUR ACCOUNT HAS BEEN BLOCKED FOR 15 MINUTES.", "/login"), HttpStatus.FORBIDDEN);
}
}else{
session.getId();
SecurityContextHolder.clearContext();
if(session != null) {
session.invalidate();
}
return new ResponseEntity<>(new ErrorObject(401,"INVALID DATA","YOU HAVE ENTERED WRONG USERNAME/PASSWORD CREDENTIALS", "/login"), HttpStatus.UNAUTHORIZED);
}
}
#Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Bean
public ObjectMapper objectMapper(){
return new ObjectMapper();
}
#Bean
public HttpSessionEventPublisher httpSessionEventPublisher() {
return new HttpSessionEventPublisher();
}
The problem is that the request is stored in cache due to your sessionCreationPolicy.
To avoid this problem, you could add .requestCache().requestCache(new NullRequestCache()) in your http security config to override the default request cache configuration, but be careful because this could create another side effect (it depends on your application).
In case you do not need the session, you can choose another session policy:
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).
Another alternative is to relay in Spring's BasicAuthenticationFilter. This filter does all the authentication logic for you. To enable it, you only have to add .httpBasic()in your http security configuration.
You may want to add a custom logic on authentication success/failure. In that case, you only have to create a custom filter (CustomBasicAuthenticationFilter) that extends BasicAuthenticationFilter class and overrides the methods onSuccessfulAuthentication()and onUnsuccessfulAuthentication(). You will not need to add .httpBasic() but you will need to insert your custom filter in the correct place:
.addFilterAfter(new CustomBasicAuthenticationFilter(authenticationManager), LogoutFilter.class).
Any of that 3 solutions will avoid your problem.
Try to write .deleteCookies("JSESSONID") in your SpringSecurityConfig class.
I have been going square trying to implement a new RESTful web service using Spring.IO. I've worked through perhaps 30 different online examples that suppose to provide this example but none of them worked 'out of the box'.
A further complication is that 95% of examples use XML configuration exclusively, which in my personal opinion is not as readable as a pure java configuration.
After a great many hours I have managed to cobble together something that 'works' but I would very much be interested in feedback as to my particular implementation. Specifically:
Assuming the client authorisation token is not compromised is my implementation secure.
I was unable to correctly AutoWire the AuthenticationTokenProcessingFilter class in WebSecurityConfig due to the constructor requiring an AuthenticationManager. If there is a way to do this that would help clean things up a little.
The main application class:
#ComponentScan({"webservice"})
#Configuration
#EnableAutoConfiguration
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
The WebSecurityConfig class (AFAIK this does the majority of the job previously performed by the XML):
#Configuration
#EnableWebMvcSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
AuthenticationTokenProcessingFilter authenticationTokenProcessingFilter;
#Autowired
CustomAuthenticationEntryPoint customAuthenticationEntryPoint;
#Override
protected void configure(HttpSecurity http) throws Exception {
authenticationTokenProcessingFilter =
new AuthenticationTokenProcessingFilter(authenticationManager());
http
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests().antMatchers("/login").permitAll()
.anyRequest().authenticated()
.and()
.addFilterBefore(authenticationTokenProcessingFilter, AnonymousAuthenticationFilter.class)
.httpBasic().authenticationEntryPoint(customAuthenticationEntryPoint);
}
}
The AuthenticationTokenProcessingFilter which performs the token authentication on client connect:
public class AuthenticationTokenProcessingFilter extends GenericFilterBean {
AuthenticationManager authManager;
public AuthenticationTokenProcessingFilter(AuthenticationManager authManager) {
this.authManager = authManager;
}
#Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
#SuppressWarnings("unchecked")
Map<String, String[]> parms = request.getParameterMap();
if(parms.containsKey("authToken")) {
String token = parms.get("authToken")[0];
// Validate the token
User user = TokenUtils.getUserFromToken(token);
// If we managed to get a user we can finish the authentication
if (user!=null) {
//Add a default authority for all users
List<GrantedAuthority> grantedAuths = new ArrayList();
grantedAuths.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
// build an Authentication object with the user's info
AbstractAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(user, token, grantedAuths);
// set the authentication into the SecurityContext
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
// continue thru the filter chain
chain.doFilter(request, response);
}
}
The TokenUtils class used by AuthenticationTokenProcessingFilter:
public class TokenUtils {
/**
* Get an authorisation token for the provided userId
*
* #param userId
* #return
*/
public static String getToken(long userId) {
throw new UnsupportedOperationException("Not implemented yet!");
}
/**
* Attempt to get a user for the provided token
*
* #param token
* #return User if found, otherwise null
*/
public static User getUserFromToken(String token) {
throw new UnsupportedOperationException("Not implemented yet!");
}
}
The CustomAuthenticationEntryPoint, which returns a 403 if the user was not successfully authenticated:
#Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
#Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
response.sendError( HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized: Authentication token was either missing or invalid." );
}
}
An finally my web service entry points:
#RestController
public class EntryPoints {
#RequestMapping(value = "/login", method={RequestMethod.POST})
public LoginResponse login(#RequestParam(value="username", required=true) String username,
#RequestParam(value="password", required=true) String password) {
LoginRequest loginRequest = new LoginRequest(username, password);
//Authenticate the user using the provided credentials
//If succesfull return authentication token
//return new LoginResponse(token);
throw new UnsupportedOperationException("Not implemented yet!");
}
#RequestMapping(value = "/account", method={RequestMethod.POST})
public AccountResponse account(#RequestParam(value="accountId", required=true) long accountId) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
//Return the request account information
throw new UnsupportedOperationException("Not implemented yet!");
}
}