Skip to main content Link Menu Expand (external link) Document Search Copy Copied

Spring Roles for User/Admin • 26 min read

Description

Lesson on roles.

Lesson Materials

Our lesson uses backend code from this repository. If you want to follow along in this lesson, please git clone with the line below and run Main.java to use localhost:

git clone https://github.com/John-sCC/Roles_BE.git

The contents of this lesson are pretty important for projects!

Person and PersonRole Relationship

Person Object

(See Person.java)

The Person object is a POJO, which stands for “Plain Old Java Object.” Essentially, it’s a Java object with no special restrictions or requirements. It contains business logic and data for modeling entities.

Of the attributes of the object Person, one is roles, which is a Collection of PersonRole objects. These are the roles that we go over in this lesson.

// FULLY IMPLEMENTED!
@ManyToMany(fetch = EAGER)
private Collection<PersonRole> roles = new ArrayList<>();

As a quick recap of SQL concepts:

  • @ManyToMany establishes that Person and PersonRole have a Many-to-Many relationship with each other (the same role can be assigned to many Person objects, and multiple Person object can be assigned to many roles).
  • fetch = EAGER establishes that, when a Person object is fetched, its roles should be fetched immediately at the same time. If it was instead fetch = LAZY, the PersonRole objects would only be fetched explicity, separate from the Person object.

PersonRole Object

(See PersonRole.java)

The PersonRole object is another POJO with its own entities separate from the Person object. They are assigned to each other. In preparation for the lesson, we added an argument constructor and an initializer method.

public class PersonRole {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @Column(unique=true)
    private String name;

    public PersonRole(String name) {
        this.name = name;
    }

    public static PersonRole[] init() {
        PersonRole student = new PersonRole("ROLE_STUDENT");
        PersonRole teacher = new PersonRole("ROLE_TEACHER");
        PersonRole admin = new PersonRole("ROLE_ADMIN");
        PersonRole[] initArray = {student, teacher, admin};
        return initArray;
    }
}

Initializing PersonRole

Now that we have an initializer for the PersonRole object, we add it to the ModelInit.java file as shown below. (Modified slightly to emphasize roles.)

public class ModelInit {  
    // ...declarations...
    @Autowired PersonRoleJpaRepository roleRepo;

    @Bean
    CommandLineRunner run() {  // runs when the application starts
        return args -> {
            // ...jokes...

            // initializing person roles
            PersonRole[] personRoles = PersonRole.init();
            for (PersonRole role : personRoles) {
                PersonRole existingRole = roleRepo.findByName(role.getName());
                if (existingRole != null) {
                    // role already exists
                    continue;
                } else {
                    // role doesn't exist
                    roleRepo.save(role);
                }
            }

            // initializing person objects
            Person[] personArray = Person.init();
            for (Person person : personArray) {
                //findByNameContainingIgnoreCaseOrEmailContainingIgnoreCase
                List<Person> personFound = personService.list(person.getName(), person.getEmail());  // lookup
                if (personFound.size() == 0) {
                    personService.save(person);  // save
                    // ...notes...
                    // adding the student role to each initial person
                    personService.addRoleToPerson(person.getEmail(), "ROLE_STUDENT");
                }
            }
            // for lesson demonstration: giving admin role to Mortensen
            personService.addRoleToPerson(personArray[4].getEmail(), "ROLE_ADMIN");
        };
    }
}

The code is written so that, if the SQL database has been emptied, the roles will be recreated, but if it hasn’t been, they won’t be created a second time, which would cause an error (since the names would be repeats).

We gave one of the test Person objects, “John Mortensen,” the “ROLE_ADMIN” role. This will be used in conjunction with roles-based security.

Viewing Person and PersonRole in SQL

Now that the roles have been created and assigned, they can be viewed in the SQL table in two different ways that offer different information.

If you open the sqlite.db and look at the “person_role” (singular) table, you’ll see something like this:

This shows each role and its corresponding ID. It works basically the same way as the “person” table.

The second table has the title “person_roles” (plural). This type of table is called a “join table,” and it represents Many-to-Many relationships by showing the corresponding IDs of objects with relationships in pairs.

All Person objects other than the one with ID 5 only have a relationship to PersonRole ID 1, which is the “ROLE_STUDENT” role given when initialized. The Person with the ID 5 was given both “ROLE_STUDENT” and “ROLE_ADMIN,” so he has two different relationships shown.

Using Roles for Security

The first step to implementing security with roles is found in the file PersonDetailsService.java. The method below finds a user based on username, and then stores its roles as a Collection of SimpleGrantedAuthority based on the role names. All user details, including email, password and authorities are returned as “userdetails”.

/* UserDetailsService Overrides and maps Person & Roles POJO into Spring Security */
@Override
public org.springframework.security.core.userdetails.UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
    Person person = personJpaRepository.findByEmail(email); // setting variable user equal to the method finding the username in the database
    if(person==null) {
        throw new UsernameNotFoundException("User not found with username: " + email);
    }
    Collection<SimpleGrantedAuthority> authorities = new ArrayList<>();
    person.getRoles().forEach(role -> { //loop through roles
        authorities.add(new SimpleGrantedAuthority(role.getName())); //create a SimpleGrantedAuthority by passed in role, adding it all to the authorities list, list of roles gets past in for spring security
    });
    // train spring security to User and Authorities
    return new org.springframework.security.core.userdetails.User(person.getEmail(), person.getPassword(), authorities);
}

Where is this method called? Why, when you make a /authenticate request, silly!

In JwtApiController.java, you can see it used when an authenticate request is called. The “userdetails” are used to create a JWT that is sent to the requester if the request is valid.

@PostMapping("/authenticate")
	public ResponseEntity<?> createAuthenticationToken(@RequestBody Person authenticationRequest) throws Exception {
		authenticate(authenticationRequest.getEmail(), authenticationRequest.getPassword());
		final UserDetails userDetails = personDetailsService
				.loadUserByUsername(authenticationRequest.getEmail()); // HERE IT IS!!
		final String token = jwtTokenUtil.generateToken(userDetails);
		final ResponseCookie tokenCookie = ResponseCookie.from("jwt", token)
			.httpOnly(true)
			.secure(true)
			.path("/")
			.maxAge(3600)
			.sameSite("None; Secure")
			.build();
		return ResponseEntity.ok().header(HttpHeaders.SET_COOKIE, tokenCookie.toString()).build();
	}

Now that we have a cookie that tells us what the logged-in user’s roles are, we can use that as authority to make certain requests. In SecurityConfig.java, we added .hasAnyAuthority("ROLE_ADMIN") to the “mvc” and “api” update and delete requests.

public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .csrf(csrf -> csrf
        	.disable()
        )
        // list the requests/endpoints need to be authenticated
        .authorizeHttpRequests(auth -> auth
        	.requestMatchers("/authenticate").permitAll()
        	.requestMatchers("/mvc/person/update/**", "/mvc/person/delete/**").hasAnyAuthority("ROLE_ADMIN") // must be admin for these
        	.requestMatchers("/api/person/post/**", "/api/person/delete/**").hasAnyAuthority("ROLE_ADMIN")
        	.requestMatchers("/**").permitAll()
    	)
}

As a result, only a signed-in user with Admin permissions will be able to make these requests successfully. Now, we will show it in action.

Frontend Application

MVC on Localhost

@GetMapping("/read")
    public String person(Model model) {
        List<Person> list = repository.listAll();
        model.addAttribute("list", list);
        return "person/read";
    }

When calling the /read endpoint the backend then returns a list of people stored in the person database. The line return"person/read" indicates to Spring that the page to be displayed is the read.html file.

For more examples of referencing, take a look at the PersonViewController.java file.

Login on Blog

 <div class="container bg-secondary py-4">
        <div class="p-5 mb-4 bg-light text-dark rounded-3">
            <h1>Login</h1>
            <label for="email">Username:</label><br>
            <input type="text" id="username" name="username"><br>
            <label for="password">Password:</label><br>
            <input type="text" id="password" name="password"><br><br>
            <input type="submit" value="Login" onclick="login()">
            <p id="message"></p>
        </div>
    </div>
    <script>
        function login() {
            var email = document.getElementById('username').value;
            var password = document.getElementById('password').value;
            var data = {email:email, password:password};
            fetch("/authenticate", {method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(data)}).then((data) => {
                if (data.status == 200) {
                    window.location.replace("/mvc/person/read");
                } else {
                    document.getElementById('message').innerHTML = "Invalid email or password"
                }
            });
        }
    </script>

The login page calls the authenticate endpoint, and allows the user to log in and generate the JWT cookie that can be used to authenticate whether the user has the right to perform certain operations on the system (as defined in the SecurityConfig.java).

Update

<tr th:each="person : ${list}">
    <td th:text="${person.id}">Person ID</td>
    <td th:text="${person.email}">Birth Date</td>
    <td th:text="${person.name}">Name</td>
    <td th:if="${person.getAge() != -1}" th:text="${person.getAge()}">Age</td>
    <td th:unless="${person.getAge() != -1}" th:text="Unknown">Unknown Age</td>
    <td>
        <!--- <a th:href="@{/mvc/notes/{id}(id = ${person.id})}">Notes</a> -->
        <a th:href="@{/mvc/person/update/{id}(id = ${person.id})}">Update</a>
        <a th:href="@{/mvc/person/delete/{id}(id = ${person.id})}">Delete</a>
    </td>
</tr>

The thymeleaf here checks if, when the user presses on the Update or Delete tags, they have appropriate roles by calling the API. It gets the user ID for each user in the table and runs the restricted endpoints /update and /delete. If the user has the appropriate role (ROLE_ADMIN), then the user is allowed to continue the operation. Otherwise it throws the 403 error.

Get a JWT

For a non-admin user, try:

  • Email: “toby@gmail.com”
  • Password: “123Toby!”

For an admin user, try:

  • Email: “jm1021@gmail.com”
  • Password: “123Qwerty!”




After clicking the “Login” button, make sure to check the console to ensure the fetch worked correctly! You’ll need to be running the localhost for the provided backend.

Anatomy of a JWT

The JWT token generated from the /authenticate endpont gives the server the cookie that determines the user’s role (it’s encrypted right now).

You can customize JWT tokens while following certain structure. A token contains a Header, Payload, and Signature in the form header.payload.signature. You should be able to see this in the cookie you received after making the authenticate request!

*Note: the following paramters are not the same in the Spring library we use. That library is ResponseCookie, which has separate online documentation for the specific paramters.

Includes the type of token and signing/encryption algorithm. Ex:

{
    "alg": "HS256",
    "typ": "JWT"
}

Payload

The payload contains the claims/description/customized properties of the token. They can be split into Registered Claims, Public Claims, and Custom Claims.

Registered

Predefined claims, basically mandatory for function. Include:

  • “iss” (Issuer)
  • “sub” (Subject)
  • “aud” (Audience)
  • “exp” (Expiration Time)
  • “iat” (Time the token was issued at)

Public

Existing, predefined claims but optional. Include:

  • “name”
  • “family”
  • “email”

Private

Custom claims such as “admin”

Signature

Created by the encoded header, used to verify the token is consistent and sent by the same person. Automatically generated when using the Spring library.

Helpful Website

If you visit this website and enter your encoded JWT, it will give you information about it.