Our application depends on a lot of files and resources to be able to run correctly. These files have been provided for you and are listed here for your reference. These files should NEVER be modified and must be left AS IS.
Maven gets all its settings from a file called pom.xml. This file determines the dependencies we will use in our project as well as the plugins we use for compiling, testing, building, ect..
Spring Boot has a large number of settings that can be set with a file called application.yml. We have already created this file for you and have filled it with some settings. There is a file for the main application as well as one for the tests.
There are two folders in this project that contain resources, and application settings, as well as files required for the tests.
There is a Single class that contain all of our test cases:
| 🗄 idm |
|---|
| 💾 idm.token_status | ||
|---|---|---|
| Column Name | Type | Attributes |
| id | INT |
NOT NULL PRIMARY KEY |
| value | VARCHAR(32) |
NOT NULL |
| 💾 idm.user_status | ||
|---|---|---|
| Column Name | Type | Attributes |
| id | INT |
NOT NULL PRIMARY KEY |
| value | VARCHAR(32) |
NOT NULL |
| 💾 idm.role | ||
|---|---|---|
| Column Name | Type | Attributes |
| id | INT |
NOT NULL PRIMARY KEY |
| name | VARCHAR(32) |
NOT NULL |
| description | VARCHAR(128) |
NOT NULL |
| precedence | INT |
NOT NULL |
| 💾 idm.user | ||
|---|---|---|
| Column Name | Type | Attributes |
| id | INT |
NOT NULL PRIMARY KEY AUTO_INCREMENT |
VARCHAR(32) |
NOT NULL UNIQUE |
|
| user_status_id | INT |
NOT NULL |
| salt | CHAR(8) |
NOT NULL |
| hashed_password | CHAR(88) |
NOT NULL |
| Constraints | ||
FOREIGN KEY (user_status_id) REFERENCES idm.user_status (id) ON UPDATE CASCADE ON DELETE RESTRICT |
||
| 💾 idm.refresh_token | ||
|---|---|---|
| Column Name | Type | Attributes |
| id | INT |
NOT NULL PRIMARY KEY AUTO_INCREMENT |
| token | CHAR(36) |
NOT NULL UNIQUE |
| user_id | INT |
NOT NULL |
| token_status_id | INT |
NOT NULL |
| expire_time | TIMESTAMP |
NOT NULL |
| max_life_time | TIMESTAMP |
NOT NULL |
| Constraints | ||
FOREIGN KEY (user_id) REFERENCES idm.user (id) ON UPDATE CASCADE ON DELETE CASCADE |
||
FOREIGN KEY (token_status_id) REFERENCES idm.token_status (id) ON UPDATE CASCADE ON DELETE RESTRICT |
||
| 💾 idm.user_role | ||
|---|---|---|
| Column Name | Type | Attributes |
| user_id | INT |
NOT NULL |
| role_id | INT |
NOT NULL |
| Constraints | ||
PRIMARY KEY (user_id, role_id) |
||
FOREIGN KEY (user_id) REFERENCES idm.user (id) ON UPDATE CASCADE ON DELETE CASCADE |
||
FOREIGN KEY (role_id) REFERENCES idm.role (id) ON UPDATE CASCADE ON DELETE RESTRICT |
||
All the data to initialize your database is found in the db folder here: db folder
All ❗ 400: Bad Request Results must be checked first, and returned before any other action is made.
The order of the checks within ❗ 400: Bad Request is not tested as each Result is tested individually.
In the case of non-successful results, where values are expected, the values should not be included, for example.
{
"result": {
"code": 32,
"message": "Data contains invalid integers"
},
"value": null
}the value key should not be included:
{
"result": {
"code": 32,
"message": "Data contains invalid integers"
}
}This is done by insuring that all null values are dropped by either:
- Having your Model extend
ResponseModel<Model>, or - Putting the
@JsonInclude(JsonInclude.Include.NON_NULL)on your Model class
All Result objects are available as static constants inside of the com.gitcodings.stack.core.result.IDMResults class.
These can be used rather than creating your own.
We have provided some Enum classes in The Type Module to help manage constants in the DB, such as idm.role, idm.user_status, and idm.token_status. These are to help translate between id in the table and type the id corresponds with and make the code more readable.
In our IDMServiceConfig we are given Duration for each of the expire types. When we are setting our expire time's in our AccessToken and RefreshToken we create a Instant that represents the time and date at which they expire.
We do this by calling Instant.now().plus(duration). More information on Instant and Duration here: Activty 2
Allows users to create login details given a valid email and password. Password must be hashed and salted with both values being stored in the idm.user table. The user is given no roles, as well as being assigned the user_status of ACTIVE, by default.
POST /register| 📥 Request | ||
|---|---|---|
| Model | Example | |
email: String
password: char[] |
{
"email": "username@example.com",
"password": ["p", "a", "s", "s", "w", "o", "r", "d"]
} |
|
| Key | Required | Description |
email | Yes | Must be of the form [email]@[domain].[extension], be between [6-32] characters (inclusive), and contain only alphanumeric characters |
password | Yes | Must be between [10-20] alphanumeric characters (inclusive), contain at least one uppercase alpha, one lowercase alpha, and one numeric |
| 📤 Response | ||
| Model | Example | |
result: Result
code: Integer
message: String |
{
"result": {
"code": 1010,
"message": "User registered successfully"
}
} |
|
| 📦 Results | ||
| Status | Code | Message |
✅ 200: Ok |
1010 | User registered successfully |
❗ 409: Conflict |
1011 | User with this email already exists |
❗ 400: Bad Request |
1000 | Password does not meet length requirements |
❗ 400: Bad Request |
1001 | Password does not meet character requirement |
❗ 400: Bad Request |
1002 | Email address has invalid format |
❗ 400: Bad Request |
1003 | Email address has invalid length |
Allows users to login with credentials created in the /register endpoint. Returns both a accessToken and a refreshToken (with the refreshToken being stored in the idm.refresh_token table). Both tokens expiration dates are determined by the values supplied in the application.yml file.
An accessToken is built as follows:
- Build a
JWTClaimsSetwith the following claims:subjectof the user's emailexpirationTimeof the "Current Time" (use Instant for this) + accessTokenExpireTime found in the configissueTimethe currentTimeclaim(JWTManager.CLAIM_ROLES)of the user's roles (user a list of the provided Role enum)- If the user has no roles then just place a empty list here. The JWT Requires that there is a value for
JWTManager.CLAIM_ROLES
- If the user has no roles then just place a empty list here. The JWT Requires that there is a value for
claim(JWTManager.CLAIM_ID)of the user's id
- Build a
JWSHeaderwith theJWTManager.JWS_ALGORITHMand the following:keyIDof the ecKeyId found in your instance ofJWTManager- found by calling
manager.getEcKey().getKeyID()
- found by calling
typeofJWTManager.JWS_TYPE
- Build a
SignedJWTwith your createdJWTClaimsSetandJWSHeader - Sign by using calling
signedJWT.sign(jwtManager.getSigner()) - Serialize by calling
signedJWT.serialize()
A refreshToken is created by creating an instance of the RefreshToken class that is provided for you and having the token attribute be set to a random UUID (in string format). You can get a random UUID by using: UUID.randomUUID() and you can get the string formated UUID by calling the toString() function on the UUID. The token will have a token_status of ACTIVE, by default. Remember this refreshToken must be stored in the db to be able to refrence later
The token attribute of RefreshToken is what you return to the user in the response labeled as refreshToken.
POST /login| 📥 Request | ||
|---|---|---|
| Model | Example | |
email: String
password: char[] |
{
"email": "username@example.com",
"password": ["p", "a", "s", "s", "w", "o", "r", "d"]
} |
|
| Key | Required | Description |
email | Yes | Must be of the form [email]@[domain].[extension], be between [6-32] characters (inclusive), and contain only alphanumeric characters |
password | Yes | Must be between [10-20] alphanumeric characters (inclusive), contain at least one uppercase alpha, one lowercase alpha, and one numeric |
| 📤 Response | ||
| Model | Example | |
result: Result
code: Integer
message: String
accessToken: String (nullable)
refreshToken: String (nullable) |
{
"result": {
"code": 1020,
"message": "User logged in successfully"
},
"accessToken": "7f832c2e054ba732f7d4b7e26..."
"refreshToken": "c46fc3c2-9791-44d6-a86e-2922ad655284"
} |
|
| 📦 Results | ||
| Status | Code | Message |
✅ 200: Ok |
1020 | User logged in successfully |
❗ 401: Unauthorized |
1021 | User not found |
❗ 403: Forbidden |
1022 | Invalid credentials |
❗ 403: Forbidden |
1023 | User is locked |
❗ 403: Forbidden |
1024 | User is banned |
❗ 400: Bad Request |
1000 | Password does not meet length requirements |
❗ 400: Bad Request |
1001 | Password does not meet character requirement |
❗ 400: Bad Request |
1002 | Email address has invalid format |
❗ 400: Bad Request |
1003 | Email address has invalid length |
Creates a new accessToken for the user given a valid and non-expired refreshToken that belongs to the user.
To validate if a UUID has the correct format we can use UUID.fromString(refreshToken). If the UUID has an invalid format this will throw an exception that you can catch to throw the correct Result to the user.
When we are given a refreshToken (this corrosponds to the token attribute in idm.refreshToken) in this endpoint we need to retrieve all the addition traits of the token (user_id, token_status_id, expire_time, max_life_time) from the database.
Anytime we update the RefreshToken's attributes we must update it with the DB, and anytime we create a new RefreshToken we must also insert it into the DB.
If the refreshToken passes basic validation ensure it follows this Flow Chart
POST /refresh| 📥 Request | ||
|---|---|---|
| Model | Example | |
refreshToken: String |
{
"refreshToken": "c46fc3c2-9791-44d6-a86e-2922ad655284"
} |
|
| Key | Required | Description |
refreshToken | Yes | Must be 36 characters in length and be a valid UUID formatted string |
| 📤 Response | ||
| Model | Example | |
result: Result
code: Integer
message: String
accessToken: String (nullable)
refreshToken: String (nullable) |
{
"result": {
"code": 1030,
"message": "AccessToken has been refreshed"
},
"accessToken": "7f832c2e054ba732f7d4b7e26..."
"refreshToken": "c46fc3c2-9791-44d6-a86e-2922ad655284"
} |
|
| 📦 Results | ||
| Status | Code | Message |
✅ 200: Ok |
1030 | AccessToken has been refreshed |
❗ 401: Unauthorized |
1031 | RefreshToken is expired |
❗ 401: Unauthorized |
1032 | RefreshToken is revoked |
❗ 401: Unauthorized |
1033 | RefreshToken not found |
❗ 400: Bad Request |
1032 | RefreshToken has invalid length |
❗ 400: Bad Request |
1033 | RefreshToken has invalid format |
Authenticates a user's accessToken to ensure it is both valid and non-expired.
We are given the JWT in serialized format, so we will need to use SignedJWT.parse() in order to turn it back into SignedJWT
An accessToken can be verified in three steps:
- Verifying that the token is valid and issued by us by calling
jwt.verify(jwtManager.getVerifier()) - Checking that the claims are consistent with what we expect by calling
jwtManager.getJwtProcessor().process(jwt, null) - Manually checking that the expireTime of the token has not passed.
POST /authenticate| 📥 Request | ||
|---|---|---|
| Model | Example | |
accessToken: String |
{
"accessToken": "7f832c2e054ba732f7d4b7e26..."
} |
|
| Key | Required | Description |
accessToken | Yes | Must be a valid JWT encoded string |
| 📤 Response | ||
| Model | Example | |
result: Result
code: Integer
message: String | {
"result": {
"code": 1040,
"message": "AccessToken is valid"
}
} |
|
| 📦 Results | ||
| Status | Code | Message |
✅ 200: Ok |
1040 | AccessToken is valid |
❗ 401: Unauthorized |
1041 | AccessToken is expired |
❗ 401: Unauthorized |
1042 | AccessToken is invalid |