Files
grafana/pkg/services/authn/clients/ext_jwt_test.go
Misi 1f3dc0533c Auth: Add tracing to auth clients and AuthToken service (#107878)
* Add tracing to auth clients + authtoken svc

* Fix span names

* Fix ext_jwt.go

* Fix idimpl/service

* Update wire_gen.go

* Add tracing to JWT client

* Lint
2025-07-10 15:41:00 +02:00

735 lines
22 KiB
Go

package clients
import (
"context"
"crypto/rand"
"crypto/rsa"
"fmt"
"net/http"
"testing"
"time"
"github.com/go-jose/go-jose/v3"
"github.com/go-jose/go-jose/v3/jwt"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
authnlib "github.com/grafana/authlib/authn"
claims "github.com/grafana/authlib/types"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/services/authn"
"github.com/grafana/grafana/pkg/setting"
)
type (
idTokenClaims = authnlib.Claims[authnlib.IDTokenClaims]
accessTokenClaims = authnlib.Claims[authnlib.AccessTokenClaims]
)
var (
validAccessTokenClaims = accessTokenClaims{
Claims: jwt.Claims{
Subject: "access-policy:this-uid",
Expiry: jwt.NewNumericDate(time.Date(2023, 5, 3, 0, 0, 0, 0, time.UTC)),
IssuedAt: jwt.NewNumericDate(time.Date(2023, 5, 2, 0, 0, 0, 0, time.UTC)),
},
Rest: authnlib.AccessTokenClaims{
Scopes: []string{"profile", "groups"},
DelegatedPermissions: []string{"dashboards:create", "folders:read", "datasources:explore", "datasources.insights:read"},
Permissions: []string{"fixed:folders:reader", "folders:read"},
Namespace: "default", // org ID of 1 is special and translates to default
},
}
validIDTokenClaims = idTokenClaims{
Claims: jwt.Claims{
Subject: "user:2",
Expiry: jwt.NewNumericDate(time.Date(2023, 5, 3, 0, 0, 0, 0, time.UTC)),
IssuedAt: jwt.NewNumericDate(time.Date(2023, 5, 2, 0, 0, 0, 0, time.UTC)),
},
Rest: authnlib.IDTokenClaims{
AuthenticatedBy: "extended_jwt",
Namespace: "default", // org ID of 1 is special and translates to default
},
}
validIDTokenClaimsWithStackSet = idTokenClaims{
Claims: jwt.Claims{
Subject: "user:2",
Expiry: jwt.NewNumericDate(time.Date(2023, 5, 3, 0, 0, 0, 0, time.UTC)),
IssuedAt: jwt.NewNumericDate(time.Date(2023, 5, 2, 0, 0, 0, 0, time.UTC)),
},
Rest: authnlib.IDTokenClaims{
AuthenticatedBy: "extended_jwt",
Namespace: "stacks-1234",
},
}
validIDTokenClaimsWithDeprecatedStackClaimSet = idTokenClaims{
Claims: jwt.Claims{
Subject: "user:2",
Expiry: jwt.NewNumericDate(time.Date(2023, 5, 3, 0, 0, 0, 0, time.UTC)),
IssuedAt: jwt.NewNumericDate(time.Date(2023, 5, 2, 0, 0, 0, 0, time.UTC)),
},
Rest: authnlib.IDTokenClaims{
AuthenticatedBy: "extended_jwt",
Namespace: "stack-1234",
},
}
validAccessTokenClaimsWildcard = accessTokenClaims{
Claims: jwt.Claims{
Subject: "access-policy:this-uid",
Expiry: jwt.NewNumericDate(time.Date(2023, 5, 3, 0, 0, 0, 0, time.UTC)),
IssuedAt: jwt.NewNumericDate(time.Date(2023, 5, 2, 0, 0, 0, 0, time.UTC)),
},
Rest: authnlib.AccessTokenClaims{
Namespace: "*",
},
}
validAccessTokenClaimsWithStackSet = accessTokenClaims{
Claims: jwt.Claims{
Subject: "access-policy:this-uid",
Expiry: jwt.NewNumericDate(time.Date(2023, 5, 3, 0, 0, 0, 0, time.UTC)),
IssuedAt: jwt.NewNumericDate(time.Date(2023, 5, 2, 0, 0, 0, 0, time.UTC)),
},
Rest: authnlib.AccessTokenClaims{
Namespace: "stacks-1234",
},
}
validAccessTokenClaimsWithDeprecatedStackClaimSet = accessTokenClaims{
Claims: jwt.Claims{
Subject: "access-policy:this-uid",
Expiry: jwt.NewNumericDate(time.Date(2023, 5, 3, 0, 0, 0, 0, time.UTC)),
IssuedAt: jwt.NewNumericDate(time.Date(2023, 5, 2, 0, 0, 0, 0, time.UTC)),
},
Rest: authnlib.AccessTokenClaims{
Namespace: "stack-1234",
},
}
invalidNamespaceIDTokenClaims = idTokenClaims{
Claims: jwt.Claims{
Subject: "user:2",
Expiry: jwt.NewNumericDate(time.Date(2023, 5, 3, 0, 0, 0, 0, time.UTC)),
IssuedAt: jwt.NewNumericDate(time.Date(2023, 5, 2, 0, 0, 0, 0, time.UTC)),
},
Rest: authnlib.IDTokenClaims{
AuthenticatedBy: "extended_jwt",
Namespace: "org-2",
},
}
invalidSubjectIDTokenClaims = idTokenClaims{
Claims: jwt.Claims{
Subject: "service-account:2",
Expiry: jwt.NewNumericDate(time.Date(2023, 5, 3, 0, 0, 0, 0, time.UTC)),
IssuedAt: jwt.NewNumericDate(time.Date(2023, 5, 2, 0, 0, 0, 0, time.UTC)),
},
Rest: authnlib.IDTokenClaims{
AuthenticatedBy: "extended_jwt",
Namespace: "default",
},
}
pk, _ = rsa.GenerateKey(rand.Reader, 4096)
)
var _ authnlib.Verifier[authnlib.IDTokenClaims] = &mockIDVerifier{}
type mockIDVerifier struct {
Claims idTokenClaims
Error error
}
func (m *mockIDVerifier) Verify(ctx context.Context, token string) (*idTokenClaims, error) {
return &m.Claims, m.Error
}
var _ authnlib.Verifier[authnlib.AccessTokenClaims] = &mockVerifier{}
type mockVerifier struct {
Claims accessTokenClaims
Error error
}
func (m *mockVerifier) Verify(ctx context.Context, token string) (*accessTokenClaims, error) {
return &m.Claims, m.Error
}
func TestExtendedJWT_Test(t *testing.T) {
type testCase struct {
name string
cfg *setting.Cfg
authHeaderFunc func() string
want bool
}
testCases := []testCase{
{
name: "should return false when extended jwt is disabled",
cfg: &setting.Cfg{
ExtJWTAuth: setting.ExtJWTSettings{
Enabled: false,
},
},
authHeaderFunc: func() string { return "eyJ" },
want: false,
},
{
name: "should return true when Authorization header contains Bearer prefix",
authHeaderFunc: func() string { return "Bearer " + generateToken(validAccessTokenClaims, pk, jose.RS256) },
want: true,
},
{
name: "should return true when Authorization header only contains the token",
authHeaderFunc: func() string { return generateToken(validAccessTokenClaims, pk, jose.RS256) },
want: true,
},
{
name: "should return false when Authorization header is empty",
authHeaderFunc: func() string { return "" },
want: false,
},
{
name: "should return false when jwt.ParseSigned fails",
authHeaderFunc: func() string { return "invalid token" },
want: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
env := setupTestCtx(tc.cfg)
validHTTPReq := &http.Request{
Header: map[string][]string{
"X-Access-Token": {tc.authHeaderFunc()},
},
}
actual := env.s.Test(context.Background(), &authn.Request{
HTTPRequest: validHTTPReq,
})
assert.Equal(t, tc.want, actual)
})
}
}
func TestExtendedJWT_Authenticate(t *testing.T) {
type testCase struct {
name string
cfg *setting.Cfg // optional, only used when overriding the cfg provided by default test setup
accessToken *accessTokenClaims
idToken *idTokenClaims
orgID int64
want *authn.Identity
wantErr error
}
testCases := []testCase{
{
name: "should authenticate as service",
accessToken: &validAccessTokenClaims,
orgID: 1,
want: &authn.Identity{
ID: "this-uid",
UID: "this-uid",
Name: "this-uid",
Type: claims.TypeAccessPolicy,
OrgID: 1,
AccessTokenClaims: &validAccessTokenClaims,
Namespace: "default",
AuthenticatedBy: "extendedjwt",
AuthID: "access-policy:this-uid",
ClientParams: authn.ClientParams{
SyncPermissions: true,
FetchPermissionsParams: authn.FetchPermissionsParams{Roles: []string{"fixed:folders:reader"}, AllowedActions: []string{"folders:read"}, K8s: []string{}}},
},
},
{
name: "should authenticate as service using wildcard namespace",
accessToken: &validAccessTokenClaimsWildcard,
orgID: 1,
want: &authn.Identity{
ID: "this-uid",
UID: "this-uid",
Name: "this-uid",
Type: claims.TypeAccessPolicy,
OrgID: 1,
AccessTokenClaims: &validAccessTokenClaimsWildcard,
Namespace: "*",
AuthenticatedBy: "extendedjwt",
AuthID: "access-policy:this-uid",
ClientParams: authn.ClientParams{
SyncPermissions: true,
},
},
},
{
name: "should authenticate as user",
accessToken: &validAccessTokenClaims,
idToken: &validIDTokenClaims,
orgID: 1,
want: &authn.Identity{
ID: "2",
Type: claims.TypeUser,
OrgID: 1,
AccessTokenClaims: &validAccessTokenClaims,
IDTokenClaims: &validIDTokenClaims,
Namespace: "default",
AuthenticatedBy: "extendedjwt",
AuthID: "access-policy:this-uid",
ClientParams: authn.ClientParams{
FetchSyncedUser: true,
SyncPermissions: true,
FetchPermissionsParams: authn.FetchPermissionsParams{
RestrictedActions: []string{"dashboards:create", "folders:read", "datasources:explore", "datasources.insights:read"},
},
},
},
},
{
name: "should authenticate as user in the user namespace",
accessToken: &validAccessTokenClaimsWildcard,
idToken: &validIDTokenClaims,
orgID: 1,
want: &authn.Identity{
ID: "2",
Type: claims.TypeUser,
OrgID: 1,
AccessTokenClaims: &validAccessTokenClaimsWildcard,
IDTokenClaims: &validIDTokenClaims,
Namespace: "default",
AuthenticatedBy: "extendedjwt",
AuthID: "access-policy:this-uid",
ClientParams: authn.ClientParams{
FetchSyncedUser: true,
SyncPermissions: true,
},
},
},
{
name: "should authenticate as user using wildcard namespace for access token, setting allowed namespace to specific",
accessToken: &validAccessTokenClaimsWildcard,
idToken: &validIDTokenClaimsWithStackSet,
orgID: 1,
cfg: &setting.Cfg{
// default org set up by the authenticator is 1
StackID: "1234",
ExtJWTAuth: setting.ExtJWTSettings{
Enabled: true,
ExpectIssuer: "http://localhost:3000",
},
},
want: &authn.Identity{
ID: "2",
Type: claims.TypeUser,
OrgID: 1,
AccessTokenClaims: &validAccessTokenClaimsWildcard,
IDTokenClaims: &validIDTokenClaimsWithStackSet,
Namespace: "stacks-1234",
AuthenticatedBy: "extendedjwt",
AuthID: "access-policy:this-uid",
ClientParams: authn.ClientParams{
FetchSyncedUser: true,
SyncPermissions: true,
},
},
},
{
name: "should authenticate as service using specific namespace claim in access token",
accessToken: &validAccessTokenClaimsWithStackSet,
orgID: 1,
cfg: &setting.Cfg{
// default org set up by the authenticator is 1
StackID: "1234",
ExtJWTAuth: setting.ExtJWTSettings{
Enabled: true,
ExpectIssuer: "http://localhost:3000",
},
},
want: &authn.Identity{
ID: "this-uid",
UID: "this-uid",
Name: "this-uid",
Type: claims.TypeAccessPolicy,
OrgID: 1,
AccessTokenClaims: &validAccessTokenClaimsWithStackSet,
Namespace: "stacks-1234",
AuthenticatedBy: "extendedjwt",
AuthID: "access-policy:this-uid",
ClientParams: authn.ClientParams{
SyncPermissions: true,
},
},
},
{
name: "should authenticate as service using specific deprecated namespace claim in access token",
accessToken: &validAccessTokenClaimsWithDeprecatedStackClaimSet,
orgID: 1,
cfg: &setting.Cfg{
// default org set up by the authenticator is 1
StackID: "1234",
ExtJWTAuth: setting.ExtJWTSettings{
Enabled: true,
ExpectIssuer: "http://localhost:3000",
},
},
want: &authn.Identity{
ID: "this-uid",
UID: "this-uid",
Name: "this-uid",
Type: claims.TypeAccessPolicy,
OrgID: 1,
AccessTokenClaims: &validAccessTokenClaimsWithDeprecatedStackClaimSet,
Namespace: "stack-1234",
AuthenticatedBy: "extendedjwt",
AuthID: "access-policy:this-uid",
ClientParams: authn.ClientParams{
SyncPermissions: true,
},
},
},
{
name: "should authenticate as user using specific deprecated namespace claim in access and id tokens",
accessToken: &validAccessTokenClaimsWithDeprecatedStackClaimSet,
idToken: &validIDTokenClaimsWithDeprecatedStackClaimSet,
orgID: 1,
cfg: &setting.Cfg{
// default org set up by the authenticator is 1
StackID: "1234",
ExtJWTAuth: setting.ExtJWTSettings{
Enabled: true,
ExpectIssuer: "http://localhost:3000",
},
},
want: &authn.Identity{
ID: "2",
Type: claims.TypeUser,
OrgID: 1,
AccessTokenClaims: &validAccessTokenClaimsWithDeprecatedStackClaimSet,
IDTokenClaims: &validIDTokenClaimsWithDeprecatedStackClaimSet,
Namespace: "stack-1234",
AuthenticatedBy: "extendedjwt",
AuthID: "access-policy:this-uid",
ClientParams: authn.ClientParams{
SyncPermissions: true,
FetchSyncedUser: true,
},
},
},
{
name: "should authenticate as user using wildcard namespace for access token, setting allowed namespace to specific",
accessToken: &validAccessTokenClaimsWildcard,
idToken: &validIDTokenClaimsWithStackSet,
orgID: 1,
cfg: &setting.Cfg{
// default org set up by the authenticator is 1
StackID: "1234",
ExtJWTAuth: setting.ExtJWTSettings{
Enabled: true,
ExpectIssuer: "http://localhost:3000",
},
},
want: &authn.Identity{
ID: "2",
Type: claims.TypeUser,
OrgID: 1,
AccessTokenClaims: &validAccessTokenClaimsWildcard,
IDTokenClaims: &validIDTokenClaimsWithStackSet,
Namespace: "stacks-1234",
AuthenticatedBy: "extendedjwt",
AuthID: "access-policy:this-uid",
ClientParams: authn.ClientParams{
FetchSyncedUser: true,
SyncPermissions: true,
},
},
},
{
name: "should return error when id token has an invalid namespace",
accessToken: &validAccessTokenClaims,
idToken: &invalidNamespaceIDTokenClaims,
orgID: 1,
wantErr: errExtJWTDisallowedNamespaceClaim,
},
{
name: "should return error when id token subject is not tied to a user",
accessToken: &validAccessTokenClaims,
idToken: &invalidSubjectIDTokenClaims,
orgID: 1,
wantErr: errExtJWTInvalidSubject,
},
{
name: "should return error when the subject is not an access-policy",
accessToken: &accessTokenClaims{
Claims: jwt.Claims{
Issuer: "http://localhost:3000",
Subject: "user:2",
Audience: jwt.Audience{"http://localhost:3000"},
ID: "1234567890",
Expiry: jwt.NewNumericDate(time.Date(2023, 5, 3, 0, 0, 0, 0, time.UTC)),
IssuedAt: jwt.NewNumericDate(time.Date(2023, 5, 2, 0, 0, 0, 0, time.UTC)),
},
Rest: authnlib.AccessTokenClaims{
Permissions: []string{"fixed:folders:reader"},
Namespace: "default",
},
},
orgID: 1,
wantErr: errExtJWTInvalidSubject,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
env := setupTestCtx(tc.cfg)
validHTTPReq := &http.Request{
Header: map[string][]string{
"X-Access-Token": {generateToken(*tc.accessToken, pk, jose.RS256)},
},
}
env.s.accessTokenVerifier = &mockVerifier{Claims: *tc.accessToken}
if tc.idToken != nil {
env.s.accessTokenVerifier = &mockVerifier{Claims: *tc.accessToken}
env.s.idTokenVerifier = &mockIDVerifier{Claims: *tc.idToken}
validHTTPReq.Header.Add(ExtJWTAuthorizationHeaderName, generateIDToken(*tc.idToken, pk, jose.RS256))
}
id, err := env.s.Authenticate(context.Background(), &authn.Request{
OrgID: tc.orgID,
HTTPRequest: validHTTPReq,
})
if tc.wantErr != nil {
assert.ErrorIs(t, err, tc.wantErr)
assert.Nil(t, id)
} else {
require.NoError(t, err)
assert.EqualValues(t, tc.want, id, fmt.Sprintf("%+v", id))
}
})
}
}
// https://datatracker.ietf.org/doc/html/rfc9068#name-data-structure
func TestVerifyRFC9068TokenFailureScenarios(t *testing.T) {
type testCase struct {
name string
payload *accessTokenClaims
idPayload *idTokenClaims
alg jose.SignatureAlgorithm
generateWrongTyp bool
}
testCases := []testCase{
{
name: "missing iss",
payload: &accessTokenClaims{
Claims: jwt.Claims{
Subject: "access-policy:this-uid",
Audience: jwt.Audience{"http://localhost:3000"},
ID: "1234567890",
Expiry: jwt.NewNumericDate(time.Date(2023, 5, 3, 0, 0, 0, 0, time.UTC)),
IssuedAt: jwt.NewNumericDate(time.Date(2023, 5, 2, 0, 0, 0, 0, time.UTC)),
},
Rest: authnlib.AccessTokenClaims{
Scopes: []string{"profile", "groups"},
},
},
},
{
name: "missing expiry",
payload: &accessTokenClaims{
Claims: jwt.Claims{
Issuer: "http://localhost:3000",
Subject: "access-policy:this-uid",
Audience: jwt.Audience{"http://localhost:3000"},
ID: "1234567890",
IssuedAt: jwt.NewNumericDate(time.Date(2023, 5, 2, 0, 0, 0, 0, time.UTC)),
},
Rest: authnlib.AccessTokenClaims{
Scopes: []string{"profile", "groups"},
},
},
},
{
name: "expired token",
payload: &accessTokenClaims{
Claims: jwt.Claims{
Issuer: "http://localhost:3000",
Subject: "access-policy:this-uid",
Audience: jwt.Audience{"http://localhost:3000"},
ID: "1234567890",
Expiry: jwt.NewNumericDate(time.Date(2023, 5, 2, 0, 0, 0, 0, time.UTC)),
IssuedAt: jwt.NewNumericDate(time.Date(2023, 5, 2, 0, 0, 0, 0, time.UTC)),
},
Rest: authnlib.AccessTokenClaims{
Scopes: []string{"profile", "groups"},
},
},
},
{
name: "missing aud",
payload: &accessTokenClaims{
Claims: jwt.Claims{
Issuer: "http://localhost:3000",
Subject: "access-policy:this-uid",
ID: "1234567890",
Expiry: jwt.NewNumericDate(time.Date(2023, 5, 3, 0, 0, 0, 0, time.UTC)),
IssuedAt: jwt.NewNumericDate(time.Date(2023, 5, 2, 0, 0, 0, 0, time.UTC)),
},
Rest: authnlib.AccessTokenClaims{
Scopes: []string{"profile", "groups"},
},
},
},
{
name: "wrong aud",
payload: &accessTokenClaims{
Claims: jwt.Claims{
Issuer: "http://localhost:3000",
Subject: "access-policy:this-uid",
Audience: jwt.Audience{"http://some-other-host:3000"},
ID: "1234567890",
Expiry: jwt.NewNumericDate(time.Date(2023, 5, 3, 0, 0, 0, 0, time.UTC)),
IssuedAt: jwt.NewNumericDate(time.Date(2023, 5, 2, 0, 0, 0, 0, time.UTC)),
},
Rest: authnlib.AccessTokenClaims{
Scopes: []string{"profile", "groups"},
},
},
},
{
name: "wrong typ",
idPayload: &validIDTokenClaims,
generateWrongTyp: true,
},
{
name: "missing sub",
payload: &accessTokenClaims{
Claims: jwt.Claims{
Issuer: "http://localhost:3000",
Audience: jwt.Audience{"http://localhost:3000"},
ID: "1234567890",
Expiry: jwt.NewNumericDate(time.Date(2023, 5, 3, 0, 0, 0, 0, time.UTC)),
IssuedAt: jwt.NewNumericDate(time.Date(2023, 5, 2, 0, 0, 0, 0, time.UTC)),
},
Rest: authnlib.AccessTokenClaims{
Scopes: []string{"profile", "groups"},
},
},
},
{
name: "missing iat",
payload: &accessTokenClaims{
Claims: jwt.Claims{
Issuer: "http://localhost:3000",
Subject: "access-policy:this-uid",
Audience: jwt.Audience{"http://localhost:3000"},
ID: "1234567890",
Expiry: jwt.NewNumericDate(time.Date(2023, 5, 3, 0, 0, 0, 0, time.UTC)),
},
Rest: authnlib.AccessTokenClaims{
Scopes: []string{"profile", "groups"},
},
},
},
{
name: "iat later than current time",
payload: &accessTokenClaims{
Claims: jwt.Claims{
Issuer: "http://localhost:3000",
Subject: "access-policy:this-uid",
Audience: jwt.Audience{"http://localhost:3000"},
ID: "1234567890",
Expiry: jwt.NewNumericDate(time.Date(2023, 5, 3, 0, 0, 0, 0, time.UTC)),
IssuedAt: jwt.NewNumericDate(time.Date(2023, 5, 2, 0, 2, 0, 0, time.UTC)),
},
Rest: authnlib.AccessTokenClaims{
Scopes: []string{"profile", "groups"},
},
},
},
{
name: "unsupported alg",
payload: &accessTokenClaims{
Claims: jwt.Claims{
Issuer: "http://localhost:3000",
Subject: "access-policy:this-uid",
Audience: jwt.Audience{"http://localhost:3000"},
ID: "1234567890",
Expiry: jwt.NewNumericDate(time.Date(2023, 5, 3, 0, 0, 0, 0, time.UTC)),
IssuedAt: jwt.NewNumericDate(time.Date(2023, 5, 2, 0, 0, 0, 0, time.UTC)),
},
Rest: authnlib.AccessTokenClaims{
Scopes: []string{"profile", "groups"},
},
},
alg: jose.RS384,
},
}
env := setupTestCtx(nil)
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if tc.alg == "" {
tc.alg = jose.RS256
}
var tokenToTest string
if tc.generateWrongTyp {
tokenToTest = generateIDToken(*tc.idPayload, pk, tc.alg)
} else {
tokenToTest = generateToken(*tc.payload, pk, tc.alg)
}
_, err := env.s.accessTokenVerifier.Verify(context.Background(), tokenToTest)
require.Error(t, err)
})
}
}
func setupTestCtx(cfg *setting.Cfg) *testEnv {
if cfg == nil {
cfg = &setting.Cfg{
// default org set up by the authenticator is 1
ExtJWTAuth: setting.ExtJWTSettings{
Enabled: true,
ExpectIssuer: "http://localhost:3000",
},
}
}
extJwtClient := ProvideExtendedJWT(cfg, tracing.InitializeTracerForTest())
return &testEnv{
s: extJwtClient,
}
}
type testEnv struct {
s *ExtendedJWT
}
func generateToken(payload accessTokenClaims, signingKey any, alg jose.SignatureAlgorithm) string {
signer, _ := jose.NewSigner(jose.SigningKey{Algorithm: alg, Key: signingKey}, &jose.SignerOptions{
ExtraHeaders: map[jose.HeaderKey]any{
jose.HeaderType: authnlib.TokenTypeAccess,
"kid": "default",
}})
result, _ := jwt.Signed(signer).Claims(payload).CompactSerialize()
return result
}
func generateIDToken(payload idTokenClaims, signingKey any, alg jose.SignatureAlgorithm) string {
signer, _ := jose.NewSigner(jose.SigningKey{Algorithm: alg, Key: signingKey}, &jose.SignerOptions{
ExtraHeaders: map[jose.HeaderKey]any{
jose.HeaderType: authnlib.TokenTypeID,
"kid": "default",
}})
result, _ := jwt.Signed(signer).Claims(payload).CompactSerialize()
return result
}