Merge pull request #106 from caos/jwt-profile-storage
fix: custom claims and sub for jwt profile
This commit is contained in:
commit
1392c0ee9a
5 changed files with 232 additions and 39 deletions
|
@ -5,6 +5,7 @@ import (
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -399,7 +400,19 @@ type AccessTokenResponse struct {
|
||||||
IDToken string `json:"id_token,omitempty" schema:"id_token,omitempty"`
|
IDToken string `json:"id_token,omitempty" schema:"id_token,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type JWTProfileAssertion struct {
|
type JWTProfileAssertionClaims interface {
|
||||||
|
GetKeyID() string
|
||||||
|
GetPrivateKey() []byte
|
||||||
|
GetIssuer() string
|
||||||
|
GetSubject() string
|
||||||
|
GetAudience() []string
|
||||||
|
GetExpiration() time.Time
|
||||||
|
GetIssuedAt() time.Time
|
||||||
|
SetCustomClaim(key string, value interface{})
|
||||||
|
GetCustomClaim(key string) interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type jwtProfileAssertion struct {
|
||||||
PrivateKeyID string `json:"-"`
|
PrivateKeyID string `json:"-"`
|
||||||
PrivateKey []byte `json:"-"`
|
PrivateKey []byte `json:"-"`
|
||||||
Issuer string `json:"iss"`
|
Issuer string `json:"iss"`
|
||||||
|
@ -407,17 +420,99 @@ type JWTProfileAssertion struct {
|
||||||
Audience Audience `json:"aud"`
|
Audience Audience `json:"aud"`
|
||||||
Expiration Time `json:"exp"`
|
Expiration Time `json:"exp"`
|
||||||
IssuedAt Time `json:"iat"`
|
IssuedAt Time `json:"iat"`
|
||||||
|
|
||||||
|
customClaims map[string]interface{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewJWTProfileAssertionFromKeyJSON(filename string, audience []string) (*JWTProfileAssertion, error) {
|
func (j *jwtProfileAssertion) MarshalJSON() ([]byte, error) {
|
||||||
|
type Alias jwtProfileAssertion
|
||||||
|
a := (*Alias)(j)
|
||||||
|
|
||||||
|
b, err := json.Marshal(a)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(j.customClaims) == 0 {
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.Unmarshal(b, &j.customClaims)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("jws: invalid map of custom claims %v", j.customClaims)
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.Marshal(j.customClaims)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *jwtProfileAssertion) UnmarshalJSON(data []byte) error {
|
||||||
|
type Alias jwtProfileAssertion
|
||||||
|
a := (*Alias)(j)
|
||||||
|
|
||||||
|
err := json.Unmarshal(data, a)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.Unmarshal(data, &j.customClaims)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *jwtProfileAssertion) GetKeyID() string {
|
||||||
|
return j.PrivateKeyID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *jwtProfileAssertion) GetPrivateKey() []byte {
|
||||||
|
return j.PrivateKey
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *jwtProfileAssertion) SetCustomClaim(key string, value interface{}) {
|
||||||
|
if j.customClaims == nil {
|
||||||
|
j.customClaims = make(map[string]interface{})
|
||||||
|
}
|
||||||
|
j.customClaims[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *jwtProfileAssertion) GetCustomClaim(key string) interface{} {
|
||||||
|
if j.customClaims == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return j.customClaims[key]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *jwtProfileAssertion) GetIssuer() string {
|
||||||
|
return j.Issuer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *jwtProfileAssertion) GetSubject() string {
|
||||||
|
return j.Subject
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *jwtProfileAssertion) GetAudience() []string {
|
||||||
|
return j.Audience
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *jwtProfileAssertion) GetExpiration() time.Time {
|
||||||
|
return time.Time(j.Expiration)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *jwtProfileAssertion) GetIssuedAt() time.Time {
|
||||||
|
return time.Time(j.IssuedAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewJWTProfileAssertionFromKeyJSON(filename string, audience []string, opts ...AssertionOption) (JWTProfileAssertionClaims, error) {
|
||||||
data, err := ioutil.ReadFile(filename)
|
data, err := ioutil.ReadFile(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return NewJWTProfileAssertionFromFileData(data, audience)
|
return NewJWTProfileAssertionFromFileData(data, audience, opts...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewJWTProfileAssertionStringFromFileData(data []byte, audience []string) (string, error) {
|
func NewJWTProfileAssertionStringFromFileData(data []byte, audience []string, opts ...AssertionOption) (string, error) {
|
||||||
keyData := new(struct {
|
keyData := new(struct {
|
||||||
KeyID string `json:"keyId"`
|
KeyID string `json:"keyId"`
|
||||||
Key string `json:"key"`
|
Key string `json:"key"`
|
||||||
|
@ -427,10 +522,22 @@ func NewJWTProfileAssertionStringFromFileData(data []byte, audience []string) (s
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
return GenerateJWTProfileToken(NewJWTProfileAssertion(keyData.UserID, keyData.KeyID, audience, []byte(keyData.Key)))
|
return GenerateJWTProfileToken(NewJWTProfileAssertion(keyData.UserID, keyData.KeyID, audience, []byte(keyData.Key), opts...))
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewJWTProfileAssertionFromFileData(data []byte, audience []string) (*JWTProfileAssertion, error) {
|
func JWTProfileDelegatedSubject(sub string) func(*jwtProfileAssertion) {
|
||||||
|
return func(j *jwtProfileAssertion) {
|
||||||
|
j.Subject = sub
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func JWTProfileCustomClaim(key string, value interface{}) func(*jwtProfileAssertion) {
|
||||||
|
return func(j *jwtProfileAssertion) {
|
||||||
|
j.customClaims[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewJWTProfileAssertionFromFileData(data []byte, audience []string, opts ...AssertionOption) (JWTProfileAssertionClaims, error) {
|
||||||
keyData := new(struct {
|
keyData := new(struct {
|
||||||
KeyID string `json:"keyId"`
|
KeyID string `json:"keyId"`
|
||||||
Key string `json:"key"`
|
Key string `json:"key"`
|
||||||
|
@ -440,11 +547,13 @@ func NewJWTProfileAssertionFromFileData(data []byte, audience []string) (*JWTPro
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return NewJWTProfileAssertion(keyData.UserID, keyData.KeyID, audience, []byte(keyData.Key)), nil
|
return NewJWTProfileAssertion(keyData.UserID, keyData.KeyID, audience, []byte(keyData.Key), opts...), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewJWTProfileAssertion(userID, keyID string, audience []string, key []byte) *JWTProfileAssertion {
|
type AssertionOption func(*jwtProfileAssertion)
|
||||||
return &JWTProfileAssertion{
|
|
||||||
|
func NewJWTProfileAssertion(userID, keyID string, audience []string, key []byte, opts ...AssertionOption) JWTProfileAssertionClaims {
|
||||||
|
j := &jwtProfileAssertion{
|
||||||
PrivateKey: key,
|
PrivateKey: key,
|
||||||
PrivateKeyID: keyID,
|
PrivateKeyID: keyID,
|
||||||
Issuer: userID,
|
Issuer: userID,
|
||||||
|
@ -452,7 +561,14 @@ func NewJWTProfileAssertion(userID, keyID string, audience []string, key []byte)
|
||||||
IssuedAt: Time(time.Now().UTC()),
|
IssuedAt: Time(time.Now().UTC()),
|
||||||
Expiration: Time(time.Now().Add(1 * time.Hour).UTC()),
|
Expiration: Time(time.Now().Add(1 * time.Hour).UTC()),
|
||||||
Audience: audience,
|
Audience: audience,
|
||||||
|
customClaims: make(map[string]interface{}),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(j)
|
||||||
|
}
|
||||||
|
|
||||||
|
return j
|
||||||
}
|
}
|
||||||
|
|
||||||
func ClaimHash(claim string, sigAlgorithm jose.SignatureAlgorithm) (string, error) {
|
func ClaimHash(claim string, sigAlgorithm jose.SignatureAlgorithm) (string, error) {
|
||||||
|
@ -473,14 +589,14 @@ func AppendClientIDToAudience(clientID string, audience []string) []string {
|
||||||
return append(audience, clientID)
|
return append(audience, clientID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func GenerateJWTProfileToken(assertion *JWTProfileAssertion) (string, error) {
|
func GenerateJWTProfileToken(assertion JWTProfileAssertionClaims) (string, error) {
|
||||||
privateKey, err := bytesToPrivateKey(assertion.PrivateKey)
|
privateKey, err := bytesToPrivateKey(assertion.GetPrivateKey())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
key := jose.SigningKey{
|
key := jose.SigningKey{
|
||||||
Algorithm: jose.RS256,
|
Algorithm: jose.RS256,
|
||||||
Key: &jose.JSONWebKey{Key: privateKey, KeyID: assertion.PrivateKeyID},
|
Key: &jose.JSONWebKey{Key: privateKey, KeyID: assertion.GetKeyID()},
|
||||||
}
|
}
|
||||||
signer, err := jose.NewSigner(key, &jose.SignerOptions{})
|
signer, err := jose.NewSigner(key, &jose.SignerOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
package oidc
|
package oidc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gopkg.in/square/go-jose.v2"
|
"gopkg.in/square/go-jose.v2"
|
||||||
|
@ -87,6 +89,50 @@ type JWTTokenRequest struct {
|
||||||
Audience Audience `json:"aud"`
|
Audience Audience `json:"aud"`
|
||||||
IssuedAt Time `json:"iat"`
|
IssuedAt Time `json:"iat"`
|
||||||
ExpiresAt Time `json:"exp"`
|
ExpiresAt Time `json:"exp"`
|
||||||
|
|
||||||
|
private map[string]interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *JWTTokenRequest) MarshalJSON() ([]byte, error) {
|
||||||
|
type Alias JWTTokenRequest
|
||||||
|
a := (*Alias)(j)
|
||||||
|
|
||||||
|
b, err := json.Marshal(a)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(j.private) == 0 {
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.Unmarshal(b, &j.private)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("jws: invalid map of custom claims %v", j.private)
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.Marshal(j.private)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *JWTTokenRequest) UnmarshalJSON(data []byte) error {
|
||||||
|
type Alias JWTTokenRequest
|
||||||
|
a := (*Alias)(j)
|
||||||
|
|
||||||
|
err := json.Unmarshal(data, a)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.Unmarshal(data, &j.private)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *JWTTokenRequest) GetCustomClaim(key string) interface{} {
|
||||||
|
return j.private[key]
|
||||||
}
|
}
|
||||||
|
|
||||||
//GetIssuer implements the Claims interface
|
//GetIssuer implements the Claims interface
|
||||||
|
|
|
@ -1,13 +1,16 @@
|
||||||
package op
|
package op
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"gopkg.in/square/go-jose.v2"
|
||||||
|
|
||||||
"github.com/caos/oidc/pkg/utils"
|
"github.com/caos/oidc/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
type KeyProvider interface {
|
type KeyProvider interface {
|
||||||
Storage() Storage
|
GetKeySet(context.Context) (*jose.JSONWebKeySet, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func keysHandler(k KeyProvider) func(http.ResponseWriter, *http.Request) {
|
func keysHandler(k KeyProvider) func(http.ResponseWriter, *http.Request) {
|
||||||
|
@ -17,7 +20,7 @@ func keysHandler(k KeyProvider) func(http.ResponseWriter, *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func Keys(w http.ResponseWriter, r *http.Request, k KeyProvider) {
|
func Keys(w http.ResponseWriter, r *http.Request, k KeyProvider) {
|
||||||
keySet, err := k.Storage().GetKeySet(r.Context())
|
keySet, err := k.GetKeySet(r.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
utils.MarshalJSON(w, err)
|
utils.MarshalJSON(w, err)
|
||||||
|
|
|
@ -74,7 +74,7 @@ func CreateRouter(o OpenIDProvider, interceptors ...HttpInterceptor) *mux.Router
|
||||||
router.HandleFunc(o.IntrospectionEndpoint().Relative(), introspectionHandler(o))
|
router.HandleFunc(o.IntrospectionEndpoint().Relative(), introspectionHandler(o))
|
||||||
router.HandleFunc(o.UserinfoEndpoint().Relative(), userinfoHandler(o))
|
router.HandleFunc(o.UserinfoEndpoint().Relative(), userinfoHandler(o))
|
||||||
router.Handle(o.EndSessionEndpoint().Relative(), intercept(endSessionHandler(o)))
|
router.Handle(o.EndSessionEndpoint().Relative(), intercept(endSessionHandler(o)))
|
||||||
router.HandleFunc(o.KeysEndpoint().Relative(), keysHandler(o))
|
router.HandleFunc(o.KeysEndpoint().Relative(), keysHandler(o.Storage()))
|
||||||
return router
|
return router
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -281,7 +281,7 @@ func (o *openIDKeySet) VerifySignature(ctx context.Context, jws *jose.JSONWebSig
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, errors.New("invalid kid")
|
return nil, errors.New("invalid kid")
|
||||||
}
|
}
|
||||||
return jws.Verify(key)
|
return jws.Verify(&key)
|
||||||
}
|
}
|
||||||
|
|
||||||
type Option func(o *openidProvider) error
|
type Option func(o *openidProvider) error
|
||||||
|
|
|
@ -13,31 +13,48 @@ import (
|
||||||
|
|
||||||
type JWTProfileVerifier interface {
|
type JWTProfileVerifier interface {
|
||||||
oidc.Verifier
|
oidc.Verifier
|
||||||
Storage() Storage
|
Storage() jwtProfileKeyStorage
|
||||||
|
CheckSubject(request *oidc.JWTTokenRequest) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type jwtProfileVerifier struct {
|
type jwtProfileVerifier struct {
|
||||||
storage Storage
|
storage jwtProfileKeyStorage
|
||||||
|
subjectCheck func(request *oidc.JWTTokenRequest) error
|
||||||
issuer string
|
issuer string
|
||||||
maxAgeIAT time.Duration
|
maxAgeIAT time.Duration
|
||||||
offset time.Duration
|
offset time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
//NewJWTProfileVerifier creates a oidc.Verifier for JWT Profile assertions (authorization grant and client authentication)
|
//NewJWTProfileVerifier creates a oidc.Verifier for JWT Profile assertions (authorization grant and client authentication)
|
||||||
func NewJWTProfileVerifier(storage Storage, issuer string, maxAgeIAT, offset time.Duration) JWTProfileVerifier {
|
func NewJWTProfileVerifier(storage jwtProfileKeyStorage, issuer string, maxAgeIAT, offset time.Duration, opts ...JWTProfileVerifierOption) JWTProfileVerifier {
|
||||||
return &jwtProfileVerifier{
|
j := &jwtProfileVerifier{
|
||||||
storage: storage,
|
storage: storage,
|
||||||
|
subjectCheck: SubjectIsIssuer,
|
||||||
issuer: issuer,
|
issuer: issuer,
|
||||||
maxAgeIAT: maxAgeIAT,
|
maxAgeIAT: maxAgeIAT,
|
||||||
offset: offset,
|
offset: offset,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(j)
|
||||||
|
}
|
||||||
|
|
||||||
|
return j
|
||||||
|
}
|
||||||
|
|
||||||
|
type JWTProfileVerifierOption func(*jwtProfileVerifier)
|
||||||
|
|
||||||
|
func SubjectCheck(check func(request *oidc.JWTTokenRequest) error) JWTProfileVerifierOption {
|
||||||
|
return func(verifier *jwtProfileVerifier) {
|
||||||
|
verifier.subjectCheck = check
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *jwtProfileVerifier) Issuer() string {
|
func (v *jwtProfileVerifier) Issuer() string {
|
||||||
return v.issuer
|
return v.issuer
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *jwtProfileVerifier) Storage() Storage {
|
func (v *jwtProfileVerifier) Storage() jwtProfileKeyStorage {
|
||||||
return v.storage
|
return v.storage
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,6 +66,10 @@ func (v *jwtProfileVerifier) Offset() time.Duration {
|
||||||
return v.offset
|
return v.offset
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (v *jwtProfileVerifier) CheckSubject(request *oidc.JWTTokenRequest) error {
|
||||||
|
return v.subjectCheck(request)
|
||||||
|
}
|
||||||
|
|
||||||
//VerifyJWTAssertion verifies the assertion string from JWT Profile (authorization grant and client authentication)
|
//VerifyJWTAssertion verifies the assertion string from JWT Profile (authorization grant and client authentication)
|
||||||
//
|
//
|
||||||
//checks audience, exp, iat, signature and that issuer and sub are the same
|
//checks audience, exp, iat, signature and that issuer and sub are the same
|
||||||
|
@ -71,9 +92,8 @@ func VerifyJWTAssertion(ctx context.Context, assertion string, v JWTProfileVerif
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if request.Issuer != request.Subject {
|
if err = v.CheckSubject(request); err != nil {
|
||||||
//TODO: implement delegation (openid core / oauth rfc)
|
return nil, err
|
||||||
return nil, errors.New("delegation not yet implemented, issuer and sub must be identical")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
keySet := &jwtProfileKeySet{v.Storage(), request.Issuer}
|
keySet := &jwtProfileKeySet{v.Storage(), request.Issuer}
|
||||||
|
@ -84,20 +104,28 @@ func VerifyJWTAssertion(ctx context.Context, assertion string, v JWTProfileVerif
|
||||||
return request, nil
|
return request, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type jwtProfileKeyStorage interface {
|
||||||
|
GetKeyByIDAndUserID(ctx context.Context, keyID, userID string) (*jose.JSONWebKey, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func SubjectIsIssuer(request *oidc.JWTTokenRequest) error {
|
||||||
|
if request.Issuer != request.Subject {
|
||||||
|
return errors.New("delegation not allowed, issuer and sub must be identical")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type jwtProfileKeySet struct {
|
type jwtProfileKeySet struct {
|
||||||
Storage
|
storage jwtProfileKeyStorage
|
||||||
userID string
|
userID string
|
||||||
}
|
}
|
||||||
|
|
||||||
//VerifySignature implements oidc.KeySet by getting the public key from Storage implementation
|
//VerifySignature implements oidc.KeySet by getting the public key from Storage implementation
|
||||||
func (k *jwtProfileKeySet) VerifySignature(ctx context.Context, jws *jose.JSONWebSignature) (payload []byte, err error) {
|
func (k *jwtProfileKeySet) VerifySignature(ctx context.Context, jws *jose.JSONWebSignature) (payload []byte, err error) {
|
||||||
keyID, alg := oidc.GetKeyIDAndAlg(jws)
|
keyID, _ := oidc.GetKeyIDAndAlg(jws)
|
||||||
key, err := k.Storage.GetKeyByIDAndUserID(ctx, keyID, k.userID)
|
key, err := k.storage.GetKeyByIDAndUserID(ctx, keyID, k.userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error fetching keys: %w", err)
|
return nil, fmt.Errorf("error fetching keys: %w", err)
|
||||||
}
|
}
|
||||||
if key.Algorithm != alg {
|
return jws.Verify(key)
|
||||||
|
|
||||||
}
|
|
||||||
return jws.Verify(&key)
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue