Skip to content

feat: migrate JWT from RSA encryption to HMAC signing with persistent…#83

Merged
omatheusmesmo merged 2 commits intoomatheusmesmo:developfrom
felipemelozx:feature/persistent-jwt-keys
Mar 8, 2026
Merged

feat: migrate JWT from RSA encryption to HMAC signing with persistent…#83
omatheusmesmo merged 2 commits intoomatheusmesmo:developfrom
felipemelozx:feature/persistent-jwt-keys

Conversation

@felipemelozx
Copy link
Copy Markdown
Contributor

Pull Request Description

This PR replaces in-memory RSA key generation with persistent HMAC-SHA256 signing, ensuring JWT tokens remain valid across application restarts.


Related Task


What was done?

  • Migrated from JWE (RSA encryption) to JWS (HMAC-SHA256 signing)
  • Replaced automatic RSA key pair generation with environment-based secret key loading
  • Added jwt.secret.key configuration in application.properties that reads from JWT_SECRET_KEY environment variable
  • Implemented lazy initialization for MACSigner and MACVerifier to handle Spring's @value injection timing
  • Updated unit tests to use HMAC signing with ReflectionTestUtils

Tests

  • Tested locally - tokens generated before restart remain valid after restart
  • Updated unit tests to reflect HMAC implementation

How to Test

  1. Set JWT_SECRET_KEY environment variable (or use the default fallback):
    export JWT_SECRET_KEY="your-secret-key-here"
  2. Start the application and generate a token by logging in
  3. Restart the application
  4. Verify that the previously generated token is still valid (before this change, restarting would invalidate all tokens)

@omatheusmesmo omatheusmesmo self-requested a review March 1, 2026 10:09
@omatheusmesmo omatheusmesmo self-assigned this Mar 1, 2026
Copy link
Copy Markdown
Owner

@omatheusmesmo omatheusmesmo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Analysis Summary

This PR implements persistent JWT signing keys using HMAC-SHA256, addressing the issue where user sessions were invalidated after a server restart (#18). However, the transition from RSA (Asymmetric/JWE) to HMAC (Symmetric/JWS) represents a security regression for the project, but that's on me.

Note: Responsibility for Algorithm Choice

I must emphasize that the shift to HMAC was a direct result of my poorly written issue description in #18. By providing the option of "RSA or HMAC" without specifying that we should maintain the previous encryption standard (JWE), I led the implementation toward a simpler but less secure path. I take full responsibility for this lack of technical precision, which resulted in the removal of the robust RSA-based encryption we already had. I'm sorry.

Identified Issues

1. Loss of Confidentiality (JWE vs. JWS)

The previous implementation used JWE (JSON Web Encryption) with RSA. This ensured that the token's content (claims) was unreadable to third parties.

  • Current State: With JWS (HMAC), the payload is only signed. Anyone can decode the Base64 and read sensitive data (e.g., usernames, IDs).
  • Misnomer: The decryptToken method in JwtService.java is now a misnomer, as it only verifies the signature and does not perform decryption.

2. Symmetric vs. Asymmetric Security

  • HMAC requires the same secret key to be present everywhere the token is validated. If this key is leaked, an attacker can forge valid tokens.
  • RSA allows other services in the future to validate the token using only the Public Key, without ever having access to the Private Key (the token generator).

3. Thread-Safety and Lazy Initialization

The initSignerAndVerifier() implementation is not thread-safe. In a high-concurrency environment during startup, multiple requests might try to initialize the signer and verifier simultaneously in the Spring Singleton.

Proposed Improvement

The original RSA implementation was excellent; it only lacked decoupling key generation from memory. No major structural changes are needed—just pointing to external key files.

Suggested Path:

  1. Revert to RSA/JWE: Maintain the high security standard of encryption.
  2. External Key Loading: Use .pem files (Public and Private Key) stored in src/main/resources/certs/ (or a path defined via environment variables).
  3. Configuration via Properties: Use @ConfigurationProperties to inject keys, eliminating the need for manual initialization and ReflectionTestUtils in tests.
  4. Security: Add the certificate folder to .gitignore.

Recommended References


Conclusion: I suggest we maintain the RSA standard we already had, simply adjusting how keys are loaded by following persistence best practices.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Mar 2, 2026

🤖 Hi @felipemelozx, I've received your request, and I'm working on it now! You can track my progress in the logs for more details.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Mar 2, 2026

🤖 I'm sorry @felipemelozx, but I was unable to process your request. Please see the logs for more details.

Copy link
Copy Markdown
Owner

@omatheusmesmo omatheusmesmo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excellent work, @felipemelozx — well done! 🎉

@omatheusmesmo omatheusmesmo merged commit 5764477 into omatheusmesmo:develop Mar 8, 2026
13 of 15 checks passed
@felipemelozx felipemelozx deleted the feature/persistent-jwt-keys branch April 19, 2026 12:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants