Skip to content

feat(token): add RS256/ES256 JWT signing and JWKS endpoint#109

Merged
appleboy merged 42 commits intomainfrom
worktree-new
Mar 21, 2026
Merged

feat(token): add RS256/ES256 JWT signing and JWKS endpoint#109
appleboy merged 42 commits intomainfrom
worktree-new

Conversation

@appleboy
Copy link
Copy Markdown
Member

Summary

  • Asymmetric JWT signing: Support RS256 (RSA) and ES256 (ECDSA P-256) in addition to default HS256, configured via JWT_SIGNING_ALGORITHM env var
  • JWKS endpoint: New GET /.well-known/jwks.json exposes public keys for resource servers to verify JWTs without shared secrets
  • OIDC Discovery update: id_token_signing_alg_values_supported reflects configured algorithm; jwks_uri included for asymmetric keys
  • 30 new tests covering key loading, RS256/ES256 token generation/validation, JWKS responses, and discovery metadata

New Config Fields

Variable Default Description
JWT_SIGNING_ALGORITHM HS256 Signing algorithm: HS256, RS256, or ES256
JWT_PRIVATE_KEY_PATH (none) PEM private key path (required for RS256/ES256)
JWT_KEY_ID (auto) kid header value (auto-derived from key fingerprint)

Key Files Changed

File Change
internal/token/key.go New: Key loading (RSA/ECDSA, PKCS1/PKCS8/SEC1) + kid derivation
internal/token/local.go Extended with Option pattern (WithSigningKey, WithKeyID)
internal/token/idtoken.go Uses dynamic signing method instead of hardcoded HS256
internal/handlers/jwks.go New: JWKS endpoint with manual JWK serialization
internal/handlers/oidc.go Discovery reflects algorithm + conditional jwks_uri
internal/config/config.go 3 new config fields + validation
internal/bootstrap/providers.go Key loading + validation at startup

Backward Compatibility

  • Default HS256 mode is fully backward compatible — no config changes needed
  • JWKS returns empty {"keys":[]} for HS256 (symmetric secrets never exposed)
  • All 30 new tests + all existing tests pass

Test plan

  • make test passes (all existing + 30 new tests)
  • make lint passes (0 issues)
  • Manual: Start with default config → verify HS256 tokens work unchanged
  • Manual: openssl genrsa -out /tmp/rsa.pem 2048 + JWT_SIGNING_ALGORITHM=RS256 JWT_PRIVATE_KEY_PATH=/tmp/rsa.pem → verify tokens and JWKS
  • Manual: openssl ecparam -genkey -name prime256v1 -noout -out /tmp/ec.pem + JWT_SIGNING_ALGORITHM=ES256 JWT_PRIVATE_KEY_PATH=/tmp/ec.pem → verify tokens and JWKS
  • Manual: curl /.well-known/openid-configuration includes jwks_uri for RS256/ES256

🤖 Generated with Claude Code

appleboy and others added 5 commits March 15, 2026 16:30
Add config fields (JWT_SIGNING_ALGORITHM, JWT_PRIVATE_KEY_PATH,
JWT_KEY_ID) and key loading utilities for asymmetric JWT signing.
Extend LocalTokenProvider with Option pattern (WithSigningKey,
WithKeyID) to support RS256/ES256 in addition to default HS256.
Bootstrap validates key-algorithm match at startup.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Expose public keys for RS256/ES256 via standard JWKS endpoint so
resource servers can verify JWTs without sharing secrets. HS256 mode
returns an empty key set. Hand-rolled JWK serialization avoids
pulling in go-jose dependency.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Discovery endpoint now returns the actual JWT_SIGNING_ALGORITHM in
id_token_signing_alg_values_supported and includes jwks_uri when
using asymmetric keys (RS256/ES256). HS256 omits jwks_uri.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- key_test.go: 12 tests for key loading (RSA/ECDSA, PKCS1/PKCS8/SEC1),
  error paths, DeriveKeyID, ValidateKeyAlgorithm
- local_test.go: 10 new tests for RS256/ES256 generate+validate, ID token,
  kid header, cross-validation, refresh tokens, backward compatibility
- jwks_test.go: 4 tests for RSA/ECDSA/HS256 JWKS responses, coordinate
  padding
- oidc_test.go: 4 new tests for discovery jwks_uri and algorithm fields

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Update .env.example with JWT_SIGNING_ALGORITHM, JWT_PRIVATE_KEY_PATH,
and JWT_KEY_ID variables. Add JWT Signing Algorithm section to
CONFIGURATION.md with key generation examples. Update CLAUDE.md to
reflect RS256/ES256 support, JWKS endpoint, and new config fields.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings March 15, 2026 08:44
@codecov
Copy link
Copy Markdown

codecov Bot commented Mar 15, 2026

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds asymmetric JWT signing (RS256/ES256) alongside HS256 and exposes public keys via a JWKS endpoint to support resource-server verification and OIDC discovery metadata.

Changes:

  • Extend LocalTokenProvider to support HS256/RS256/ES256 with signing key + kid options.
  • Add key loading/validation utilities and a new public JWKS endpoint (/.well-known/jwks.json).
  • Update OIDC discovery metadata to reflect configured signing algorithm and include jwks_uri for asymmetric modes; add tests/docs/env updates.

Reviewed changes

Copilot reviewed 18 out of 18 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
internal/token/local.go Option-based asymmetric signing support + dynamic verification key selection
internal/token/idtoken.go ID token signing now uses provider-selected algorithm/key
internal/token/key.go New helpers for PEM key loading, algorithm validation, and kid derivation
internal/token/local_test.go New RS256/ES256 coverage for access/refresh/ID tokens and kid header
internal/token/key_test.go Tests for key loading, kid derivation, and algorithm/key validation
internal/handlers/jwks.go New JWKS handler with manual RSA/EC JWK serialization
internal/handlers/jwks_test.go Tests for RSA/EC JWKS output and EC coordinate padding
internal/handlers/oidc.go Discovery metadata now reflects configured signing algorithm and conditional jwks_uri
internal/handlers/oidc_test.go Tests for discovery algorithm values and jwks_uri behavior
internal/config/config.go Adds JWT signing algorithm/key path/kid config + validation
internal/bootstrap/providers.go Boot-time key load/validate/derive-kid and provider wiring
internal/bootstrap/bootstrap.go Stores token provider in app for handler initialization
internal/bootstrap/services.go Accepts token provider as dependency (no longer constructed inside)
internal/bootstrap/handlers.go Builds JWKS handler from token provider (or empty JWKS fallback)
internal/bootstrap/router.go Registers /.well-known/jwks.json route
docs/CONFIGURATION.md Documents signing algorithms, key generation, JWKS, and rotation
.env.example Adds JWT signing algorithm/key configuration examples
CLAUDE.md Updates token provider documentation for new signing/JWKS support

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/config/config.go
Comment thread internal/handlers/oidc.go Outdated
Comment thread internal/token/key.go Outdated
Comment thread internal/token/local.go
Comment thread internal/token/local_test.go Outdated
Comment thread internal/token/local_test.go
Comment thread internal/token/local_test.go
Comment thread internal/token/key_test.go
- Increase DeriveKeyID truncation from 8 to 16 bytes to reduce collision risk
- Update DeriveKeyID comment to accurately describe SHA-256 thumbprint derivation
- Gate discovery jwks_uri on actual JWKS key availability instead of algorithm name
- Add defensive panic in NewLocalTokenProvider when asymmetric key is missing
- Add Keys accessor method to JWKSHandler for key availability checks
- Check rsa.GenerateKey errors in tests instead of discarding them

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds asymmetric JWT signing (RS256/ES256) alongside HS256, plus a JWKS endpoint and OIDC discovery metadata updates so resource servers can verify JWTs via public keys instead of shared secrets.

Changes:

  • Add configurable JWT signing algorithm selection (HS256/RS256/ES256) with PEM key loading and kid derivation.
  • Add /.well-known/jwks.json endpoint and conditionally include jwks_uri + algorithm in OIDC discovery metadata.
  • Expand test coverage for asymmetric signing, key loading/validation, JWKS responses, and discovery metadata.

Reviewed changes

Copilot reviewed 18 out of 18 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
internal/token/local.go Adds option-based configuration for signing keys/kid and supports RS256/ES256 in token generation/validation.
internal/token/idtoken.go Switches ID token signing from fixed HS256 to provider-selected algorithm/key.
internal/token/key.go Adds PEM private key loading, algorithm/key validation, and key-id derivation.
internal/token/local_test.go Adds RS256/ES256 generation/validation tests and HS256 compatibility checks.
internal/token/key_test.go Adds unit tests for key parsing, key-id derivation, and algorithm/key validation.
internal/handlers/jwks.go Implements JWKS endpoint and JWK serialization for RSA/EC public keys.
internal/handlers/jwks_test.go Tests JWKS output for RSA/EC keys and HS256 empty key set behavior.
internal/handlers/oidc.go Adds configurable id_token_signing_alg_values_supported and conditional jwks_uri.
internal/handlers/oidc_test.go Updates handler construction + adds discovery tests for alg/jwks_uri behavior.
internal/config/config.go Adds env/config fields for algorithm/key path/kid and validates algorithm + key-path requirement.
internal/bootstrap/providers.go Loads/validates asymmetric keys at startup and wires provider options (signing key + kid).
internal/bootstrap/handlers.go Builds JWKS handler from token provider and passes jwks availability into OIDC handler.
internal/bootstrap/router.go Registers /.well-known/jwks.json route.
internal/bootstrap/services.go Plumbs token provider into service initialization rather than constructing inside.
internal/bootstrap/bootstrap.go Stores token provider on the app struct for handler initialization.
docs/CONFIGURATION.md Documents JWT algorithms, key generation, JWKS endpoint, and rotation guidance.
.env.example Adds commented config examples for signing algorithm/private key path/kid.
CLAUDE.md Updates project documentation to reflect configurable JWT signing and JWKS endpoint.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/bootstrap/handlers.go
Comment thread internal/token/local.go Outdated
Comment thread docs/CONFIGURATION.md Outdated
- Clarify the JWT key rotation procedure in the documentation and explicitly note that only a single active JWKS key is supported, with guidance on handling rotation downtime
- Tighten the JWKS public key interface to return a crypto.PublicKey instead of an untyped value
- Remove a defensive panic in the local token provider and rely on bootstrap validation for signing key requirements

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds asymmetric JWT signing (RS256/ES256) alongside existing HS256 and exposes a JWKS endpoint so relying parties can verify signatures using published public keys.

Changes:

  • Extend LocalTokenProvider to support configurable signing algorithms and optional kid JWT header.
  • Add PEM key loading/validation utilities and a new /.well-known/jwks.json handler + route.
  • Update OIDC discovery metadata to advertise the configured signing algorithm and (when applicable) jwks_uri, plus add extensive tests/docs/configuration.

Reviewed changes

Copilot reviewed 18 out of 18 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
internal/token/local.go Adds algorithm-aware signing/verification and option pattern for keys/kid.
internal/token/idtoken.go Uses provider-selected signing method/key (no longer hardcoded HS256).
internal/token/key.go New helpers for loading PEM private keys, deriving kid, and validating key/alg compatibility.
internal/handlers/jwks.go New JWKS endpoint with manual RSA/EC JWK serialization.
internal/handlers/oidc.go Discovery response now reflects signing algorithm and conditionally includes jwks_uri.
internal/bootstrap/providers.go Loads/validates asymmetric keys and wires LocalTokenProvider options at startup.
internal/bootstrap/handlers.go Builds JWKS handler and passes JWKS availability into OIDC handler.
internal/bootstrap/router.go Registers /.well-known/jwks.json route.
internal/bootstrap/services.go Accepts token provider as dependency instead of creating it internally.
internal/bootstrap/bootstrap.go Stores token provider on Application for handler initialization.
internal/config/config.go Adds new JWT signing config fields and validation.
internal/token/local_test.go Adds RS256/ES256 coverage and backward-compat HS256 tests.
internal/token/key_test.go New tests for key loading, kid derivation, and key/alg validation.
internal/handlers/jwks_test.go Tests JWKS responses for RSA/EC and coordinate padding.
internal/handlers/oidc_test.go Tests discovery metadata behavior for alg + jwks_uri.
docs/CONFIGURATION.md Documents algorithm selection, key generation, JWKS endpoint, and rotation guidance.
.env.example Adds example env vars for JWT signing algorithm and key settings.
CLAUDE.md Updates project documentation to reflect configurable signing and JWKS endpoint.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/config/config.go Outdated
Comment thread internal/token/key.go Outdated
Comment thread internal/token/local.go
Comment thread internal/handlers/jwks.go Outdated
appleboy and others added 2 commits March 20, 2026 12:22
…ning

- fix(config): only require JWT_PRIVATE_KEY_PATH when TOKEN_PROVIDER_MODE=local
  (RS256/ES256 with http_api token provider no longer triggers false validation error)

- fix(token): NewLocalTokenProvider returns error if RS256/ES256 used without WithSigningKey
  (prevents nil pointer panic at signing time; update all call sites)

- fix(token): LoadSigningKey loops through all PEM blocks instead of only the first
  (correctly handles EC keys with leading EC PARAMETERS block)

- fix(token): DeriveKeyID uses full 32-byte SHA-256 hash instead of truncated 16 bytes
  (reduces collision risk; update length comment in tests)

- fix(handlers): Keys() returns a copy of the internal slice to prevent external mutation

- test: fix ignored rsa/ecdsa.GenerateKey errors in key_test.go with require.NoError
- test: add TestNewLocalTokenProvider_RS256/ES256_NoKey_Error for constructor validation
- test: add TestLoadSigningKey_MultiBlock_ECAfterUnknown for PEM loop fix
- test: add TestDeriveKeyID_FullHashLength for 43-char base64url output
- test: add TestJWKS_Keys_ReturnsCopy for immutability guarantee
- test: add TestConfig_Validate_JWTSigningAlgorithm covering all algorithm/mode combos

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- Replace fmt.Errorf with errors.New for static error strings (perfsprint)
- Fix gofmt alignment in config_test.go struct literal
- Wrap long assert.Equal line in jwks_test.go (golines)

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds support for asymmetric JWT signing (RS256/ES256) alongside HS256, exposes a JWKS endpoint for public key distribution, and updates OIDC discovery metadata to reflect the active signing configuration.

Changes:

  • Extend LocalTokenProvider to support HS256/RS256/ES256 with key/kid options and validation behavior updates across token generation/validation (access/refresh/ID tokens).
  • Add PEM key loading + kid derivation utilities and wire asymmetric key loading at bootstrap startup.
  • Introduce /.well-known/jwks.json and update OIDC discovery to conditionally advertise jwks_uri and supported ID token signing algorithms.

Reviewed changes

Copilot reviewed 25 out of 25 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
internal/token/local.go Adds algorithm selection, key options, kid header support, and asymmetric verification logic.
internal/token/local_test.go Updates for new constructor signature + adds RS256/ES256 coverage and kid header checks.
internal/token/key.go New utilities for PEM key loading, kid derivation, and algorithm/key validation.
internal/token/key_test.go Tests for key parsing formats, kid derivation determinism, and algorithm validation.
internal/token/idtoken.go ID token signing now uses the provider’s configured method/key (not hardcoded HS256).
internal/handlers/jwks.go New JWKS handler and manual JWK serialization for RSA/EC public keys.
internal/handlers/jwks_test.go Tests JWKS responses for RSA/EC, padding rules, and defensive copying.
internal/handlers/oidc.go Discovery response now reflects configured signing algorithm and conditionally includes jwks_uri.
internal/handlers/oidc_test.go Tests discovery behavior for HS256/RS256/ES256 and jwks_uri presence/absence.
internal/bootstrap/providers.go Bootstraps local provider with asymmetric key loading/validation and kid derivation.
internal/bootstrap/handlers.go Builds JWKS handler from the active token provider and passes JWKS availability into OIDC handler.
internal/bootstrap/router.go Registers the new public JWKS endpoint route.
internal/bootstrap/services.go Threads the initialized token provider into service initialization (avoids duplicate init).
internal/bootstrap/bootstrap.go Stores initialized token provider on the application and passes it into handlers/services.
internal/config/config.go Adds env/config fields for signing algorithm, private key path, and key id + validation logic.
internal/config/config_test.go Adds validation tests for algorithm/key-path rules under different provider modes.
internal/services/token_test.go Updates tests for NewLocalTokenProvider returning an error.
internal/services/token_introspect_test.go Updates tests for NewLocalTokenProvider returning an error.
internal/services/token_client_credentials_test.go Updates tests for NewLocalTokenProvider returning an error.
internal/handlers/token_introspect_test.go Updates tests for NewLocalTokenProvider returning an error.
internal/handlers/token_client_credentials_test.go Updates tests for NewLocalTokenProvider returning an error.
internal/handlers/session_test.go Updates tests for NewLocalTokenProvider returning an error.
docs/CONFIGURATION.md Documents algorithm selection, key generation, JWKS endpoint behavior, and rotation notes.
.env.example Adds example env vars for JWT algorithm and key configuration.
CLAUDE.md Updates repo docs to reflect configurable signing algorithm + JWKS endpoint.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/token/local.go Outdated
Comment thread internal/token/key.go Outdated
Comment thread internal/handlers/jwks.go Outdated
Comment thread internal/services/token_test.go Outdated
…rs, and improve test diagnostics

- Reject unsupported JWT signing algorithms explicitly instead of silently defaulting to HS256
- Change DeriveKeyID to return (string, error) so callers handle marshal failures
- Add omitempty to JWKS kid field to avoid emitting empty string values
- Replace panic with require.NoError in test helper for better failure diagnostics

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 25 out of 25 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/token/key.go Outdated
- Only return RSA and ECDSA keys from the PKCS#8 parsing branch
- Skip unsupported key types (e.g. Ed25519) and continue scanning
  remaining PEM blocks, matching the function documented contract

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 25 out of 25 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +603 to +617
func TestLocalTokenProvider_RS256_GenerateAndValidate(t *testing.T) {
rsaKey, err := rsa.GenerateKey(rand.Reader, 2048)
require.NoError(t, err)

cfg := &config.Config{
JWTSigningAlgorithm: "RS256",
JWTExpiration: 1 * time.Hour,
RefreshTokenExpiration: 30 * 24 * time.Hour,
BaseURL: "http://localhost:8080",
}
provider, err := NewLocalTokenProvider(cfg,
WithSigningKey(rsaKey, &rsaKey.PublicKey),
WithKeyID("test-rsa-kid"),
)
require.NoError(t, err)
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

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

These RS256/ES256 tests generate fresh 2048-bit RSA keys multiple times across the file, which can noticeably slow the unit test suite. Consider using a test helper that reuses a single generated key per package (e.g., via sync.Once) or embedding a static PEM test key to reduce runtime while keeping coverage.

Copilot uses AI. Check for mistakes.
Comment on lines +27 to +45
func TestLoadSigningKey_RSA(t *testing.T) {
rsaKey, err := rsa.GenerateKey(rand.Reader, 2048)
require.NoError(t, err)
der := x509.MarshalPKCS1PrivateKey(rsaKey)
path := writeTestPEM(t, "RSA PRIVATE KEY", der)

key, err := LoadSigningKey(path)
require.NoError(t, err)
_, ok := key.(*rsa.PrivateKey)
assert.True(t, ok, "expected *rsa.PrivateKey, got %T", key)
}

func TestLoadSigningKey_RSA_PKCS8(t *testing.T) {
rsaKey, err := rsa.GenerateKey(rand.Reader, 2048)
require.NoError(t, err)
der, err := x509.MarshalPKCS8PrivateKey(rsaKey)
require.NoError(t, err)
path := writeTestPEM(t, "PRIVATE KEY", der)

Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

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

This test file generates several 2048-bit RSA keys (and EC keys) which can make the test suite slower and flakier on constrained CI runners. Consider reusing a single generated key across tests (sync.Once) or using embedded static PEM fixtures for faster, deterministic tests.

Copilot uses AI. Check for mistakes.
Comment thread internal/handlers/jwks_test.go
Comment thread internal/token/idtoken.go Outdated
…nc.Once

- Extract shared signClaims method to eliminate duplicate JWT signing
  logic between generateJWT and GenerateIDToken
- Add sync.Once shared test key fixtures in local_test.go and
  jwks_test.go to avoid repeated 2048-bit RSA key generation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 25 out of 25 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/token/local_test.go Outdated
Comment thread internal/handlers/jwks_test.go Outdated
- Store key generation errors and assert with require.NoError after Do
- Avoids crashing entire package test run on key generation failure

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 25 out of 25 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/token/local.go
Comment thread internal/token/local.go Outdated
… error

- Reject RSA keys shorter than 2048 bits at construction time
- Use %w for both errors in signClaims to enable proper error unwrapping

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 25 out of 25 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/bootstrap/providers.go Outdated
Comment on lines +67 to +100
if cfg.JWTSigningAlgorithm == "HS256" || cfg.JWTSigningAlgorithm == "" {
log.Printf("Token signing: HS256 (symmetric)")
p, err := token.NewLocalTokenProvider(cfg)
if err != nil {
log.Fatalf("Failed to create token provider: %v", err)
}
return p
}

// Load asymmetric key
privateKey, err := token.LoadSigningKey(cfg.JWTPrivateKeyPath)
if err != nil {
log.Fatalf("Failed to load JWT private key from %s: %v", cfg.JWTPrivateKeyPath, err)
}

// Derive kid if not explicitly set
kid := cfg.JWTKeyID
if kid == "" {
var err error
kid, err = token.DeriveKeyID(privateKey.Public())
if err != nil {
log.Fatalf("Failed to derive JWT key ID: %v", err)
}
}

log.Printf("Token signing: %s (kid=%s)", cfg.JWTSigningAlgorithm, kid)
p, err := token.NewLocalTokenProvider(cfg,
token.WithSigningKey(privateKey, privateKey.Public()),
token.WithKeyID(kid),
)
if err != nil {
log.Fatalf("Failed to create token provider: %v", err)
}
return p
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

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

newLocalTokenProvider treats any non-HS256 algorithm as requiring an asymmetric key and attempts to read JWTPrivateKeyPath before verifying the algorithm is one of the supported asymmetric values (RS256/ES256). If JWT_SIGNING_ALGORITHM is mis-set (or future algorithms are added), this will fail with a confusing “read key file” fatal instead of an “unsupported algorithm” error. Consider switching on cfg.JWTSigningAlgorithm and only loading a key for RS256/ES256; for other values, let token.NewLocalTokenProvider return the unsupported-algorithm error (or fatal with that message).

Suggested change
if cfg.JWTSigningAlgorithm == "HS256" || cfg.JWTSigningAlgorithm == "" {
log.Printf("Token signing: HS256 (symmetric)")
p, err := token.NewLocalTokenProvider(cfg)
if err != nil {
log.Fatalf("Failed to create token provider: %v", err)
}
return p
}
// Load asymmetric key
privateKey, err := token.LoadSigningKey(cfg.JWTPrivateKeyPath)
if err != nil {
log.Fatalf("Failed to load JWT private key from %s: %v", cfg.JWTPrivateKeyPath, err)
}
// Derive kid if not explicitly set
kid := cfg.JWTKeyID
if kid == "" {
var err error
kid, err = token.DeriveKeyID(privateKey.Public())
if err != nil {
log.Fatalf("Failed to derive JWT key ID: %v", err)
}
}
log.Printf("Token signing: %s (kid=%s)", cfg.JWTSigningAlgorithm, kid)
p, err := token.NewLocalTokenProvider(cfg,
token.WithSigningKey(privateKey, privateKey.Public()),
token.WithKeyID(kid),
)
if err != nil {
log.Fatalf("Failed to create token provider: %v", err)
}
return p
switch cfg.JWTSigningAlgorithm {
case "", "HS256":
log.Printf("Token signing: HS256 (symmetric)")
p, err := token.NewLocalTokenProvider(cfg)
if err != nil {
log.Fatalf("Failed to create token provider: %v", err)
}
return p
case "RS256", "ES256":
// Load asymmetric key
privateKey, err := token.LoadSigningKey(cfg.JWTPrivateKeyPath)
if err != nil {
log.Fatalf("Failed to load JWT private key from %s: %v", cfg.JWTPrivateKeyPath, err)
}
// Derive kid if not explicitly set
kid := cfg.JWTKeyID
if kid == "" {
var err error
kid, err = token.DeriveKeyID(privateKey.Public())
if err != nil {
log.Fatalf("Failed to derive JWT key ID: %v", err)
}
}
log.Printf("Token signing: %s (kid=%s)", cfg.JWTSigningAlgorithm, kid)
p, err := token.NewLocalTokenProvider(cfg,
token.WithSigningKey(privateKey, privateKey.Public()),
token.WithKeyID(kid),
)
if err != nil {
log.Fatalf("Failed to create token provider: %v", err)
}
return p
default:
// Let token.NewLocalTokenProvider validate and report unsupported algorithms.
p, err := token.NewLocalTokenProvider(cfg)
if err != nil {
log.Fatalf("Failed to create token provider: %v", err)
}
return p
}

Copilot uses AI. Check for mistakes.
Comment thread internal/config/config.go
Comment on lines +512 to +523
// Validate token provider mode
switch c.TokenProviderMode {
case "", TokenProviderModeLocal, TokenProviderModeHTTPAPI:
// ok
default:
return fmt.Errorf(
"invalid TOKEN_PROVIDER_MODE value: %q (must be empty, %q, or %q)",
c.TokenProviderMode,
TokenProviderModeLocal,
TokenProviderModeHTTPAPI,
)
}
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

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

Config.Validate currently accepts an empty TokenProviderMode, but bootstrap validation (validateTokenProviderConfig) only accepts explicit "local" or "http_api". Even if env loading normally defaults this to "local", having two validators disagree increases the risk of confusing behavior for programmatic configs/tests. Consider normalizing "" to TokenProviderModeLocal in one place (or disallowing "" here) so the validation rules are consistent across the app.

Copilot uses AI. Check for mistakes.
Comment thread internal/token/local.go Outdated
func (p *LocalTokenProvider) keyFunc(token *jwt.Token) (any, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
if token.Method.Alg() != p.method.Alg() {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

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

keyFunc verifies the algorithm via token.Method.Alg() but the error message prints token.Header["alg"], which can be missing or differ in formatting and makes debugging harder. Consider including both the expected algorithm (p.method.Alg()) and the actual algorithm (token.Method.Alg()) in the error message, and avoid relying on the raw header value.

Suggested change
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
return nil, fmt.Errorf("unexpected signing method: expected %s, got %s", p.method.Alg(), token.Method.Alg())

Copilot uses AI. Check for mistakes.
…improve error messages

- Check algorithm is RS256/ES256 before attempting key file read in bootstrap
- Normalize empty TokenProviderMode to local in Config.Validate
- Include expected and actual algorithm in keyFunc error message

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 25 out of 25 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (1)

internal/handlers/oidc.go:1

  • /.well-known/openid-configuration is an OIDC Discovery document, and id_token_signing_alg_values_supported is a required field for OIDC Providers (and many clients will expect it to be present). With omitempty + idTokenSupported == false, this field is omitted entirely, which can break standards-compliant discovery clients. If this server intentionally supports “OAuth-only” mode, consider returning a non-OIDC metadata document (or disabling this endpoint) in that mode; otherwise, always include id_token_signing_alg_values_supported (and the OpenID scopes) when serving OIDC discovery.
package handlers

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/config/config.go
Comment thread internal/handlers/jwks.go
Comment thread internal/token/local_test.go Outdated
…ation test key

- Remove stale 'must be empty' from TOKEN_PROVIDER_MODE error message
- Add Cache-Control: public, max-age=3600 to JWKS endpoint
- Cache second RSA key via sync.Once for cross-validation test

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@appleboy appleboy merged commit 70ae93c into main Mar 21, 2026
11 of 15 checks passed
@appleboy appleboy deleted the worktree-new branch March 21, 2026 00:03
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 25 out of 25 changed files in this pull request and generated no new comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

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.

2 participants