Jwt-Spring-Security-JPA
A demo project explaining the backend authentication using JWT (Json Web Token) authentication using Spring Security & MySQL JPA.
There's support for the following features:
- Conventional email/username based registration with admin support.
- Conventional Login using Spring Security and generation of JWT token.
- Multiple device login and logout support.
- In memory store for blacklisting JWT tokens upon user logout.
- Expiration bases email verification. Mail is sent upon registration.
- Resend the email confirmation email if old one expires.
- Forgot-password functionality with password reset token validations.
- Admin protected urls leveraging Spring security.
- Refresh JWT tokens once the temporary JWT expires.
- Check availability of username/email during registration.
JWT
JSON Web Tokens are an open, industry standard RFC 7519 method for representing claims securely between two parties.
Swagger Docs
The project has been configured with a Swagger docket that exposes the APIs with the schema
Accessible at http://localhost:9004/swagger-ui
once the app is running.
Exception Handling
- The app throws custom exceptions wherever necessary which are captured through a controller advice. It then returns the appropriate error response to the caller
- Moreover, entities are validated using JSR-303 Validation constraints.
Getting Started
Clone the application
$ git clone https://github.com/isopropylcyanide/Jwt-Spring-Security-JPA.git
$ cd Jwt-Spring-Security-JPA
Create a MySQL database
$ create database login_db
Change MySQL username and password as per your MySQL installation
- Edit
spring.datasource.username
andspring.datasource.password
properties as per your mysql installation insrc/main/resources/application.properties
- Edit
spring.mail.username
andspring.mail.password
properties as per your mail serversrc/main/resources/mail.properties
Run the app
./mvnw spring-boot:run # For UNIX/Linux based operating systems
mvnw.cmd spring-boot:run # For Windows based operating systems
- The server will start on
server.port:9004
and will create the tables for you. - Every run of the app will reset your state. To not do that, modify
spring.jpa.hibernate.ddl-auto: update
API
Registering a User
curl --location --request POST 'localhost:9004/api/auth/register' \
--header 'Content-Type: application/json' \
--data-raw '{
"email": "[email protected]",
"password": "amangarg",
"registerAsAdmin": true
}'
β οΈ If you re-register an email twice, you'll get the "email in use" error
Logging in an unverified user
curl --location --request POST 'localhost:9004/api/auth/login' \
--header 'Content-Type: application/json' \
--data-raw '{
"email": "[email protected]",
"password": "amangarg",
"deviceInfo": {
"deviceId": "D1",
"deviceType": "DEVICE_TYPE_ANDROID",
"notificationToken": "N1"
}
}'
Confirming the user email verification token
curl --location --request GET 'localhost:9004/api/auth/registrationConfirmation?token=bcbf8764-dbf2-4676-9ebd-2c74436293b9' \
--header 'Content-Type: application/json' \
--data-raw '{
"email": "[email protected]",
"password": "HI12",
"deviceInfo": {
"deviceId": "D1",
"deviceType": "DEVICE_TYPE_ANDROID",
"notificationToken": "N1"
}
}'
β οΈ If you pass the incorrect token you will get a "Token Mismatch error"
β Don't know the token?: Check your email inmail.properties
β Still didn't get it?: Look inside the databaseemail_verification_token#token
Logging in the user with valid credentials
curl --location --request POST 'localhost:9004/api/auth/login' \
--header 'Content-Type: application/json' \
--data-raw '{
"email": "[email protected]",
"password": "amangarg",
"deviceInfo": {
"deviceId": "D1",
"deviceType": "DEVICE_TYPE_ANDROID",
"notificationToken": "N1"
}
}'
β οΈ If you do not enter correct credentials you will get a "Bad credentials error"
β οΈ If your email is not verified (refer the above API) you will get an "Unauthorized" error
β Device information is required to enable a multi device login and logout functionality.
Using the JWT token to access a user resource
curl --location --request GET 'localhost:9004/api/user/me' \
--header 'Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiIxIiwiaWF0IjoxNjM1NjE0NTY4LCJleHAiOjE2MzU2MTU0Njh9.d8CJYduoC44njutphODoezheSt_so3Doc9g1RSiMaDU_qJwY0_3Ym4092hFkHsh-jbyB_9i66LbwSEE-szAgEw'
β οΈ If you enter an invalid token (obtained post login), you will get an "Incorrect JWT Signature" error.
β οΈ If you enter a malformed JWT token, you will get a "Malformed JWT Signature" error.
β οΈ If you enter an expired JWT token (default:app.jwt.expiration
, you will get an "Expired JWT Signature" error and clients should refresh the JWT token.
Using the JWT token to access an admin resource
curl --location --request GET 'localhost:9004/api/user/admins' \
--header 'Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiIxIiwiaWF0IjoxNjM1NjE0NTY4LCJleHAiOjE2MzU2MTU0Njh9.d8CJYduoC44njutphODoezheSt_so3Doc9g1RSiMaDU_qJwY0_3Ym4092hFkHsh-jbyB_9i66LbwSEE-szAgEw'
β οΈ If you registered a user withregisterAsAdmin: false
, then you will get a "Forbidden" error.
β οΈ JWT has to be valid (same constraints as the above user resource API)
Logout user
curl --location --request POST 'localhost:9004/api/user/logout' \
--header 'Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiIxIiwiaWF0IjoxNjM1NjE0NTY4LCJleHAiOjE2MzU2MTU0Njh9.d8CJYduoC44njutphODoezheSt_so3Doc9g1RSiMaDU_qJwY0_3Ym4092hFkHsh-jbyB_9i66LbwSEE-szAgEw' \
--header 'Content-Type: application/json' \
--data-raw '{
"deviceInfo": {
"deviceId": "D1",
"deviceType": "DEVICE_TYPE_ANDROID",
"notificationToken": "N1"
}
}'
β Logging out also deletes the refresh token associated with the device. In real production, this token should be specifically invalidated.
β οΈ If the JWT isn't passed then you will get an "Unauthorized" error.
β οΈ If you try to log out same user twice (without an app restart), you will get a "Token Expired" error. This works because on logout we invalidate the JWT
β οΈ If you try to log out a logged-in user against an invalid device (say D2), you will get an "Invalid Device" error.
Request a reset password link for a registered user
curl --location --request POST 'localhost:9004/api/auth/password/resetlink' \
--header 'Content-Type: application/json' \
--data-raw '{
"email": "[email protected]"
}'
β You can request a password reset multiple times. The reset token would be generated multiple times with anapp.token.password.reset.duration
β You can request a password reset for a user even when they have not verified their email once. This is okay for our demo case.
β οΈ If you try to request a password reset for an unregistered user, you will get a "No matching user" error
Reset password for a registered user
curl --location --request POST 'localhost:9004/api/auth/password/reset' \
--header 'Content-Type: application/json' \
--data-raw '{
"email": "[email protected]",
"password": "P1",
"confirmPassword": "P1",
"token": "880ab6f1-4b4b-4d04-92bd-8995b4063205"
}'
β οΈ If your new passwords do not match, there will be an error
β οΈ If your password reset token is not valid or is for some other user, you'll get a "Password Reset Token Not Found" error.
β οΈ If you try to use a password reset token twice, you will get a "Token Inactive" error
Refreshing the JWT token for longer login sessions
curl --location --request POST 'localhost:9004/api/auth/refresh' \
--header 'Content-Type: application/json' \
--data-raw '{
"refreshToken": "d029e0fa-80f5-4768-837c-7e85a0f94960"
}'
β You can refresh a JWT multiple times against the refresh token. That is the purpose of refresh. Refresh token expiry can be controlled withapp.token.refresh.duration
β οΈ If you pass an invalid refresh token (obtained through login), you will get a "No token found" error
Check email in use
curl --location --request GET 'localhost:9004/api/auth/[email protected]'
β The API can be accessed insecurely and hence should be rate limited in production to prevent a DDOS attack.
β You can request a password reset for a user even when they have not verified their email once. This is okay for our demo case.
β οΈ If you try to request a password reset for an unregistered user, you will get a "No matching user" error
Roles
- The spring boot app uses role based authorization powered by spring security
- Tables and role data should have been created by default upon the first startup.
- Any new user who signs up to the app is assigned the
ROLE_USER
by default. - In case the role entries aren't created, please execute the following sql queries in the database to insert the
USER
andADMIN
roles.
INSERT INTO `login_db.role` (ROLE_NAME)
VALUES ('ROLE_USER');
INSERT INTO `login_db.role` (ROLE_NAME)
VALUES ('ROLE_ADMIN');
Contribution
- Remember, the project is a demo and should not be used into production directly.
- Please fork the project and adapt it to your use case.
- Submit a pull request with proper motivation and test plan.
- Postman collection dump available here)
- Not everything is in scope for this demo project. Feel free to fork the project and extend the functionality.
- Project is equipped with a JUnit but lacks tests in most places. Would really appreciate your contributions here.