1. Introduction
1.1. Global overview
-
Implementing a very simple security using hard coded username and password to get it started
-
Add real DB username and password
1.2. Assumptions
-
you are used to create normal package names
-
nl.acme.applicationName.repository for the repositories
-
nl.acme.applicationName.service for the services
-
-
you are aware of JPA and know how to create an @Entity
-
You know what Maven is
-
You know how to handle your database whether MySQL or H2
1.3. Problems you might run into
-
@EnableGlobalMethodSecurity in SecurityConfig should be changed to ⇒
-
@EnableGlobalMethodSecurity(securedEnabled = true)
1
2
3
4
5
6
7
8
9
@Configuration
@EnableGlobalMethodSecurity // <= !!!!!!!!!!!!!!!!!!!!!!!!
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
public static final String API = "API";
.
.
.
1
2
3
4
5
6
7
8
9
@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true) // <= !!!!!!!!!!!!!!!!!!!!!!!!
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
public static final String API = "API";
.
.
.
-
Symptom: Something like there is no password encoding
-
Cause: Since Spring Security 5 something has changed using password encoding
-
Fix: Add the following line to the SecurityConfig class his configureGlobal method
-
passwordEncoder(NoOpPasswordEncoder.getInstance())
-
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth,
@Value("${api.username}") String user,
@Value("${api.password}") String pass) throws Exception{
auth.inMemoryAuthentication()
.passwordEncoder(NoOpPasswordEncoder.getInstance()) // !!!
.withUser(user)
.password(pass)
.roles(API);
-
Symptom: When starting the H2-console the screen appears to be blank
-
Cause: FrameOptions not set
-
Fix: Add the following line to the SecurityConfig::configure method
-
httpSecurity.headers().frameOptions().disable();
-
.
.
.
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
// we don't need CSRF because our token is invulnerable
.csrf().disable()
...
...
...
// for using the H2 Console
httpSecurity.headers().frameOptions().disable(); // <= Add this line!!!!!!!!!!!!!!!!!!
}
-
org.springframework.security
-
org.slf4j
2. Topic: Simple Security
2.1. Add Maven dependency for introducting Spring Security
<!-- for using Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
Be aware that after this addition in pom.xml the application will not behave normal anymore |
2.2. Implement SecurityConfig to have a simple REALM
In fact we are creating a REALM now; which is a user and password database
Create the following SecurityConfig.java file in a package nl.acme.applicationName.config You might copy and paste this code. (What else should you) but you MUST know that you are creating a realm and stating that some URLs are not open anymore.
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
@Configuration
@EnableGlobalMethodSecurity
@EnableWebSecurity
// see
// http://docs.spring.io/spring-security/site/docs/3.2.x/reference/htmlsingle/#multiple-httpsecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
public static final String API = "API";
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth,
@Value("${api.username}") String apiUsername,
@Value("${api.password}") String apiPassword) throws Exception{
auth.inMemoryAuthentication().withUser(apiUsername).password(apiPassword).roles(API);
}
@Configuration
@Order(1)
public static class AuWebSecurityAdapterRest extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity.csrf().disable();
httpSecurity.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
httpSecurity.authorizeRequests().antMatchers("/api/**").hasRole(API).and().httpBasic();
httpSecurity.authorizeRequests().anyRequest().permitAll();
// the rest is implicit denied
}
}
}
2.3. Add user and password to application.properties
api.username=<choose your username>
api.password=<choose your password>
you should remote the < and > brackets also |
2.4. Validating
In Postman try to access your endpoint which should fail
The trainer should demo you the Postman settings for getting username and password in but if you are impatient :-) you might use it like this

3. Topic: Advanced Security
3.1. Add a domain class for the User
@Entity
public class User {
@Id
@GeneratedValue
private long id;
private String username;
private String password;
// add getters and setters (except setId)
}
@Repository
public interface UserRepository extends CrudRepository<User, Long>{
Optional<User> findByUsernameAndPassword(String username, String password);
}
3.2. Create a UserService
@Service
public class UserService {
@Autowired
private UserRepository repository;
public boolean authenticate(String username, String password) {
return this.repository.findByUsernameAndPassword(username, password).isPresent();
}
3.3. Create a UserAuthenticationProvider
Create this file somewhere in a package called security e.g. nl.acme.applicationName.security
@Component
public class UserAuthenticationProvider implements AuthenticationProvider {
private static final Logger LOGGER = LoggerFactory.getLogger(UserAuthenticationProvider.class);
@Autowired
private UserService userService;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
final String username = authentication.getName();
final String password = authentication.getCredentials().toString();
if (this.userService.authenticate(username, password)) {
List<GrantedAuthority> grantedAuths = new ArrayList<>();
grantedAuths.add(new AuthenticationTokenGrantedAuthority());
LOGGER.debug("Validation succeeded for [{}]!", username);
return new UsernamePasswordAuthenticationToken(username, password, grantedAuths);
}
else {
LOGGER.error("Validation failed for [{}]", username);
throw new AuthenticationTokenAuthenticationException(String.format("Validation failed for %s", username));
}
}
@Override
public boolean supports(Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
}
// this is a class in the same file as the above created UserAuthenticationProvider
class AuthenticationTokenAuthenticationException extends AuthenticationException {
private static final long serialVersionUID = -9139561336028106640L;
public AuthenticationTokenAuthenticationException(String msg) {
super(msg);
}
}
class AuthenticationTokenGrantedAuthority implements GrantedAuthority {
private static final long serialVersionUID = -4893914488018936401L;
@Override
public String getAuthority() {
return "ROLE_"+SecurityConfig.API;
}
}
3.4. Finally wire the UserAuthenticationProvider in the SecurityConfig
@Autowired
private UserAuthenticationProvider authenticationProvider; // <=
public static final String API = "API";
The parameters annotated with @Value in SecurityConfig::configureGlobal can now be removed. You can also remove the api.username and api.password entries from your application.properties file
In the method configureGlobal(…) in SecurityConfig remove the ENTIRE body of the method
In the method configureGlobal(…) in the class SecurityConfig add the following line to the body of the method
auth.authenticationProvider(authenticationProvider);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import nl.capgemini.divingweb.security.UserAuthenticationProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
// see
// http://docs.spring.io/spring-security/site/docs/3.2.x/reference/htmlsingle/#multiple-httpsecurity
@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true)
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
public static final String API = "API";
@Autowired
private UserAuthenticationProvider authenticationProvider;
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authenticationProvider);
}
@Configuration
@Order(1)
public static class AuWebSecurityAdapterRest extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity.csrf().disable();
httpSecurity.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
httpSecurity.authorizeRequests().antMatchers("/api/**").hasRole(API).and().httpBasic();
httpSecurity.authorizeRequests().anyRequest().permitAll();
// the rest is implicit denied
}
}
}
3.5. Adding users to the database
Using your knowledge of MySQL just insert a username and password the the table User of your database of create a RestController which is able to insert a User using a POST to api/users
3.6. Validating
The REST service should now normally respond like the first part of this exercise
4. Topic: Using Bcrypt
4.1. Introduction
In the previous section we learned that we can save or username and password in plain text. That was OK for that moment. In the wild it would not suffice. In the wild the saving of the password should be hashed.
The fixing of that problem will be the subject of this section
4.2. What you will learn
-
How to create a bean for using for PasswordEncoding
-
How to create a bean for using for DaoAuthentication
-
How to add a UserService which implements UserDetailsService for
-
saving users with password encoding
-
fetching a user
-
4.3. Why: Using Bcrypt
To prevent that a person with criminal intent can read the password directly from the database
4.4. When: Using Bcrypt
When you want to prevent that a person with criminal intent can read the password directly from the database - and that is always in the wild
4.5. What: Using Bcrypt
Get a big cup of coffee and Read this to get informed regarding Bcrypt
4.6. How: Using Bcrypt
Below you find the steps to take to get the passwords hashed with Bcrypt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package nl.capgemini.divingweb.config;
import nl.capgemini.divingweb.service.security.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
public class MyBeans {
@Autowired
private UserService userService;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public DaoAuthenticationProvider authenticationProvider(@Autowired PasswordEncoder passwordEncoder) {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(this.userService);
authProvider.setPasswordEncoder(passwordEncoder);
return authProvider;
}
}
-
12: @Configuration: to tell Spring Boot that he has to look here (just like @Repository and @RestController)
-
15: The wired in UserService
-
19: the passwordEncoder; THE BEAN where this story is all about
-
25: the DaoAuthenticationProvider. The bean used to be used as the authenticationProvider WHICH NOW uses the userDetailsService!!!
-
change the field::UserAuthenticationProvider to DaoAuthenticationProvider
1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true)
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
public static final String API = "API";
@Autowired
private DaoAuthenticationProvider authenticationProvider;
.
.
.
-
change this line: httpSecurity.authorizeRequests().antMatchers("/api/**").hasRole(API).and().httpBasic();
-
to this line: httpSecurity.authorizeRequests().antMatchers("/api/**").authenticated().and().httpBasic();//hasRole(API).and().httpBasic();
-
To have username and password running first and later add roles again
1
2
3
4
5
6
7
@Repository
public interface UserRepository extends CrudRepository<User, Long> {
Optional<User> findByUsernameAndPassword(String username, String password);
User findByUsername(String username); // <=
}
Remove the UserAuthenticationProvider AND HIS inner classes!!!!!!!!!!!
-
Autowire the passwordEncoder in the UserService
-
Add a @PostConstruct method to the UserService to have one or more users when starting
-
UserService should implement UserDetailsService
-
implement the UserDetailsService interface method loadUserByUsername(String username) to the UserService
-
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
package nl.capgemini.divingweb.service.security;
import nl.capgemini.divingweb.model.security.User;
import nl.capgemini.divingweb.persistence.security.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
@Service
public class UserService implements UserDetailsService {
private static final String dummyUser = "john";
private static final String dummyPassword = "doe";
@Autowired
private UserRepository repository;
@Autowired
private PasswordEncoder passwordEncoder;
@PostConstruct
public void setUp() {
Optional<User> optionalUser = this.repository.findByUsernameAndPassword(dummyUser, dummyPassword);
if(!optionalUser.isPresent()) { // OK insert him
User user = new User();
user.setUsername(dummyUser);
user.setPassword(passwordEncoder.encode(dummyPassword));
this.repository.save(user);
}
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = this.repository.findByUsername(username);
UserDetails userDetails = new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), new ArrayList<>());
return userDetails;
}
}
4.7. Assignment: Using Bcrypt
To learn working with BCrypt hashing in a Spring Boot application
During this assignment you will add BCrypt hashing to the user authentication process
-
Given the code above
-
Implement it to the application
-
Run the app and try to login using Postman with username:john and password:doe
4.8. Conclusion
During this section we learned how to make use of BCrypt so that we have a secure security model In the following section we will learn that we can (also) make use of Role(s) to authorize a request
4.9. Follow-up: Using Bcrypt
Below you find for this topic some extra resources to watch after the week the topic Using Bcrypt is introduced during the training
-
movie 1
-
link one
5. Topic: Add Authorities
5.1. Introduction
In the previous section we learned that we can sign on using a username and password and we use a database for authorization.
In the wild we also want to use Roles (called Authorities) to authorize a request for some url
Using Authorities with a Database oriented authentication scheme is the subject of this section
5.2. What you will learn
-
How to setup the Authorities for autorisation
5.3. Why and When: Add Authorities
When you want to authorise based on a kind of Role / Authority and not only based on username and password
5.4. What: Add Authorities
-
An authority is a relationship (using JPA) from the User table to the table Authority
5.5. How: Add Authorities
1
2
3
4
5
package nl.capgemini.divingweb.model.security;
public enum AuthorityName {
USER, API
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package nl.capgemini.divingweb.model.security;
import org.springframework.security.core.GrantedAuthority;
import javax.persistence.*;
import javax.validation.constraints.NotNull;
import java.util.ArrayList;
import java.util.List;
@Entity
public class Authority implements GrantedAuthority {
@Id
@GeneratedValue(strategy= GenerationType.AUTO)
private Long id;
@NotNull
@Enumerated(EnumType.STRING)
private AuthorityName name;
@ManyToMany(mappedBy = "authorities", fetch = FetchType.EAGER)
private List<User> users = new ArrayList<>();
public Long getId() {
return id;
}
public void setName(AuthorityName name) {
this.name = name;
}
public List<User> getUsers() {
return users;
}
@Override
public String getAuthority() {
return this.name.toString();
}
}
-
11: The class which we are going to use for the Authority should implement the interface GrantedAuthority which makes the implementation of String getAuthority() mandatory
-
18: The AuthorityName name which is an enum should be persisted as String and not the ordinal number of the enum
-
21 and beyond: The Authority has some users
1
2
3
4
5
6
7
8
9
package nl.capgemini.divingweb.persistence.security;
import nl.capgemini.divingweb.model.security.Authority;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface AuthorityRepository extends CrudRepository<Authority, Long> {
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package nl.capgemini.divingweb.service.security;
import nl.capgemini.divingweb.model.security.Authority;
import nl.capgemini.divingweb.persistence.security.AuthorityRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class AuthorityService {
@Autowired
private AuthorityRepository authorityRepository;
public Authority save(Authority a) {
return this.authorityRepository.save(a);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package nl.capgemini.divingweb.model.security;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
private String username;
private String password;
@ManyToMany(fetch = FetchType.EAGER)
private List<Authority> authorities = new ArrayList<>();
public long getId() {
return 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 List<Authority> getAuthorities() {
return authorities;
}
public void setAuthorities(List<Authority> authorities) {
this.authorities = authorities;
}
}
-
Simply said, the inverse relationship of the Authority which is mentioned previously
-
Autowire the AuthorityService in the UserService
-
Modify the setUp method for having some users to persist the Authority for the user
-
In the loadUserByUsername method set the corresponding Authorities for the user
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
package nl.capgemini.divingweb.service.security;
import nl.capgemini.divingweb.model.security.Authority;
import nl.capgemini.divingweb.model.security.AuthorityName;
import nl.capgemini.divingweb.model.security.User;
import nl.capgemini.divingweb.persistence.security.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.util.Optional;
@Service
public class UserService implements UserDetailsService {
private static final String dummyUser = "john";
private static final String dummyPassword = "doe";
@Autowired
private UserRepository repository;
@Autowired
private AuthorityService authorityService; // <==
@Autowired
private PasswordEncoder passwordEncoder;
@PostConstruct
public void setUp() {
Optional<User> optionalUser = this.repository.findByUsernameAndPassword(dummyUser, dummyPassword);
if(!optionalUser.isPresent()) { // OK insert him
User user = new User();
user.setUsername(dummyUser);
user.setPassword(passwordEncoder.encode(dummyPassword));
// from here changes occur ================================
Authority a = new Authority();
a.setName(AuthorityName.API);
this.authorityService.save(a);
user.getAuthorities().add(a);
a.getUsers().add(user);
this.repository.save(user);
this.authorityService.save(a);
// end of the changes =====================================
}
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = this.repository.findByUsername(username);
UserDetails userDetails = new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), user.getAuthorities()); // <= the final parameter (List) is now the list of Authorities
return userDetails;
}
}
1
httpSecurity.authorizeRequests().antMatchers("/api/**").hasAuthority(AuthorityName.API.toString()).and().httpBasic(); // <= this line already existed. So change it! Don't add it!
5.6. Assignment: Add Authorities
To learn how to work with the User and Authority classes
During this assignment you will add the entity Authority to have authorities per user
-
Given the code above
-
Implement it in your source code
-
Open postman
-
Login with username: john and password: doe
-
You should be authorised
5.7. Follow-up: Add Authorities
Below you find for this topic some extra resources to watch after the week the topic Add Authorities is introduced during the training
-
movie 1
-
link one
6. Summary
We have created a in memory and DB oriented Spring Security solution for securing the REST endpoints
-
is that we have Spring Security for securing REST endpoints
-
is that you can use Basic Authentication for securing the REST endpoint
-
we learned how to add BCrypt
-
we learned how to use Authorities for Authorization in our Spring Boot application
-
is that securing endpoints using Basic Authentication is only enough when you are also using SSLv3.x/TLS since otherwise the username and password Base64 encoded String can be reverted trivially.
-
The trainer might (and should) show you …
-