In this tutorial we will learn how to implement secured APIs in Spring Boot with using JWT tokens. First of all, we have to understand what is a JSON Web Token (JWT).
JWT is an open standard (RFC7519) to share information between entities in a JSON format. It is digitally signed thus it can be verified by entities. It is not encrypted but encoded which means if the man in the middle gets the token then it can be used for accessing secured resources. Because of this it shouldn't contain any critical information like password, credit card number etc. This can be prevented by encryption with TLS or you can basically choose to encrypt JWT as well which is optional.
The most common usage of JWT is for accessing authorized resources. Here is an example illustration of JWT usage;
User sends a request to authenticate with his credentials.
Application server authenticates the user with given credentials and generates a JWT.
Return back the generated JWT to the user.
User will send this JWT each time to access resources on application server.
Application server will validate the JWT token and check if user is authorized for the resource.
Return data from the resource.
JWT consists of 3 parts;
Each JWT part is a base64 encoded string concatenated with with dot (.), so a compact JWT structure will look like;
This is the first part which contains two fields; the type (typ) of the token (which is always JWT) and the name of signature algorithm (alg) to create JWT signature. Here is the JSON format for header;
HS512 stands for HMAC + SHA512. List of the supported algorithms can be found RFC7518
Payload is the part where it can contain predefined fields (or claims in other words) or custom fields for sharing information between entities. Here is an example JSON format for Payload section;
sub - Subject (or Id/Name) of the principal for the JWT.
iat - (Issued At) - Numeric time value which represents the time this JWT is issued.
exp - (Expiration Time) - Numeric time value which represents expiration time of this JWT.
userId - Custom field added by us.
Hence the fact that, JWT size will become larger when the number of claims increased.
Signature section is created by joining base64 encoded Header and Payload sections with . and applying the algorithm we defined in alg section along with a defined secret to sign it. Basically, the formula will be like;
This is the section our application checks if JWT is tampered.
We will implement a Spring Boot application containing 3 endpoints to illustrate authentication and authorization for a user by using JWT. The 3 endpoints will be;
POST /register - Registering a user with username and password. We will keep this information in a PostgreSQL database.
POST /authenticate - Authenticating a user with given username and password.
GET /hello-world - Protected resource which will be accessible with a valid JWT.
We will use a very basic users table including only a few fields along with a sequence counter for id generation.
DAO and Repository Classes
Here are the simple UserDAO and Repository implementations we will use in this application.
As I mentioned above we will use a sequencer named users_seq to assign ids for each newly registered user. Our repository will only have findByUsername to get UserDAO object from database.
JWT And Security Components
Our first goal is going to implement a utility class for JWT operations like; generating and validating a jwt, parsing claims (fields of Payload section) from a jwt. We will give 6 hours for expiration time in milliseconds and we will use HS512 algorighm (HMAC + SHA512) for signing the jwt along with a secret which is springjwt in this case (it is coming from application.properties). Additionally, we will use subject, expiration and issuedAt claims for this example. Note that claims is basically a Map object which lets us to add custom claims.
Now we will create an implementation of UserDetailsService which will get a user from UserRepository.
As the next step, we will create an implementation of OncePerRequest to be defined in Spring Security Filter Chain. doFilterInternal is the only method we need to override. This is the core place where we will resolve jwt and check if a valid username is provided.
Our final step is configuring security for our spring application. We will only have two endpoints which doesn't need any authentication.
@EnableWebSecurity annotation enables us to override/configure HttpSecurity object. Furthermore, there are a few parts to focus in configure method. We are adding JWTRequestFilter into Spring Security Filter Chain to make it run before UsernamePasswordAuthenticationFilter. We are marking /register and /authenticate endpoints to not require any authentication. Any other endpoint will require an authentication which is a valid jwt. Also, we are telling our Spring application to not create any sessions because our application is stateless.
REST Controllers and Services
This is the final part of our Spring application. We will now add the 3 endpoints we mentioned earlier in this tutorial. First one is for registering a user into our database, second one is for authentication which will return the jwt we spoke about and finally the third api is an authenticated one which requires a valid jwt and will return hello-world response.
/register endpoint accepts a JSON object (UserDTO in this case) which has username and password fields. Then it checks if the given username has been already taken. If it is an existing one it will throw UsernameAlreadyExistException which extends from RunTimeException. If it is not an existing one we will add new user into our database and return its id.
/authenticate endpoint also accepts a JSON object contains username and password and will return a valid jwt if input is valid. Otherwise, it will return 403 Forbidden response which is the default one.
Our last controller will contain hello-world endpoint which requires authentication.
which will return a new id of the newly added user. Now before calling /authenticate with correct credentials we will make a call with invalid credentials to verify our authentication works as expected.