I am trying to implement an Auth server for my REST API in Spring Boot and I am struggling to autowire my own user repository into the the configuration. Could someone suggest how to do this correctly?
I have the following auth server configuration:
#Configuration
#EnableAuthorizationServer
public class OAuth2Config extends AuthorizationServerConfigurerAdapter {
#Autowired
#Qualifier("userDetailsService")
private UserDetailsService userDetailsService;
#Autowired
private AuthenticationManager authenticationManager;
#Value("${gigsterous.oauth.tokenTimeout:3600}")
private int expiration;
#Override
public void configure(AuthorizationServerEndpointsConfigurer configurer) throws Exception {
configurer.authenticationManager(authenticationManager);
configurer.userDetailsService(userDetailsService);
configurer.accessTokenConverter(accessTokenConverter());
}
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("gigsterous")
.secret("secret")
.accessTokenValiditySeconds(expiration)
.scopes("read", "write")
.authorizedGrantTypes("password", "refresh_token")
.resourceIds("resource");
}
/**
* Use custom jwt token converter to enhance the token with custom fields
*
* #return CustomTokenConverter
*/
#Bean
public CustomTokenConverter accessTokenConverter() {
return new CustomTokenConverter();
}
}
User service:
#Service("userDetailsService")
public class UserService implements UserDetailsService {
#Autowired
private UserRepository userRepository;
public List<User> getUsers() {
return userRepository.findAll();
}
public User getUser(Long id) {
return userRepository.findById(id);
}
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return (UserDetails) userRepository.findOneByUsername(username);
}
}
User:
#Entity
#Table(name = "users")
#Getter
#Setter
public class User {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "user_id", nullable = false, updatable = false)
private Long id;
#Column(name = "email", nullable = false, unique = true)
private String email;
#Column(name = "username", nullable = false, unique = true)
private String username;
#Column(name = "password", nullable = false)
private String password;
protected User() {
// hibernate
}
}
I am using Flyway and currently storing my users in H2 in memory database (this is working correctly so I omit this part of code from my question to avoid confusion).
When I try to authorise some user from my database using this command:
curl -X POST --user 'gigsterous:secret' -d 'grant_type=password&username=peter&password=password' http://localhost:9000/gigsterous-auth/oauth/token
I get the following response:
{"error":"unauthorized","error_description":"com.gigsterous.auth.domain.User cannot be cast to org.springframework.security.core.userdetails.UserDetails"}
Apparently, I cannot covert my user POJO into UserDetails object but I cannot figure out how to construct it correctly since it is an anonymous class.
It's been long time I've not worked on Spring, my memory might be not good. But if I'm not wrong, you want to use your User as UserDetails, you have to implement it, UserDetails is an interface. :D
And I recommend to change name of your class, User is already existed in OAuth2 library
Related
I have implemented a spring boot application with oauth2. when I am trying to access token by providing clientId and Secret then unauthorized(401) response is returned.
oauth_client_detals table is designed in the oracle database with the following schema and secret column value is stored in BCrypt format.
insert into oauth_client_details(client_id,client_secret,web_server_redirect_uri,
scope,accsess_token_validity,refresh_token_validity,resource_id,authorized_grant_types,authorities,
additional_information,autoapprove) values ('web','{bcrypt}$2y$12$FCIQkEmh7ai/6oP99yNOEuWnKt9OjrGEczCxnEnFGDRSOHumOChQO',
'','READ,WRITE','900','3600','','password,authorization_code,refresh_token,implicit','ROLE_ADMIN,ROLE_USER,ROLE_MANAGER','','');
AuthorizationConfig.class
#Configuration
#EnableAuthorizationServer
public class AuthorizationServer extends AuthorizationServerConfigurerAdapter {
#Autowired
private AuthenticationManager authenticationManager;
#Autowired
private DataSource dataSource;
#Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.checkTokenAccess("isAuthenticated()").tokenKeyAccess("permitAll()");
}
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.jdbc(dataSource).passwordEncoder(new BCryptPasswordEncoder());
}
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager);
}
}
SecurityConfig.class
#Configuration
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private UserDetailsService userDetailsService;
#Autowired
private AuthEntryPoint authEntryPoint;
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authenticationProvider1());
}
private AuthenticationProvider authenticationProvider1()
{
DaoAuthenticationProvider provider=new DaoAuthenticationProvider();
provider.setUserDetailsService(userDetailsService);
provider.setPasswordEncoder(new BCryptPasswordEncoder());
return provider;
}
#Override
#Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().exceptionHandling().authenticationEntryPoint(authEntryPoint)
.and().authorizeRequests().anyRequest().authenticated().and().httpBasic();
}
}
UserDetailsServiceImpl.class
#Service("userDetailsService")
public class UserDetailsServiceImpl implements UserDetailsService {
#Autowired
private UserDAO userDAO;
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user= userDAO.findByUserName(username)
.orElseThrow(()->new UsernameNotFoundException("data not found with "+username));
return AuthUserDetails.builder(user);
}
}
AuthUserDetails.class
public class AuthUserDetails implements UserDetails{
private String userName;
private String password;
private List<GrantedAuthority> authorities;
private boolean accNonExpired;
private boolean accNonLocked;
private boolean credentialNonExpired;
private boolean active;
public AuthUserDetails()
{
}
public AuthUserDetails(boolean active, List<GrantedAuthority> authorities, String userName, String password,
boolean accNonExpired, boolean credentialNonExpired, boolean accNonLocked) {
this.active = active;
this.authorities = authorities;
this.userName = userName;
this.password = password;
this.accNonExpired = accNonExpired;
this.credentialNonExpired = credentialNonExpired;
this.accNonLocked = accNonLocked;
}
public static UserDetails builder(User user)
{
List<GrantedAuthority> grantedAuthorities=new ArrayList<>();
user.getRoles().forEach(role-> {
grantedAuthorities.add(new SimpleGrantedAuthority(role.getName().name()));
role.getPermissions().forEach(perm->{
grantedAuthorities.add(new SimpleGrantedAuthority(perm.getName().name()));
});
});
return new AuthUserDetails((user.getActive()==1),grantedAuthorities,user.getUserName(),user.getPassword(),
(user.getAccNonExpired()==1), (user.getCredentialNonExpired()==1),(user.getAccNonLocked()==1));
}
#Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
#Override
public String getPassword() {
return password;
}
#Override
public String getUsername() {
return userName;
}
#Override
public boolean isAccountNonExpired() {
return accNonExpired;
}
#Override
public boolean isAccountNonLocked() {
return accNonLocked;
}
#Override
public boolean isCredentialsNonExpired() {
return credentialNonExpired;
}
#Override
public boolean isEnabled() {
return active;
}
}
User.class
#Entity
#Table(name="user56",schema = Schema.OAUTH2,uniqueConstraints = #UniqueConstraint(
columnNames = "username"
))
#Getter
#Setter
public class User {
#Id
#SequenceGenerator(name="user_id_gen",sequenceName = Schema.OAUTH2+".user_id_seq",initialValue = 1003,allocationSize = 1)
#GeneratedValue(generator = "user_id_gen",strategy = GenerationType.SEQUENCE)
#Column(name = "user_id")
private int userId;
#Column(name = "username")
private String userName;
#Column(name = "password")
private String password;
#Column(name = "email")
private String email;
#Column(name = "active")
private int active;
#Column(name = "acc_non_expired")
private int accNonExpired;
#Column(name = "credential_non_expired")
private int credentialNonExpired;
#Column(name = "acc_non_locked")
private int accNonLocked;
#ManyToMany(cascade = CascadeType.ALL,fetch = FetchType.EAGER)
#JoinTable(name = "role_user",
joinColumns = {#JoinColumn(name = "user_id",referencedColumnName = "user_id")},
inverseJoinColumns = {#JoinColumn(name = "role_id",referencedColumnName = "id")})
private Set<Role> roles;
}
OAuthClient.class
#Entity
#Table(name = "oauth_client_details",schema = Schema.OAUTH2)
#Getter
#Setter
public class OAuthClient {
#Id
#Column(name = "client_id")
private String clientId;
#Column(name="client_secret")
private String clientSecret;
#Column(name = "web_server_redirect_uri")
private String webServerRedirectUri;
#Column(name = "scope")
private String scope;
#Column(name = "accsess_token_validity")
private String accessTokenValidity;
#Column(name = "refresh_token_validity")
private String refreshTokenValidity;
#Column(name = "resource_id")
private String resourceId;
#Column(name="authorized_grant_types")
private String authorizedGrantType;
#Column(name = "authorities")
private String authorities;
#Column(name = "additional_information")
private String additionalInformation;
#Column(name = "autoapprove")
private String autoApprove;
}
response unauthorized(401) through postman
Updated
AuthEntryPoint.class
#Component
public class AuthEntryPoint implements AuthenticationEntryPoint {
Logger ERROR_LOGGER= LoggerFactory.getLogger(AuthEntryPoint.class);
#Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
ERROR_LOGGER.error("Unauthorized error : {}",authException.getMessage());
response.sendError(HttpServletResponse.SC_UNAUTHORIZED,"Error : Unauthorized");
}
}
Updated 2
when I tried to run the application under the debugging mode then the following error will occur
FileNotFoundException#769
I have downloaded you project from here and, initially, I only have found some typo in the columns: accsess_token_validity and resource_id
After that I have added the required tables of your project and include some dummy information. For that reason, I'm pretty sure your problem is related with the password values in your oauth_client_details and user56 tables, because only when the stored value did not match with the expected one, I received 401.
Take into account you have defined two different BCryptPasswordEncoder instances:
provider.setPasswordEncoder(new BCryptPasswordEncoder()) for the users included in user56.
clients.jdbc(dataSource).passwordEncoder(new BCryptPasswordEncoder()) for the users included in oauth_clients_detail
Once I fixed it and store the expected ones, I achieved the endpoint returned the expected result:
As I told you in previous comments, there several classes will help you to find the cause of the error:
BasicAuthenticationFilter will get from the request the Basic Auth provided and executes the authentication process.
BasicAuthenticationConverter will extract Basic Auth provided from the request really.
JbdcClientDetailsService will get from oauth_clients_detail the information related with provided client_id in Basic Auth.
The following pictures are the most important to verify if your passwords match. That method will invoke twice: first for client_id / client_pass and second for username / password.
I have included some useful information in the "debug area"
Finally, I have found the place where the problem is occurring, When I send the request to the server then unauthorized(401) error occurs with the following message,
Encoded password does not look like BCrypt
So I have replaced the following code in SecurityConfig.class
#Bean
public PasswordEncoder getPasswordEncoder() {
return new BCryptPasswordEncoder();
}
// replace to
#Bean
public PasswordEncoder getPasswordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
It works fine but still, I couldn't find why BCrytptPasswordEncoder is not worked even secret values are stored in BCrypt format. Anyway thank you very much #doctore for your answer
I successfully build in-memory authentication. But when I going to build it with database comes this error.
There is no PasswordEncoder mapped for the id "null"
This is followed tutorial - Spring Boot Tutorial for Beginners, 10 - Advanced Authentication using Spring Security | Mighty Java
There are classes
SpringSecurityConfiguration.java
#Configuration
#EnableWebSecurity
public class SpringSecurityConfiguration extends
WebSecurityConfigurerAdapter{
#Autowired
private AuthenticationEntryPoint entryPoint;
#Autowired
private MyUserDetailsService userDetailsService;
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().authenticated().and().httpBasic()
.authenticationEntryPoint(entryPoint);
}
}
AuthenticationEntryPoint.java
#Configuration
public class AuthenticationEntryPoint extends BasicAuthenticationEntryPoint{
#Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
response.addHeader("WWW-Authenticate", "Basic realm -" +getRealmName());
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
PrintWriter writer = response.getWriter();
writer.println("Http Status 401 "+authException.getMessage());
}
#Override
public void afterPropertiesSet() throws Exception {
setRealmName("MightyJava");
super.afterPropertiesSet();
}
}
MyUserDetailsService .java
#Service
public class MyUserDetailsService implements UserDetailsService{
#Autowired
private UserRepository userRepository;
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username);
if(user == null){
throw new UsernameNotFoundException("User Name "+username +"Not Found");
}
return new org.springframework.security.core.userdetails.User(user.getUserName(),user.getPassword(),getGrantedAuthorities(user));
}
private Collection<GrantedAuthority> getGrantedAuthorities(User user){
Collection<GrantedAuthority> grantedAuthority = new ArrayList<>();
if(user.getRole().getName().equals("admin")){
grantedAuthority.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
}
grantedAuthority.add(new SimpleGrantedAuthority("ROLE_USER"));
return grantedAuthority;
}
}
UserRepository interface
public interface UserRepository extends JpaRepository<User, Long>{
#Query("FROM User WHERE userName =:username")
User findByUsername(#Param("username") String username);
}
Role.java
#Entity
public class Role extends AbstractPersistable<Long>{
private String name;
#OneToMany(targetEntity = User.class , mappedBy = "role" , fetch = FetchType.LAZY ,cascade = CascadeType.ALL)
private Set<User> users;
//getter and setter
}
User.java
#Entity
public class User extends AbstractPersistable<Long>{
//AbstractPersistable class ignore primary key and column annotation(#Column)
private String userId;
private String userName;
private String password;
#ManyToOne
#JoinColumn(name = "role_id")
private Role role;
#OneToMany(targetEntity = Address.class, mappedBy = "user",fetch= FetchType.LAZY ,cascade =CascadeType.ALL)
private Set<Address> address; //Instead of Set(Unordered collection and not allow duplicates) we can use list(ordered and allow duplicate values) as well
//getter and setter}
If you have any idea plese inform. Thank you.
I changed MyUserDetailsService class adding passwordEncoder method.
Added lines
BCryptPasswordEncoder encoder = passwordEncoder();
Changed Line
//changed, user.getPassword() as encoder.encode(user.getPassword())
return new org.springframework.security.core.userdetails.User(--)
MyUserDetailsService.java
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
BCryptPasswordEncoder encoder = passwordEncoder();
User user = userRepository.findByUsername(username);
if(user == null){
throw new UsernameNotFoundException("User Name "+username +"Not Found");
}
return new org.springframework.security.core.userdetails.User(user.getUserName(),encoder.encode(user.getPassword()),getGrantedAuthorities(user));
}
#Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
As from Spring Security 5.x, Spring Security enforces you to use a password encoder if you're working with other than in-memory (production) databases.
Spring Security enforces this by activating the default DelegatingPasswordEncoder, which looks for PasswordEncoder beans.
By adding a BCryptPasswordEncoder, the DelegatingPasswordEncoder will return that instance to encrypt passwords.
#Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
I don't recommend you to do this, but if you really want to, you can override password encoding by adding {noop} to the password value.
This will treat the password by activating the NoOpPasswordEncoder instead of the default DelegatingPasswordEncoder and will treat your password as plain text.
Please note that this is not recommended if you deploy your app to a production environment!
I,m trying to do toDoList Rest API and have some problems with it.
Help me!
I decided to do multiuser project, separate users using basic auth of Spring Security. Here is my Entities: Task.java
#Entity
public class Task {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
#NotNull
private String title;
#JsonFormat(pattern = "yyyy-MM-dd")
private LocalDate date;
private boolean status;
#ManyToOne
private User user;
public Task() {};
+Getters and Setters
#Entity
public class User {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String firstName;
private String lastName;
private String username;
#JsonIgnore
private String password;
#JsonIgnore
private String[] roles;
public User() {};
+Getters and Setter;
Both entity interface extends CrudRepository. Task have #ManyToOne relationship to User.
Java helps me to create bd in h2 according to settings in application.properties
# H2
spring.h2.console.enabled=true
spring.h2.console.path=/h2
# Datasource
spring.datasource.url=jdbc:h2:./data/ToDoList
spring.datasource.username=sa
spring.datasource.password=
spring.datasource.driver-class-name=org.h2.Driver
spring.jpa.show-sql = true
spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.H2Dialect
# Hibernate ddl auto (create, create-drop, validate, update)
spring.jpa.hibernate.ddl-auto = update
Problem #1: I can't open h2 console. All crud operations are working well, i tested it it Postman, but i want to have access to console.
Then i implement UserDetailService to grab users attributes:
#Component
public class DetailsService implements UserDetailsService{
#Autowired
private UserRepository users;
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = users.findByUsername(username);
if (user == null) {
throw new UsernameNotFoundException(username + " was not found");
}
return new org.springframework.security.core.userdetails.User(
user.getUsername(),
user.getPassword(),
AuthorityUtils.createAuthorityList(user.getRoles())
);
}
}
And configure WebSecurity:
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
#Autowired
private DetailsService userDetailsService;
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.httpBasic()
.and()
.csrf().disable();
}
}
And here is Problem #2: how to take information about tasks of only one user?
In findAll method for example.
P.S. Sorry for my french (c).
UPD.
Try to grad User's data from SecurityContext. Create new file UserController. And that is works fine. Still have problems with access to H2 Console...
#RestController
#RequestMapping("/api/v1")
public class UserController {
#Autowired
private TaskRepository tasks;
#GetMapping("/tasks")
public List<Task> getAllTasks() {
List<Task> allTasks = tasks.findAll();
List<Task> userTasks = new ArrayList<>();
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
String username = ((UserDetails)principal).getUsername();
for (Task t: allTasks) {
if (t.getUser().getUsername().equals(username)) {
userTasks.add(t);
}
}
return userTasks;
}
}
I'm creating a REST API in java for an online store with Spring Boot, I want to securely store user passwords in the database,
for this I am using BCrypt that comes included with spring security, I use MySQL and JPA-Hibernate for persistence.
And I am implementing it as follows:
This is the user entity:
#Entity
#SelectBeforeUpdate
#DynamicUpdate
#Table (name = "USER")
public class User {
#Id
#GeneratedValue
#Column(name = "USER_ID")
private Long userId;
#Column(name = "ALIAS")
private String alias;
#Column(name = "NAME")
private String name;
#Column(name = "LAST_NAME")
private String lastName;
#Column(name = "TYPE")
private String type;
#Column(name = "PASSWORD")
private String password;
public String getPassword() {
return password;
}
/**
* When adding the password to the user class the setter asks if it is necessary or not to add the salt,
* if this is necessary the method uses the method BCrypt.hashpw (password, salt),
* if it is not necessary to add the salt the string That arrives is added intact
*/
public void setPassword(String password, boolean salt) {
if (salt) {
this.password = BCrypt.hashpw(password, BCrypt.gensalt());
} else {
this.password = password;
}
}
//Setters and Getters and etc.
}
This is the repository of the user class:
#Repository
public interface UserRepository extends JpaRepository<User, Long> {
}
This is the service of the user class:
#Service
public class UserService{
private UserRepository userRepository;
#Autowired
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public User addEntity(User user) {
//Here we tell the password setter to generate the salt
user.setPassword(user.getPassword(), true);
return userRepository.save(user);
}
public User updateEntity(User user) {
User oldUser = userRepository.findOne(user.getUserId());
/*
*This step is necessary to maintain the same password since if we do not do this
*in the database a null is generated in the password field,
*this happens since the JSON that arrives from the client application does not
*contain the password field, This is because to carry out the modification of
*the password a different procedure has to be performed
*/
user.setPassword(oldUser.getPassword(), false);
return userRepository.save(user);
}
/**
* By means of this method I verify if the password provided by the client application
* is the same as the password that is stored in the database which is already saved with the salt,
* returning a true or false boolean depending on the case
*/
public boolean isPassword(Object password, Long id) {
User user = userRepository.findOne(id);
//To not create an entity that only has a field that says password, I perform this mapping operation
String stringPassword = (String)((Map)password).get("password");
//This method generates boolean
return BCrypt.checkpw(stringPassword, user.getPassword());
}
/**
*This method is used to update the password in the database
*/
public boolean updatePassword(Object passwords, Long id) {
User user = userRepository.findOne(id);
//Here it receive a JSON with two parameters old password and new password, which are transformed into strings
String oldPassword = (String)((Map)passwords).get("oldPassword");
String newPassword = (String)((Map)passwords).get("newPassword");
if (BCrypt.checkpw(oldPassword, user.getPassword())){
//If the old password is the same as the one currently stored in the database then the new password is updated
//in the database for this a new salt is generated
user.setPassword(newPassword, true);
//We use the update method, passing the selected user
updateEntity(user);
//We return a true boolean
return true;
}else {
//If the old password check fails then we return a false boolean
return false;
}
}
//CRUD basic methods omitted because it has no case for the question
}
This is the controller that exposes the API endpoints:
#RestController
#CrossOrigin
#RequestMapping("/api/users")
public class UserController implements{
UserService userService;
#Autowired
public UserController(UserService userService) {
this.userService = userService;
}
#RequestMapping( value = "", method = RequestMethod.POST )
public User addEntity(#RequestBody User user) {
return userService.addEntity(user);
}
#RequestMapping( value = "", method = RequestMethod.PUT )
public User updateEntity(#RequestBody User user) {
return userService.updateEntity(user);
}
#RequestMapping( value = "/{id}/checkPassword", method = RequestMethod.POST )
public boolean isPassword(#PathVariable(value="id") Long id, #RequestBody Object password) {
return userService.isPassword(password, id);
}
#RequestMapping( value = "/{id}/updatePassword", method = RequestMethod.POST )
public boolean updatePassword(#PathVariable(value="id") Long id, #RequestBody Object password) {
return userService.updatePassword(password, id);
}
}
This is where my question comes, my method is working but I feel it is not the best way, I do not feel comfortable changing the password setter I would prefer to keep the standard form of a setter, as in the user service I think there is Opportunity to handle the user and password update differently, so try to use the #DynamicUpdate annotation in the entity but it simply does not work properly since the fields not provided in the update instead of leaving them as they were are saved Like nulls.
What I'm looking for is a better way to handle the security of passwords using Spring Boot.
First of all you would like to have a unique field for each user in your online store (f.e. alias, or email), to use it as an identifier, without exposing id value to the end users.
Also, as I understand, you want to use Spring Security to secure your web application. Spring security uses ROLEs to indicate user authorities (f.e. ROLE_USER, ROLE_ADMIN). So it would be nice to have a field (a list, a separate UserRole entity) to keep track of user roles.
Let's assume, that you added unique constraint to User field alias (private String alias;) and added simple private String role; field. Now you want to set up Spring Security to keep '/shop' and all sub-resources (f.e. '/shop/search') open to everyone, unsecured, resource '/discounts' available only for registered users and resource '/admin' available for administrator only.
To implement it, you need to define several classes. Let's start with implementation of UserDetailsService (needed by Spring Security to get user information):
#Service
public class UserDetailsServiceImpl implements UserDetailsService {
private final UserRepository repository;
#Autowired
public UserDetailsServiceImpl(UserRepository repository) {
this.repository = repository;
}
#Override
public UserDetails loadUserByUsername(String alias) {
User user = repository.findByAlias(alias);
if (user == null) {
//Do something about it :) AFAIK this method must not return null in any case, so an un-/ checked exception might be a good option
throw new RuntimeException(String.format("User, identified by '%s', not found", alias));
}
return new org.springframework.security.core.userdetails.User(
user.getAlias(), user.getPassword(),
AuthorityUtils.createAuthorityList(user.getRole()));
}
}
Then, the main class for configuring Spring Security is one, that extends WebSecurityConfigurerAdapter (the example was taken from the application with a form based authentication, but you can adjust it for your needs):
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private UserDetailsService userDetailsService;
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/", "/shop/**").permitAll()
.antMatchers("/discounts/**").hasRole("USER")
.antMatchers("/admin/**").hasRole("ADMIN")
.and()
.formLogin()
.usernameParameter("alias")
.passwordParameter("password")
.loginPage("/login").failureUrl("/login?error").defaultSuccessUrl("/")
.permitAll()
.and()
.logout()
.logoutUrl("/logout")
.clearAuthentication(true)
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID", "remember-me")
.logoutSuccessUrl("/")
.permitAll();
}
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth
.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder());
}
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
Then, in your UserService you can use something like:
...
#Autowired
private PasswordEncoder passwordEncoder;
public User addEntity(User user) {
...
user.setPassword(passwordEncoder.encode(user.getPassword()))
...
}
All other checks (f.e. for login attempt or for accessing resource) Spring Security will do automatically, according to the configuration. There are many more things to setup and consider, but I hope I was able to explain the overall idea.
EDIT
Define bean as follows within any spring Component or Configuration
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
Then autowire it in your UserService class
#Service
public class UserService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
#Autowired
public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder) {
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
}
public User addEntity(User user) {
user.setPassword(passwordEncoder.encode(user.getPassword());
return userRepository.save(user);
}
...
public boolean isPassword(Object password, Long id) {
User user = userRepository.findOne(id);
String stringPassword = (String)((Map)password).get("password");
return passwordEncoder.matches(stringPassword, user.getPassword());
}
public boolean updatePassword(Object passwords, Long id) {
User user = userRepository.findOne(id);
String oldPassword = (String)((Map)passwords).get("oldPassword");
String newPassword = (String)((Map)passwords).get("newPassword");
if (!passwordEncoder.matches(oldPassword, newPassword)) {
return false;
}
user.setPassword(passwordEncoder.encode(newPassword));
updateEntity(user);
return true;
}
...
}
After that you can keep simple setter in User class.
I am actually beginner in Spring framework. For today i've faced with problems and i want ask you a couple questions to figure it out.
I want to make simple user login/authorisation app with Android client.
First of all I want to post my code:
Model:
#Entity
#Table(name = "Users")
public class User {
public User() {
}
#Id
#GeneratedValue(generator = "increment")
#GenericGenerator(name = "increment", strategy = "increment")
#Column(name = "Id", unique = true, nullable = false)
private long id;
#Column(name = "userName", nullable = false)
private String userName;
#Column(name = "userPassword", nullable = false)
private String userPassword;
#Transient
private String confirmPassword;
public long getId() {
return id;
}
public String getUsername() {
return userName;
}
public String getPassword() {
return userPassword;
}
public String getConfirmPassword() {
return confirmPassword;
}
public void setId(long id) {
this.id = id;
}
public void setName(String userName) {
this.userName = userName;
}
public void setPassword(String userPassword) {
this.userPassword = userPassword;
}
public void setConfirmPassword(String confirmPassword) {
this.confirmPassword = confirmPassword;
}
}
Services:
User details:
#Service("userDetailsServiceImpl")
public class UserDetailsServiceImpl implements UserDetailsService {
#Autowired
private UserRepository userRepository;
#Override
#Transactional(readOnly = true)
public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
User user = userRepository.findByUserName(userName);
Set<GrantedAuthority> grantedAuthorities = new HashSet<GrantedAuthority>();
return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), grantedAuthorities);
}
}
Impl of UserSerice interface:
#Service
public class UserServiceImpl implements UserService {
#Autowired
private UserRepository userRepository;
#Autowired
private BCryptPasswordEncoder passwordEncoder;
public void save(User user) {
user.setPassword(passwordEncoder.encode(user.getPassword()));
userRepository.save(user);
}
public User findByUserName(String userName) {
return userRepository.findByUserName(userName);
}
}
User validator:
#Component
public class UserValidator implements Validator {
#Autowired
private UserService userService;
#Override
public boolean supports(Class aClass) {
return User.class.equals(aClass);
}
#Override
public void validate(Object o, Errors errors) {
User user = (User) o;
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "username", "Required");
if (user.getUsername().length() < 8 || user.getUsername().length() > 32) {
errors.rejectValue("username", "Size.userForm.username");
}
if (userService.findByUserName(user.getUsername()) != null) {
errors.rejectValue("username", "Duplicate.userForm.username");
}
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "password", "Required");
if (user.getPassword().length() < 8 || user.getPassword().length() > 32) {
errors.rejectValue("password", "Size.userForm.password");
}
if (!user.getConfirmPassword().equals(user.getPassword())) {
errors.rejectValue("confirmPassword", "Different.userForm.password");
}
}
}
Impl of SecurityService
#Service
public class SecurityServiceImpl implements SecurityService {
#Autowired
private AuthenticationManager authenticationManager;
#Autowired
private UserDetailsService userDetailsService;
#Override
public String findLoggedInUsername() {
Object userDetails = SecurityContextHolder.getContext().getAuthentication().getDetails();
if (userDetails instanceof UserDetails) {
return ((UserDetails) userDetails).getUsername();
}
return null;
}
#Override
public void autoLogin(String username, String password) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(userDetails, password, userDetails.getAuthorities());
authenticationManager.authenticate(authenticationToken);
if (authenticationToken.isAuthenticated()) {
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
}
}
SecurityConfig:
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(securedEnabled = true)
#ComponentScan("com.webserverconfig.user")
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
#Qualifier("userDetailsServiceImpl")
UserDetailsService userDetailsService;
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
#Bean
public PasswordEncoder passwordEncoder() {
PasswordEncoder encoder = new BCryptPasswordEncoder();
return encoder;
}
#Bean
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
And the lsat one -> Controller:
#RestController
#RequestMapping("/user")
public class UserController {
#Autowired
private UserService userService;
#Autowired
private SecurityService securityService;
#Autowired
private UserValidator userValidator;
#RequestMapping(value = "/registration", method = RequestMethod.POST)
#ResponseBody
#ResponseStatus(value = HttpStatus.CREATED)
public User registration(#RequestBody User user, BindingResult bindingResult, Model model) {
userValidator.validate(user, bindingResult);
if (bindingResult.hasErrors()) {
//What should i return here to my Android client ?
}
userService.save(user);
securityService.autoLogin(user.getUsername(), user.getConfirmPassword());
return user;
}
}
I am missing some classes here for saving space.
I am asking you for help me with my questions, please:
1) When i am trying to send JSON using Postman:
{
"id": 1,
"userName": "Andrew",
"userPassword": "apoyark123",
"confirmPassword": "apoyark123"
}
I am getting next erorr:
DefaultHandlerExceptionResolver - Failed to read HTTP message: org.springframework.http.converter.HttpMessageNotReadableException: Could not read document: Unrecognized field "userName" (class com.webserverconfig.user.entity.User), not marked as ignorable (4 known properties: "id", "name", "password", "confirmPassword"])
at [Source: java.io.PushbackInputStream#ddd416; line: 3, column: 16] (through reference chain: com.webserverconfig.user.entity.User["userName"]); nested exception is com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field "userName" (class com.webserverconfig.user.entity.User), not marked as ignorable (4 known properties: "id", "name", "password", "confirmPassword"])
at [Source: java.io.PushbackInputStream#ddd416; line: 3, column: 16] (through reference chain: com.webserverconfig.user.entity.User["userName"])
Am i missing some annotation ?
This issue resolved by Mehdi in the first comment. !!!
Please help me with second question.
2) I am haven't tested security validation part yet, because this code doesn't working, but i have a question -> How my client will understand that login/authorisation have gone wrong/good ?
If we take a look on controller's method registration(...) -> what should i return to the client if first "if" false ?
What if validation is not correct ? What should i return to the client and how ?