Files
auth0mock/auth/jwt.go
T
renovate 30282f3458
Release / release (push) Successful in 56s
auth0mock / build (push) Successful in 5m0s
fix(deps): update module github.com/lestrrat-go/jwx/v3 to v4 (#297)
This PR contains the following updates:

| Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [github.com/lestrrat-go/jwx/v3](https://github.com/lestrrat-go/jwx) | `v3.1.0` → `v4.0.0` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2flestrrat-go%2fjwx%2fv3/v4.0.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2flestrrat-go%2fjwx%2fv3/v3.1.0/v4.0.0?slim=true) |

---

### Release Notes

<details>
<summary>lestrrat-go/jwx (github.com/lestrrat-go/jwx/v3)</summary>

### [`v4.0.0`](https://github.com/lestrrat-go/jwx/releases/tag/v4.0.0)

[Compare Source](https://github.com/lestrrat-go/jwx/compare/v3.1.0...v4.0.0)

# Changes

v4 has many incompatibilities with v3. To see the full list of differences between
v3 and v4, please read the [Changes-v4.md file](./Changes-v4.md). Coding Agents should read [MIGRATION-v4.md](./MICRATION-v4.md)

v4.0.0 - 19 Apr 2026

- Initial v4 release. Major features:
  - Lighter: Core / Companion module separation. Less dependencies in core.
  - Faster: Use of generics and other optimizations make v4 2x\~3x faster than before.
  - Quantum-Ready: ML-KEM and ML-DSA, HPKE (+Hybrid) are supported through companion modules.
- See Changes-v4.md for a full set of Changes since v3.

</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - At any time (no schedule defined)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4xMjYuMCIsInVwZGF0ZWRJblZlciI6IjQzLjEyNi4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

---------

Co-authored-by: Joakim Olsson <joakim@unbound.se>
Reviewed-on: #297
Co-authored-by: Renovate Bot <renovate@unbound.se>
Co-committed-by: Renovate Bot <renovate@unbound.se>
2026-04-22 09:05:25 +00:00

213 lines
5.0 KiB
Go

package auth
import (
"crypto/rand"
"crypto/rsa"
"encoding/json"
"fmt"
"time"
"github.com/google/uuid"
"github.com/lestrrat-go/jwx/v4/jwa"
"github.com/lestrrat-go/jwx/v4/jwk"
"github.com/lestrrat-go/jwx/v4/jws"
"github.com/lestrrat-go/jwx/v4/jwt"
)
const (
// TokenExpiry is the default token expiration time
TokenExpiry = 2 * time.Hour
)
// JWTService handles JWT signing and JWKS generation
type JWTService struct {
privateKey *rsa.PrivateKey
jwkSet jwk.Set
issuer string
audience string
adminClaim string
emailClaim string
}
// NewJWTService creates a new JWT service with a generated RSA key pair
func NewJWTService(issuer, audience, adminClaim, emailClaim string) (*JWTService, error) {
// Generate RSA 2048-bit key pair
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return nil, fmt.Errorf("generate RSA key: %w", err)
}
// Create JWK from private key
key, err := jwk.Import[jwk.Key](privateKey)
if err != nil {
return nil, fmt.Errorf("create JWK from private key: %w", err)
}
// Set key metadata
keyID := uuid.New().String()
if err := key.Set(jwk.KeyIDKey, keyID); err != nil {
return nil, fmt.Errorf("set key ID: %w", err)
}
if err := key.Set(jwk.AlgorithmKey, jwa.RS256()); err != nil {
return nil, fmt.Errorf("set algorithm: %w", err)
}
if err := key.Set(jwk.KeyUsageKey, "sig"); err != nil {
return nil, fmt.Errorf("set key usage: %w", err)
}
// Create public key for JWKS
publicKey, err := key.PublicKey()
if err != nil {
return nil, fmt.Errorf("get public key: %w", err)
}
// Create JWKS with public key
jwkSet := jwk.NewSet()
if err := jwkSet.AddKey(publicKey); err != nil {
return nil, fmt.Errorf("add key to set: %w", err)
}
return &JWTService{
privateKey: privateKey,
jwkSet: jwkSet,
issuer: issuer,
audience: audience,
adminClaim: adminClaim,
emailClaim: emailClaim,
}, nil
}
// SignToken creates a signed JWT with the given claims
func (s *JWTService) SignToken(claims map[string]interface{}) (string, error) {
// Build JWT token
builder := jwt.NewBuilder()
now := time.Now()
builder.Issuer(s.issuer)
builder.IssuedAt(now)
builder.Expiration(now.Add(TokenExpiry))
// Add all claims
for key, value := range claims {
builder.Claim(key, value)
}
token, err := builder.Build()
if err != nil {
return "", fmt.Errorf("build token: %w", err)
}
// Create JWK from private key for signing
key, err := jwk.Import[jwk.Key](s.privateKey)
if err != nil {
return "", fmt.Errorf("create signing key: %w", err)
}
// Get key ID from JWKS
pubKey, _ := s.jwkSet.Key(0)
keyID, _ := pubKey.KeyID()
if err := key.Set(jwk.KeyIDKey, keyID); err != nil {
return "", fmt.Errorf("set key ID: %w", err)
}
// Sign the token
signed, err := jwt.Sign(token, jwt.WithKey(jwa.RS256(), key))
if err != nil {
return "", fmt.Errorf("sign token: %w", err)
}
return string(signed), nil
}
// SignAccessToken creates an access token for the given subject
func (s *JWTService) SignAccessToken(subject, clientID, email string, customClaims []map[string]interface{}) (string, error) {
claims := map[string]interface{}{
"sub": subject,
"aud": []string{s.audience},
"azp": clientID,
}
// Add custom claims
for _, cc := range customClaims {
for k, v := range cc {
claims[k] = v
}
}
// Add email claim
claims[s.emailClaim] = email
return s.SignToken(claims)
}
// SignIDToken creates an ID token for the given subject
func (s *JWTService) SignIDToken(subject, clientID, nonce, email, name, givenName, familyName, picture string, customClaims []map[string]interface{}) (string, error) {
claims := map[string]interface{}{
"sub": subject,
"aud": clientID,
"azp": clientID,
"name": name,
"given_name": givenName,
"family_name": familyName,
"email": email,
"picture": picture,
}
if nonce != "" {
claims["nonce"] = nonce
}
// Add custom claims
for _, cc := range customClaims {
for k, v := range cc {
claims[k] = v
}
}
// Add email claim
claims[s.emailClaim] = email
return s.SignToken(claims)
}
// GetJWKS returns the JSON Web Key Set as JSON bytes
func (s *JWTService) GetJWKS() ([]byte, error) {
return json.Marshal(s.jwkSet)
}
// DecodeToken decodes a JWT without verifying the signature
func (s *JWTService) DecodeToken(tokenString string) (map[string]interface{}, error) {
// Parse without verification
msg, err := jws.Parse([]byte(tokenString))
if err != nil {
return nil, fmt.Errorf("parse token: %w", err)
}
var claims map[string]interface{}
if err := json.Unmarshal(msg.Payload(), &claims); err != nil {
return nil, fmt.Errorf("unmarshal claims: %w", err)
}
return claims, nil
}
// Issuer returns the issuer URL
func (s *JWTService) Issuer() string {
return s.issuer
}
// Audience returns the audience
func (s *JWTService) Audience() string {
return s.audience
}
// AdminClaim returns the admin custom claim key
func (s *JWTService) AdminClaim() string {
return s.adminClaim
}
// EmailClaim returns the email custom claim key
func (s *JWTService) EmailClaim() string {
return s.emailClaim
}