Skip to content

feat: implement JWKS endpoint (#171)#199

Open
TharunvenkateshN wants to merge 3 commits into
roshankumar0036singh:mainfrom
TharunvenkateshN:feat/jwks-endpoint
Open

feat: implement JWKS endpoint (#171)#199
TharunvenkateshN wants to merge 3 commits into
roshankumar0036singh:mainfrom
TharunvenkateshN:feat/jwks-endpoint

Conversation

@TharunvenkateshN

@TharunvenkateshN TharunvenkateshN commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

Description

Closes #171

This PR implements the JSON Web Key Set (JWKS) endpoint to allow downstream resource servers to cryptographically verify JWTs autonomously using asymmetric encryption.

Changes Made

  • Algorithm Migration: Transitioned token signing from HS256 (symmetric) to RS256 (asymmetric RSA).
  • Key Management: Updated internal/config/config.go to parse private.pem and public.pem. Added fallback logic to dynamically generate an in-memory temporary key pair for seamless local development if keys are not provided.
  • JWT Headers: Injected the kid (Key ID) header during token generation so clients know which public key to use.
  • JWKS Endpoint: Created internal/handler/jwks_handler.go to safely expose the RSA public key (modulus and exponent) in a standards-compliant JWK format at GET /.well-known/jwks.json.
  • Tests Updated: Updated token_service_test.go and other tests to generate and utilize RS256 keys.

How to Test

  1. Start the server.
  2. Navigate to http://localhost:3000/.well-known/jwks.json to view the exposed public keys.
  3. Authenticate to retrieve a JWT, decode it on jwt.io, and verify that the algorithm is RS256 with a kid header.

Summary by CodeRabbit

Release Notes

  • New Features
    • Added /.well-known/jwks.json endpoint for JWT key distribution following OpenID Connect standards.
    • JWT tokens now signed using RSA keys (RS256) instead of HMAC secrets with support for key identifiers.

@coderabbitai

coderabbitai Bot commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@TharunvenkateshN, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 26 minutes and 38 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 79744048-a28a-4f90-b689-111738990cd3

📥 Commits

Reviewing files that changed from the base of the PR and between e2d828d and 636bc70.

📒 Files selected for processing (13)
  • docs/docs.go
  • docs/swagger.json
  • docs/swagger.yaml
  • internal/config/config.go
  • internal/handler/auth_handler_protected_test.go
  • internal/handler/auth_handler_test.go
  • internal/handler/jwks_handler.go
  • internal/handler/oauth_handler_test.go
  • internal/middleware/auth_test.go
  • internal/service/security_phase1_test.go
  • internal/service/token_service.go
  • internal/service/token_service_test.go
  • internal/testutils/setup.go
📝 Walkthrough

Walkthrough

JWT signing is migrated from HMAC shared secrets to RSA key pairs (RS256) across config, token service, and tests. JWTConfig now holds RSA key pointers loaded from PEM files (with an in-memory dev fallback). A new /.well-known/jwks.json endpoint is added to serve the RSA public key in standard JWKS format.

Changes

RSA JWT Migration and JWKS Endpoint

Layer / File(s) Summary
JWTConfig struct and RSA key loading
internal/config/config.go
JWTConfig replaces AccessSecret/RefreshSecret string fields with PrivateKey *rsa.PrivateKey, PublicKey *rsa.PublicKey, and KeyID string. LoadConfig calls a new loadRSAKeys() helper that reads PEM files from JWT_PRIVATE_KEY_PATH/JWT_PUBLIC_KEY_PATH or generates a temporary 2048-bit RSA keypair when files are absent; JWT_KEY_ID defaults to default-key-1.
Token service: HS256 → RS256 signing and validation
internal/service/token_service.go, internal/service/token_service_test.go
GenerateAccessToken, GenerateRefreshToken, and GenerateMFAToken switch from jwt.SigningMethodHS256 + byte secrets to jwt.SigningMethodRS256 + RSA private key, and set the kid header. ValidateAccessToken, ValidateRefreshToken, and ValidateMFAToken key functions now require *jwt.SigningMethodRSA and return the RSA public key. Tests add a getTestRSAKeys() helper and update config construction to use RSA fields.
JWKS DTO, handler, and route registration
internal/dto/jwks.go, internal/handler/jwks_handler.go, internal/routes/routes.go
New JWK and JWKSResponse DTOs define the JWKS JSON shape. JWKSHandler.GetJWKS base64url-encodes the RSA public key modulus and exponent, assembles a JWKSResponse, and returns it as HTTP 200; returns HTTP 500 if the public key is nil. Route GET /.well-known/jwks.json is registered in SetupRoutes.

Sequence Diagram

sequenceDiagram
    participant ResourceServer
    participant GinRouter
    participant JWKSHandler
    participant TokenService
    participant Config

    ResourceServer->>GinRouter: GET /.well-known/jwks.json
    GinRouter->>JWKSHandler: GetJWKS(c *gin.Context)
    JWKSHandler->>Config: Read JWT.PublicKey, JWT.KeyID
    JWKSHandler-->>ResourceServer: HTTP 200 { keys: [{ kty, alg, use, kid, n, e }] }

    ResourceServer->>ResourceServer: Cache public key by kid
    ResourceServer->>TokenService: Verify JWT (RS256, kid lookup)
    TokenService->>Config: Load RSA PrivateKey for signing
    TokenService-->>ResourceServer: Validated claims
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • roshankumar0036singh/auth-server#119: Modifies GenerateMFAToken/ValidateMFAToken and their JWT purpose-scoped claims logic, which this PR also updates when migrating MFA token signing from HMAC to RSA.

Suggested labels

Hard, SSoC26

Suggested reviewers

  • roshankumar0036singh

Poem

🐇 Hop hop, the secrets are gone from plain sight,
RSA keys now guard each token with might.
A PEM file read, or keys spun on the fly,
The JWKS endpoint waves its key to the sky.
No more shared strings — just math, pub, and priv,
Base64url encoded, for all to believe! 🔑

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: implement JWKS endpoint (#171)' clearly and concisely describes the main change—implementing a JWKS endpoint—directly addressing the primary objective of the PR.
Description check ✅ Passed The PR description is comprehensive, detailing all major changes including algorithm migration, key management, JWT headers, JWKS endpoint implementation, and testing updates, with clear testing instructions provided.
Linked Issues check ✅ Passed The code changes fully implement the acceptance criteria from #171: the JWKS endpoint exposes RSA public keys in standard JWK format at /.well-known/jwks.json, enabling resource servers to dynamically fetch and cache keys for autonomous token validation.
Out of Scope Changes check ✅ Passed All code changes are directly scoped to implementing the JWKS endpoint objective: config updates for RSA keys, token service migration to RS256, JWKS handler and DTO additions, and test updates supporting the primary feature.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/config/config.go`:
- Around line 197-204: The condition checking `err1 != nil || err2 != nil` is
too broad and treats all read failures (permission errors, invalid paths, config
errors) as reasons to generate temporary in-memory RSA keys. This can cause
silent key rotation and token invalidation across restarts. Instead, narrow the
fallback logic to only generate temporary keys when the RSA key files are
specifically not found (using os.IsNotExist() checks on err1 and err2), and let
other types of errors fail fast by returning or logging them as fatal errors
rather than silently falling back to temporary key generation.

In `@internal/handler/jwks_handler.go`:
- Around line 31-33: The error response in the pubKey == nil check is missing
the required `code` field that must be included in all handler error responses.
Update the gin.H map in this condition (currently only containing the `error`
field) to also include a `code` field with an appropriate error code identifier
to comply with the handler contract that requires the format
{"error":"message","code":"ERROR_CODE"}.
- Around line 22-28: The Swagger annotations on the GetJWKS method in
JWKSHandler are correctly defined, but the generated documentation artifacts in
the docs directory are out of date and do not reflect the new endpoint. Run the
make swagger command to regenerate the documentation files (swagger.json,
swagger.yaml, and docs.go) so that the /.well-known/jwks.json endpoint is
included in the API documentation.

In `@internal/service/token_service_test.go`:
- Around line 14-16: The getTestRSAKeys helper function ignores the error
returned by rsa.GenerateKey, which could cause a nil pointer panic if key
generation fails. Add a *testing.T parameter to the getTestRSAKeys function
signature, capture the error return value from rsa.GenerateKey instead of
ignoring it with the blank identifier, and add an explicit error check that
calls t.Fatalf to fail the test if the error is not nil, ensuring the helper
fails the test gracefully rather than potentially panicking.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: bc793b31-f794-4842-bf01-ef22f6a481e0

📥 Commits

Reviewing files that changed from the base of the PR and between f5d1fcf and e2d828d.

📒 Files selected for processing (6)
  • internal/config/config.go
  • internal/dto/jwks.go
  • internal/handler/jwks_handler.go
  • internal/routes/routes.go
  • internal/service/token_service.go
  • internal/service/token_service_test.go

Comment thread internal/config/config.go Outdated
Comment on lines +197 to +204
if err1 != nil || err2 != nil {
log.Println("RSA keys not found at provided paths, generating temporary in-memory keys for development/testing...")
privKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
log.Fatalf("Failed to generate temp RSA key: %v", err)
}
return privKey, &privKey.PublicKey
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Narrow RSA fallback to dev-only missing-file cases.

At Line 197, the err1 != nil || err2 != nil branch treats all read failures as “generate temp keys,” including permission/path/config errors. This can silently rotate signing keys and invalidate tokens across restarts/instances instead of failing fast.

Suggested fix
 import (
+	"errors"
 	"crypto/rand"
 	"crypto/rsa"
 	"log"
 	"os"
 	"strconv"
@@
 	privBytes, err1 := os.ReadFile(privPath)
 	pubBytes, err2 := os.ReadFile(pubPath)

-	if err1 != nil || err2 != nil {
-		log.Println("RSA keys not found at provided paths, generating temporary in-memory keys for development/testing...")
-		privKey, err := rsa.GenerateKey(rand.Reader, 2048)
-		if err != nil {
-			log.Fatalf("Failed to generate temp RSA key: %v", err)
-		}
-		return privKey, &privKey.PublicKey
-	}
+	if err1 != nil || err2 != nil {
+		appEnv := getEnv("APP_ENV", "development")
+		bothMissing := errors.Is(err1, os.ErrNotExist) && errors.Is(err2, os.ErrNotExist)
+		if appEnv != "production" && bothMissing {
+			log.Println("RSA key files not found; generating temporary in-memory keys for development/testing")
+			privKey, err := rsa.GenerateKey(rand.Reader, 2048)
+			if err != nil {
+				log.Fatalf("Failed to generate temp RSA key: %v", err)
+			}
+			return privKey, &privKey.PublicKey
+		}
+		log.Fatalf("Failed to read RSA key files (private: %v, public: %v)", err1, err2)
+	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/config/config.go` around lines 197 - 204, The condition checking
`err1 != nil || err2 != nil` is too broad and treats all read failures
(permission errors, invalid paths, config errors) as reasons to generate
temporary in-memory RSA keys. This can cause silent key rotation and token
invalidation across restarts. Instead, narrow the fallback logic to only
generate temporary keys when the RSA key files are specifically not found (using
os.IsNotExist() checks on err1 and err2), and let other types of errors fail
fast by returning or logging them as fatal errors rather than silently falling
back to temporary key generation.

Comment thread internal/handler/jwks_handler.go
Comment thread internal/handler/jwks_handler.go
Comment thread internal/service/token_service_test.go Outdated
@sonarqubecloud

Copy link
Copy Markdown

@TharunvenkateshN

Copy link
Copy Markdown
Contributor Author

I have read the CLA and agree to its terms.

@roshankumar0036singh

Copy link
Copy Markdown
Owner

@TharunvenkateshN A couple of things worth looking at before merging:

  1. Access and refresh tokens now share the same signing key: previously they used separate secrets (AccessSecret vs RefreshSecret) which meant a refresh token couldn't be used where an access token was expected. Now both go through signToken() with the same private key, which removes that separation. Worth adding a separate key pair for refresh tokens or at minimum validating the token type on parse.
  2. Auto-generating keys in production is risky : if the key files are missing, it silently generates in-memory keys and carries on. That means every restart produces different keys, instantly invalidating all existing tokens. Should probably log.Fatal in production and only allow auto-generation in development/test environments. Something like checking cfg.App.Env before deciding.
  3. t.Fatalf inside sync.Once : calling t.Fatalf inside a Once.Do closure can cause a panic since it calls runtime.Goexit in a context that doesn't expect it. Better to use panic() there or just handle the error outside the Once.
  4. Minor : there are a few extra blank lines left in routes.go after the JWKS route, worth cleaning up.

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.

JWKS Endpoint (/.well-known/jwks.json)

2 participants