chore: upgrade jwx to v4 (#1730)

This commit is contained in:
Alessandro (Ale) Segala
2026-08-31 14:12:35 -07:00
committed by GitHub
parent fa8aa44706
commit 01cc44246a
26 changed files with 94 additions and 103 deletions
+1 -1
View File
@@ -7,7 +7,7 @@ import (
"os"
"strings"
"github.com/lestrrat-go/jwx/v3/jwa"
"github.com/lestrrat-go/jwx/v4/jwa"
"github.com/spf13/cobra"
"gorm.io/gorm"
@@ -7,7 +7,7 @@ import (
"testing"
"github.com/gin-gonic/gin"
"github.com/lestrrat-go/jwx/v3/jwa"
"github.com/lestrrat-go/jwx/v4/jwa"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
+1 -1
View File
@@ -4,7 +4,7 @@ import (
"context"
"testing"
"github.com/lestrrat-go/jwx/v3/jwa"
"github.com/lestrrat-go/jwx/v4/jwa"
"github.com/ory/fosite"
"github.com/stretchr/testify/require"
"gorm.io/gorm"
+3 -3
View File
@@ -6,7 +6,7 @@ import (
"net/url"
"time"
"github.com/lestrrat-go/jwx/v3/jwt"
"github.com/lestrrat-go/jwx/v4/jwt"
"github.com/pocket-id/pocket-id/backend/internal/apperror"
"github.com/pocket-id/pocket-id/backend/internal/common"
"github.com/pocket-id/pocket-id/backend/internal/dto"
@@ -116,8 +116,8 @@ func (s *endSessionService) verifyIDTokenHint(tokenString string) (jwt.Token, er
// id_token_hint must be an ID token, never an access token (both are signed with the same
// key). An expired ID token is still accepted here, as required by OIDC RP-Initiated Logout.
var tokenType string
if err := token.Get(common.TokenTypeClaim, &tokenType); err != nil || tokenType != idTokenType {
tokenType, err := jwt.Get[string](token, common.TokenTypeClaim)
if err != nil || tokenType != idTokenType {
return nil, apperror.TokenInvalid()
}
@@ -9,8 +9,8 @@ import (
"testing"
"time"
"github.com/lestrrat-go/jwx/v3/jwa"
"github.com/lestrrat-go/jwx/v3/jwt"
"github.com/lestrrat-go/jwx/v4/jwa"
"github.com/lestrrat-go/jwx/v4/jwt"
"github.com/stretchr/testify/require"
"github.com/pocket-id/pocket-id/backend/internal/apperror"
+13 -16
View File
@@ -11,11 +11,12 @@ import (
"strings"
"time"
"github.com/jwx-go/jwkfetch/v4"
"github.com/lestrrat-go/httprc/v3"
"github.com/lestrrat-go/httprc/v3/errsink"
"github.com/lestrrat-go/jwx/v3/jwk"
"github.com/lestrrat-go/jwx/v3/jws"
"github.com/lestrrat-go/jwx/v3/jwt"
"github.com/lestrrat-go/jwx/v4/jwk"
"github.com/lestrrat-go/jwx/v4/jws"
"github.com/lestrrat-go/jwx/v4/jwt"
"github.com/ory/fosite"
"github.com/pocket-id/pocket-id/backend/internal/model"
@@ -38,7 +39,7 @@ type federatedClientStore interface {
type federatedClientAuthenticator struct {
clients federatedClientStore
httpClient *http.Client
jwksCache *jwk.Cache
jwksCache *jwkfetch.Cache
defaultAudience string
}
@@ -58,7 +59,7 @@ func newFederatedClientAuthenticator(ctx context.Context, clients federatedClien
return authenticator, nil
}
func (a *federatedClientAuthenticator) getJWKCache(ctx context.Context) (*jwk.Cache, error) {
func (a *federatedClientAuthenticator) getJWKCache(ctx context.Context) (*jwkfetch.Cache, error) {
// We need to create a custom HTTP client to set a timeout.
client := a.httpClient
if client == nil {
@@ -76,11 +77,12 @@ func (a *federatedClientAuthenticator) getJWKCache(ctx context.Context) (*jwk.Ca
client.Transport = transport
}
return jwk.NewCache(ctx,
return jwkfetch.NewCache(ctx,
httprc.NewClient(
httprc.WithErrorSink(errsink.NewSlog(slog.Default())),
httprc.WithHTTPClient(client),
),
jwkfetch.WithHTTPClient(client),
)
}
@@ -224,17 +226,12 @@ func (a *federatedClientAuthenticator) fetchJWKSet(ctx context.Context, jwksURL
registerCtx, registerCancel := context.WithTimeout(ctx, 15*time.Second)
defer registerCancel()
registerOptions := []jwk.RegisterOption{
jwk.WithMaxInterval(24 * time.Hour),
jwk.WithMinInterval(15 * time.Minute),
jwk.WithWaitReady(true),
}
if a.httpClient != nil {
registerOptions = append(registerOptions, jwk.WithHTTPClient(a.httpClient))
}
// We need to register the URL
err := a.jwksCache.Register(registerCtx, jwksURL, registerOptions...)
err := a.jwksCache.Register(registerCtx, jwksURL,
jwkfetch.WithMaxInterval(24*time.Hour),
jwkfetch.WithMinInterval(15*time.Minute),
jwkfetch.WithWaitReady(true),
)
// In case of race conditions (two goroutines calling jwkCache.Register at the same time), it's possible we can get a conflict anyways, so we ignore that error
if err != nil && !errors.Is(err, httprc.ErrResourceAlreadyExists()) {
return nil, fmt.Errorf("failed to register JWK set: %w", err)
@@ -11,9 +11,9 @@ import (
"testing"
"time"
"github.com/lestrrat-go/jwx/v3/jwa"
"github.com/lestrrat-go/jwx/v3/jwk"
"github.com/lestrrat-go/jwx/v3/jwt"
"github.com/lestrrat-go/jwx/v4/jwa"
"github.com/lestrrat-go/jwx/v4/jwk"
"github.com/lestrrat-go/jwx/v4/jwt"
"github.com/ory/fosite"
"github.com/stretchr/testify/require"
@@ -13,9 +13,9 @@ import (
"time"
"github.com/gin-gonic/gin"
"github.com/lestrrat-go/jwx/v3/jwa"
"github.com/lestrrat-go/jwx/v3/jwk"
"github.com/lestrrat-go/jwx/v3/jwt"
"github.com/lestrrat-go/jwx/v4/jwa"
"github.com/lestrrat-go/jwx/v4/jwk"
"github.com/lestrrat-go/jwx/v4/jwt"
"github.com/ory/fosite"
"github.com/stretchr/testify/require"
+1 -1
View File
@@ -9,7 +9,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/italypaleale/francis/host/local"
"github.com/lestrrat-go/jwx/v3/jwa"
"github.com/lestrrat-go/jwx/v4/jwa"
"github.com/pocket-id/pocket-id/backend/internal/model"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
"gorm.io/gorm"
+1 -1
View File
@@ -6,7 +6,7 @@ import (
"net/url"
"time"
jwxjwt "github.com/lestrrat-go/jwx/v3/jwt"
jwxjwt "github.com/lestrrat-go/jwx/v4/jwt"
"github.com/ory/fosite"
"github.com/pocket-id/pocket-id/backend/internal/model"
+1 -1
View File
@@ -16,7 +16,7 @@ import (
"testing"
"time"
"github.com/lestrrat-go/jwx/v3/jwa"
"github.com/lestrrat-go/jwx/v4/jwa"
"github.com/ory/fosite"
fositeoauth2 "github.com/ory/fosite/handler/oauth2"
"github.com/pocket-id/pocket-id/backend/internal/model"
+3 -3
View File
@@ -17,9 +17,9 @@ import (
"github.com/go-webauthn/webauthn/protocol"
"github.com/italypaleale/francis/actor"
"github.com/italypaleale/francis/host/local"
"github.com/lestrrat-go/jwx/v3/jwa"
"github.com/lestrrat-go/jwx/v3/jwk"
"github.com/lestrrat-go/jwx/v3/jwt"
"github.com/lestrrat-go/jwx/v4/jwa"
"github.com/lestrrat-go/jwx/v4/jwk"
"github.com/lestrrat-go/jwx/v4/jwt"
"github.com/ory/fosite"
"github.com/ory/fosite/compose"
fositejwt "github.com/ory/fosite/token/jwt"
+6 -9
View File
@@ -8,9 +8,9 @@ import (
"time"
"uuid"
"github.com/lestrrat-go/jwx/v3/jwa"
"github.com/lestrrat-go/jwx/v3/jwk"
"github.com/lestrrat-go/jwx/v3/jwt"
"github.com/lestrrat-go/jwx/v4/jwa"
"github.com/lestrrat-go/jwx/v4/jwk"
"github.com/lestrrat-go/jwx/v4/jwt"
"gorm.io/gorm"
"github.com/pocket-id/pocket-id/backend/internal/common"
@@ -294,8 +294,7 @@ func (s *JwtService) GetAuthenticationMethod(token jwt.Token) (string, error) {
if !token.Has(common.AuthenticationMethodsClaim) {
return "", nil
}
var rawAuthenticationMethods []any
err := token.Get(common.AuthenticationMethodsClaim, &rawAuthenticationMethods)
rawAuthenticationMethods, err := jwt.Get[[]any](token, common.AuthenticationMethodsClaim)
if err != nil {
return "", fmt.Errorf("failed to get '%s' claim from token: %w", common.AuthenticationMethodsClaim, err)
}
@@ -344,8 +343,7 @@ func SetAudienceString(token jwt.Token, audience string) error {
// TokenTypeValidator is a validator function that checks the "type" claim in the token
func TokenTypeValidator(expectedTokenType string) jwt.ValidatorFunc {
return func(_ context.Context, t jwt.Token) error {
var tokenType string
err := t.Get(TokenTypeClaim, &tokenType)
tokenType, err := jwt.Get[string](t, TokenTypeClaim)
if err != nil {
return fmt.Errorf("failed to get token type claim: %w", err)
}
@@ -357,7 +355,6 @@ func TokenTypeValidator(expectedTokenType string) jwt.ValidatorFunc {
}
func (s *JwtService) GetPrivateKey() any {
var privateKey any
_ = jwk.Export(s.privateKey, &privateKey)
privateKey, _ := jwk.Export[any](s.privateKey)
return privateKey
}
+14 -10
View File
@@ -10,9 +10,9 @@ import (
"testing"
"time"
"github.com/lestrrat-go/jwx/v3/jwa"
"github.com/lestrrat-go/jwx/v3/jwk"
"github.com/lestrrat-go/jwx/v3/jwt"
"github.com/lestrrat-go/jwx/v4/jwa"
"github.com/lestrrat-go/jwx/v4/jwk"
"github.com/lestrrat-go/jwx/v4/jwt"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gorm.io/gorm"
@@ -177,8 +177,7 @@ func TestJwtService_Init(t *testing.T) {
_ = assert.True(t, ok) &&
assert.Equal(t, jwa.EdDSA().String(), alg.String(), "Loaded key has the incorrect algorithm")
var curve jwa.EllipticCurveAlgorithm
err := svc.privateKey.Get("crv", &curve)
curve, err := jwk.Get[jwa.EllipticCurveAlgorithm](svc.privateKey, "crv")
_ = assert.NoError(t, err, "Failed to get 'crv' claim") &&
assert.Equal(t, jwa.Ed25519().String(), curve.String(), "Curve does not match expected value")
@@ -333,7 +332,8 @@ func TestGenerateVerifyAccessToken(t *testing.T) {
assert.Equal(t, user.ID, subject, "Token subject should match user ID")
isAdmin := false
if claims.Has(IsAdminClaim) {
require.NoError(t, claims.Get(IsAdminClaim, &isAdmin), "Failed to get isAdmin claim")
isAdmin, err = jwt.Get[bool](claims, IsAdminClaim)
require.NoError(t, err, "Failed to get isAdmin claim")
}
assert.False(t, isAdmin, "isAdmin should be false")
authenticationMethod, err := service.GetAuthenticationMethod(claims)
@@ -370,7 +370,8 @@ func TestGenerateVerifyAccessToken(t *testing.T) {
isAdmin := false
if claims.Has(IsAdminClaim) {
require.NoError(t, claims.Get(IsAdminClaim, &isAdmin), "Failed to get isAdmin claim")
isAdmin, err = jwt.Get[bool](claims, IsAdminClaim)
require.NoError(t, err, "Failed to get isAdmin claim")
}
assert.True(t, isAdmin, "isAdmin should be true")
subject, ok := claims.Subject()
@@ -422,7 +423,8 @@ func TestGenerateVerifyAccessToken(t *testing.T) {
assert.Equal(t, user.ID, subject, "Token subject should match user ID")
isAdmin := false
if claims.Has(IsAdminClaim) {
require.NoError(t, claims.Get(IsAdminClaim, &isAdmin), "Failed to get isAdmin claim")
isAdmin, err = jwt.Get[bool](claims, IsAdminClaim)
require.NoError(t, err, "Failed to get isAdmin claim")
}
assert.True(t, isAdmin, "isAdmin should be true")
@@ -460,7 +462,8 @@ func TestGenerateVerifyAccessToken(t *testing.T) {
assert.Equal(t, user.ID, subject, "Token subject should match user ID")
isAdmin := false
if claims.Has(IsAdminClaim) {
require.NoError(t, claims.Get(IsAdminClaim, &isAdmin), "Failed to get isAdmin claim")
isAdmin, err = jwt.Get[bool](claims, IsAdminClaim)
require.NoError(t, err, "Failed to get isAdmin claim")
}
assert.True(t, isAdmin, "isAdmin should be true")
@@ -498,7 +501,8 @@ func TestGenerateVerifyAccessToken(t *testing.T) {
assert.Equal(t, user.ID, subject, "Token subject should match user ID")
isAdmin := false
if claims.Has(IsAdminClaim) {
require.NoError(t, claims.Get(IsAdminClaim, &isAdmin), "Failed to get isAdmin claim")
isAdmin, err = jwt.Get[bool](claims, IsAdminClaim)
require.NoError(t, err, "Failed to get isAdmin claim")
}
assert.True(t, isAdmin, "isAdmin should be true")
+1 -1
View File
@@ -4,7 +4,7 @@ import (
"context"
"fmt"
"github.com/lestrrat-go/jwx/v3/jwk"
"github.com/lestrrat-go/jwx/v4/jwk"
"gorm.io/gorm"
"github.com/pocket-id/pocket-id/backend/internal/common"
@@ -7,7 +7,7 @@ import (
"fmt"
"time"
"github.com/lestrrat-go/jwx/v3/jwk"
"github.com/lestrrat-go/jwx/v4/jwk"
"gorm.io/gorm"
"gorm.io/gorm/clause"
@@ -7,7 +7,7 @@ import (
"encoding/base64"
"testing"
"github.com/lestrrat-go/jwx/v3/jwk"
"github.com/lestrrat-go/jwx/v4/jwk"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -44,7 +44,7 @@ func TestKeyProviderDatabase_LoadKey(t *testing.T) {
pk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.NoError(t, err)
key, err := jwk.Import(pk)
key, err := jwk.Import[jwk.Key](pk)
require.NoError(t, err)
t.Run("LoadKey with no existing key", func(t *testing.T) {
@@ -213,7 +213,7 @@ func TestKeyProviderDatabase_SaveKey(t *testing.T) {
pk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.NoError(t, err)
key, err := jwk.Import(pk)
key, err := jwk.Import[jwk.Key](pk)
require.NoError(t, err)
t.Run("SaveKey and verify database record", func(t *testing.T) {
+1 -1
View File
@@ -6,7 +6,7 @@ import (
"errors"
"fmt"
"github.com/lestrrat-go/jwx/v3/jwk"
"github.com/lestrrat-go/jwx/v4/jwk"
)
var (
@@ -4,8 +4,8 @@ import (
"encoding/json"
"testing"
"github.com/lestrrat-go/jwx/v3/jwa"
"github.com/lestrrat-go/jwx/v3/jwk"
"github.com/lestrrat-go/jwx/v4/jwa"
"github.com/lestrrat-go/jwx/v4/jwk"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -54,7 +54,7 @@ func TestParsePublicKey(t *testing.T) {
})
t.Run("rejects symmetric keys", func(t *testing.T) {
symmetricKey, err := jwk.Import([]byte("this-is-a-shared-secret"))
symmetricKey, err := jwk.Import[jwk.Key]([]byte("this-is-a-shared-secret"))
require.NoError(t, err)
require.NoError(t, symmetricKey.Set(jwk.KeyIDKey, "symmetric"))
encoded, err := json.Marshal(symmetricKey)
+3 -3
View File
@@ -13,8 +13,8 @@ import (
"fmt"
"io"
"github.com/lestrrat-go/jwx/v3/jwa"
"github.com/lestrrat-go/jwx/v3/jwk"
"github.com/lestrrat-go/jwx/v4/jwa"
"github.com/lestrrat-go/jwx/v4/jwk"
"github.com/pocket-id/pocket-id/backend/internal/common"
"github.com/pocket-id/pocket-id/backend/internal/utils/crypto"
@@ -58,7 +58,7 @@ func LoadKeyEncryptionKey(envConfig *common.EnvConfigSchema, instanceID string)
// ImportRawKey imports a crypto key in "raw" format (e.g. crypto.PrivateKey) into a jwk.Key.
// It also populates additional fields such as the key ID, usage, and alg.
func ImportRawKey(rawKey any, alg string, crv string) (jwk.Key, error) {
key, err := jwk.Import(rawKey)
key, err := jwk.Import[jwk.Key](rawKey)
if err != nil {
return nil, fmt.Errorf("failed to import generated private key: %w", err)
}
+10 -14
View File
@@ -9,8 +9,8 @@ import (
"encoding/hex"
"testing"
"github.com/lestrrat-go/jwx/v3/jwa"
"github.com/lestrrat-go/jwx/v3/jwk"
"github.com/lestrrat-go/jwx/v4/jwa"
"github.com/lestrrat-go/jwx/v4/jwk"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -130,8 +130,7 @@ func TestGenerateKey(t *testing.T) {
assert.True(t, ok, "key usage should be set")
assert.Equal(t, KeyUsageSigning, usage)
var crv any
_ = key.Get("crv", &crv)
crv, _ := jwk.Get[any](key, "crv")
// Verify key type matches expected algorithm
switch tt.expectedAlg {
@@ -162,7 +161,7 @@ func TestEnsureAlgInKey(t *testing.T) {
t.Run("does not change alg already set", func(t *testing.T) {
// Import the RSA key
key, err := jwk.Import(rsaKey)
key, err := jwk.Import[jwk.Key](rsaKey)
require.NoError(t, err)
// Pre-set the algorithm
@@ -224,7 +223,7 @@ func TestEnsureAlgInKey(t *testing.T) {
rawKey, err := tt.keyGen()
require.NoError(t, err)
key, err := jwk.Import(rawKey)
key, err := jwk.Import[jwk.Key](rawKey)
require.NoError(t, err)
// Ensure no algorithm is set initially
@@ -241,8 +240,7 @@ func TestEnsureAlgInKey(t *testing.T) {
// Verify curve if expected
if tt.expectedCrv != "" {
var crv any
_ = key.Get("crv", &crv)
crv, _ := jwk.Get[any](key, "crv")
require.NotNil(t, crv)
eca, ok := crv.(jwa.EllipticCurveAlgorithm)
require.True(t, ok)
@@ -291,7 +289,7 @@ func TestEnsureAlgInKey(t *testing.T) {
rawKey, err := tt.keyGen()
require.NoError(t, err)
key, err := jwk.Import(rawKey)
key, err := jwk.Import[jwk.Key](rawKey)
require.NoError(t, err)
// Ensure no algorithm is set initially
@@ -308,8 +306,7 @@ func TestEnsureAlgInKey(t *testing.T) {
// Verify curve if expected
if tt.expectedCrv != "" {
var crv any
_ = key.Get("crv", &crv)
crv, _ := jwk.Get[any](key, "crv")
require.NotNil(t, crv)
eca, ok := crv.(jwa.EllipticCurveAlgorithm)
require.True(t, ok)
@@ -323,7 +320,7 @@ func TestEnsureAlgInKey(t *testing.T) {
rsaKey, err := rsa.GenerateKey(rand.Reader, 2048)
require.NoError(t, err)
key, err := jwk.Import(rsaKey)
key, err := jwk.Import[jwk.Key](rsaKey)
require.NoError(t, err)
// Call EnsureAlgInKey with invalid curve
@@ -334,8 +331,7 @@ func TestEnsureAlgInKey(t *testing.T) {
require.True(t, ok)
assert.Equal(t, jwa.RS256().String(), alg.String())
var crv any
_ = key.Get("crv", &crv)
crv, _ := jwk.Get[any](key, "crv")
assert.Nil(t, crv)
})
}
+3 -3
View File
@@ -3,15 +3,15 @@ package utils
import (
"fmt"
"github.com/lestrrat-go/jwx/v3/jwt"
"github.com/lestrrat-go/jwx/v4/jwt"
)
func GetClaimsFromToken(token jwt.Token) (map[string]any, error) {
keys := token.Keys()
claims := make(map[string]any, len(keys))
for _, key := range keys {
var value any
if err := token.Get(key, &value); err != nil {
value, err := jwt.Get[any](token, key)
if err != nil {
return nil, fmt.Errorf("failed to get claim %s: %w", key, err)
}
claims[key] = value
+1 -1
View File
@@ -8,7 +8,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/italypaleale/francis/host/local"
"github.com/lestrrat-go/jwx/v3/jwt"
"github.com/lestrrat-go/jwx/v4/jwt"
"gorm.io/gorm"
"github.com/pocket-id/pocket-id/backend/internal/appconfig"
+3 -3
View File
@@ -8,7 +8,7 @@ import (
"github.com/go-webauthn/webauthn/protocol"
gowebauthn "github.com/go-webauthn/webauthn/webauthn"
"github.com/lestrrat-go/jwx/v3/jwt"
"github.com/lestrrat-go/jwx/v4/jwt"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -62,8 +62,8 @@ func (s *fakeSigner) GetAuthenticationMethod(token jwt.Token) (string, error) {
if !token.Has(common.AuthenticationMethodsClaim) {
return "", nil
}
var methods []string
if err := token.Get(common.AuthenticationMethodsClaim, &methods); err != nil {
methods, err := jwt.Get[[]string](token, common.AuthenticationMethodsClaim)
if err != nil {
return "", err
}
if len(methods) == 0 {