diff --git a/.env-example b/.env-example
index b3624f3..4608c81 100644
--- a/.env-example
+++ b/.env-example
@@ -1,6 +1 @@
-#
-# You can generate your own key pair
-#
-PUBLIC_KEY='-----BEGIN PUBLIC KEY-----MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAt8FNmx/af8Ul2eo+dwL/JjnC2cJGhjSMT0AmM3NpGEPNuBC7gPtaMiPMkreoZ9aL5wD9M/Cz1jWdQdj5fT1nxSDut+9Wm+stqAyF5pkw4hXN9Qqhi048sZsVsmqi8M4XJqOg2W2a9+CvQPU1jvSQuwJEEFoXfnPvtVs6aB/YcUoHNuBUlYGVjcZonkJz3JByfn5+q/gI4hsTXzHSuL81n6Uwr2cL8K9DbC5Q8jICK+p8rNZy3z9FuGFtt4f5MG5lVtI8TlTrzhoZ6mKsGaeV+6a6jSFAK4uunGpowfoOKO2aS3GBp6sHz3kEY2vWomr5qvN5z7YSJm0kmtA4fp77MkXuezVLPfjc0G26fTtOoreLKCgFd9YsfacX/Q4nVZv8K5lZ+qQBhX5fl+k3qa8NnDsyGmJnj0UPwbKkdRpRpWPfmc5cnVYkEaQZ/D/St0UvdiqaVUhPyEMMKAYMa7PbQe6L2MQmA3ZsECnbksn6ZF9KWkK/slqOzAhKVo/r7d9X1wT/uVt/b0fBriwGIkORtnQtRQFdslbjrUXDt2GWk3/tm9OHBn/g/jK+NK4TyTzuiXawo39gg/CWZ0MQFU6hLXdn15244Nef2mIWbJece6CLwF/Gn43+z+SuPop+zG1JmkReJ8FDiEjq8KRLNf9UHRFXITsKThsr5s/4+jBTPJkCAwEAAQ==-----END PUBLIC KEY-----'
-PRIVATE_KEY='-----BEGIN PRIVATE KEY-----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-----END PRIVATE KEY-----'
SPRING_PROFILES_ACTIVE=development
\ No newline at end of file
diff --git a/HealthCheck b/HealthCheck
new file mode 100644
index 0000000..417c4be
--- /dev/null
+++ b/HealthCheck
@@ -0,0 +1,27 @@
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.URI;
+import java.util.Arrays;
+
+class HealthCheck {
+
+ static final int OK = 0;
+ static final int NOK = -1;
+
+ public static void main(String[] args) {
+ try {
+ System.exit(doHealthCheck(URI.create(Arrays.stream(args).findFirst().orElseThrow())));
+ } catch (Exception ignored) {
+ // Just quietly exit with NOK
+ System.exit(NOK);
+ }
+ }
+
+ private static int doHealthCheck(URI uri) throws IOException {
+ if (!(uri.toURL().openConnection() instanceof HttpURLConnection httpConnection)) {
+ return NOK;
+ }
+
+ return HttpURLConnection.HTTP_OK == httpConnection.getResponseCode() ? OK : NOK;
+ }
+}
\ No newline at end of file
diff --git a/README.md b/README.md
index 55ca84e..427c200 100644
--- a/README.md
+++ b/README.md
@@ -32,17 +32,6 @@
### Specifying Environment Variables
-There are two mandatory environment variables that you need to set before running the service:
-
-| Variable Name | Description | Remarks |
-|---------------|------------------------------------------------------------------------|-------------------------------------|
-| `PUBLIC_KEY` | The public key used to verify JWT authorization | RSA public key (2048 bits or more) |
-| `PRIVATE_KEY` | The private key used to generate access tokens for authenticated users | RSA private key (2048 bits or more) |
-
-> [!TIP]
->
-> You can visit [this link](https://travistidwell.com/jsencrypt/demo/) to generate a pair of RSA keys for testing purposes.
-
The `SPRING_PROFILES_ACTIVE` variable is optional, but you can set it to `development` for local development.
You can either specify these environment variables in a `.env` file or set them directly in Run Configurations.
@@ -51,11 +40,11 @@ An example `.env` file can be copied from [this file](/.env-example).
### Running the Required Containers
-You can run [this script (Windows only)](/run-docker.cmd) and it will start the required containers for local development: PostgreSQL and Redis.
+You can run [this script (Windows only)](/initialize-postgres-keycloak.cmd) and it will start the required containers for local development: PostgreSQL and KeyCloak.
## Running the Compose Stack
-You can run [this script](/run-docker.cmd) and it will build the service image and start the containers for you.
+You can run [this script(Windows only)](/run-docker-compose-stack.cmd) and it will build the service image and start the containers for you.
# Additional Notes
diff --git a/docker-compose.yml b/docker-compose.yml
index 4ddb50f..c0c231b 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -8,19 +8,17 @@ services:
context: .
dockerfile: ./Dockerfile
ports:
- - "8080:8080"
+ - "8088:8088"
environment:
- SPRING_PROFILES_ACTIVE=development
- DATABASE_HOST=postgresql
- DATABASE_USERNAME=postgres
- - REDIS_HOST=redis
- # Those keys are randomly generated, please replace them if you can
- - PUBLIC_KEY=-----BEGIN PUBLIC KEY-----MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEArEl0hMdt0ozeuilVp+5qfjWUdLieuB8F1LTgeYSfLg6W2EYLQ0mLl498h3fCOCnChWc+Rxb0PbUfPyO61pZvQp5822RD53t1Qvued9lmFti7HITlPjPYXMk2MPCMQAE7AR77VMdNZfiFUXqy9T+GKVmkrZe1c8Zu17Qtm4rjto1nbbHJDHB6FSfdWGHDdJeK696b8IaZ+Zu4nySTs5foTvfHZxNzemkOMsqStsWABTtkuK3vvfDJlFZUvZQSDSM+APqSfBmcdSa5dPSK8Pm5mRfpH//SYVk0XK6xKT4iuP1cXATmYtc1A05IHw+dAzzarbRK9uIRkV5wYa8D6WPRGP7jdOXDkFgqg9tUAOJ6gnp3OP2KotTAyzkfhtydTqEiOWsUITZ5eb2Q5FnLqV1pGqJXpOak/fw5U/kXorzPy4dM2Iwr1XQRjOTfWk9xzqBLXzSapwHCx1TSnXYtDbMDMprlGcNLtCm0MjzMnykqljRe0l3EGh44A14dx2PJVzaRVyj2+liLZTLgb89YFvwaR8gGaz01YXWUb3u3MAwr8tgmONjTcF+F9epfwB6OOad2mx2Y5svu7yc01vMP+5E5D/lkYIlUcKUk9CUlp1jZB3AqUOXFPVjgYRxGHc92/55fyMc4f23wqWj5FHsBvVljPKlhT/dUEH91IPh6YBKClMcCAwEAAQ==-----END PUBLIC KEY-----
- - PRIVATE_KEY=-----BEGIN PRIVATE KEY-----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-----END PRIVATE KEY-----
+ - SERVER_PORT=8088
+ - KEYCLOAK_HOST=http://keycloak:8080
depends_on:
postgresql:
condition: service_healthy
- redis:
+ keycloak:
condition: service_healthy
postgresql:
networks:
@@ -44,29 +42,30 @@ services:
interval: 10s
timeout: 5s
retries: 5
- redis:
+ keycloak:
networks:
- spring-base-network
- image: 'redis:8.2.3-bookworm'
- container_name: redis
+ image: quay.io/keycloak/keycloak:26.4
+ container_name: standalone-keycloak
+ command: start-dev
+ environment:
+ - KC_BOOTSTRAP_ADMIN_USERNAME=admin
+ - KC_BOOTSTRAP_ADMIN_PASSWORD=123456
+ - KC_HEALTH_ENABLED=true
+ - KC_METRICS_ENABLED=true
+ - KC_HOSTNAME=keycloak
ports:
- - "6379:6379"
- volumes:
- - redis-volume:/data
+ - "8080:8080"
+ - "9000:9000"
healthcheck:
- test: [ "CMD", "redis-cli", "ping" ]
+ test: ['CMD-SHELL', '[ -f /tmp/HealthCheck.java ] || echo "public class HealthCheck { public static void main(String[] args) throws java.lang.Throwable { java.net.URI uri = java.net.URI.create(args[0]); System.exit(java.net.HttpURLConnection.HTTP_OK == ((java.net.HttpURLConnection)uri.toURL().openConnection()).getResponseCode() ? 0 : 1); } }" > /tmp/HealthCheck.java && java /tmp/HealthCheck.java http://localhost:9000/health/live']
interval: 5s
timeout: 5s
- retries: 5
- command:
- - redis-server
- - --save 60 1
- - --loglevel warning
+ retries: 15
+ start_period: 10s
volumes:
postgres-volume:
name: postgres-volume
- redis-volume:
- name: redis-volume
networks:
spring-base-network:
driver: bridge
\ No newline at end of file
diff --git a/initialize-postgres-keycloak.cmd b/initialize-postgres-keycloak.cmd
new file mode 100644
index 0000000..d3d90b8
--- /dev/null
+++ b/initialize-postgres-keycloak.cmd
@@ -0,0 +1,119 @@
+@echo off
+SETLOCAL EnableDelayedExpansion
+
+docker info >nul 2>&1
+if errorlevel 1 (
+ echo Error: Docker daemon is not running.
+ echo Please start Docker Desktop or Docker service and run this script again.
+ exit /b 1
+)
+
+:: --- PostgreSQL Setup ---
+SET PG_CONTAINER_NAME=standalone-postgresql
+SET PG_VOLUME_NAME=postgres-volume
+SET PG_COMMAND=docker run -d --name !PG_CONTAINER_NAME! -e "POSTGRES_USER=postgres" -e "POSTGRES_PASSWORD=123456" -e "POSTGRES_DB=myspringdatabase" -p 5432:5432 -v !PG_VOLUME_NAME!:/var/lib/postgresql/data postgres:18.0-alpine3.22
+
+echo Checking PostgreSQL container [%PG_CONTAINER_NAME%]...
+docker ps -a | findstr /C:"!PG_CONTAINER_NAME!" >nul
+if errorlevel 1 (
+ echo Container [%PG_CONTAINER_NAME%] not existed, creating with volume [%PG_VOLUME_NAME%]...
+ !PG_COMMAND!
+) else (
+ docker ps | findstr /C:"!PG_CONTAINER_NAME!" >nul
+ if errorlevel 1 (
+ echo Container with the same name [%PG_CONTAINER_NAME%] stopped, restarting...
+ docker start !PG_CONTAINER_NAME!
+ ) else (
+ echo Container [%PG_CONTAINER_NAME%] is already running...
+ )
+)
+
+:: KEYCLOAK_REALM and CLIENT_ID should match the values in application.properties
+
+:: --- Keycloak Setup ---
+set KEYCLOAK_IMAGE=quay.io/keycloak/keycloak:26.4
+set KEYCLOAK_CONTAINER=standalone-keycloak
+
+:: application-properties.security.realm-name
+set KEYCLOAK_REALM=spring-base
+
+:: application-properties.security.client-name
+set CLIENT_ID=spring-base-client
+
+set KEYCLOAK_OVERLORD=admin
+set KEYCLOAK_ADMIN_PASSWORD=123456
+
+echo Stopping and removing old Keycloak containers/images...
+docker container rm -f %KEYCLOAK_CONTAINER% 2>nul
+
+echo Starting Keycloak container [%KEYCLOAK_CONTAINER%]...
+docker run --name %KEYCLOAK_CONTAINER% --detach -p 8080:8080 -p 9000:9000 -e "KC_BOOTSTRAP_ADMIN_USERNAME=%KEYCLOAK_OVERLORD%" -e "KC_BOOTSTRAP_ADMIN_PASSWORD=%KEYCLOAK_ADMIN_PASSWORD%" -e "KC_HEALTH_ENABLED=true" -e "KC_METRICS_ENABLED=true" %KEYCLOAK_IMAGE% start-dev
+
+docker cp HealthCheck %KEYCLOAK_CONTAINER%:/tmp/HealthCheck.java
+
+:health_check
+docker exec %KEYCLOAK_CONTAINER% sh -c "java /tmp/HealthCheck.java http://localhost:9000/health/live"
+if !ERRORLEVEL!==0 (
+ echo Keycloak is serviceable!
+ goto after_health_check
+) else (
+ echo Waiting for Keycloak to be healthy...
+ timeout /t 5 >nul
+ goto health_check
+)
+
+:after_health_check
+
+set KCADM_PATH=/opt/keycloak/bin/kcadm.sh
+
+echo Configuring KCADM credentials...
+docker exec %KEYCLOAK_CONTAINER% %KCADM_PATH% config credentials --server http://localhost:8080 --realm master --user %KEYCLOAK_OVERLORD% --password %KEYCLOAK_ADMIN_PASSWORD%
+
+docker exec %KEYCLOAK_CONTAINER% %KCADM_PATH% create realms -s realm=%KEYCLOAK_REALM% -s enabled=true
+
+echo Creating client [%CLIENT_ID%]...
+
+:: if anyone has any better idea to not use variable, let me know
+set "CLIENT_CREATE_CMD=docker exec %KEYCLOAK_CONTAINER% %KCADM_PATH% create clients -r %KEYCLOAK_REALM% -s clientId=%CLIENT_ID% -s enabled=true -s publicClient=true -s directAccessGrantsEnabled=true -i"
+for /f "usebackq delims=" %%i in (`!CLIENT_CREATE_CMD!`) do (
+ set CLIENT_UUID=%%i
+)
+
+:: --- Role and User Setup ---
+SET ROLE_ADMIN=ADMIN
+SET ROLE_POWER_USER=POWER_USER
+SET ROLE_USER=USER
+
+set ADMIN_USERNAME=administrator
+set POWER_USER_USERNAME=power_user
+set USER_USERNAME=user
+
+docker exec %KEYCLOAK_CONTAINER% %KCADM_PATH% create clients/%CLIENT_UUID%/roles -r %KEYCLOAK_REALM% -s name=%ROLE_ADMIN%
+docker exec %KEYCLOAK_CONTAINER% %KCADM_PATH% create clients/%CLIENT_UUID%/roles -r %KEYCLOAK_REALM% -s name=%ROLE_POWER_USER%
+docker exec %KEYCLOAK_CONTAINER% %KCADM_PATH% create clients/%CLIENT_UUID%/roles -r %KEYCLOAK_REALM% -s name=%ROLE_USER%
+
+echo Creating admin user [%ADMIN_USERNAME%]...
+docker exec %KEYCLOAK_CONTAINER% %KCADM_PATH% create users -r %KEYCLOAK_REALM% -s username=%ADMIN_USERNAME% -s enabled=true -s email=administrator@email.com -s firstName=Administrator -s lastName=User
+docker exec %KEYCLOAK_CONTAINER% %KCADM_PATH% set-password -r %KEYCLOAK_REALM% --username %ADMIN_USERNAME% --new-password 123456
+
+echo Assigning client role [%ROLE_ADMIN%] to user [%ADMIN_USERNAME%] in client [%CLIENT_ID%]...
+docker exec %KEYCLOAK_CONTAINER% %KCADM_PATH% add-roles -r %KEYCLOAK_REALM% --uusername %ADMIN_USERNAME% --cclientid %CLIENT_ID% --rolename %ROLE_ADMIN%
+echo Created user [%ADMIN_USERNAME%] with password 123456 and client role [%ROLE_ADMIN%] in client [%CLIENT_ID%]
+
+echo Creating regular user [%USER_USERNAME%]...
+docker exec %KEYCLOAK_CONTAINER% %KCADM_PATH% create users -r %KEYCLOAK_REALM% -s username=%USER_USERNAME% -s enabled=true -s email=user@email.com -s firstName=Normal -s lastName=User
+docker exec %KEYCLOAK_CONTAINER% %KCADM_PATH% set-password -r %KEYCLOAK_REALM% --username %USER_USERNAME% --new-password 123456
+
+echo Assigning client role [%ROLE_USER%] to user [%USER_USERNAME%] in client [%CLIENT_ID%]...
+docker exec %KEYCLOAK_CONTAINER% %KCADM_PATH% add-roles -r %KEYCLOAK_REALM% --uusername %USER_USERNAME% --cclientid %CLIENT_ID% --rolename %ROLE_USER%
+echo Created user [%USER_USERNAME%] with password 123456 and client role [%ROLE_USER%] in client [%CLIENT_ID%]
+
+echo Creating power user [%POWER_USER_USERNAME%]...
+docker exec %KEYCLOAK_CONTAINER% %KCADM_PATH% create users -r %KEYCLOAK_REALM% -s username=%POWER_USER_USERNAME% -s enabled=true -s email=power_user@email.com -s firstName=Power -s lastName=User
+docker exec %KEYCLOAK_CONTAINER% %KCADM_PATH% set-password -r %KEYCLOAK_REALM% --username %POWER_USER_USERNAME% --new-password 123456
+
+echo Assigning client role [%ROLE_POWER_USER%] to user [%POWER_USER_USERNAME%] in client [%CLIENT_ID%]...
+docker exec %KEYCLOAK_CONTAINER% %KCADM_PATH% add-roles -r %KEYCLOAK_REALM% --uusername %POWER_USER_USERNAME% --cclientid %CLIENT_ID% --rolename %ROLE_POWER_USER%
+echo Created user [%POWER_USER_USERNAME%] with password 123456 and client role [%ROLE_POWER_USER%] in client [%CLIENT_ID%]
+
+ENDLOCAL
\ No newline at end of file
diff --git a/initialize-postgres-keycloak.sh b/initialize-postgres-keycloak.sh
new file mode 100644
index 0000000..23f875e
--- /dev/null
+++ b/initialize-postgres-keycloak.sh
@@ -0,0 +1,105 @@
+#!/bin/sh
+
+# Check if Docker is running
+docker info >/dev/null 2>&1
+if [ $? -ne 0 ]; then
+ echo "Error: Docker daemon is not running."
+ echo "Please start Docker and run this script again."
+ exit 1
+fi
+
+# --- PostgreSQL Setup ---
+PG_CONTAINER_NAME="standalone-postgresql"
+PG_VOLUME_NAME="postgres-volume"
+PG_COMMAND="docker run -d --name ${PG_CONTAINER_NAME} -e \"POSTGRES_USER=postgres\" -e \"POSTGRES_PASSWORD=123456\" -e \"POSTGRES_DB=myspringdatabase\" -p 5432:5432 -v ${PG_VOLUME_NAME}:/var/lib/postgresql/data postgres:18.0-alpine3.22"
+
+echo "Checking PostgreSQL container [${PG_CONTAINER_NAME}]..."
+if ! docker ps -a | grep -q "${PG_CONTAINER_NAME}"; then
+ echo "Container [${PG_CONTAINER_NAME}] not existed, creating with volume [${PG_VOLUME_NAME}]..."
+ eval "${PG_COMMAND}"
+else
+ if ! docker ps | grep -q "${PG_CONTAINER_NAME}"; then
+ echo "Container with the same name [${PG_CONTAINER_NAME}] stopped, restarting..."
+ docker start "${PG_CONTAINER_NAME}"
+ else
+ echo "Container [${PG_CONTAINER_NAME}] is already running..."
+ fi
+fi
+
+# KEYCLOAK_REALM and CLIENT_ID should match the values in application.properties
+# --- Keycloak Setup ---
+KEYCLOAK_IMAGE="quay.io/keycloak/keycloak:26.4"
+KEYCLOAK_CONTAINER="standalone-keycloak"
+# application-properties.security.realm-name
+KEYCLOAK_REALM="spring-base"
+# application-properties.security.client-name
+CLIENT_ID="spring-base-client"
+KEYCLOAK_OVERLORD="admin"
+KEYCLOAK_ADMIN_PASSWORD="123456"
+
+echo "Stopping and removing old Keycloak containers/images..."
+docker container rm -f "${KEYCLOAK_CONTAINER}" 2>/dev/null
+
+echo "Starting Keycloak container [${KEYCLOAK_CONTAINER}]..."
+docker run --name "${KEYCLOAK_CONTAINER}" --detach -p 8080:8080 -p 9000:9000 -e "KC_BOOTSTRAP_ADMIN_USERNAME=${KEYCLOAK_OVERLORD}" -e "KC_BOOTSTRAP_ADMIN_PASSWORD=${KEYCLOAK_ADMIN_PASSWORD}" -e "KC_HEALTH_ENABLED=true" -e "KC_METRICS_ENABLED=true" "${KEYCLOAK_IMAGE}" start-dev
+
+docker cp HealthCheck "${KEYCLOAK_CONTAINER}":/tmp/HealthCheck.java
+
+# Health check loop
+while true; do
+ docker exec "${KEYCLOAK_CONTAINER}" sh -c "java /tmp/HealthCheck.java http://localhost:9000/health/live"
+ if [ $? -eq 0 ]; then
+ echo "Keycloak is serviceable!"
+ break
+ else
+ echo "Waiting for Keycloak to be healthy..."
+ sleep 5
+ fi
+done
+
+KCADM_PATH="/opt/keycloak/bin/kcadm.sh"
+
+echo "Configuring KCADM credentials..."
+docker exec "${KEYCLOAK_CONTAINER}" "${KCADM_PATH}" config credentials --server http://localhost:8080 --realm master --user "${KEYCLOAK_OVERLORD}" --password "${KEYCLOAK_ADMIN_PASSWORD}"
+
+docker exec "${KEYCLOAK_CONTAINER}" "${KCADM_PATH}" create realms -s realm="${KEYCLOAK_REALM}" -s enabled=true
+
+echo "Creating client [${CLIENT_ID}]..."
+
+CLIENT_UUID=$(docker exec "${KEYCLOAK_CONTAINER}" "${KCADM_PATH}" create clients -r "${KEYCLOAK_REALM}" -s clientId="${CLIENT_ID}" -s enabled=true -s publicClient=true -s directAccessGrantsEnabled=true -i)
+
+# --- Role and User Setup ---
+ROLE_ADMIN="ADMIN"
+ROLE_POWER_USER="POWER_USER"
+ROLE_USER="USER"
+ADMIN_USERNAME="administrator"
+POWER_USER_USERNAME="power_user"
+USER_USERNAME="user"
+
+docker exec "${KEYCLOAK_CONTAINER}" "${KCADM_PATH}" create clients/"${CLIENT_UUID}"/roles -r "${KEYCLOAK_REALM}" -s name="${ROLE_ADMIN}"
+docker exec "${KEYCLOAK_CONTAINER}" "${KCADM_PATH}" create clients/"${CLIENT_UUID}"/roles -r "${KEYCLOAK_REALM}" -s name="${ROLE_POWER_USER}"
+docker exec "${KEYCLOAK_CONTAINER}" "${KCADM_PATH}" create clients/"${CLIENT_UUID}"/roles -r "${KEYCLOAK_REALM}" -s name="${ROLE_USER}"
+
+echo "Creating admin user [${ADMIN_USERNAME}]..."
+docker exec "${KEYCLOAK_CONTAINER}" "${KCADM_PATH}" create users -r "${KEYCLOAK_REALM}" -s username="${ADMIN_USERNAME}" -s enabled=true -s email=administrator@email.com -s firstName=Administrator -s lastName=User
+docker exec "${KEYCLOAK_CONTAINER}" "${KCADM_PATH}" set-password -r "${KEYCLOAK_REALM}" --username "${ADMIN_USERNAME}" --new-password 123456
+
+echo "Assigning client role [${ROLE_ADMIN}] to user [${ADMIN_USERNAME}] in client [${CLIENT_ID}]..."
+docker exec "${KEYCLOAK_CONTAINER}" "${KCADM_PATH}" add-roles -r "${KEYCLOAK_REALM}" --uusername "${ADMIN_USERNAME}" --cclientid "${CLIENT_ID}" --rolename "${ROLE_ADMIN}"
+echo "Created user [${ADMIN_USERNAME}] with password 123456 and client role [${ROLE_ADMIN}] in client [${CLIENT_ID}]"
+
+echo "Creating regular user [${USER_USERNAME}]..."
+docker exec "${KEYCLOAK_CONTAINER}" "${KCADM_PATH}" create users -r "${KEYCLOAK_REALM}" -s username="${USER_USERNAME}" -s enabled=true -s email=user@email.com -s firstName=Normal -s lastName=User
+docker exec "${KEYCLOAK_CONTAINER}" "${KCADM_PATH}" set-password -r "${KEYCLOAK_REALM}" --username "${USER_USERNAME}" --new-password 123456
+
+echo "Assigning client role [${ROLE_USER}] to user [${USER_USERNAME}] in client [${CLIENT_ID}]..."
+docker exec "${KEYCLOAK_CONTAINER}" "${KCADM_PATH}" add-roles -r "${KEYCLOAK_REALM}" --uusername "${USER_USERNAME}" --cclientid "${CLIENT_ID}" --rolename "${ROLE_USER}"
+echo "Created user [${USER_USERNAME}] with password 123456 and client role [${ROLE_USER}] in client [${CLIENT_ID}]"
+
+echo "Creating power user [${POWER_USER_USERNAME}]..."
+docker exec "${KEYCLOAK_CONTAINER}" "${KCADM_PATH}" create users -r "${KEYCLOAK_REALM}" -s username="${POWER_USER_USERNAME}" -s enabled=true -s email=power_user@email.com -s firstName=Power -s lastName=User
+docker exec "${KEYCLOAK_CONTAINER}" "${KCADM_PATH}" set-password -r "${KEYCLOAK_REALM}" --username "${POWER_USER_USERNAME}" --new-password 123456
+
+echo "Assigning client role [${ROLE_POWER_USER}] to user [${POWER_USER_USERNAME}] in client [${CLIENT_ID}]..."
+docker exec "${KEYCLOAK_CONTAINER}" "${KCADM_PATH}" add-roles -r "${KEYCLOAK_REALM}" --uusername "${POWER_USER_USERNAME}" --cclientid "${CLIENT_ID}" --rolename "${ROLE_POWER_USER}"
+echo "Created user [${POWER_USER_USERNAME}] with password 123456 and client role [${ROLE_POWER_USER}] in client [${CLIENT_ID}]"
\ No newline at end of file
diff --git a/initialize-postgres-redis.cmd b/initialize-postgres-redis.cmd
deleted file mode 100644
index d2ca468..0000000
--- a/initialize-postgres-redis.cmd
+++ /dev/null
@@ -1,56 +0,0 @@
-@echo off
-SETLOCAL EnableDelayedExpansion
-
-docker info >nul 2>&1
-if errorlevel 1 (
- echo Error: Docker daemon is not running.
- echo Please start Docker Desktop or Docker service and run this script again.
- pause
- exit /b 1
-)
-
-SET PG_CONTAINER_NAME=standalone-postgresql
-SET PG_VOLUME_NAME=postgres-volume
-SET PG_COMMAND=docker run -d --name !PG_CONTAINER_NAME! -e "POSTGRES_USER=postgres" -e "POSTGRES_PASSWORD=123456" -e "POSTGRES_DB=myspringdatabase" -p 5432:5432 -v !PG_VOLUME_NAME!:/var/lib/postgresql/data postgres:18.0-alpine3.22
-
-echo Checking PostgreSQL container...
-docker ps -a | findstr /C:"!PG_CONTAINER_NAME!" >nul
-
-if errorlevel 1 (
- echo Container [!PG_CONTAINER_NAME!] not existed, creating with volume [!PG_VOLUME_NAME!]...
- !PG_COMMAND!
-) else (
- docker ps | findstr /C:"!PG_CONTAINER_NAME!" >nul
- if errorlevel 1 (
- echo Container with the same name [!PG_CONTAINER_NAME!] stopped, restarting...
- docker start !PG_CONTAINER_NAME!
- ) else (
- echo Container [!PG_CONTAINER_NAME!] is already running...
- )
-)
-
-SET REDIS_CONTAINER_NAME=standalone-redis
-SET REDIS_VOLUME_NAME=redis-volume
-SET REDIS_COMMAND=docker run --detach --name !REDIS_CONTAINER_NAME! -v !REDIS_VOLUME_NAME!:/data -p 6379:6379 redis:8.2.3-bookworm redis-server --save 60 1 --loglevel warning
-
-echo Checking Redis container...
-docker ps -a | findstr /C:"!REDIS_CONTAINER_NAME!" >nul
-
-if errorlevel 1 (
- echo Container [!REDIS_CONTAINER_NAME!] not existed, creating with volume [!REDIS_VOLUME_NAME!]...
- !REDIS_COMMAND!
-) else (
- docker ps | findstr /C:"!REDIS_CONTAINER_NAME!" >nul
- if errorlevel 1 (
- echo Container with the same name [!REDIS_CONTAINER_NAME!] stopped, restarting...
- docker start !REDIS_CONTAINER_NAME!
- ) else (
- echo Container [!REDIS_CONTAINER_NAME!] is already running...
- )
-)
-
-echo.
-echo Currently running containers:
-docker ps
-
-ENDLOCAL
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index 3349874..6bb9ab7 100644
--- a/pom.xml
+++ b/pom.xml
@@ -33,9 +33,6 @@
2.8.14
-
- 4.5.0
-
2.1.0
@@ -51,9 +48,6 @@
2.0.2
-
- 2.2.4
-
1.21.3
@@ -64,6 +58,9 @@
3.20.0
+
+
+ 4.0.0
@@ -99,15 +96,11 @@
org.springframework.boot
- spring-boot-starter-actuator
+ spring-boot-starter-oauth2-resource-server
org.springframework.boot
- spring-boot-starter-cache
-
-
- org.springframework.boot
- spring-boot-starter-data-redis
+ spring-boot-starter-actuator
org.springframework.boot
@@ -174,14 +167,6 @@
-
-
- com.auth0
- java-jwt
- ${java-jwt.version}
-
-
-
org.springdoc
@@ -242,9 +227,9 @@
test
- com.redis
- testcontainers-redis
- ${testcontainers-redis.version}
+ com.github.dasniko
+ testcontainers-keycloak
+ ${testcontainers-keycloak.version}
test
diff --git a/run-docker-compose-stack.cmd b/run-docker-compose-stack.cmd
new file mode 100644
index 0000000..a982040
--- /dev/null
+++ b/run-docker-compose-stack.cmd
@@ -0,0 +1,73 @@
+@echo off
+SETLOCAL EnableDelayedExpansion
+
+REM Check if Docker daemon is running
+docker info >nul 2>&1
+if errorlevel 1 (
+ echo Error: Docker daemon is not running.
+ echo Please start Docker Desktop or Docker service and run this script again.
+ exit /b 1
+)
+
+docker compose down
+docker rmi --force spring-base:1.0.0
+docker compose up --detach
+
+SET KEYCLOAK_CONTAINER=standalone-keycloak
+SET KCADM_PATH=/opt/keycloak/bin/kcadm.sh
+SET KEYCLOAK_REALM=spring-base
+SET CLIENT_ID=spring-base-client
+SET KEYCLOAK_OVERLORD=admin
+SET KEYCLOAK_ADMIN_PASSWORD=123456
+
+echo Configuring KCADM credentials...
+docker exec %KEYCLOAK_CONTAINER% %KCADM_PATH% config credentials --server http://localhost:8080 --realm master --user %KEYCLOAK_OVERLORD% --password %KEYCLOAK_ADMIN_PASSWORD%
+
+docker exec %KEYCLOAK_CONTAINER% %KCADM_PATH% create realms -s realm=%KEYCLOAK_REALM% -s enabled=true
+
+echo Creating client [%CLIENT_ID%]...
+
+:: if anyone has any better idea to not use variable, let me know
+set "CLIENT_CREATE_CMD=docker exec %KEYCLOAK_CONTAINER% %KCADM_PATH% create clients -r %KEYCLOAK_REALM% -s clientId=%CLIENT_ID% -s enabled=true -s publicClient=true -s directAccessGrantsEnabled=true -i"
+for /f "usebackq delims=" %%i in (`!CLIENT_CREATE_CMD!`) do (
+ set CLIENT_UUID=%%i
+)
+
+:: --- Role and User Setup ---
+SET ROLE_ADMIN=ADMIN
+SET ROLE_POWER_USER=POWER_USER
+SET ROLE_USER=USER
+
+set ADMIN_USERNAME=administrator
+set POWER_USER_USERNAME=power_user
+set USER_USERNAME=user
+
+docker exec %KEYCLOAK_CONTAINER% %KCADM_PATH% create clients/%CLIENT_UUID%/roles -r %KEYCLOAK_REALM% -s name=%ROLE_ADMIN%
+docker exec %KEYCLOAK_CONTAINER% %KCADM_PATH% create clients/%CLIENT_UUID%/roles -r %KEYCLOAK_REALM% -s name=%ROLE_POWER_USER%
+docker exec %KEYCLOAK_CONTAINER% %KCADM_PATH% create clients/%CLIENT_UUID%/roles -r %KEYCLOAK_REALM% -s name=%ROLE_USER%
+
+echo Creating admin user [%ADMIN_USERNAME%]...
+docker exec %KEYCLOAK_CONTAINER% %KCADM_PATH% create users -r %KEYCLOAK_REALM% -s username=%ADMIN_USERNAME% -s enabled=true -s email=administrator@email.com -s firstName=Administrator -s lastName=User
+docker exec %KEYCLOAK_CONTAINER% %KCADM_PATH% set-password -r %KEYCLOAK_REALM% --username %ADMIN_USERNAME% --new-password 123456
+
+echo Assigning client role [%ROLE_ADMIN%] to user [%ADMIN_USERNAME%] in client [%CLIENT_ID%]...
+docker exec %KEYCLOAK_CONTAINER% %KCADM_PATH% add-roles -r %KEYCLOAK_REALM% --uusername %ADMIN_USERNAME% --cclientid %CLIENT_ID% --rolename %ROLE_ADMIN%
+echo Created user [%ADMIN_USERNAME%] with password 123456 and client role [%ROLE_ADMIN%] in client [%CLIENT_ID%]
+
+echo Creating regular user [%USER_USERNAME%]...
+docker exec %KEYCLOAK_CONTAINER% %KCADM_PATH% create users -r %KEYCLOAK_REALM% -s username=%USER_USERNAME% -s enabled=true -s email=user@email.com -s firstName=Normal -s lastName=User
+docker exec %KEYCLOAK_CONTAINER% %KCADM_PATH% set-password -r %KEYCLOAK_REALM% --username %USER_USERNAME% --new-password 123456
+
+echo Assigning client role [%ROLE_USER%] to user [%USER_USERNAME%] in client [%CLIENT_ID%]...
+docker exec %KEYCLOAK_CONTAINER% %KCADM_PATH% add-roles -r %KEYCLOAK_REALM% --uusername %USER_USERNAME% --cclientid %CLIENT_ID% --rolename %ROLE_USER%
+echo Created user [%USER_USERNAME%] with password 123456 and client role [%ROLE_USER%] in client [%CLIENT_ID%]
+
+echo Creating power user [%POWER_USER_USERNAME%]...
+docker exec %KEYCLOAK_CONTAINER% %KCADM_PATH% create users -r %KEYCLOAK_REALM% -s username=%POWER_USER_USERNAME% -s enabled=true -s email=power_user@email.com -s firstName=Power -s lastName=User
+docker exec %KEYCLOAK_CONTAINER% %KCADM_PATH% set-password -r %KEYCLOAK_REALM% --username %POWER_USER_USERNAME% --new-password 123456
+
+echo Assigning client role [%ROLE_POWER_USER%] to user [%POWER_USER_USERNAME%] in client [%CLIENT_ID%]...
+docker exec %KEYCLOAK_CONTAINER% %KCADM_PATH% add-roles -r %KEYCLOAK_REALM% --uusername %POWER_USER_USERNAME% --cclientid %CLIENT_ID% --rolename %ROLE_POWER_USER%
+echo Created user [%POWER_USER_USERNAME%] with password 123456 and client role [%ROLE_POWER_USER%] in client [%CLIENT_ID%]
+
+ENDLOCAL
\ No newline at end of file
diff --git a/run-docker-compose-stack.sh b/run-docker-compose-stack.sh
new file mode 100644
index 0000000..bbcc5bf
--- /dev/null
+++ b/run-docker-compose-stack.sh
@@ -0,0 +1,66 @@
+#!/bin/bash
+
+# Check if Docker daemon is running
+if ! docker info > /dev/null 2>&1; then
+ echo "Error: Docker daemon is not running."
+ echo "Please start Docker service and run this script again."
+ read -p "Press any key to continue..."
+ exit 1
+fi
+
+docker compose down
+docker rmi --force spring-base:1.0.0
+docker compose up --detach
+
+KEYCLOAK_CONTAINER="standalone-keycloak"
+KCADM_PATH="/opt/keycloak/bin/kcadm.sh"
+KEYCLOAK_REALM="spring-base"
+CLIENT_ID="spring-base-client"
+KEYCLOAK_OVERLORD="admin"
+KEYCLOAK_ADMIN_PASSWORD="123456"
+
+echo "Configuring KCADM credentials..."
+docker exec "$KEYCLOAK_CONTAINER" "$KCADM_PATH" config credentials --server http://localhost:8080 --realm master --user "$KEYCLOAK_OVERLORD" --password "$KEYCLOAK_ADMIN_PASSWORD"
+
+docker exec "$KEYCLOAK_CONTAINER" "$KCADM_PATH" create realms -s realm="$KEYCLOAK_REALM" -s enabled=true
+
+echo "Creating client [$CLIENT_ID]..."
+
+CLIENT_UUID=$(docker exec "$KEYCLOAK_CONTAINER" "$KCADM_PATH" create clients -r "$KEYCLOAK_REALM" -s clientId="$CLIENT_ID" -s enabled=true -s publicClient=true -s directAccessGrantsEnabled=true -i)
+
+# --- Role and User Setup ---
+ROLE_ADMIN="ADMIN"
+ROLE_POWER_USER="POWER_USER"
+ROLE_USER="USER"
+
+ADMIN_USERNAME="administrator"
+POWER_USER_USERNAME="power_user"
+USER_USERNAME="user"
+
+docker exec "$KEYCLOAK_CONTAINER" "$KCADM_PATH" create clients/"$CLIENT_UUID"/roles -r "$KEYCLOAK_REALM" -s name="$ROLE_ADMIN"
+docker exec "$KEYCLOAK_CONTAINER" "$KCADM_PATH" create clients/"$CLIENT_UUID"/roles -r "$KEYCLOAK_REALM" -s name="$ROLE_POWER_USER"
+docker exec "$KEYCLOAK_CONTAINER" "$KCADM_PATH" create clients/"$CLIENT_UUID"/roles -r "$KEYCLOAK_REALM" -s name="$ROLE_USER"
+
+echo "Creating admin user [$ADMIN_USERNAME]..."
+docker exec "$KEYCLOAK_CONTAINER" "$KCADM_PATH" create users -r "$KEYCLOAK_REALM" -s username="$ADMIN_USERNAME" -s enabled=true -s email=administrator@email.com -s firstName=Administrator -s lastName=User
+docker exec "$KEYCLOAK_CONTAINER" "$KCADM_PATH" set-password -r "$KEYCLOAK_REALM" --username "$ADMIN_USERNAME" --new-password 123456
+
+echo "Assigning client role [$ROLE_ADMIN] to user [$ADMIN_USERNAME] in client [$CLIENT_ID]..."
+docker exec "$KEYCLOAK_CONTAINER" "$KCADM_PATH" add-roles -r "$KEYCLOAK_REALM" --uusername "$ADMIN_USERNAME" --cclientid "$CLIENT_ID" --rolename "$ROLE_ADMIN"
+echo "Created user [$ADMIN_USERNAME] with password 123456 and client role [$ROLE_ADMIN] in client [$CLIENT_ID]"
+
+echo "Creating regular user [$USER_USERNAME]..."
+docker exec "$KEYCLOAK_CONTAINER" "$KCADM_PATH" create users -r "$KEYCLOAK_REALM" -s username="$USER_USERNAME" -s enabled=true -s email=user@email.com -s firstName=Normal -s lastName=User
+docker exec "$KEYCLOAK_CONTAINER" "$KCADM_PATH" set-password -r "$KEYCLOAK_REALM" --username "$USER_USERNAME" --new-password 123456
+
+echo "Assigning client role [$ROLE_USER] to user [$USER_USERNAME] in client [$CLIENT_ID]..."
+docker exec "$KEYCLOAK_CONTAINER" "$KCADM_PATH" add-roles -r "$KEYCLOAK_REALM" --uusername "$USER_USERNAME" --cclientid "$CLIENT_ID" --rolename "$ROLE_USER"
+echo "Created user [$USER_USERNAME] with password 123456 and client role [$ROLE_USER] in client [$CLIENT_ID]"
+
+echo "Creating power user [$POWER_USER_USERNAME]..."
+docker exec "$KEYCLOAK_CONTAINER" "$KCADM_PATH" create users -r "$KEYCLOAK_REALM" -s username="$POWER_USER_USERNAME" -s enabled=true -s email=power_user@email.com -s firstName=Power -s lastName=User
+docker exec "$KEYCLOAK_CONTAINER" "$KCADM_PATH" set-password -r "$KEYCLOAK_REALM" --username "$POWER_USER_USERNAME" --new-password 123456
+
+echo "Assigning client role [$ROLE_POWER_USER] to user [$POWER_USER_USERNAME] in client [$CLIENT_ID]..."
+docker exec "$KEYCLOAK_CONTAINER" "$KCADM_PATH" add-roles -r "$KEYCLOAK_REALM" --uusername "$POWER_USER_USERNAME" --cclientid "$CLIENT_ID" --rolename "$ROLE_POWER_USER"
+echo "Created user [$POWER_USER_USERNAME] with password 123456 and client role [$ROLE_POWER_USER] in client [$CLIENT_ID]"
\ No newline at end of file
diff --git a/run-docker.cmd b/run-docker.cmd
deleted file mode 100644
index c457268..0000000
--- a/run-docker.cmd
+++ /dev/null
@@ -1,14 +0,0 @@
-@echo off
-
-REM Check if Docker daemon is running
-docker info >nul 2>&1
-if errorlevel 1 (
- echo Error: Docker daemon is not running.
- echo Please start Docker Desktop or Docker service and run this script again.
- pause
- exit /b 1
-)
-
-docker compose down
-docker rmi --force spring-base
-docker compose up --detach
\ No newline at end of file
diff --git a/src/main/java/com/vulinh/configuration/AsyncSpringEventsConfiguration.java b/src/main/java/com/vulinh/configuration/AsyncSpringEventsConfiguration.java
deleted file mode 100644
index 7e27d8e..0000000
--- a/src/main/java/com/vulinh/configuration/AsyncSpringEventsConfiguration.java
+++ /dev/null
@@ -1,49 +0,0 @@
-package com.vulinh.configuration;
-
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.boot.system.JavaVersion;
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
-import org.springframework.context.event.ApplicationEventMulticaster;
-import org.springframework.context.event.SimpleApplicationEventMulticaster;
-import org.springframework.core.task.TaskExecutor;
-import org.springframework.core.task.VirtualThreadTaskExecutor;
-import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
-
-@Slf4j
-@Configuration
-public class AsyncSpringEventsConfiguration {
-
- static final String THREAD_NAME_PREFIX = "event_executor_";
-
- @Bean
- public ApplicationEventMulticaster applicationEventMulticaster() {
- var eventMulticaster = new SimpleApplicationEventMulticaster();
-
- var taskExecutor = getTaskExecutor();
-
- eventMulticaster.setTaskExecutor(taskExecutor);
-
- log.info(
- "Configured ApplicationEventMulticaster with async task executor: {}",
- taskExecutor.getClass().getCanonicalName());
-
- return eventMulticaster;
- }
-
- // Use virtual threads only if Java version is 21
- // Not that it is necessary, but a safeguard nonetheless
- static TaskExecutor getTaskExecutor() {
- if (JavaVersion.getJavaVersion().isEqualOrNewerThan(JavaVersion.TWENTY_ONE)) {
- return new VirtualThreadTaskExecutor(THREAD_NAME_PREFIX);
- }
-
- log.info("Virtual Threads not supported, using ThreadPoolTaskExecutor...");
-
- var executor = new ThreadPoolTaskExecutor();
-
- executor.setThreadNamePrefix(THREAD_NAME_PREFIX);
-
- return executor;
- }
-}
diff --git a/src/main/java/com/vulinh/configuration/CustomAuthenticationManager.java b/src/main/java/com/vulinh/configuration/CustomAuthenticationManager.java
deleted file mode 100644
index dee2357..0000000
--- a/src/main/java/com/vulinh/configuration/CustomAuthenticationManager.java
+++ /dev/null
@@ -1,41 +0,0 @@
-package com.vulinh.configuration;
-
-import com.vulinh.configuration.data.CustomAuthentication;
-import com.vulinh.data.dto.carrier.DecodedJwtPayloadCarrier;
-import com.vulinh.data.mapper.UserMapper;
-import com.vulinh.data.repository.UserRepository;
-import com.vulinh.service.sessions.UserSessionService;
-import lombok.RequiredArgsConstructor;
-import org.springframework.lang.NonNull;
-import org.springframework.security.authentication.AuthenticationManager;
-import org.springframework.security.authentication.InternalAuthenticationServiceException;
-import org.springframework.security.core.Authentication;
-import org.springframework.stereotype.Component;
-import org.springframework.transaction.annotation.Transactional;
-
-@Component
-@RequiredArgsConstructor
-public class CustomAuthenticationManager implements AuthenticationManager {
-
- final UserRepository userRepository;
-
- final UserSessionService userSessionService;
-
- @Override
- @Transactional
- @NonNull
- public CustomAuthentication authenticate(Authentication authentication) {
- if (!(authentication.getPrincipal()
- instanceof DecodedJwtPayloadCarrier(var userId, var sessionId))) {
- throw new InternalAuthenticationServiceException(
- "Invalid authentication principal: %s".formatted(authentication));
- }
-
- var userDTO =
- UserMapper.INSTANCE.toBasicUserDTO(
- userRepository.findActiveUser(userId),
- userSessionService.findUserSession(userId, sessionId));
-
- return new CustomAuthentication(userDTO);
- }
-}
diff --git a/src/main/java/com/vulinh/configuration/PasswordEncoderConfiguration.java b/src/main/java/com/vulinh/configuration/PasswordEncoderConfiguration.java
deleted file mode 100644
index 07cf7e0..0000000
--- a/src/main/java/com/vulinh/configuration/PasswordEncoderConfiguration.java
+++ /dev/null
@@ -1,19 +0,0 @@
-package com.vulinh.configuration;
-
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
-import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
-import org.springframework.security.crypto.password.PasswordEncoder;
-
-@Configuration
-@Slf4j
-public class PasswordEncoderConfiguration {
-
- @Bean
- public PasswordEncoder passwordEncoder() {
- log.info("Using BCryptPassword as password encoder");
-
- return new BCryptPasswordEncoder();
- }
-}
diff --git a/src/main/java/com/vulinh/configuration/SecurityConfiguration.java b/src/main/java/com/vulinh/configuration/SecurityConfiguration.java
index bad3eb0..f5ce763 100644
--- a/src/main/java/com/vulinh/configuration/SecurityConfiguration.java
+++ b/src/main/java/com/vulinh/configuration/SecurityConfiguration.java
@@ -3,21 +3,14 @@
import module java.base;
import com.vulinh.configuration.data.ApplicationProperties;
+import com.vulinh.configuration.data.ApplicationProperties.SecurityProperties;
import com.vulinh.data.constant.UserRole;
-import com.vulinh.exception.AuthorizationException;
-import com.vulinh.locale.ServiceErrorCode;
-import com.vulinh.service.token.AccessTokenValidator;
-import com.vulinh.utils.security.Auth0Utils;
-import jakarta.servlet.FilterChain;
-import jakarta.servlet.http.HttpServletRequest;
-import jakarta.servlet.http.HttpServletResponse;
+import com.vulinh.utils.JwtUtils;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
-import org.springframework.http.HttpHeaders;
-import org.springframework.lang.NonNull;
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl;
import org.springframework.security.config.Customizer;
@@ -26,18 +19,11 @@
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
-import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.SecurityFilterChain;
-import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
-import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;
-import org.springframework.security.web.authentication.preauth.PreAuthenticatedGrantedAuthoritiesWebAuthenticationDetails;
import org.springframework.security.web.header.writers.XXssProtectionHeaderWriter.HeaderValue;
-import org.springframework.util.AntPathMatcher;
import org.springframework.web.cors.CorsConfiguration;
+import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
-import org.springframework.web.filter.CorsFilter;
-import org.springframework.web.filter.OncePerRequestFilter;
-import org.springframework.web.servlet.HandlerExceptionResolver;
@Configuration
@EnableWebSecurity
@@ -45,23 +31,20 @@
@Slf4j
public class SecurityConfiguration {
- static final AntPathMatcher ANT_PATH_MATCHER = new AntPathMatcher();
-
- static final UserRole ROLE_ADMIN = UserRole.ADMIN;
+ static final String ROLE_ADMIN_NAME = UserRole.ADMIN.name();
final ApplicationProperties applicationProperties;
- final HandlerExceptionResolver handlerExceptionResolver;
- final AccessTokenValidator accessTokenValidator;
- final CustomAuthenticationManager customAuthenticationManager;
-
@Bean
@SneakyThrows
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) {
+ var security = applicationProperties.security();
+
return httpSecurity
.headers(
- shc ->
- shc.xssProtection(
+ headersConfigurer ->
+ headersConfigurer
+ .xssProtection(
xssConfig -> xssConfig.headerValue(HeaderValue.ENABLED_MODE_BLOCK))
.contentSecurityPolicy(cps -> cps.policyDirectives("script-src 'self'")))
.csrf(AbstractHttpConfigurer::disable)
@@ -69,14 +52,18 @@ public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) {
.sessionManagement(
sessionManagementConfigurer ->
sessionManagementConfigurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
- .authorizeHttpRequests(this::configureAuthorizeHttpRequestCustomizer)
- .addFilterBefore(new JwtFilter(), UsernamePasswordAuthenticationFilter.class)
- .addFilterBefore(createCorsFilter(), UsernamePasswordAuthenticationFilter.class)
- .exceptionHandling(
- exceptionHandlingCustomizer ->
- exceptionHandlingCustomizer
- .accessDeniedHandler(this::handleSecurityException)
- .authenticationEntryPoint(this::handleSecurityException))
+ .authorizeHttpRequests(
+ authorizeHttpRequestsCustomizer ->
+ configureAuthorizeHttpRequestCustomizer(authorizeHttpRequestsCustomizer, security))
+ .cors(corsConfigurer -> corsConfigurer.configurationSource(createCorsFilter()))
+ .oauth2ResourceServer(
+ oAuth2ResourceServerProperties ->
+ oAuth2ResourceServerProperties.jwt(
+ jwtConfigurer ->
+ jwtConfigurer.jwtAuthenticationConverter(
+ jwt ->
+ JwtUtils.parseAuthoritiesByCustomClaims(
+ jwt, security.clientName()))))
.build();
}
@@ -85,7 +72,7 @@ public RoleHierarchy roleHierarchy() {
return RoleHierarchyImpl.fromHierarchy(toHierarchyPhrase());
}
- CorsFilter createCorsFilter() {
+ CorsConfigurationSource createCorsFilter() {
var config = new CorsConfiguration();
config.setAllowCredentials(true);
@@ -97,14 +84,13 @@ CorsFilter createCorsFilter() {
source.registerCorsConfiguration("/**", config);
- return new CorsFilter(source);
+ return source;
}
- void configureAuthorizeHttpRequestCustomizer(
+ static void configureAuthorizeHttpRequestCustomizer(
AuthorizeHttpRequestsConfigurer.AuthorizationManagerRequestMatcherRegistry
- authorizeHttpRequestsCustomizer) {
- var securityProperties = applicationProperties.security();
-
+ authorizeHttpRequestsCustomizer,
+ SecurityProperties securityProperties) {
for (var verbUrl : securityProperties.noAuthenticatedVerbUrls()) {
authorizeHttpRequestsCustomizer.requestMatchers(verbUrl.method(), verbUrl.url()).permitAll();
}
@@ -112,30 +98,18 @@ void configureAuthorizeHttpRequestCustomizer(
for (var privilegeVerbUrl : securityProperties.highPrivilegeVerbUrls()) {
authorizeHttpRequestsCustomizer
.requestMatchers(privilegeVerbUrl.method(), privilegeVerbUrl.url())
- .hasAuthority(ROLE_ADMIN.name());
+ .hasAuthority(ROLE_ADMIN_NAME);
}
authorizeHttpRequestsCustomizer
.requestMatchers(securityProperties.noAuthenticatedUrls().toArray(String[]::new))
.permitAll()
.requestMatchers(securityProperties.highPrivilegeUrls().toArray(String[]::new))
- .hasAuthority(ROLE_ADMIN.name())
+ .hasAuthority(ROLE_ADMIN_NAME)
.anyRequest()
.authenticated();
}
- void handleSecurityException(
- HttpServletRequest request, HttpServletResponse response, Throwable authException) {
- handlerExceptionResolver.resolveException(
- request,
- response,
- null,
- AuthorizationException.invalidAuthorization(
- "Invalid user authorization",
- ServiceErrorCode.MESSAGE_INVALID_AUTHORIZATION,
- authException));
- }
-
static String toHierarchyPhrase() {
var sortedRoles =
Arrays.stream(UserRole.values())
@@ -159,81 +133,4 @@ static String toHierarchyPhrase() {
return result.toString();
}
-
- // Temporary solution to avoid filter being called twice for every request
- class JwtFilter extends OncePerRequestFilter {
-
- @Override
- protected void doFilterInternal(
- @NonNull HttpServletRequest request,
- @NonNull HttpServletResponse response,
- @NonNull FilterChain filterChain) {
- try {
- var header = request.getHeader(HttpHeaders.AUTHORIZATION);
-
- if (header != null) {
- var jwtPayload =
- accessTokenValidator.validateAccessToken(Auth0Utils.parseBearerToken(header));
-
- var authentication =
- customAuthenticationManager.authenticate(
- new PreAuthenticatedAuthenticationToken(jwtPayload, null));
-
- var customAuthentication =
- authentication.addDetails(
- new PreAuthenticatedGrantedAuthoritiesWebAuthenticationDetails(
- request, authentication.getAuthorities()));
-
- SecurityContextHolder.getContext().setAuthentication(customAuthentication);
- }
-
- filterChain.doFilter(request, response);
- } catch (Exception exception) {
- handlerExceptionResolver.resolveException(request, response, null, exception);
- }
- }
-
- @Override
- protected boolean shouldNotFilter(@NonNull HttpServletRequest request) {
- var requestURI = request.getRequestURI();
-
- var securityProperties = applicationProperties.security();
-
- return securityProperties.noAuthenticatedUrls().stream()
- .anyMatch(
- antUrl -> {
- var result = ANT_PATH_MATCHER.match(antUrl, requestURI);
-
- if (result) {
- log.debug(
- "Request URI [{}] matched against ant pattern [{}]", requestURI, antUrl);
- }
-
- return result;
- })
- || securityProperties.noAuthenticatedVerbUrls().stream()
- .anyMatch(
- verbAntUrl -> {
- var requestMethod = request.getMethod();
-
- var antUrl = verbAntUrl.url();
- var antMethod = verbAntUrl.method();
-
- var result =
- antMethod.name().equals(requestMethod)
- && ANT_PATH_MATCHER.match(antUrl, requestURI);
-
- if (result) {
- log.debug(
- "Request URI [{} {}] matched against ant pattern [{} {}]",
- requestMethod,
- requestURI,
- antMethod,
- antUrl);
- }
-
- return result;
- });
- }
- }
}
diff --git a/src/main/java/com/vulinh/configuration/cache/FromUserSessionEntityKeyGenerator.java b/src/main/java/com/vulinh/configuration/cache/FromUserSessionEntityKeyGenerator.java
deleted file mode 100644
index cedea4b..0000000
--- a/src/main/java/com/vulinh/configuration/cache/FromUserSessionEntityKeyGenerator.java
+++ /dev/null
@@ -1,34 +0,0 @@
-package com.vulinh.configuration.cache;
-
-import module java.base;
-
-import com.vulinh.data.entity.UserSession;
-import com.vulinh.data.entity.ids.UserSessionId;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.cache.interceptor.KeyGenerator;
-import org.springframework.lang.NonNull;
-import org.springframework.stereotype.Component;
-
-@Component(FromUserSessionEntityKeyGenerator.BEAN_NAME)
-@Slf4j
-public class FromUserSessionEntityKeyGenerator implements KeyGenerator {
-
- public static final String BEAN_NAME = "fromUserSessionEntity";
-
- @Override
- @NonNull
- public UserSessionId generate(@NonNull Object target, @NonNull Method method, Object... params) {
- var obj = params[0];
-
- if (!(obj instanceof UserSession session)) {
- throw new ClassCastException(
- "Expected class %s, got %s instead".formatted(UserSession.class, obj.getClass()));
- }
-
- var userSessionId = session.getId();
-
- log.debug("{}#{} | key = {}", method.getDeclaringClass(), method.getName(), userSessionId);
-
- return userSessionId;
- }
-}
diff --git a/src/main/java/com/vulinh/configuration/data/ApplicationProperties.java b/src/main/java/com/vulinh/configuration/data/ApplicationProperties.java
index 7746834..1b0268d 100644
--- a/src/main/java/com/vulinh/configuration/data/ApplicationProperties.java
+++ b/src/main/java/com/vulinh/configuration/data/ApplicationProperties.java
@@ -7,24 +7,18 @@
import org.springframework.http.HttpMethod;
@ConfigurationProperties(prefix = "application-properties")
-public record ApplicationProperties(
- SecurityProperties security, SchedulingTaskProperties schedule) {
+public record ApplicationProperties(SecurityProperties security) {
// Testability
@Builder
public record SecurityProperties(
- String publicKey,
- String privateKey,
List noAuthenticatedUrls,
List noAuthenticatedVerbUrls,
List highPrivilegeUrls,
List highPrivilegeVerbUrls,
- Duration jwtDuration,
- Duration refreshJwtDuration,
- Duration passwordResetCodeDuration,
- String issuer) {}
-
- public record SchedulingTaskProperties(String cleanExpiredUserSessions) {}
+ String issuerUri,
+ String realmName,
+ String clientName) {}
public record VerbUrl(HttpMethod method, String url) {}
}
diff --git a/src/main/java/com/vulinh/configuration/data/CustomAuthentication.java b/src/main/java/com/vulinh/configuration/data/CustomAuthentication.java
deleted file mode 100644
index 4f5959e..0000000
--- a/src/main/java/com/vulinh/configuration/data/CustomAuthentication.java
+++ /dev/null
@@ -1,42 +0,0 @@
-package com.vulinh.configuration.data;
-
-import module java.base;
-
-import com.vulinh.data.dto.response.UserBasicResponse;
-import com.vulinh.data.dto.response.data.RoleData;
-import lombok.EqualsAndHashCode;
-import lombok.Getter;
-import org.springframework.security.authentication.AbstractAuthenticationToken;
-import org.springframework.security.core.authority.SimpleGrantedAuthority;
-
-@Getter
-@EqualsAndHashCode(callSuper = true)
-public class CustomAuthentication extends AbstractAuthenticationToken {
-
- @Serial private static final long serialVersionUID = 8271426933575028085L;
-
- final UserBasicResponse principal;
-
- public CustomAuthentication(UserBasicResponse userBasicResponse) {
- super(
- userBasicResponse.userRoles().stream()
- .map(RoleData::id)
- .map(String::valueOf)
- .map(SimpleGrantedAuthority::new)
- .toList());
-
- principal = userBasicResponse;
- setAuthenticated(true);
- }
-
- public CustomAuthentication addDetails(Object detail) {
- setDetails(detail);
-
- return this;
- }
-
- @Override
- public Object getCredentials() {
- return null;
- }
-}
diff --git a/src/main/java/com/vulinh/controller/api/AuthAPI.java b/src/main/java/com/vulinh/controller/api/AuthAPI.java
deleted file mode 100644
index b23b87b..0000000
--- a/src/main/java/com/vulinh/controller/api/AuthAPI.java
+++ /dev/null
@@ -1,201 +0,0 @@
-package com.vulinh.controller.api;
-
-import module java.base;
-
-import com.vulinh.data.constant.CommonConstant;
-import com.vulinh.data.constant.EndpointConstant;
-import com.vulinh.data.constant.EndpointConstant.AuthEndpoint;
-import com.vulinh.data.dto.carrier.TokenResponse;
-import com.vulinh.data.dto.request.PasswordChangeRequest;
-import com.vulinh.data.dto.request.RefreshTokenRequest;
-import com.vulinh.data.dto.request.UserLoginRequest;
-import com.vulinh.data.dto.request.UserRegistrationRequest;
-import com.vulinh.data.dto.response.GenericResponse;
-import com.vulinh.data.dto.response.SingleUserResponse;
-import io.swagger.v3.oas.annotations.Operation;
-import io.swagger.v3.oas.annotations.media.Content;
-import io.swagger.v3.oas.annotations.media.ExampleObject;
-import io.swagger.v3.oas.annotations.responses.ApiResponse;
-import io.swagger.v3.oas.annotations.tags.Tag;
-import jakarta.servlet.http.HttpServletRequest;
-import org.springframework.http.HttpHeaders;
-import org.springframework.http.HttpStatus;
-import org.springframework.http.ResponseEntity;
-import org.springframework.web.bind.annotation.*;
-
-@RequestMapping(EndpointConstant.ENDPOINT_AUTH)
-@Tag(
- name = "Authentication controller",
- description =
- "Public controller that allows user login, user registration and user confirmation")
-public interface AuthAPI {
-
- @PostMapping(AuthEndpoint.LOGIN)
- @Operation(
- summary = "User login",
- description = "Login with username and password",
- requestBody =
- @io.swagger.v3.oas.annotations.parameters.RequestBody(
- content =
- @Content(
- examples =
- @ExampleObject(
- value =
- """
- {
- "username": "admin",
- "password": "12345678"
- }
- """))),
- responses = {
- @ApiResponse(
- responseCode = CommonConstant.MESSAGE_OK,
- description = "Authentication successfully",
- content =
- @Content(
- examples =
- @ExampleObject(
- value =
- """
- {
- "errorCode": "M0000",
- "displayMessage": "Success",
- "data": {
- "accessToken": "your-access-token",
- "issuedAt": "ISO Date and Time",
- "expiration": "ISO Date and Time"
- }
- }
- """))),
- @ApiResponse(
- responseCode = CommonConstant.MESSAGE_UNAUTHORIZED,
- description = "Invalid credentials",
- content =
- @Content(
- examples =
- @ExampleObject(
- value =
- """
- {
- "errorCode": "M0401",
- "displayMessage": "Invalid credentials"
- }
- """))),
- @ApiResponse(
- responseCode = CommonConstant.MESSAGE_INTERNAL_SERVER_CODE,
- description = CommonConstant.MESSAGE_INTERNAL_SERVER_SUMMARY,
- content =
- @Content(
- examples =
- @ExampleObject(value = CommonConstant.MESSAGE_INTERNAL_SERVER_ERROR)))
- })
- GenericResponse login(@RequestBody UserLoginRequest userLoginRequest);
-
- @PostMapping(AuthEndpoint.REGISTER)
- @Operation(
- summary = "User Registration",
- description = "Register a new user",
- requestBody =
- @io.swagger.v3.oas.annotations.parameters.RequestBody(
- content =
- @Content(
- examples =
- @ExampleObject(
- value =
- """
- {
- "username": "username",
- "password": "12345678",
- "email": "email@company.com",
- "fullName": "Name"
- }
- """))),
- responses = {
- @ApiResponse(
- responseCode = CommonConstant.MESSAGE_CREATED,
- description = "User created",
- content =
- @Content(
- examples =
- @ExampleObject(
- value =
- """
- {
- "errorCode": "M0000",
- "displayMessage": "Success",
- "data": {
- "id": "user-uuid-here",
- "username": "username",
- "fullName": "Name",
- "email": "email@company.com",
- "createdDate": "ISO Date and Time",
- "updatedDate": "ISO Date and Time"
- }
- }
- """))),
- @ApiResponse(
- responseCode = CommonConstant.MESSAGE_BAD_REQUEST,
- description = CommonConstant.MESSAGE_USER_FIELD_INVALID,
- content =
- @Content(
- examples =
- @ExampleObject(
- value = CommonConstant.MESSAGE_USER_CREATION_VALIDATION_FAILED))),
- @ApiResponse(
- responseCode = CommonConstant.MESSAGE_INTERNAL_SERVER_CODE,
- description = CommonConstant.MESSAGE_INTERNAL_SERVER_SUMMARY,
- content =
- @Content(
- examples =
- @ExampleObject(value = CommonConstant.MESSAGE_INTERNAL_SERVER_ERROR)))
- })
- @ResponseStatus(HttpStatus.CREATED)
- GenericResponse register(
- @RequestBody UserRegistrationRequest userRegistrationRequest);
-
- @GetMapping(AuthEndpoint.CONFIRM_USER)
- @Operation(
- summary = "Confirm user registration",
- description = "Confirm the user registration via a public link",
- responses = {
- @ApiResponse(
- responseCode = CommonConstant.MESSAGE_OK,
- description = "User registered successfully",
- content = @Content(examples = @ExampleObject(value = CommonConstant.MESSAGE_SUCCESS))),
- @ApiResponse(
- responseCode = CommonConstant.MESSAGE_NOT_FOUND,
- description = "Invalid user or confirmation code",
- content =
- @Content(
- examples =
- @ExampleObject(
- value =
- """
- {
- "errorCode": "M0405",
- "displayMessage": "Invalid user confirmation"
- }
- """))),
- @ApiResponse(
- responseCode = CommonConstant.MESSAGE_INTERNAL_SERVER_CODE,
- description = CommonConstant.MESSAGE_INTERNAL_SERVER_SUMMARY,
- content =
- @Content(
- examples =
- @ExampleObject(value = CommonConstant.MESSAGE_INTERNAL_SERVER_ERROR)))
- })
- ResponseEntity> confirmUser(
- @RequestParam UUID userId, @RequestParam String code);
-
- @PatchMapping(AuthEndpoint.CHANGE_PASSWORD)
- ResponseEntity