This document explains how to configure SSL/TLS for the embedded Tomcat server in the iSLDevs application using Spring Boot 3.4.4.
- Java 21
- Spring Boot 3.4.4
- Tomcat 10.1.x (embedded with Spring Boot 3.4.4)
- Valid SSL certificate (or self-signed for development)
For development environments, generate a self-signed certificate:
keytool -genkeypair -alias yourserver -keyalg RSA -keysize 4096 \
-validity 365 -storetype PKCS12 -keystore src/main/resources/yourserver.p12 \
-storepass changeit -keypass changeit \
-dname "CN=localhost" -ext "SAN=DNS:localhost,IP:127.0.0.1"
Check certificate details
keytool -list -v -keystore src/main/resources/yourserver.p12 -storepass changeit
Check supported protocols (should only show TLS 1.2/1.3)
nmap --script ssl-enum-ciphers -p 8443 localhost
Java configuration class:
- src/main/java/com/base/config/TomcatSSLConfig.java
This project demonstrates how to configure PostgreSQL in Spring Boot 3.4.4 using environment variables instead of property files, following security best practices.
- π 100% environment variable driven configuration
- π Production-ready HikariCP connection pooling
- β‘ No hardcoded credentials in property files
- π Works across all environments (local, Docker, Kubernetes, cloud)
- Java 21
- Spring Boot 3.4.4
- MySQL 8.0 / PostgreSQL 16.8
- Maven/Gradle
Variable | Example Value | Description |
---|---|---|
DB_URL |
jdbc:{youurl}://localhost:{port}}/db |
JDBC URL |
DB_USERNAME |
youruser |
Username |
DB_PASSWORD |
yourpass |
Password |
DB_DRIVER_CLASS |
yourdriverclassname |
Driver class name |
Variable | Default | Description |
---|---|---|
DB_POOL_MAX_SIZE |
10 | Maximum connection pool size |
DB_POOL_MIN_IDLE |
5 | Minimum idle connections |
DB_CONNECTION_TIMEOUT |
5000 (ms) | Fail fast in production (5s) |
DB_IDLE_TIMEOUT |
120000 (ms) | Reclaim unused connections faster (2m) |
DB_MAX_LIFETIME |
1800000 (ms) | Prevent stale connections (30m) |
DB_LEAK_DETECTION_THRESHOLD |
60000 (ms) | Lead to resource exhaustion and application crashes (60s) |
- Set environment variables:
# Linux/macOS export DB_URL=jdbc:{youurl}://localhost:{port}}/db export DB_USERNAME=youruser export DB_PASSWORD=yourpass export DB_DRIVER_CLASS=yourdriverclassname # Windows set DB_URL=jdbc:{youurl}://localhost:{port}}/db set DB_USERNAME=youruser set DB_PASSWORD=yourpass set DB_DRIVER_CLASS=yourdriverclassname
-
Open Run Configurations
- Click the dropdown near "Run" β Edit Configurations...
-
Add Environment Variables
- Select your Spring Boot application configuration
- Under Environment Variables, add:
DB_URL=jdbc:{youurl}://localhost:{port}}/db DB_USERNAME=youruser DB_PASSWORD=yourpass DB_DRIVER_CLASS=yourdriverclassname
- Click Apply β OK
-
Open Run Configurations
- Right-click project β Run As β Run Configurations...
-
Add Environment Variables
- Select your Spring Boot application
- Go to Environment tab β Add:
Name Value DB_URL
jdbc:{youurl}://localhost:{port}}/db
DB_USERNAME
youruser
DB_PASSWORD
yourpass
DB_DRIVER_CLASS
yourdriverclassname
- Click Apply β Run
- Select your Spring Boot application
application.properties
spring.profiles.active=dev
spring.env.file=classpath:config/.dev
#spring.profiles.active=prod
#spring.env.file:classpath:config/.prod
resources/config/.dev
DB_URL=jdbc:{youurl}://localhost:{port}}/db
DB_USERNAME=youruser
DB_PASSWORD=yourpass
DB_DRIVER_CLASS=yourdriverclassname
DB_POOL_MAX_SIZE=15
DB_POOL_MIN_IDLE=5
DB_CONNECTION_TIMEOUT=30000
DB_IDLE_TIMEOUT=600000
DB_MAX_LIFETIME=1800000
DB_LEAK_DETECTION_THRESHOLD=0
HIBERNATE_DIALECT=yourhibernatedialect
HIBERNATE_DDL_AUTO=update
HIBERNATE_SHOW_SQL=true
HIBERNATE_FORMAT_SQL=true
resources/config/.prod
DB_URL=jdbc:{youurl}://localhost:{port}}/db
DB_USERNAME=youruser
DB_PASSWORD=yourpass
DB_DRIVER_CLASS=yourdriverclassname
DB_POOL_MAX_SIZE=25
DB_POOL_MIN_IDLE=10
DB_CONNECTION_TIMEOUT=5000
DB_IDLE_TIMEOUT=120000
DB_MAX_LIFETIME=900000
DB_LEAK_DETECTION_THRESHOLD=60000
HIBERNATE_DIALECT=yourhibernatedialect
HIBERNATE_DDL_AUTO=validate
HIBERNATE_SHOW_SQL=false
HIBERNATE_FORMAT_SQL=true
This provides specific Postman endpoint configurations for testing each client ID flow.
- Postman: Download and install Postman from www.postman.com.
- Running Application: Ensure your Spring Boot application is running and accessible.
- Environment Variables (Optional): Consider setting up Postman environment variables for host, port, and client secrets.
Here are the Postman endpoints for each client ID flow:
-
Client ID:
{client-id}
-
Grant Type: Authorization Code with PKCE
-
Authorization Endpoint:
- Method:
GET
- URL:
http://{your-authorization-server-host}:{port}/oauth2/authorize
- Query Parameters:
response_type
:code
client_id
:{client-id}
redirect_uri
:http://{your-redirect-uri}:{port}/login/oauth2/code/client
scope
:openid profile email
code_challenge
:{code_challenge}
(Generate using PKCE; see PKCE generation below)code_challenge_method
:S256
state
:{state}
(Optional; recommended for security)
- Method:
-
Token Endpoint (Authorization Code):
- Method:
POST
- URL:
http://{your-authorization-server-host}:{port}/oauth2/token
- Headers:
Content-Type
:application/x-www-form-urlencoded
- Body (x-www-form-urlencoded):
grant_type
:authorization_code
code
:{authorization_code}
(Obtained from the Authorization Endpoint redirect)redirect_uri
:http://{your-redirect-uri}:{port}/login/oauth2/code/client
client_id
:{client-id}
client_secret
:{client-secret}
code_verifier
:{code_verifier}
(Generated with the code challenge)
- Method:
-
Token Endpoint (Refresh Token):
- Method:
POST
- URL:
http://{your-authorization-server-host}:{port}/oauth2/token
- Headers:
Content-Type
:application/x-www-form-urlencoded
- Body (x-www-form-urlencoded):
grant_type
:refresh_token
refresh_token
:{refresh_token}
(Obtained from the initial token response)client_id
:{client-id}
client_secret
:{client-secret}
- Method:
-
PKCE Code Generation (JavaScript Example):
-
Generate Code Verifier:
-
Create a random string. It should be between 43 and 128 characters long.
-
Example (JavaScript):
function generateCodeVerifier() { const randomString = (Math.random() + 1).toString(36).substring(7); return randomString; } const codeVerifier = generateCodeVerifier(); console.log("Code Verifier:", codeVerifier);
-
-
Generate Code Challenge:
-
Hash the code verifier using SHA-256 and base64url-encode the result.
-
Example (JavaScript):
async function generateCodeChallenge(codeVerifier) { function base64url(string) { return btoa(String.fromCharCode.apply(null, new Uint8Array(string))) .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); } const encoder = new TextEncoder(); const data = encoder.encode(codeVerifier); const digest = await crypto.subtle.digest('SHA-256', data); return base64url(digest); } generateCodeChallenge(codeVerifier).then(codeChallenge => { console.log("Code Challenge:", codeChallenge); });
-
-
Use in Authorization Request:
- Send the
code_challenge
andcode_challenge_method=S256
in the authorization endpoint request.
- Send the
-
Use Code verifier in Token Request:
- Send the original
code_verifier
in the token endpoint request.
- Send the original
-
-
-
Client ID:
service-account
-
Grant Type: Client Credentials
- Token Endpoint:
- Method:
POST
- URL:
http://{your-authorization-server-host}:{port}/oauth2/token
- Headers:
Content-Type
:application/x-www-form-urlencoded
- Body (x-www-form-urlencoded):
grant_type
:client_credentials
client_id
:{client-id}
client_secret
:{client-secret}
scope
:api.internal monitoring.read
- Method:
- Token Endpoint:
-
Client ID:
microservice
-
Grant Type: JWT Bearer
-
Token Endpoint:
- Method:
POST
- URL:
http://{your-authorization-server-host}:{port}/oauth2/token
- Headers:
Content-Type
:application/x-www-form-urlencoded
- Body (x-www-form-urlencoded):
grant_type
:urn:ietf:params:oauth:grant-type:jwt-bearer
assertion
:{signed_jwt}
(Generated JWT; see JWT creation section)client_id
:{client-id}
- Method:
-
JWT Creation Notes:
-
Obtain Private Key:
- Retrieve the private key associated with the
microservice
client. This key is used to sign the JWT.
- Retrieve the private key associated with the
-
Construct JWT Claims:
-
Create a JSON object with the required JWT claims.
-
Required claims: *
iss
(Issuer): The issuer of the JWT (your authorization server). *sub
(Subject): Theclient_id
(microservice
). *aud
(Audience): The token endpoint URL. *exp
(Expiration Time): The expiration time of the JWT (in seconds since epoch). *iat
(Issued At): The time the JWT was issued (in seconds since epoch). -
Example (JSON):
{ "iss": "http://{your-authorization-server-host}:{port}", "sub": "microservice", "aud": "http://{your-authorization-server-host}:{port}/oauth2/token", "exp": 1678886400, "iat": 1678882800 }
-
-
Sign the JWT:
- Sign the JWT using the private key and the appropriate algorithm (e.g., RS256).
- Libraries in most programming languages can handle this.
-
Use in Token Request:
- Send the signed JWT as the
assertion
parameter in the token endpoint request.
- Send the signed JWT as the
-
-
-
Client ID:
iot-device
-
Grant Type: Device Code
-
Device Authorization Endpoint:
- Method:
POST
- URL:
http://{your-authorization-server-host}:{port}/oauth2/device_authorization
- Headers:
Content-Type
:application/x-www-form-urlencoded
- Body (x-www-form-urlencoded):
client_id
:{client-id}
scope
:openid device.manage
- Method:
-
Token Endpoint (Polling):
- Method:
POST
- URL:
http://{your-authorization-server-host}:{port}/oauth2/token
- Headers:
Content-Type
:application/x-www-form-urlencoded
- Body (x-www-form-urlencoded):
grant_type
:urn:ietf:params:oauth:grant-type:device_code
device_code
:{device_code}
(Obtained from the Device Authorization Endpoint)client_id
:{client-id}
client_secret
:{client-secret}
- Method:
-
Token Endpoint (Refresh Token):
- Method:
POST
- URL:
http://{your-authorization-server-host}:{port}/oauth2/token
- Headers:
Content-Type
:application/x-www-form-urlencoded
- Body (x-www-form-urlencoded):
grant_type
:refresh_token
refresh_token
:{refresh_token}
(Obtained from the initial token response)client_id
:{client-id}
client_secret
:{client-secret}
- Method:
-
- Replace
{your-authorization-server-host}
and{port}
with your actual host and port values. - The
secret
client secrets are for development purposes only. Use secure storage in production. - For the JWT Bearer flow, you'll need to implement JWT creation in your application.
- For the Device Code flow, be aware of the user interaction required.
- For all flows, ensure your client secrets are handled securely.
- Remember to replace any bracketed placeholders with actual values.
- Ensure that your redirect URIs are correct for your frontend application.
- Adjust the scopes to match the access requirements of your clients.
- Be mindful of token expiration times and refresh token usage.
This project uses the gradle-license-plugin
by Hierynomus to automatically add the Apache 2.0 license header to all Java source files. This ensures compliance with the Apache 2.0 license and clearly defines the terms under which the software is distributed.
Steps to Add the License Header:
-
Add the Plugin Dependency to
build.gradle
:Open your
build.gradle
file and add the following line to theplugins
block:plugins { // ... other plugins id "com.github.hierynomus.license" version "0.16.1" }
Make sure to check the plugin's GitHub repository (https://github.com/hierynomus/gradle-license) for the latest version if needed.
-
Create the
LICENSE_HEADER
File:In the root of your project directory, create a new file named
LICENSE_HEADER
. -
Populate
LICENSE_HEADER
with the Apache 2.0 License Text:Paste the following content into the
LICENSE_HEADER
file, making sure to replace[yyyy]
with the appropriate year and[name of copyright owner]
with the correct copyright holder:Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
-
Configure the License Plugin in
build.gradle
:Add the following
license
block to yourbuild.gradle
file:license { header = rootProject.file('LICENSE_HEADER') // You can add exclusions if needed: // exclude "**/*.properties" // exclude "**/*.yml" // exclude "**/*.md" }
-
Apply the License Headers:
Open your terminal in the root of your project directory and run the following Gradle command:
./gradlew licenseFormat
This will add the license header to all your Java source files that are missing it or have an incorrect one.
-
Verify License Headers:
You can verify that all files have the correct license header by running:
./gradlew licenseCheck
This task will fail if any files are missing the header or have an incorrect one.
Important Considerations:
- Backup: Before running
licenseFormat
for the first time, it's recommended to commit your current changes or create a backup of your project. - Existing Headers: The plugin might replace existing license headers with the configured one. Review the changes carefully after running
licenseFormat
. - Exclusions: Use the
exclude
configuration option to prevent the plugin from adding headers to specific files or directories (e.g., configuration files, documentation). - Copyright Year: Ensure the copyright year in your
LICENSE_HEADER
is accurate. - CI/CD Integration: Consider adding the
gradle licenseCheck
task to your GitHub Actions workflow (ingradle.yml
) to automatically verify license headers during your CI builds. This helps maintain consistency.
This project uses the Gradle Versions Plugin to maintain dependency hygiene, security, and stability. The system provides automated checks for outdated libraries with configurable update policies.
1.Standard check
./gradlew dependencyUpdate
2.Stable releases only
./gradlew dependencyUpdate -Drevision=release
This document explains how to manage and configure RSA key pairs for JWT signing and verification, including secure storage and key loading in a Spring Boot application.
Use OpenSSL to generate a 2048-bit RSA private key and extract its public key:
# Generate private key
openssl genrsa -out key.private 2048
# Extract public key
openssl rsa -in key.private -pubout -out key.public
Place the generated keys (key.private and key.public) in the src/main/resources directory for easy classpath loading.
The salt value is used for encrypting the private key or other cryptographic operations. You can derive the salt from a meaningful string by converting it to hexadecimal.
For example, for "isldevs":
echo -n "isldevs" | xxd -p
# Output: 69736c64657673
# Use the hex string 69736c64657673 as your salt value.
Configure your application.properties or application.yml with the following properties:
jwt.key.public=classpath:key.public
jwt.key.private=classpath:key.private
jwt.key.id=isldevs
jwt.persistence.password=iamsldevs
jwt.persistence.salt=69736c64657673
This project uses Gradle dependency locking to ensure consistent builds across all environments.
A gradle.lockfile is automatically generated and tracked in version control.
This file captures the exact resolved versions of all dependencies and prevents unexpected upgrades due to dynamic or transitive versions.
Dependency locking is enabled via dependencyLocking in build.gradle.
- π§ To update the lock file: Use the following command to regenerate it after modifying dependencies:
./gradlew dependencies --write-locks
- To refresh all dependencies and update the lock file:
./gradlew dependencies --write-locks --refresh-dependencies
π‘ The gradle.lockfile is intentionally located at the project root, following Gradle's standard behavior.