I have trouble resolving different encoding types from my LDAP server (which is in SSHA) and the authenticating value of just plain text.
I am using LDAP Repository to retrieve my LDAP server and used AuthenticationManager and my own implementation of `UserDetailService.
The problem is always receive a Bad Credentials exception when i try to authenticate.
As seen on my codes below;
when i request to /auth, i have a plain text username and password. However, i when try to authenticate it via AuthenticationManager, it gives me a Bad Credentials. Now, i know that this has got to do with the UserDetailService.getPassword() and the password i provided in UsernamePasswordAuthenticationToken. Its about the different encryption type. My LDAP has an {SSHA}, also translated to a binary (bytes) and the encoder is Bcrypt and String.
Now my question is how do you properly authenticate a password against the password return by LdapRespository?
UPDATES
2020-04-12 17:09:46.655 INFO 6672 --- [nio-8080-exec-1] s.w.s.SantaRestApiSecurityConfiguration : Raw Password: jdoe123 | Encoded Password: {SSHA}kMgk3gD4prAa/9m4wsPbuAoGhO7UvH2v6+W0Dg==
2020-04-12 17:09:58.110 INFO 6672 --- [nio-8080-exec-1] s.w.s.SantaRestApiSecurityConfiguration : Raw Password: {SSHA}kMgk3gD4prAa/9m4wsPbuAoGhO7UvH2v6+W0Dg== matches Encoded Password: {SSHA}k1Pp3NICHbwuxFFdT7zno/iG/NTILZGL = false
Above logs is during password matching in PasswordEncoder. Based from it. it shows that the Raw Password (inputted by user) has a different hash from the Encoded Password (from ldap server).
I used LdapShaPasswordEncoder.encode to hash user inputted password.
Also, i noticed that everytime i want to authenticate (call http://locahost:xxxx/auth), the user inputted password's hash changes everytime. I think this is normal when hashing
Security Configuration NOTE: I am allowing all REST-API Endpoints for now, i gonna restrict it later
#Configuration
#EnableWebSecurity
public class SantaRestApiSecurityConfiguration extends WebSecurityConfigurerAdapter {
#Autowired
AuthService authService;
#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(authService).passwordEncoder(passwordEncoder());
}
#Bean
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Override
protected void configure(HttpSecurity http) throws Exception {
// TODO Auto-generated method stub
http.csrf()
.disable()
.authorizeRequests().antMatchers("/*").permitAll();
}
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/*");
}
/*
* i am not sure about this since my LDAP server has
* a SSHA password encrpytion
*
* */
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
LoginController
#RestController
#RequestMapping("/auth")
public class LoginController {
public static Logger logger = LoggerFactory.getLogger(LoginController.class);
#Autowired
private AuthenticationManager authManager;
#PostMapping
public ResponseEntity<String> login(#RequestBody Map<String, String> credentials) {
String username = credentials.get("username");
String password = credentials.get("password");
authManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
return new ResponseEntity<String>("", HttpStatus.OK);
}
}
UserDetailService implementation
#Service
public class AuthService implements UserDetailsService {
public static Logger logger = LoggerFactory.getLogger(AuthService.class);
#Autowired
UserLdapRepository ldapRepo;
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserLdap e = ldapRepo.findByUid(username);
logger.info("{}",e.toString());
return new AuthDetails(e.getCn(),
e.getPassword(),
e.getUid(),
e.getMail(),
e.getSn(),
Long.toString( e.getUidNumber()));
}
}
UserDetails implemenation
public class AuthDetails implements UserDetails {
public static Logger logger = LoggerFactory.getLogger(AuthDetails.class);
private String username;
private String password;
private String email;
private String uidNumber;
private String sn;
private String uid;
public AuthDetails(String username, String password, String uid, String email,String sn, String uidNumber) {
this.uid = uid;
this.password = password;
this.username = username;
this.email = email;
this.sn = sn;
this.uidNumber = uidNumber;
}
/*
* Here i am not sure how to implement this one to satisfy AuthenticationManager
*
*
**/
#Override
public String getPassword() {
return this.password;
}
#Override
public String getUsername() {
// TODO Auto-generated method stub
return this.username;
}
// some method to implement
}
You are using BCryptPasswordEncoder but in LDAP is using the SSHA encryption.
You need to modify the method passwordEncoder in class SantaRestApiSecurityConfiguration
#Bean
public PasswordEncoder passwordEncoder() {
return new LdapShaPasswordEncoder();
}
public Class UserLdap {
#Attribute(name = "userPassword", type = Type.BINARY)
private byte[] password;
}
and change the method loadUserByUsername
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserLdap e = ldapRepo.findByUid(username);
logger.info("{}",e.toString());
String password = "{SSHA}" + new String(e.getPassword());
return new AuthDetails(e.getCn(),
password,
e.getUid(),
e.getMail(),
e.getSn(),
Long.toString( e.getUidNumber()));
}
Related
I've multiple users which I want to configure into spring-boot's UserDetailsService for basic authentication.
User has a additional field id tagged with it.
import org.springframework.security.core.userdetails.UserDetails;
public class User implements UserDetails {
private final String username;
private final String password;
private final String id;
private static final String ROLE_USER = "ROLE_USER";
#Override
public Collection<? extends GrantedAuthority> getAuthorities() {
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(ROLE_USER);
return Stream.of(simpleGrantedAuthority).collect(Collectors.toList());
}
// Getter & setter
}
Properties yml looks like:
basic-auth:
oliver:
password: twist
id: 1
ruskin:
password: bond
id: 2
mark:
password: twain
id: 3
In UserDetailsService, I'm not sure how to configure users using application properties dynamically.
public class UserService implements UserDetailsService {
#Override
public UserDetails loadUserByUsername(String username) {
String encodedPassword = passwordEncoder.encode( // How to fetch password );
String id = // How to fetch id
return new User(username, encodedPassword, id);
}
}
You can do this in different ways.
For example, let's assume your Basic Authentication properties YAML file, with minimum tweaks for simplicity:
basic-auth:
users:
oliver:
password: twist
id: 1
ruskin:
password: bond
id: 2
mark:
password: twain
id: 3
To make this information accessible to your UserDetailsService service you can use encapsulate this information in form of #ConfigurationProperties, something like the following:
#Component
#ConfigurationProperties(prefix = "basic-auth")
public class BasicAuthProperties {
private Map<String, Credential> users = new HashMap<>();
// Getter & setter
// Convenient method
public Optional<Credential> getCredential(String username) {
Optional.ofNullable(users.get(username));
}
public static class Credential {
private int id;
private String password;
// Getter & setter
}
}
And use this #ConfigurationProperties in your UserDetailsService:
public class UserService implements UserDetailsService {
private final BasicAuthProperties basicAuthProperties;
public UserService(BasicAuthProperties basicAuthProperties) {
this.basicAuthProperties = basicAuthProperties;
}
#Override
public UserDetails loadUserByUsername(String username) {
Optional<BasicAuthProperties.Credential> optCredential = basicAuthProperties.getCredential(username);
if (!optCredential.isPresent()) {
// handle as appropriate, consider at least logging it
return null;
}
BasicAuthProperties.Credential credential = optCredential.get();
String encodedPassword = passwordEncoder.encode(credential.getPassword());
// I assume int, the value is numeric in your YAML file, but modify it
// if necessary
int id = credential.getId();
return new User(username, encodedPassword, id);
}
}
This is my solution for Basic Authentication with yaml from scratch.
1.You need to put this file to the resource folder
appUserList.yml:
constants:
appUserList:
- userId: 1
userName: oliver
password: twist
role: USER
- userId: 2
userName: ruskin
password: bond
role: USER
- userId: 3
userName: mark
password: twain
role: ADMIN
2.You need to make an AppUser class:
public class AppUser {
private String userId;
private String userName;
private String password;
private String role;
public AppUser() {
}
public AppUser(String userId, String userName, String password, String role) {
this.userId = userId;
this.userName = userName;
this.password = password;
this.role = role;
}
//Getter and Setters
}
3.You need to make a YamlPropertySourceFactory class:
public class YamlPropertySourceFactory implements PropertySourceFactory {
#Override
public PropertySource<?> createPropertySource(String name, EncodedResource encodedResource) throws IOException {
YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean();
factory.setResources(encodedResource.getResource());
Properties properties = factory.getObject();
return new PropertiesPropertySource(encodedResource.getResource().getFilename(), properties);
}
}
4.Now you can use the yaml file and the other two classes to create the AppUserProperties class:
#Component
#PropertySource(value = "classpath:appUserList.yml", factory = YamlPropertySourceFactory.class)
#ConfigurationProperties(prefix = "constants")
public class AppUserProperties {
private List<AppUser> appUserList;
public List<AppUser> getAppUserList() {
return appUserList;
}
public void setAppUserList(List<AppUser> appUserList) {
this.appUserList = appUserList;
}
}
5.Now you need to make a SecurityConf class where you will get the users with this method: appUserProperties.getAppUserList(). If you want use other identifiers for the users, just create the parameters in the AppUser class.
#EnableGlobalMethodSecurity(securedEnabled = true)
#Configuration
public class SecurityConf extends WebSecurityConfigurerAdapter {
#Autowired
AppUserProperties appUserProperties;
#Autowired
public void configureAuth(AuthenticationManagerBuilder auth) throws Exception {
List<AppUser> appUserList = appUserProperties.getAppUserList();
for (AppUser appUser : appUserList) {
auth
.inMemoryAuthentication()
.withUser(appUser.getUserName())
.password(appUser.getPassword())
.roles(appUser.getRole());
}
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.permitAll()
.and()
.logout()
.logoutSuccessUrl("/login?logout")
.permitAll();
}
}
These are all the important steps for it to work.
Otherwise, here is the fully working code:
https://github.com/FerencHrutka/Basic-Auth-with-yaml-file
I hope this answers your question.
After configuring a custom user details service that uses an embedded H2 database to retrieve user credentials, I keep receiving 401 Unauthorized errors when completing a post command even though the username and password are available. I don't know if this makes any difference, but the console still prints out the generated security password (although the auto generated credentials return the same 401 error). Please take a look at my code below and let me know any suggestions or fixes available.
User model...
#Entity
public class ApplicationUser {
#Id
private Long id;
private String username, password, role;
public ApplicationUser(String username, String role, String password) {
this.username=username;
this.password=password;
this.role=role;
}
public ApplicationUser() {
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getRole() {
return role;
}
public void setRole(String role) {
this.role = role;
}
#Override
public String toString() {
return "ApplicationUser{" +
"username='" + username + '\'' +
", password='" + password + '\'' +
", role='" + role + '\'' +
'}';
}
}
User repository class...
#Repository
public class AppUserRepository {
#Autowired
JdbcTemplate jdbcTemplate;
static class ApplicationUserRowMapper implements RowMapper<ApplicationUser> {
#Override
public ApplicationUser mapRow(ResultSet rs, int rowNum) throws SQLException {
ApplicationUser applicationUser = new ApplicationUser();
applicationUser.setUsername(rs.getString("username"));
applicationUser.setPassword(rs.getString("password"));
applicationUser.setRole(rs.getString("userrole"));
return applicationUser;
}
}
public List<ApplicationUser> findAll() {
return jdbcTemplate.query("select u.username, u.password, ur.userrole from ApplicationUsers u, ApplicationUsers_Role ur where u.username = ur.username",
new ApplicationUserRowMapper());
}
public ApplicationUser findByUsername(String username) {
return jdbcTemplate.queryForObject("select u.username, u.password, ur.userrole from ApplicationUsers u, ApplicationUsers_Role ur where u.username = ? and u.username = ur.username",
new Object[] {username}, new ApplicationUserRowMapper());
}
}
Security config class...
#Configuration
#EnableWebSecurity
#ComponentScan(basePackages = "security_package")
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
CustomUserDetailsService customUserDetailsService;
#Autowired
CustomAuthenticationProvider customAuthenticationProvider;
#Bean
public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }
#Autowired
public void configureGlobal (AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(customAuthenticationProvider);
auth.userDetailsService(customUserDetailsService).passwordEncoder(passwordEncoder());
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests().antMatchers("/login").permitAll().anyRequest().authenticated()
.and()
.formLogin().loginPage("/login").usernameParameter("username").passwordParameter("password");
}
}
Custom authentication provider...
#Service
public class CustomAuthenticationProvider implements AuthenticationProvider {
#Autowired
AppUserRepository userRepository;
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String name = authentication.getName();
String password = authentication.getCredentials().toString();
ApplicationUser user = userRepository.findByUsername(name);
if(user == null) {
throw new BadCredentialsException("Authentication failed.");
}
List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
grantedAuthorities.add(new SimpleGrantedAuthority(user.getRole()));
return new UsernamePasswordAuthenticationToken(name, password, grantedAuthorities);
}
#Override
public boolean supports(Class<?> aClass) {
return aClass.equals(UsernamePasswordAuthenticationToken.class);
}
}
Main app class...
#SpringBootApplication(exclude={
HazelcastAutoConfiguration.class,
DataSourceAutoConfiguration.class,
SecurityAutoConfiguration.class
})
#EnableScheduling
#Slf4j
public class AwsorchestratorApplication implements CommandLineRunner{
#Override
public void run(String... arg0) throws Exception {
if (arg0.length > 0 && arg0[0].equals("exitcode")) {
throw new ExitException();
}
}
public static void main(String[] args) throws Exception {
if ( System.getProperty("spring.profiles.active") == null )
{
System.setProperty("spring.profiles.active","local");
}
new SpringApplication(AwsorchestratorApplication.class).run(args);
}
class ExitException extends RuntimeException implements ExitCodeGenerator {
private static final long serialVersionUID = 1L;
#Override
public int getExitCode() {
return 10;
}
}
}
UPDATE ONE::...
#Service
public class CustomUserDetailsService implements UserDetailsService {
#Autowired
private AppUserRepository userRepo;
#Override
public UserDetails loadUserByUsername(String username) {
ApplicationUser user = userRepo.findByUsername(username);
if(user == null) {
throw new UsernameNotFoundException("User '" + username + "' not found.");
}
GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(user.getRole());
return new User(user.getUsername(), user.getPassword(), Collections.singletonList(grantedAuthority));
}
}
I was able to correct this issue by removing my custom authentication class (although I don't think this was actually part of the problem), and editing my web security config to match the below.
#Configuration
#EnableWebSecurity
#ComponentScan(basePackages = package_location)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
public CustomUserDetailsService detailsService;
#Bean
public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }
#Autowired
public void configureGlobal (AuthenticationManagerBuilder auth) throws Exception {
//the password encoder here is only to support a mix of encoding - that change can be removed
auth.userDetailsService(detailsService).passwordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder());
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.headers()
.frameOptions()
.disable()
.and()
.authorizeRequests()
.antMatchers("/awsorchestrator/**")
.hasAnyRole("ADMIN", "USER")
.and()
.formLogin()
.usernameParameter("username")
.passwordParameter("password")
.and()
.csrf()
.disable();
}
}
The important part that directly correlated to my problem was the passwordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder()) portion. I also had to change the password stored in the database to be prefixed with {bcrypt} and now I am able to login with no issues.
Thanks R.G for the tips.
I'm implementing a project using Spring security oauth2, everything works perfectly, now I want to start digging deeper beyond the basics. I want to check if the user making the request is the actual user owner of the resource, the end result would be for example:
/private/users/{uuid}/clients returns all clients for the specified user.
So my controller now looks like this:
#RestController
public class HomeController {
#Autowired
private UserService userService;
#GetMapping(value = "/")
public String index() {
return "Hello world";
}
#GetMapping(value = "/private")
public String privateTest(Principal principal) {
User user = userService.get(principal.getName());
return user.getUuid();
}
}
EDIT: The full security code (working) for a better explanation.
ResourceServerConfig
#Configuration
#EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
#Override
public void configure(HttpSecurity http) throws Exception {
http.headers().frameOptions().disable().and()
.authorizeRequests()
.antMatchers("/","/home","/register","/login").permitAll()
.antMatchers("/private/**").authenticated();
}
}
CustomUserDetails with getters and setters off course
public class CustomUserDetails implements UserDetails {
private Collection<? extends GrantedAuthority> authorities;
private String password;
private String username;
private String uuid;
public CustomUserDetails(User user) {
this.username = user.getUsername();
this.password = user.getPassword();
this.uuid = user.getUuid();
this.authorities = translate(user.getRoles());
}
}
AuthorizationServerConfig
#Configuration
#EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
#Autowired
private AuthenticationManager authenticationManager;
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager);
}
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory().withClient("my-trusted-client")
.authorizedGrantTypes("client_credentials", "password")
.authorities("ROLE_CLIENT","ROLE_TRUSTED_CLIENT").scopes("read","write","trust")
.resourceIds("oauth2-resource").accessTokenValiditySeconds(5000).secret("secret");
}
#Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.checkTokenAccess("isAuthenticated()");
}
}
Main
#SpringBootApplication
public class DummyOauthApplication {
#Autowired
private PasswordEncoder passwordEncoder;
public static void main(String[] args) {
SpringApplication.run(DummyOauthApplication.class, args);
}
#Autowired
public void authenticationManager(AuthenticationManagerBuilder builder, UserRepository repository, UserService service) throws Exception {
//Setup a default user if db is empty
if (repository.count() == 0) {
service.save(new User("user", "password", UUID.randomUUID().toString(), Arrays.asList(new Role("USER"), new Role("ACTUATOR"))));
}
builder.userDetailsService(userDetailsService(repository)).passwordEncoder(passwordEncoder);
}
private UserDetailsService userDetailsService(final UserRepository repository) {
return username -> new CustomUserDetails(repository.findByUsername(username));
}
}
So, using the way I've implemented. I can get the actual user but it implies a database query every time an endpoint is called. Getting the user and match with the user uuid.
I want to find another way that I can get the user and then compare if the uuid = user.getUuid()
Thanks in advance.
After some time and a lot of mistakes, I've managed to find a solution that I leave here. The CustomUserDetails can be seen in the question and from there you can easily get the uuid and match with the requested one.
public static CustomUserDetails getCurrentUser() {
SecurityContext securityContext = SecurityContextHolder.getContext();
Authentication authentication = securityContext.getAuthentication();
if (authentication != null) {
if (authentication.getPrincipal() instanceof CustomUserDetails) {
return (CustomUserDetails) authentication.getPrincipal();
}
}
throw new IllegalStateException("User not found!");
}
EDIT: if you want to return the user you do something like this
public class CustomUserDetails implements UserDetails {
private Collection<? extends GrantedAuthority> authorities;
private String password;
private String username;
private User user;
public CustomUserDetails(User user) {
this.username = user.getUsername();
this.password = user.getPassword();
this.user = user;
this.authorities = translate(user.getRoles());
}
}
And then in a Utils or something,
public static User getCurrentUser() {
SecurityContext securityContext = SecurityContextHolder.getContext();
Authentication authentication = securityContext.getAuthentication();
if (authentication != null) {
if (authentication.getPrincipal() instanceof CustomUserDetails) {
CustomUserDetails userDetails = (CustomUserDetails) authentication.getPrincipal();
return userDetails.getUser();
}
}
throw new IllegalStateException("User not found!");
}
Thanks for all the effort.
Perhaps you could implement a custom AuthenticationProvider and store user details as Principal
Spring Security Authentication Provider
I'm trying to add bcrypt to a spring app of mine. Authentication works just fine without. But when I try to encode using bcrypt I get "Reason: Bad credentials" when trying to login.
My user model looks as follows.
#Entity
#Table(name="users") // user is a reserved word in postgresql
public class User extends BaseEntity {
private PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
...
#Column(nullable=false)
private String password;
...
public String getPassword() {
return password;
}
public void setPassword(String password) {
String hashedPassword = passwordEncoder.encode(password);
this.password = hashedPassword;
}
...
}
My SecurityConfig looks as follows.
#Configuration
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private CustomUserDetailsService userDetailsService;
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth
.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder());
}
private BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
...
}
Do the above seem right? Do I need to do more than what I've already done?
My bad for not posting enough code. Naturally my user model didn't tell the entire story. I also have a class called SecurityUser which I've posted below. Due to the copy constructor the password gets hashed twice.
public class SecurityUser extends User implements UserDetails {
private static final long serialVersionUID = 867280338717707274L;
public SecurityUser(User user) {
if(user != null)
{
this.setId(user.getId());
this.setName(user.getName());
this.setEmail(user.getEmail());
this.setPassword(user.getPassword());
this.setRoles(user.getRoles());
}
}
#Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> authorities = new ArrayList<>();
Set<Role> userRoles = this.getRoles();
if(userRoles != null)
{
for (Role role : userRoles) {
SimpleGrantedAuthority authority = new SimpleGrantedAuthority(role.getName());
authorities.add(authority);
}
}
return authorities;
}
...
}
I've made my passwordEncoder method public and promoted it to a bean so I can autowire it into my UserService which is shown below. That way I only have to change encoder in one place if I ever decide to do so.
#Service
public class UserService {
#Autowired
private UserRepository userRepository;
#Autowired
private PasswordEncoder passwordEncoder;
public User create(User user) {
String hashedPassword = passwordEncoder.encode(user.getPassword());
user.setPassword(hashedPassword);
return userRepository.save(user);
}
...
}
Here is how I would set it up.
User Table has 4 properties (amongst others)
id (auto increment)
username (or email_address) field
password field.
enabled (value will be either 1 or 0)
Role table (3 properties)
id (auto increment)
user_id (user table foreign key)
authority;
Create Java Entities for the two tables.
Spring Security Configuration Class looks like:
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
String usrsByUnameQry = "SELECT u.email_address, u.password, u.enabled FROM user u WHERE u.email_address=?";
3String authByUnameQry = "SELECT u.email_address, r.authority FROM user u, role r WHERE u.id=r.user_id AND u.email_address=?";
auth
.eraseCredentials(false)
.jdbcAuthentication()
.dataSource(dataSource)
.passwordEncoder(passwordEncoder())
.usersByUsernameQuery(usrsByUnameQry)
.authoritiesByUsernameQuery(authByUnameQry);
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.formLogin()
.usernameParameter("username") //username property defined in html form
.passwordParameter("password") //password property defined in html form
// url that holds login form
.loginPage("/signin")
.loginProcessingUrl("/signin/authenticate")
.failureUrl("/loginfail")
// Grant access to users to the login page
.permitAll()
.and()
.logout()
.logoutUrl("/signout")
.deleteCookies("JSESSIONID")
.logoutSuccessUrl("/signin")
.and()
.authorizeRequests()
.antMatchers("/foo/**").permitAll()//Grant access to all (no auth reuired)
.antMatchers("/").hasAnyAuthority("ROLE_USER","ROLE_ADMIN") //Grant access to only users with role "ROLE_USER" or "ROLE_ADMIN"
}
#Bean(name = "authenticationManager")
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Bean
public BCryptPasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
#Bean
public TextEncryptor textEncryptor(){
return Encryptors.noOpText();
}
I got stuck with a problem regarding token based authentication using the spring security framework.
What Iam trying to achieve is to authenticate an angular js client with a webservice based on spring.
When the client logs in, I am generating a token which is passed along with every request made to the back end. So far this is working quite well.
The token is generated and returned to the client successfully. Passing the token for each request is also working. My problem is that my custom filter is authenticating the client successfully, but after going through the rest of the filter chain it is always returning a HTTP 403 and I have no clue where it is coming from.
The code is inspired by https://github.com/philipsorst/angular-rest-springsecurity and I tried to integrate it into my own app.
So here is my set up:
Java Spring Config Application.java:
#Configuration
#ComponentScan
#EnableAutoConfiguration
#ImportResource("classpath:applicationContext.xml")
#Order(1)
public class Application {
static Logger log = Logger.getLogger(Application.class.getName());
private static ConfigurableApplicationContext context;
public static void main(String[] args) {
context = SpringApplication.run(Application.class);
DBUtil dbUtil = context.getBean(DBUtil.class);
dbUtil.insertDestData();
}
}
#EnableWebMvcSecurity
#EnableWebSecurity(debug = true)
#ComponentScan
#Configuration
#EnableAutoConfiguration
#Order(2)
class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private ConfigurableApplicationContext context;
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http.sessionManagement().sessionCreationPolicy(
SessionCreationPolicy.STATELESS);
String[] restEndpointsToSecure = { "api/recipe", "api/ingredient" };
//for (String endpoint : restEndpointsToSecure) {
http.authorizeRequests().antMatchers("/api/ingredient/create/")
.hasRole("USER");
//}
SecurityConfigurer<DefaultSecurityFilterChain, HttpSecurity> securityConfigurerAdapter = new XAuthTokenConfigurer(
userDetailsServiceBean(), authenticationManagerBean());
http.apply(securityConfigurerAdapter);
}
#Override
protected void configure(AuthenticationManagerBuilder authManagerBuilder)
throws Exception {
UserDAO userDAO = context.getBean(UserDAO.class);
authManagerBuilder.userDetailsService(userDAO);
}
#Bean(name = "myAuthManager")
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
The user entity implementing the UserDetail interface
#Entity
#Table(name = "RB_USER")
public class User extends BaseAuditEntity implements UserDetails {
/**
*
*/
private static final long serialVersionUID = 3719799602561353931L;
public User() {
super();
}
#Column(name = "NAME")
private String userName;
#Column(name = "PASSWORD")
private String password;
#Column(name = "SECTOKEN")
private String secToken;
#OneToMany(fetch = FetchType.LAZY, mappedBy = "user")
private List<UserRole> userRoles;
#Column(name = "IS_ACCOUNT_NON_LOCKED")
private boolean isAccountNonLocked;
#Column(name = "IS_ENABLED")
private boolean isEnabled;
#Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Set<String> roles = new HashSet<String>();
for (UserRole userRole : getUserRoles()) {
roles.add(userRole.getRole().getName());
}
Set<GrantedAuthority> authorities = new HashSet<GrantedAuthority>();
for (String role : roles) {
authorities.add(new SimpleGrantedAuthority(role));
}
return authorities;
}
#Override
public String getPassword() {
return password;
}
#Override
public String getUsername() {
return userName;
}
#Override
public boolean isAccountNonExpired() {
return true;
}
#Override
public boolean isAccountNonLocked() {
return isAccountNonLocked;
}
#Override
public boolean isCredentialsNonExpired() {
return true;
}
#Override
public boolean isEnabled() {
return isEnabled;
}
public String getSecToken() {
return secToken;
}
public void setSecToken(String secToken) {
this.secToken = secToken;
}
public List<UserRole> getUserRoles() {
return userRoles;
}
public void setUserRole(List<UserRole> userRoles) {
this.userRoles = userRoles;
}
public void setUserName(String name) {
this.userName = name;
}
public void setPassword(String password) {
this.password = password;
}
public void setEnabled(boolean isEnabled) {
this.isEnabled = isEnabled;
}
public void setAccountNonLocked(boolean isAccountNonLocked) {
this.isAccountNonLocked = isAccountNonLocked;
}
}
The user dao implementing the user detail service interface
#Repository
#Transactional
public class UserDAOImpl extends GenericDAOImpl<User> implements UserDAO,
UserDetailsService {
public UserDAOImpl() {
super();
setClazz(User.class);
}
#Override
public User findByUserName(String userName) {
String queryString = "SELECT user FROM User user "
+ "LEFT JOIN FETCH user.userRoles userRoles "
+ "WHERE user.userName like ?1";
Query query = createQuery(queryString);
query.setParameter(1, userName);
return (User) query.getSingleResult();
}
#Override
public UserDetails loadUserByUsername(String username) {
String queryString = "SELECT user FROM User user "
+ "LEFT JOIN FETCH user.userRoles userRoles "
+ "WHERE user.userName like ?1";
Query query = createQuery(queryString);
query.setParameter(1, username);
return (User) query.getSingleResult();
}
}
My extended SecurityConfigurerAdapter applying the custom filter to the filter chain
public class XAuthTokenConfigurer extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
private UserDetailsService detailsService;
private AuthenticationManager authenticationManager;
public XAuthTokenConfigurer(UserDetailsService detailsService, AuthenticationManager authenticationManager) {
this.detailsService = detailsService;
this.authenticationManager = authenticationManager;
}
#Override
public void configure(HttpSecurity http) throws Exception {
XAuthTokenFilter customFilter = new XAuthTokenFilter(detailsService, authenticationManager);
http.addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class);
}
}
And what is basically the important part the filter itself
public class XAuthTokenFilter extends GenericFilterBean {
protected Logger log = Logger.getLogger(this.getClass().getName());
private final UserDetailsService detailsService;
private final TokenUtils tokenUtils = new TokenUtils();
private final AuthenticationManager authenticationManager;
private String xAuthTokenHeaderName = "x-auth-token";
public XAuthTokenFilter(UserDetailsService userDetailsService, AuthenticationManager authenticationManager) {
this.detailsService = userDetailsService;
this.authenticationManager = authenticationManager;
}
#Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
FilterChain filterChain) throws IOException, ServletException {
try {
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
String authToken = httpServletRequest
.getHeader(this.xAuthTokenHeaderName);
if (StringUtils.hasText(authToken)) {
String username = this.tokenUtils
.getUserNameFromToken(authToken);
UserDetails details = this.detailsService
.loadUserByUsername(username);
if (this.tokenUtils.validateToken(authToken, details)) {
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
details.getUsername(), details.getPassword(),
details.getAuthorities());
token.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest));
Authentication authentication = this.authenticationManager
.authenticate(token);
SecurityContextHolder.getContext().setAuthentication(authentication);
log.info("========================> " + SecurityContextHolder.getContext().getAuthentication().getName() + " , " + SecurityContextHolder.getContext().getAuthentication().isAuthenticated());
}
}
filterChain.doFilter(servletRequest, servletResponse);
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
}
As I mentioned above the login is working perfectly as the back end is returning a token and the angular js app is applying the token to each request.
Now when I am for example trying to add a new ingredient which is for the moment the only endpoint I am securing (for test purposes) I always get a 403 HTTP error.
Request
Accept application/json, text/plain, */*
Accept-Encoding gzip, deflate
Accept-Language en-US,en;q=0.5
Content-Length 15
Content-Type application/json;charset=utf-8
Cookie user=%7B%22id%22%3A1%2C%22version%22%3A0%2C%22createdAt%22%3A1402390667294%2C%22createdBy%22%3Anull%2C%22updatedAt%22%3Anull%2C%22updatedBy%22%3Anull%2C%22password%22%3A%22123%22%2C%22secToken%22%3A%22Vincent%3A1402394417353%3A62c641b65418ceecb47aad47d5fc5378%22%2C%22userRoles%22%3A%5B%7B%22id%22%3A1%2C%22version%22%3A0%2C%22createdAt%22%3A1402390667299%2C%22createdBy%22%3Anull%2C%22updatedAt%22%3Anull%2C%22updatedBy%22%3Anull%2C%22role%22%3A%7B%22id%22%3A1%2C%22version%22%3A0%2C%22createdAt%22%3A1402390667287%2C%22createdBy%22%3Anull%2C%22updatedAt%22%3Anull%2C%22updatedBy%22%3Anull%2C%22name%22%3A%22USER%22%7D%7D%5D%2C%22authorities%22%3A%5B%7B%22authority%22%3A%22USER%22%7D%5D%2C%22accountNonLocked%22%3Atrue%2C%22accountNonExpired%22%3Atrue%2C%22credentialsNonExpired%22%3Atrue%2C%22enabled%22%3Atrue%2C%22username%22%3A%22Vincent%22%7D
Host localhost:8080
Referer http://localhost:8080/
User-Agent Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:29.0) Gecko/20100101 Firefox/29.0
x-auth-token Vincent:1402394417353:62c641b65418ceecb47aad47d5fc5378
As you can see the token is passed along as expected and the log output states the the user Vincent was authenticated successfully
2014-06-10 11:00:56.201 INFO 4495 --- [nio-8080-exec-4] c.receiptbook.security.XAuthTokenFilter : ========================> Vincent , true
Now when the filter chain gets processed it is returning a 403 error in the end saying the resource cant be accessed
POST /api/ingredient/create/
403 Forbidden
localhost:8080
So I really appreciate any hint or tip to solve this problem, cause I am really running out of ideas how to track down this issue.
Regards,
Vincent
UPDATE:
While debugging the filter chain I came across the fact that the UsernamePasswordAuthenticationFilter is not even in the chain ...? The ExceptionTranslationFilter seems to react with a 403 HTTP error, but from the filter chain I am not able to say why he's reacting like that.
[0] WebAsyncManagerIntegrationFilter (id=75)
[1] SecurityContextPersistenceFilter (id=74)
[2] HeaderWriterFilter (id=71)
[3] LogoutFilter (id=83)
[4] XAuthTokenFilter (id=70)
[5] RequestCacheAwareFilter (id=116)
[6] SecurityContextHolderAwareRequestFilter (id=127)
[7] AnonymousAuthenticationFilter (id=130)
[8] SessionManagementFilter (id=133)
[9] ExceptionTranslationFilter (id=134)
[10] FilterSecurityInterceptor (id=138)
RESOLVED:
So in the end and after a few ours of debugging I finally resolved the issue. The only problem was my role naming. My roles we're just named "USER" or "ADMIN", but spring security expects the names to have the format "ROLE_USER" or "ROLE_ADMIN". This is for sure configurable, but also pretty bad documented.
Anyways I hope that this information could be usefull for someone else.