JWT Authentication and Role-Based Authorization with Java Spring Boot
Introduction to JSON Web Tokens (JWTs), Authentication, and Authorization
JSON Web Tokens (JWTs) are a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret or a public/private key pair using the RSA or ECDSA algorithms.
Role in Authentication
Authentication is the act of validating that users are who they claim to be using passwords, biometrics, one-time passwords, and more. JWTs can be used to establish the identity of users. When a user logs in, the server generates a JWT that encodes a set of claims. The server then returns this token to the client, and the client sends the token back with each request. The server verifies the token and only allows the request to proceed if the token is valid.
Role in Authorization
Authorization is the process by which individual users or groups of users are granted access to a specific resource or function. JWTs also play a crucial role in authorization, as they enable servers to know what resources a user can have access to. When the JWT includes claims like user roles or permissions, the server can determine whether to grant or deny access to certain operations or resources.
Importance in API Security and Access Control
Securing APIs and managing access control are critical in web applications. JWTs help in securing APIs by allowing the server to process requests without having to look up too much user information since the token is self-contained. The authorization process using JWTs enables role-based access control, ensuring that users only have access to the resources and operations their role permits.
Overall, JWTs are integral to the security architecture of modern web applications, enabling reliable authentication and fine-grained authorization while also being lightweight and easily transmittable over the web.
Project Setup
Overview of Tools and Technologies
In this project, we are going to employ several key tools and technologies:
Java Spring Boot: An open-source Java-based framework used to create a microservice. It is easy to create stand-alone, production-grade Spring based applications with minimal effort.
H2 Database: A lightweight in-memory database that can be embedded in Java applications or run in the client-server mode. It's perfect for development and testing since it doesn't require any setup like a regular database.
JSON Web Tokens (JWT): A standard token format used in authentication and authorizations that allows you to securely transmit data between parties as a JSON object.
Make sure to consult the official documentation for up-to-date version numbers.
Set Properties in `application.resources`
We will need to configure a few properties related to our H2 database, logging, JWT authentication, and authorization. Here are the settings used in our project:
To secure our web application, we implement Spring Security by defining a SecurityConfig class. This class utilizes a combination of annotations to integrate Spring Security with our project effectively.
Below is an overview of what the SecurityConfig class accomplishes:
@EnableWebSecurity: This annotation activates Spring Security’s web security support and provides the Spring MVC integration. It also extends the Spring Security configuration using HTTPSecurity.
@EnableMethodSecurity: This enables method-level security based on annotations. It allows us to secure methods in our controllers by specifying roles and conditions for access.
@Configuration: The class is marked as a configuration class, and it defines Spring Beans.
@RequiredArgsConstructor: This is a Lombok annotation that generates a constructor with required dependencies (in this case, JwtAuthenticationFilter, UserService, and PasswordEncoder).
The SecurityConfig includes the following Beans:
AuthenticationProvider: This Bean sets the custom user details service and password encoder. It is necessary for authenticating users based on username and password.
AuthenticationManager: This Bean gets the authentication manager from Spring Security's AuthenticationConfiguration which in turns help manage the authentication procedure.
SecurityFilterChain: This crucial Bean within the Spring Security filter chain, allows us to configure the rules and conditions for security along different paths in our application. We disable CSRF protection as it is not needed for REST APIs and define stateless session management. Furthermore, we specify URL patterns and the associated security settings, permitting everyone to access the signup and signin endpoints, whereas protecting all other endpoints. Finally, we ensure that the JWT filter is applied before the UsernamePasswordAuthenticationFilter to extract and verify the token.
This setup is pivotal for JWT-based authentication, ensuring that each request is properly authenticated and has the correct authority to access various resources.
Implement JWT Authentication
JWT Authentication is a key aspect in securing web applications by validating the identity of users via tokens. Let's dive into how the JwtService class in the provided code snippet helps in implementing JWT authentication:
The JwtService class performs several essential actions:
Token Generation: generateToken(UserDetails userDetails) method generates a new token for a user based on their UserDetails. It includes the user's username as the subject, current time as the issue time, and sets the expiration time based on the jwtExpirationMs value.
Token Validation: isTokenValid(String token, UserDetails userDetails) method checks if the token is valid by comparing the username in the token with the username in the passed UserDetails, and also checks if the token has expired.
Extract Claims: Various methods such as extractAllClaims, extractExpiration, and extractUserName help in retrieving specific claims from the token like subject (username) and expiration.
Secret Key: It uses the jwtSecretKey declared in application.properties to decode and sign the tokens.
This code snippet is a sample controller which demonstrates how role-based access-control can be implemented using Spring Security's @PreAuthorize annotation. It provides access to different REST endpoints based on the authenticated user's role.
GET /api/v1/test/users is secured with @PreAuthorize("hasRole('USER')"), restricting access to this endpoint to only allow authenticated users with the USER role.
GET /api/v1/test/admins is annotated with @PreAuthorize("hasRole('ADMIN')"), restricting access to this endpoint to only allow authenticated users with the ADMIN role.
GET /api/v1/test/role_dependent programmatically checks the user's role without using @PreAuthorize, offering a more dynamic approach to role-based access-control.
A more robust BookControllerthat interfaces with our database will be implemented in the next step. This BookController will use these principles of role-based authorization to manage access to book-related resources.
@RestController@RequestMapping("/api/v1/test")publicclassTestController { @GetMapping("/anon")publicStringanonEndPoint() {return"everyone can see this"; } @GetMapping("/users") @PreAuthorize("hasRole('USER')")publicStringusersEndPoint() {return"ONLY users can see this"; } @GetMapping("/admins") @PreAuthorize("hasRole('ADMIN')")publicStringadminsEndPoint() {return"ONLY admins can see this"; } @GetMapping("/role_dependent")publicStringroleDependentEndPoint() {// Get authenticated userAuthentication authentication =SecurityContextHolder.getContext().getAuthentication();// Check if admin roleif (authentication.getAuthorities().stream().anyMatch(a ->a.getAuthority().equals("ROLE_ADMIN"))) {return"Data for admins"; } else {String username =authentication.getName();return"Data for user: "+ username; } }}
Build the Books API
Now that we have set up our project, implemented JWT-based authentication and role-based authorization, let's build the Books API to manage book-related resources. We'll create a BookController to handle CRUD (Create, Read, Update, Delete) operations on books.
@RestController@RequestMapping("/api/v1")@RequiredArgsConstructorpublicclassBookController { @AutowiredprivateBookRepository bookRepository; @GetMapping("/books")publicList<Book> getAllBooks() {// Get the authenticated userAuthentication authentication =SecurityContextHolder.getContext().getAuthentication();String username =authentication.getName();// If user is admin, return all booksif (authentication.getAuthorities().stream().anyMatch(a ->a.getAuthority().equals("ROLE_ADMIN"))) {returnbookRepository.findAll(); } else {// Otherwise, return books created by the userreturnbookRepository.findByCreatedBy(username); } } @GetMapping("/books/{id}")publicResponseEntity<Book> getBookById(@PathVariable(value ="id") Long bookId) throwsConfigDataResourceNotFoundException {// Get the authenticated userAuthentication authentication =SecurityContextHolder.getContext().getAuthentication();String username =authentication.getName();Book book =bookRepository.findById(bookId).orElseThrow(() ->newResponseStatusException(HttpStatus.NOT_FOUND,"Book not found for this id :: "+ bookId));// Check if the book belongs to the authenticated user or if the user is adminif (book.getCreatedBy().equals(username) ||authentication.getAuthorities().stream().anyMatch(a ->a.getAuthority().equals("ROLE_ADMIN"))) {returnResponseEntity.ok().body(book); } else {thrownewResponseStatusException(HttpStatus.FORBIDDEN,"You are not authorized to access this resource"); } }// More endpoints for handling creation, updating, deleting...
Test the Application
Now we can start our application, sign up and sign with a new user or sign in with a pre-populated one, and use the JWT returned from that request to access our various endpoints according to the role assigned to our user!
Here's how we can sign up our user:
POST http://localhost:8080/api/v1/signupContent-Type:application/json{"firstName":"John","lastName":"Doe","email":"john.doe@example.com","password":"password"}
POST http://localhost:8080/api/v1/signinContent-Type:application/json{"email":"john.doe@example.com","password":"password"}
Or log in with our admin user:
POST http://localhost:8080/api/v1/signinContent-Type:application/json{"email":"admin@admin.com","password":"password"}
Experiment with different calls with different roles to see how our application restricts what data is available to a user based on their role. Check the /src/test directory in the repo to view some example API calls. You will need to provide the JWT token fetched in the /signin step, and remember, it expires after 1 hour!
Here's an example of accessing the user-restricted endpoint as a user role:
GET http://localhost:8080/api/v1/test/usersAuthorization:Bearer {{USER_AUTH_TOKEN}}