mirror of
https://github.com/grafana/grafana.git
synced 2025-08-06 20:59:35 +08:00
Authn: Support access token wildcard namespace (#87816)
* Authn+ExtJWT: allow wildcard namespace for access tokens and restructure validation
This commit is contained in:
@ -18,7 +18,6 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/services/org"
|
"github.com/grafana/grafana/pkg/services/org"
|
||||||
"github.com/grafana/grafana/pkg/services/quota"
|
"github.com/grafana/grafana/pkg/services/quota"
|
||||||
"github.com/grafana/grafana/pkg/services/rendering"
|
"github.com/grafana/grafana/pkg/services/rendering"
|
||||||
"github.com/grafana/grafana/pkg/services/signingkeys"
|
|
||||||
"github.com/grafana/grafana/pkg/services/user"
|
"github.com/grafana/grafana/pkg/services/user"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
)
|
)
|
||||||
@ -34,7 +33,7 @@ func ProvideRegistration(
|
|||||||
loginAttempts loginattempt.Service, quotaService quota.Service,
|
loginAttempts loginattempt.Service, quotaService quota.Service,
|
||||||
authInfoService login.AuthInfoService, renderService rendering.Service,
|
authInfoService login.AuthInfoService, renderService rendering.Service,
|
||||||
features *featuremgmt.FeatureManager, oauthTokenService oauthtoken.OAuthTokenService,
|
features *featuremgmt.FeatureManager, oauthTokenService oauthtoken.OAuthTokenService,
|
||||||
socialService social.Service, cache *remotecache.RemoteCache, signingKeysService signingkeys.Service,
|
socialService social.Service, cache *remotecache.RemoteCache,
|
||||||
ldapService service.LDAP, settingsProviderService setting.Provider,
|
ldapService service.LDAP, settingsProviderService setting.Provider,
|
||||||
) Registration {
|
) Registration {
|
||||||
logger := log.New("authn.registration")
|
logger := log.New("authn.registration")
|
||||||
@ -86,7 +85,7 @@ func ProvideRegistration(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if cfg.ExtJWTAuth.Enabled && features.IsEnabledGlobally(featuremgmt.FlagAuthAPIAccessTokenAuth) {
|
if cfg.ExtJWTAuth.Enabled && features.IsEnabledGlobally(featuremgmt.FlagAuthAPIAccessTokenAuth) {
|
||||||
authnSvc.RegisterClient(clients.ProvideExtendedJWT(userService, cfg, signingKeysService))
|
authnSvc.RegisterClient(clients.ProvideExtendedJWT(cfg))
|
||||||
}
|
}
|
||||||
|
|
||||||
for name := range socialService.GetOAuthProviders() {
|
for name := range socialService.GetOAuthProviders() {
|
||||||
|
@ -4,7 +4,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/go-jose/go-jose/v3/jwt"
|
"github.com/go-jose/go-jose/v3/jwt"
|
||||||
@ -13,9 +12,8 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
|
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
|
||||||
"github.com/grafana/grafana/pkg/services/authn"
|
"github.com/grafana/grafana/pkg/services/authn"
|
||||||
"github.com/grafana/grafana/pkg/services/login"
|
"github.com/grafana/grafana/pkg/services/login"
|
||||||
"github.com/grafana/grafana/pkg/services/signingkeys"
|
|
||||||
"github.com/grafana/grafana/pkg/services/user"
|
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
|
"github.com/grafana/grafana/pkg/util/errutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ authn.Client = new(ExtendedJWT)
|
var _ authn.Client = new(ExtendedJWT)
|
||||||
@ -26,9 +24,23 @@ const (
|
|||||||
extJWTAccessTokenExpectAudience = "grafana"
|
extJWTAccessTokenExpectAudience = "grafana"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ProvideExtendedJWT(userService user.Service, cfg *setting.Cfg,
|
var (
|
||||||
signingKeys signingkeys.Service) *ExtendedJWT {
|
errExtJWTInvalid = errutil.Unauthorized(
|
||||||
verifier := authlib.NewAccessTokenVerifier(authlib.VerifierConfig{
|
"ext.jwt.invalid", errutil.WithPublicMessage("Failed to verify JWT"),
|
||||||
|
)
|
||||||
|
errExtJWTInvalidSubject = errutil.Unauthorized(
|
||||||
|
"ext.jwt.invalid-subject", errutil.WithPublicMessage("Invalid token subject"),
|
||||||
|
)
|
||||||
|
errExtJWTMisMatchedNamespaceClaims = errutil.Unauthorized(
|
||||||
|
"ext.jwt.namespace-mismatch", errutil.WithPublicMessage("Namespace claims didn't match between id token and access token"),
|
||||||
|
)
|
||||||
|
errExtJWTDisallowedNamespaceClaim = errutil.Unauthorized(
|
||||||
|
"ext.jwt.namespace-disallowed", errutil.WithPublicMessage("Namespace claim doesn't allow access to requested namespace"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
func ProvideExtendedJWT(cfg *setting.Cfg) *ExtendedJWT {
|
||||||
|
accessTokenVerifier := authlib.NewAccessTokenVerifier(authlib.VerifierConfig{
|
||||||
SigningKeysURL: cfg.ExtJWTAuth.JWKSUrl,
|
SigningKeysURL: cfg.ExtJWTAuth.JWKSUrl,
|
||||||
AllowedAudiences: []string{extJWTAccessTokenExpectAudience},
|
AllowedAudiences: []string{extJWTAccessTokenExpectAudience},
|
||||||
})
|
})
|
||||||
@ -42,11 +54,8 @@ func ProvideExtendedJWT(userService user.Service, cfg *setting.Cfg,
|
|||||||
return &ExtendedJWT{
|
return &ExtendedJWT{
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
log: log.New(authn.ClientExtendedJWT),
|
log: log.New(authn.ClientExtendedJWT),
|
||||||
userService: userService,
|
|
||||||
signingKeys: signingKeys,
|
|
||||||
accessTokenVerifier: verifier,
|
|
||||||
namespaceMapper: request.GetNamespaceMapper(cfg),
|
namespaceMapper: request.GetNamespaceMapper(cfg),
|
||||||
|
accessTokenVerifier: accessTokenVerifier,
|
||||||
idTokenVerifier: idTokenVerifier,
|
idTokenVerifier: idTokenVerifier,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -54,8 +63,6 @@ func ProvideExtendedJWT(userService user.Service, cfg *setting.Cfg,
|
|||||||
type ExtendedJWT struct {
|
type ExtendedJWT struct {
|
||||||
cfg *setting.Cfg
|
cfg *setting.Cfg
|
||||||
log log.Logger
|
log log.Logger
|
||||||
userService user.Service
|
|
||||||
signingKeys signingkeys.Service
|
|
||||||
accessTokenVerifier authlib.Verifier[authlib.AccessTokenClaims]
|
accessTokenVerifier authlib.Verifier[authlib.AccessTokenClaims]
|
||||||
idTokenVerifier authlib.Verifier[authlib.IDTokenClaims]
|
idTokenVerifier authlib.Verifier[authlib.IDTokenClaims]
|
||||||
namespaceMapper request.NamespaceMapper
|
namespaceMapper request.NamespaceMapper
|
||||||
@ -67,7 +74,7 @@ func (s *ExtendedJWT) Authenticate(ctx context.Context, r *authn.Request) (*auth
|
|||||||
claims, err := s.accessTokenVerifier.Verify(ctx, jwtToken)
|
claims, err := s.accessTokenVerifier.Verify(ctx, jwtToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.log.Error("Failed to verify access token", "error", err)
|
s.log.Error("Failed to verify access token", "error", err)
|
||||||
return nil, errJWTInvalid.Errorf("Failed to verify access token: %w", err)
|
return nil, errExtJWTInvalid.Errorf("failed to verify access token: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
idToken := s.retrieveAuthorizationToken(r.HTTPRequest)
|
idToken := s.retrieveAuthorizationToken(r.HTTPRequest)
|
||||||
@ -75,7 +82,7 @@ func (s *ExtendedJWT) Authenticate(ctx context.Context, r *authn.Request) (*auth
|
|||||||
idTokenClaims, err := s.idTokenVerifier.Verify(ctx, idToken)
|
idTokenClaims, err := s.idTokenVerifier.Verify(ctx, idToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.log.Error("Failed to verify id token", "error", err)
|
s.log.Error("Failed to verify id token", "error", err)
|
||||||
return nil, errJWTInvalid.Errorf("Failed to verify id token: %w", err)
|
return nil, errExtJWTInvalid.Errorf("failed to verify id token: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return s.authenticateAsUser(idTokenClaims, claims)
|
return s.authenticateAsUser(idTokenClaims, claims)
|
||||||
@ -88,40 +95,43 @@ func (s *ExtendedJWT) IsEnabled() bool {
|
|||||||
return s.cfg.ExtJWTAuth.Enabled
|
return s.cfg.ExtJWTAuth.Enabled
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ExtendedJWT) authenticateAsUser(idTokenClaims *authlib.Claims[authlib.IDTokenClaims],
|
func (s *ExtendedJWT) authenticateAsUser(
|
||||||
accessTokenClaims *authlib.Claims[authlib.AccessTokenClaims]) (*authn.Identity, error) {
|
idTokenClaims *authlib.Claims[authlib.IDTokenClaims],
|
||||||
// compare the incoming namespace claim against what namespaceMapper returns
|
accessTokenClaims *authlib.Claims[authlib.AccessTokenClaims],
|
||||||
|
) (*authn.Identity, error) {
|
||||||
|
// Only allow id tokens signed for namespace configured for this instance.
|
||||||
if allowedNamespace := s.namespaceMapper(s.getDefaultOrgID()); idTokenClaims.Rest.Namespace != allowedNamespace {
|
if allowedNamespace := s.namespaceMapper(s.getDefaultOrgID()); idTokenClaims.Rest.Namespace != allowedNamespace {
|
||||||
return nil, errJWTDisallowedNamespaceClaim
|
return nil, errExtJWTDisallowedNamespaceClaim.Errorf("unexpected id token namespace: %s", idTokenClaims.Rest.Namespace)
|
||||||
}
|
|
||||||
// since id token claims can never have a wildcard ("*") namespace claim, the below comparison effectively
|
|
||||||
// disallows wildcard claims in access tokens here in Grafana (they are only meant for service layer)
|
|
||||||
if accessTokenClaims.Rest.Namespace != idTokenClaims.Rest.Namespace {
|
|
||||||
return nil, errJWTMismatchedNamespaceClaims.Errorf("id token namespace: %s, access token namespace: %s", idTokenClaims.Rest.Namespace, accessTokenClaims.Rest.Namespace)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only allow access policies to impersonate
|
// Allow access tokens with either the same namespace as the validated id token namespace or wildcard (`*`).
|
||||||
if !strings.HasPrefix(accessTokenClaims.Subject, fmt.Sprintf("%s:", authn.NamespaceAccessPolicy)) {
|
if !accessTokenClaims.Rest.NamespaceMatches(idTokenClaims.Rest.Namespace) {
|
||||||
s.log.Error("Invalid subject", "subject", accessTokenClaims.Subject)
|
return nil, errExtJWTMisMatchedNamespaceClaims.Errorf("unexpected access token namespace: %s", accessTokenClaims.Rest.Namespace)
|
||||||
return nil, errJWTInvalid.Errorf("Failed to parse sub: %s", "invalid subject format")
|
|
||||||
}
|
|
||||||
// Allow only user impersonation
|
|
||||||
_, err := strconv.ParseInt(strings.TrimPrefix(idTokenClaims.Subject, fmt.Sprintf("%s:", authn.NamespaceUser)), 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
s.log.Error("Failed to parse sub", "error", err)
|
|
||||||
return nil, errJWTInvalid.Errorf("Failed to parse sub: %w", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
id, err := authn.ParseNamespaceID(idTokenClaims.Subject)
|
accessID, err := authn.ParseNamespaceID(accessTokenClaims.Subject)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, errExtJWTInvalidSubject.Errorf("unexpected identity: %s", accessID.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
if !accessID.IsNamespace(authn.NamespaceAccessPolicy) {
|
||||||
|
return nil, errExtJWTInvalid.Errorf("unexpected identity: %s", accessID.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
userID, err := authn.ParseNamespaceID(idTokenClaims.Subject)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errExtJWTInvalid.Errorf("failed to parse id token subject: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !userID.IsNamespace(authn.NamespaceUser) {
|
||||||
|
return nil, errExtJWTInvalidSubject.Errorf("unexpected identity: %s", userID.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
return &authn.Identity{
|
return &authn.Identity{
|
||||||
ID: id,
|
ID: userID,
|
||||||
OrgID: s.getDefaultOrgID(),
|
OrgID: s.getDefaultOrgID(),
|
||||||
AuthenticatedBy: login.ExtendedJWTModule,
|
AuthenticatedBy: login.ExtendedJWTModule,
|
||||||
AuthID: accessTokenClaims.Subject,
|
AuthID: accessID.String(),
|
||||||
ClientParams: authn.ClientParams{
|
ClientParams: authn.ClientParams{
|
||||||
SyncPermissions: true,
|
SyncPermissions: true,
|
||||||
FetchPermissionsParams: authn.FetchPermissionsParams{
|
FetchPermissionsParams: authn.FetchPermissionsParams{
|
||||||
@ -132,19 +142,18 @@ func (s *ExtendedJWT) authenticateAsUser(idTokenClaims *authlib.Claims[authlib.I
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *ExtendedJWT) authenticateAsService(claims *authlib.Claims[authlib.AccessTokenClaims]) (*authn.Identity, error) {
|
func (s *ExtendedJWT) authenticateAsService(claims *authlib.Claims[authlib.AccessTokenClaims]) (*authn.Identity, error) {
|
||||||
if !strings.HasPrefix(claims.Subject, fmt.Sprintf("%s:", authn.NamespaceAccessPolicy)) {
|
// Allow access tokens with that has a wildcard namespace or a namespace matching this instance.
|
||||||
s.log.Error("Invalid subject", "subject", claims.Subject)
|
if allowedNamespace := s.namespaceMapper(s.getDefaultOrgID()); !claims.Rest.NamespaceMatches(allowedNamespace) {
|
||||||
return nil, errJWTInvalid.Errorf("Failed to parse sub: %s", "invalid subject format")
|
return nil, errExtJWTDisallowedNamespaceClaim.Errorf("unexpected access token namespace: %s", claims.Rest.Namespace)
|
||||||
}
|
|
||||||
|
|
||||||
// same as asUser, disallows wildcard claims in access tokens here in Grafana (they are only meant for service layer)
|
|
||||||
if allowedNamespace := s.namespaceMapper(s.getDefaultOrgID()); claims.Rest.Namespace != allowedNamespace {
|
|
||||||
return nil, errJWTDisallowedNamespaceClaim
|
|
||||||
}
|
}
|
||||||
|
|
||||||
id, err := authn.ParseNamespaceID(claims.Subject)
|
id, err := authn.ParseNamespaceID(claims.Subject)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, fmt.Errorf("failed to parse access token subject: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !id.IsNamespace(authn.NamespaceAccessPolicy) {
|
||||||
|
return nil, errExtJWTInvalidSubject.Errorf("unexpected identity: %s", id.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
return &authn.Identity{
|
return &authn.Identity{
|
||||||
|
@ -15,102 +15,108 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
authlib "github.com/grafana/authlib/authn"
|
authnlib "github.com/grafana/authlib/authn"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/models/roletype"
|
|
||||||
"github.com/grafana/grafana/pkg/services/authn"
|
"github.com/grafana/grafana/pkg/services/authn"
|
||||||
"github.com/grafana/grafana/pkg/services/signingkeys"
|
|
||||||
"github.com/grafana/grafana/pkg/services/signingkeys/signingkeystest"
|
|
||||||
"github.com/grafana/grafana/pkg/services/user"
|
|
||||||
"github.com/grafana/grafana/pkg/services/user/usertest"
|
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
JWTAccessTokenClaims = authlib.Claims[authlib.AccessTokenClaims]
|
idTokenClaims = authnlib.Claims[authnlib.IDTokenClaims]
|
||||||
JWTIDTokenClaims = authlib.Claims[authlib.IDTokenClaims]
|
accessTokenClaims = authnlib.Claims[authnlib.AccessTokenClaims]
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
validPayload = JWTAccessTokenClaims{
|
validAccessTokenClaims = accessTokenClaims{
|
||||||
Claims: &jwt.Claims{
|
Claims: &jwt.Claims{
|
||||||
Issuer: "http://localhost:3000",
|
|
||||||
Subject: "access-policy:this-uid",
|
Subject: "access-policy:this-uid",
|
||||||
Audience: jwt.Audience{extJWTAccessTokenExpectAudience},
|
|
||||||
ID: "1234567890",
|
|
||||||
Expiry: jwt.NewNumericDate(time.Date(2023, 5, 3, 0, 0, 0, 0, time.UTC)),
|
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)),
|
IssuedAt: jwt.NewNumericDate(time.Date(2023, 5, 2, 0, 0, 0, 0, time.UTC)),
|
||||||
},
|
},
|
||||||
Rest: authlib.AccessTokenClaims{
|
Rest: authnlib.AccessTokenClaims{
|
||||||
Scopes: []string{"profile", "groups"},
|
Scopes: []string{"profile", "groups"},
|
||||||
DelegatedPermissions: []string{"dashboards:create", "folders:read", "datasources:explore", "datasources.insights:read"},
|
DelegatedPermissions: []string{"dashboards:create", "folders:read", "datasources:explore", "datasources.insights:read"},
|
||||||
Permissions: []string{"fixed:folders:reader"},
|
Permissions: []string{"fixed:folders:reader"},
|
||||||
Namespace: "default", // org ID of 1 is special and translates to default
|
Namespace: "default", // org ID of 1 is special and translates to default
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
validIDPayload = JWTIDTokenClaims{
|
validIDTokenClaims = idTokenClaims{
|
||||||
Claims: &jwt.Claims{
|
Claims: &jwt.Claims{
|
||||||
Issuer: "http://localhost:3000",
|
|
||||||
Subject: "user:2",
|
Subject: "user:2",
|
||||||
Audience: jwt.Audience{"stack:1"},
|
|
||||||
ID: "1234567890",
|
|
||||||
Expiry: jwt.NewNumericDate(time.Date(2023, 5, 3, 0, 0, 0, 0, time.UTC)),
|
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)),
|
IssuedAt: jwt.NewNumericDate(time.Date(2023, 5, 2, 0, 0, 0, 0, time.UTC)),
|
||||||
},
|
},
|
||||||
Rest: authlib.IDTokenClaims{
|
Rest: authnlib.IDTokenClaims{
|
||||||
AuthenticatedBy: "extended_jwt",
|
AuthenticatedBy: "extended_jwt",
|
||||||
Namespace: "default", // org ID of 1 is special and translates to default
|
Namespace: "default", // org ID of 1 is special and translates to default
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
validPayloadWildcardNamespace = JWTAccessTokenClaims{
|
validAcessTokenClaimsWildcard = accessTokenClaims{
|
||||||
Claims: &jwt.Claims{
|
Claims: &jwt.Claims{
|
||||||
Issuer: "http://localhost:3000",
|
|
||||||
Subject: "access-policy:this-uid",
|
Subject: "access-policy:this-uid",
|
||||||
Audience: jwt.Audience{extJWTAccessTokenExpectAudience},
|
|
||||||
ID: "1234567890",
|
|
||||||
Expiry: jwt.NewNumericDate(time.Date(2023, 5, 3, 0, 0, 0, 0, time.UTC)),
|
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)),
|
IssuedAt: jwt.NewNumericDate(time.Date(2023, 5, 2, 0, 0, 0, 0, time.UTC)),
|
||||||
},
|
},
|
||||||
Rest: authlib.AccessTokenClaims{
|
Rest: authnlib.AccessTokenClaims{
|
||||||
Namespace: "*",
|
Namespace: "*",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
mismatchingNamespaceIDPayload = JWTIDTokenClaims{
|
invalidWildcardNamespaceIDTokenClaims = idTokenClaims{
|
||||||
Claims: &jwt.Claims{
|
Claims: &jwt.Claims{
|
||||||
Issuer: "http://localhost:3000",
|
|
||||||
Subject: "user:2",
|
Subject: "user:2",
|
||||||
Audience: jwt.Audience{"stack:1234"},
|
|
||||||
ID: "1234567890",
|
|
||||||
Expiry: jwt.NewNumericDate(time.Date(2023, 5, 3, 0, 0, 0, 0, time.UTC)),
|
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)),
|
IssuedAt: jwt.NewNumericDate(time.Date(2023, 5, 2, 0, 0, 0, 0, time.UTC)),
|
||||||
},
|
},
|
||||||
Rest: authlib.IDTokenClaims{
|
Rest: authnlib.IDTokenClaims{
|
||||||
|
AuthenticatedBy: "extended_jwt",
|
||||||
|
Namespace: "*",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
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",
|
AuthenticatedBy: "extended_jwt",
|
||||||
Namespace: "org-2",
|
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)
|
pk, _ = rsa.GenerateKey(rand.Reader, 4096)
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ authlib.Verifier[authlib.IDTokenClaims] = &mockIDVerifier{}
|
var _ authnlib.Verifier[authnlib.IDTokenClaims] = &mockIDVerifier{}
|
||||||
|
|
||||||
type mockIDVerifier struct {
|
type mockIDVerifier struct {
|
||||||
Claims JWTIDTokenClaims
|
Claims idTokenClaims
|
||||||
Error error
|
Error error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockIDVerifier) Verify(ctx context.Context, token string) (*JWTIDTokenClaims, error) {
|
func (m *mockIDVerifier) Verify(ctx context.Context, token string) (*idTokenClaims, error) {
|
||||||
return &m.Claims, m.Error
|
return &m.Claims, m.Error
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ authlib.Verifier[authlib.AccessTokenClaims] = &mockVerifier{}
|
var _ authnlib.Verifier[authnlib.AccessTokenClaims] = &mockVerifier{}
|
||||||
|
|
||||||
type mockVerifier struct {
|
type mockVerifier struct {
|
||||||
Claims JWTAccessTokenClaims
|
Claims accessTokenClaims
|
||||||
Error error
|
Error error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockVerifier) Verify(ctx context.Context, token string) (*JWTAccessTokenClaims, error) {
|
func (m *mockVerifier) Verify(ctx context.Context, token string) (*accessTokenClaims, error) {
|
||||||
return &m.Claims, m.Error
|
return &m.Claims, m.Error
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -136,13 +142,13 @@ func TestExtendedJWT_Test(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "should return true when Authorization header contains Bearer prefix",
|
name: "should return true when Authorization header contains Bearer prefix",
|
||||||
cfg: nil,
|
cfg: nil,
|
||||||
authHeaderFunc: func() string { return "Bearer " + generateToken(validPayload, pk, jose.RS256) },
|
authHeaderFunc: func() string { return "Bearer " + generateToken(validAccessTokenClaims, pk, jose.RS256) },
|
||||||
want: true,
|
want: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "should return true when Authorization header only contains the token",
|
name: "should return true when Authorization header only contains the token",
|
||||||
cfg: nil,
|
cfg: nil,
|
||||||
authHeaderFunc: func() string { return generateToken(validPayload, pk, jose.RS256) },
|
authHeaderFunc: func() string { return generateToken(validAccessTokenClaims, pk, jose.RS256) },
|
||||||
want: true,
|
want: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -165,7 +171,7 @@ func TestExtendedJWT_Test(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
authHeaderFunc: func() string {
|
authHeaderFunc: func() string {
|
||||||
payload := validPayload
|
payload := validAccessTokenClaims
|
||||||
payload.Issuer = "http://unknown-issuer"
|
payload.Issuer = "http://unknown-issuer"
|
||||||
return generateToken(payload, pk, jose.RS256)
|
return generateToken(payload, pk, jose.RS256)
|
||||||
},
|
},
|
||||||
@ -195,17 +201,16 @@ func TestExtendedJWT_Test(t *testing.T) {
|
|||||||
func TestExtendedJWT_Authenticate(t *testing.T) {
|
func TestExtendedJWT_Authenticate(t *testing.T) {
|
||||||
type testCase struct {
|
type testCase struct {
|
||||||
name string
|
name string
|
||||||
payload *JWTAccessTokenClaims
|
accessToken *accessTokenClaims
|
||||||
idPayload *JWTIDTokenClaims
|
idToken *idTokenClaims
|
||||||
orgID int64
|
orgID int64
|
||||||
want *authn.Identity
|
want *authn.Identity
|
||||||
initTestEnv func(env *testEnv)
|
|
||||||
wantErr error
|
wantErr error
|
||||||
}
|
}
|
||||||
testCases := []testCase{
|
testCases := []testCase{
|
||||||
{
|
{
|
||||||
name: "successful authentication as service",
|
name: "should authenticate as service",
|
||||||
payload: &validPayload,
|
accessToken: &validAccessTokenClaims,
|
||||||
orgID: 1,
|
orgID: 1,
|
||||||
want: &authn.Identity{
|
want: &authn.Identity{
|
||||||
ID: authn.MustParseNamespaceID("access-policy:this-uid"),
|
ID: authn.MustParseNamespaceID("access-policy:this-uid"),
|
||||||
@ -217,23 +222,27 @@ func TestExtendedJWT_Authenticate(t *testing.T) {
|
|||||||
SyncPermissions: true,
|
SyncPermissions: true,
|
||||||
FetchPermissionsParams: authn.FetchPermissionsParams{Roles: []string{"fixed:folders:reader"}}},
|
FetchPermissionsParams: authn.FetchPermissionsParams{Roles: []string{"fixed:folders:reader"}}},
|
||||||
},
|
},
|
||||||
wantErr: nil,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "successful authentication as user",
|
name: "should authenticate as service using wildcard namespace",
|
||||||
payload: &validPayload,
|
accessToken: &validAcessTokenClaimsWildcard,
|
||||||
idPayload: &validIDPayload,
|
|
||||||
orgID: 1,
|
orgID: 1,
|
||||||
initTestEnv: func(env *testEnv) {
|
want: &authn.Identity{
|
||||||
env.userSvc.ExpectedSignedInUser = &user.SignedInUser{
|
ID: authn.MustParseNamespaceID("access-policy:this-uid"),
|
||||||
UserID: 2,
|
UID: authn.MustParseNamespaceID("access-policy:this-uid"),
|
||||||
OrgID: 1,
|
OrgID: 1,
|
||||||
OrgRole: roletype.RoleAdmin,
|
AuthenticatedBy: "extendedjwt",
|
||||||
Name: "John Doe",
|
AuthID: "access-policy:this-uid",
|
||||||
Email: "johndoe@grafana.com",
|
ClientParams: authn.ClientParams{
|
||||||
Login: "johndoe",
|
SyncPermissions: true,
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "should authenticate as user",
|
||||||
|
accessToken: &validAccessTokenClaims,
|
||||||
|
idToken: &validIDTokenClaims,
|
||||||
|
orgID: 1,
|
||||||
want: &authn.Identity{
|
want: &authn.Identity{
|
||||||
ID: authn.MustParseNamespaceID("user:2"),
|
ID: authn.MustParseNamespaceID("user:2"),
|
||||||
OrgID: 1,
|
OrgID: 1,
|
||||||
@ -247,45 +256,49 @@ func TestExtendedJWT_Authenticate(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
wantErr: nil,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "fail authentication as user when access token namespace claim doesn't match id token namespace",
|
name: "should authenticate as user using wildcard namespace for access token",
|
||||||
payload: &validPayload,
|
accessToken: &validAcessTokenClaimsWildcard,
|
||||||
idPayload: &mismatchingNamespaceIDPayload,
|
idToken: &validIDTokenClaims,
|
||||||
orgID: 1,
|
orgID: 1,
|
||||||
initTestEnv: func(env *testEnv) {
|
want: &authn.Identity{
|
||||||
env.userSvc.ExpectedSignedInUser = &user.SignedInUser{
|
ID: authn.MustParseNamespaceID("user:2"),
|
||||||
UserID: 2,
|
|
||||||
OrgID: 1,
|
OrgID: 1,
|
||||||
OrgRole: roletype.RoleAdmin,
|
AuthenticatedBy: "extendedjwt",
|
||||||
Name: "John Doe",
|
AuthID: "access-policy:this-uid",
|
||||||
Email: "johndoe@grafana.com",
|
ClientParams: authn.ClientParams{
|
||||||
Login: "johndoe",
|
FetchSyncedUser: true,
|
||||||
}
|
SyncPermissions: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
wantErr: errJWTMismatchedNamespaceClaims.Errorf("id token namespace: %s, access token namespace: %s", mismatchingNamespaceIDPayload.Rest.Namespace, validPayload.Rest.Namespace),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "fail authentication as user when id token namespace claim doesn't match allowed namespace",
|
name: "should return error when id token namespace is a wildcard",
|
||||||
payload: &validPayloadWildcardNamespace,
|
accessToken: &validAccessTokenClaims,
|
||||||
idPayload: &validIDPayload,
|
idToken: &invalidWildcardNamespaceIDTokenClaims,
|
||||||
orgID: 1,
|
orgID: 1,
|
||||||
initTestEnv: func(env *testEnv) {
|
wantErr: errExtJWTDisallowedNamespaceClaim,
|
||||||
env.userSvc.ExpectedSignedInUser = &user.SignedInUser{
|
|
||||||
UserID: 2,
|
|
||||||
OrgID: 1,
|
|
||||||
OrgRole: roletype.RoleAdmin,
|
|
||||||
Name: "John Doe",
|
|
||||||
Email: "johndoe@grafana.com",
|
|
||||||
Login: "johndoe",
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
wantErr: errJWTDisallowedNamespaceClaim,
|
{
|
||||||
|
name: "should return error when id token has wildcard 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",
|
name: "should return error when the subject is not an access-policy",
|
||||||
payload: &JWTAccessTokenClaims{
|
accessToken: &accessTokenClaims{
|
||||||
Claims: &jwt.Claims{
|
Claims: &jwt.Claims{
|
||||||
Issuer: "http://localhost:3000",
|
Issuer: "http://localhost:3000",
|
||||||
Subject: "user:2",
|
Subject: "user:2",
|
||||||
@ -294,34 +307,31 @@ func TestExtendedJWT_Authenticate(t *testing.T) {
|
|||||||
Expiry: jwt.NewNumericDate(time.Date(2023, 5, 3, 0, 0, 0, 0, time.UTC)),
|
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)),
|
IssuedAt: jwt.NewNumericDate(time.Date(2023, 5, 2, 0, 0, 0, 0, time.UTC)),
|
||||||
},
|
},
|
||||||
Rest: authlib.AccessTokenClaims{
|
Rest: authnlib.AccessTokenClaims{
|
||||||
Permissions: []string{"fixed:folders:reader"},
|
Permissions: []string{"fixed:folders:reader"},
|
||||||
|
Namespace: "default",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
orgID: 1,
|
orgID: 1,
|
||||||
want: nil,
|
wantErr: errExtJWTInvalidSubject,
|
||||||
wantErr: errJWTInvalid.Errorf("Failed to parse sub: %s", "invalid subject format"),
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
env := setupTestCtx(nil)
|
env := setupTestCtx(nil)
|
||||||
if tc.initTestEnv != nil {
|
|
||||||
tc.initTestEnv(env)
|
|
||||||
}
|
|
||||||
|
|
||||||
validHTTPReq := &http.Request{
|
validHTTPReq := &http.Request{
|
||||||
Header: map[string][]string{
|
Header: map[string][]string{
|
||||||
"X-Access-Token": {generateToken(*tc.payload, pk, jose.RS256)},
|
"X-Access-Token": {generateToken(*tc.accessToken, pk, jose.RS256)},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
env.s.accessTokenVerifier = &mockVerifier{Claims: *tc.payload}
|
env.s.accessTokenVerifier = &mockVerifier{Claims: *tc.accessToken}
|
||||||
if tc.idPayload != nil {
|
if tc.idToken != nil {
|
||||||
env.s.accessTokenVerifier = &mockVerifier{Claims: *tc.payload}
|
env.s.accessTokenVerifier = &mockVerifier{Claims: *tc.accessToken}
|
||||||
env.s.idTokenVerifier = &mockIDVerifier{Claims: *tc.idPayload}
|
env.s.idTokenVerifier = &mockIDVerifier{Claims: *tc.idToken}
|
||||||
validHTTPReq.Header.Add(extJWTAuthorizationHeaderName, generateIDToken(*tc.idPayload, pk, jose.RS256))
|
validHTTPReq.Header.Add(extJWTAuthorizationHeaderName, generateIDToken(*tc.idToken, pk, jose.RS256))
|
||||||
}
|
}
|
||||||
|
|
||||||
id, err := env.s.Authenticate(context.Background(), &authn.Request{
|
id, err := env.s.Authenticate(context.Background(), &authn.Request{
|
||||||
@ -330,7 +340,8 @@ func TestExtendedJWT_Authenticate(t *testing.T) {
|
|||||||
Resp: nil,
|
Resp: nil,
|
||||||
})
|
})
|
||||||
if tc.wantErr != nil {
|
if tc.wantErr != nil {
|
||||||
require.ErrorIs(t, err, tc.wantErr)
|
assert.ErrorIs(t, err, tc.wantErr)
|
||||||
|
assert.Nil(t, id)
|
||||||
} else {
|
} else {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.EqualValues(t, tc.want, id, fmt.Sprintf("%+v", id))
|
assert.EqualValues(t, tc.want, id, fmt.Sprintf("%+v", id))
|
||||||
@ -343,8 +354,8 @@ func TestExtendedJWT_Authenticate(t *testing.T) {
|
|||||||
func TestVerifyRFC9068TokenFailureScenarios(t *testing.T) {
|
func TestVerifyRFC9068TokenFailureScenarios(t *testing.T) {
|
||||||
type testCase struct {
|
type testCase struct {
|
||||||
name string
|
name string
|
||||||
payload *JWTAccessTokenClaims
|
payload *accessTokenClaims
|
||||||
idPayload *JWTIDTokenClaims
|
idPayload *idTokenClaims
|
||||||
alg jose.SignatureAlgorithm
|
alg jose.SignatureAlgorithm
|
||||||
generateWrongTyp bool
|
generateWrongTyp bool
|
||||||
}
|
}
|
||||||
@ -352,7 +363,7 @@ func TestVerifyRFC9068TokenFailureScenarios(t *testing.T) {
|
|||||||
testCases := []testCase{
|
testCases := []testCase{
|
||||||
{
|
{
|
||||||
name: "missing iss",
|
name: "missing iss",
|
||||||
payload: &JWTAccessTokenClaims{
|
payload: &accessTokenClaims{
|
||||||
Claims: &jwt.Claims{
|
Claims: &jwt.Claims{
|
||||||
Subject: "access-policy:this-uid",
|
Subject: "access-policy:this-uid",
|
||||||
Audience: jwt.Audience{"http://localhost:3000"},
|
Audience: jwt.Audience{"http://localhost:3000"},
|
||||||
@ -360,14 +371,14 @@ func TestVerifyRFC9068TokenFailureScenarios(t *testing.T) {
|
|||||||
Expiry: jwt.NewNumericDate(time.Date(2023, 5, 3, 0, 0, 0, 0, time.UTC)),
|
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)),
|
IssuedAt: jwt.NewNumericDate(time.Date(2023, 5, 2, 0, 0, 0, 0, time.UTC)),
|
||||||
},
|
},
|
||||||
Rest: authlib.AccessTokenClaims{
|
Rest: authnlib.AccessTokenClaims{
|
||||||
Scopes: []string{"profile", "groups"},
|
Scopes: []string{"profile", "groups"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "missing expiry",
|
name: "missing expiry",
|
||||||
payload: &JWTAccessTokenClaims{
|
payload: &accessTokenClaims{
|
||||||
Claims: &jwt.Claims{
|
Claims: &jwt.Claims{
|
||||||
Issuer: "http://localhost:3000",
|
Issuer: "http://localhost:3000",
|
||||||
Subject: "access-policy:this-uid",
|
Subject: "access-policy:this-uid",
|
||||||
@ -375,14 +386,14 @@ func TestVerifyRFC9068TokenFailureScenarios(t *testing.T) {
|
|||||||
ID: "1234567890",
|
ID: "1234567890",
|
||||||
IssuedAt: 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: authlib.AccessTokenClaims{
|
Rest: authnlib.AccessTokenClaims{
|
||||||
Scopes: []string{"profile", "groups"},
|
Scopes: []string{"profile", "groups"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "expired token",
|
name: "expired token",
|
||||||
payload: &JWTAccessTokenClaims{
|
payload: &accessTokenClaims{
|
||||||
Claims: &jwt.Claims{
|
Claims: &jwt.Claims{
|
||||||
Issuer: "http://localhost:3000",
|
Issuer: "http://localhost:3000",
|
||||||
Subject: "access-policy:this-uid",
|
Subject: "access-policy:this-uid",
|
||||||
@ -391,14 +402,14 @@ func TestVerifyRFC9068TokenFailureScenarios(t *testing.T) {
|
|||||||
Expiry: jwt.NewNumericDate(time.Date(2023, 5, 2, 0, 0, 0, 0, time.UTC)),
|
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)),
|
IssuedAt: jwt.NewNumericDate(time.Date(2023, 5, 2, 0, 0, 0, 0, time.UTC)),
|
||||||
},
|
},
|
||||||
Rest: authlib.AccessTokenClaims{
|
Rest: authnlib.AccessTokenClaims{
|
||||||
Scopes: []string{"profile", "groups"},
|
Scopes: []string{"profile", "groups"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "missing aud",
|
name: "missing aud",
|
||||||
payload: &JWTAccessTokenClaims{
|
payload: &accessTokenClaims{
|
||||||
Claims: &jwt.Claims{
|
Claims: &jwt.Claims{
|
||||||
Issuer: "http://localhost:3000",
|
Issuer: "http://localhost:3000",
|
||||||
Subject: "access-policy:this-uid",
|
Subject: "access-policy:this-uid",
|
||||||
@ -406,14 +417,14 @@ func TestVerifyRFC9068TokenFailureScenarios(t *testing.T) {
|
|||||||
Expiry: jwt.NewNumericDate(time.Date(2023, 5, 3, 0, 0, 0, 0, time.UTC)),
|
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)),
|
IssuedAt: jwt.NewNumericDate(time.Date(2023, 5, 2, 0, 0, 0, 0, time.UTC)),
|
||||||
},
|
},
|
||||||
Rest: authlib.AccessTokenClaims{
|
Rest: authnlib.AccessTokenClaims{
|
||||||
Scopes: []string{"profile", "groups"},
|
Scopes: []string{"profile", "groups"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "wrong aud",
|
name: "wrong aud",
|
||||||
payload: &JWTAccessTokenClaims{
|
payload: &accessTokenClaims{
|
||||||
Claims: &jwt.Claims{
|
Claims: &jwt.Claims{
|
||||||
Issuer: "http://localhost:3000",
|
Issuer: "http://localhost:3000",
|
||||||
Subject: "access-policy:this-uid",
|
Subject: "access-policy:this-uid",
|
||||||
@ -422,19 +433,19 @@ func TestVerifyRFC9068TokenFailureScenarios(t *testing.T) {
|
|||||||
Expiry: jwt.NewNumericDate(time.Date(2023, 5, 3, 0, 0, 0, 0, time.UTC)),
|
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)),
|
IssuedAt: jwt.NewNumericDate(time.Date(2023, 5, 2, 0, 0, 0, 0, time.UTC)),
|
||||||
},
|
},
|
||||||
Rest: authlib.AccessTokenClaims{
|
Rest: authnlib.AccessTokenClaims{
|
||||||
Scopes: []string{"profile", "groups"},
|
Scopes: []string{"profile", "groups"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "wrong typ",
|
name: "wrong typ",
|
||||||
idPayload: &validIDPayload,
|
idPayload: &validIDTokenClaims,
|
||||||
generateWrongTyp: true,
|
generateWrongTyp: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "missing sub",
|
name: "missing sub",
|
||||||
payload: &JWTAccessTokenClaims{
|
payload: &accessTokenClaims{
|
||||||
Claims: &jwt.Claims{
|
Claims: &jwt.Claims{
|
||||||
Issuer: "http://localhost:3000",
|
Issuer: "http://localhost:3000",
|
||||||
Audience: jwt.Audience{"http://localhost:3000"},
|
Audience: jwt.Audience{"http://localhost:3000"},
|
||||||
@ -442,14 +453,14 @@ func TestVerifyRFC9068TokenFailureScenarios(t *testing.T) {
|
|||||||
Expiry: jwt.NewNumericDate(time.Date(2023, 5, 3, 0, 0, 0, 0, time.UTC)),
|
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)),
|
IssuedAt: jwt.NewNumericDate(time.Date(2023, 5, 2, 0, 0, 0, 0, time.UTC)),
|
||||||
},
|
},
|
||||||
Rest: authlib.AccessTokenClaims{
|
Rest: authnlib.AccessTokenClaims{
|
||||||
Scopes: []string{"profile", "groups"},
|
Scopes: []string{"profile", "groups"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "missing iat",
|
name: "missing iat",
|
||||||
payload: &JWTAccessTokenClaims{
|
payload: &accessTokenClaims{
|
||||||
Claims: &jwt.Claims{
|
Claims: &jwt.Claims{
|
||||||
Issuer: "http://localhost:3000",
|
Issuer: "http://localhost:3000",
|
||||||
Subject: "access-policy:this-uid",
|
Subject: "access-policy:this-uid",
|
||||||
@ -457,14 +468,14 @@ func TestVerifyRFC9068TokenFailureScenarios(t *testing.T) {
|
|||||||
ID: "1234567890",
|
ID: "1234567890",
|
||||||
Expiry: jwt.NewNumericDate(time.Date(2023, 5, 3, 0, 0, 0, 0, time.UTC)),
|
Expiry: jwt.NewNumericDate(time.Date(2023, 5, 3, 0, 0, 0, 0, time.UTC)),
|
||||||
},
|
},
|
||||||
Rest: authlib.AccessTokenClaims{
|
Rest: authnlib.AccessTokenClaims{
|
||||||
Scopes: []string{"profile", "groups"},
|
Scopes: []string{"profile", "groups"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "iat later than current time",
|
name: "iat later than current time",
|
||||||
payload: &JWTAccessTokenClaims{
|
payload: &accessTokenClaims{
|
||||||
Claims: &jwt.Claims{
|
Claims: &jwt.Claims{
|
||||||
Issuer: "http://localhost:3000",
|
Issuer: "http://localhost:3000",
|
||||||
Subject: "access-policy:this-uid",
|
Subject: "access-policy:this-uid",
|
||||||
@ -473,14 +484,14 @@ func TestVerifyRFC9068TokenFailureScenarios(t *testing.T) {
|
|||||||
Expiry: jwt.NewNumericDate(time.Date(2023, 5, 3, 0, 0, 0, 0, time.UTC)),
|
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)),
|
IssuedAt: jwt.NewNumericDate(time.Date(2023, 5, 2, 0, 2, 0, 0, time.UTC)),
|
||||||
},
|
},
|
||||||
Rest: authlib.AccessTokenClaims{
|
Rest: authnlib.AccessTokenClaims{
|
||||||
Scopes: []string{"profile", "groups"},
|
Scopes: []string{"profile", "groups"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "unsupported alg",
|
name: "unsupported alg",
|
||||||
payload: &JWTAccessTokenClaims{
|
payload: &accessTokenClaims{
|
||||||
Claims: &jwt.Claims{
|
Claims: &jwt.Claims{
|
||||||
Issuer: "http://localhost:3000",
|
Issuer: "http://localhost:3000",
|
||||||
Subject: "access-policy:this-uid",
|
Subject: "access-policy:this-uid",
|
||||||
@ -489,7 +500,7 @@ func TestVerifyRFC9068TokenFailureScenarios(t *testing.T) {
|
|||||||
Expiry: jwt.NewNumericDate(time.Date(2023, 5, 3, 0, 0, 0, 0, time.UTC)),
|
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)),
|
IssuedAt: jwt.NewNumericDate(time.Date(2023, 5, 2, 0, 0, 0, 0, time.UTC)),
|
||||||
},
|
},
|
||||||
Rest: authlib.AccessTokenClaims{
|
Rest: authnlib.AccessTokenClaims{
|
||||||
Scopes: []string{"profile", "groups"},
|
Scopes: []string{"profile", "groups"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -528,30 +539,21 @@ func setupTestCtx(cfg *setting.Cfg) *testEnv {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
signingKeysSvc := &signingkeystest.FakeSigningKeysService{
|
extJwtClient := ProvideExtendedJWT(cfg)
|
||||||
ExpectedSinger: pk,
|
|
||||||
ExpectedKeyID: signingkeys.ServerPrivateKeyID,
|
|
||||||
}
|
|
||||||
|
|
||||||
userSvc := &usertest.FakeUserService{}
|
|
||||||
|
|
||||||
extJwtClient := ProvideExtendedJWT(userSvc, cfg, signingKeysSvc)
|
|
||||||
|
|
||||||
return &testEnv{
|
return &testEnv{
|
||||||
userSvc: userSvc,
|
|
||||||
s: extJwtClient,
|
s: extJwtClient,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type testEnv struct {
|
type testEnv struct {
|
||||||
userSvc *usertest.FakeUserService
|
|
||||||
s *ExtendedJWT
|
s *ExtendedJWT
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateToken(payload JWTAccessTokenClaims, signingKey any, alg jose.SignatureAlgorithm) string {
|
func generateToken(payload accessTokenClaims, signingKey any, alg jose.SignatureAlgorithm) string {
|
||||||
signer, _ := jose.NewSigner(jose.SigningKey{Algorithm: alg, Key: signingKey}, &jose.SignerOptions{
|
signer, _ := jose.NewSigner(jose.SigningKey{Algorithm: alg, Key: signingKey}, &jose.SignerOptions{
|
||||||
ExtraHeaders: map[jose.HeaderKey]any{
|
ExtraHeaders: map[jose.HeaderKey]any{
|
||||||
jose.HeaderType: authlib.TokenTypeAccess,
|
jose.HeaderType: authnlib.TokenTypeAccess,
|
||||||
"kid": "default",
|
"kid": "default",
|
||||||
}})
|
}})
|
||||||
|
|
||||||
@ -559,10 +561,10 @@ func generateToken(payload JWTAccessTokenClaims, signingKey any, alg jose.Signat
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateIDToken(payload JWTIDTokenClaims, signingKey any, alg jose.SignatureAlgorithm) string {
|
func generateIDToken(payload idTokenClaims, signingKey any, alg jose.SignatureAlgorithm) string {
|
||||||
signer, _ := jose.NewSigner(jose.SigningKey{Algorithm: alg, Key: signingKey}, &jose.SignerOptions{
|
signer, _ := jose.NewSigner(jose.SigningKey{Algorithm: alg, Key: signingKey}, &jose.SignerOptions{
|
||||||
ExtraHeaders: map[jose.HeaderKey]any{
|
ExtraHeaders: map[jose.HeaderKey]any{
|
||||||
jose.HeaderType: authlib.TokenTypeID,
|
jose.HeaderType: authnlib.TokenTypeID,
|
||||||
"kid": "default",
|
"kid": "default",
|
||||||
}})
|
}})
|
||||||
|
|
||||||
|
@ -27,10 +27,6 @@ var (
|
|||||||
"jwt.missing_claim", errutil.WithPublicMessage("Missing mandatory claim in JWT"))
|
"jwt.missing_claim", errutil.WithPublicMessage("Missing mandatory claim in JWT"))
|
||||||
errJWTInvalidRole = errutil.Forbidden(
|
errJWTInvalidRole = errutil.Forbidden(
|
||||||
"jwt.invalid_role", errutil.WithPublicMessage("Invalid Role in claim"))
|
"jwt.invalid_role", errutil.WithPublicMessage("Invalid Role in claim"))
|
||||||
errJWTMismatchedNamespaceClaims = errutil.Unauthorized(
|
|
||||||
"jwt.namespace_mismatch", errutil.WithPublicMessage("Namespace claims didn't match between id token and access token"))
|
|
||||||
errJWTDisallowedNamespaceClaim = errutil.Unauthorized(
|
|
||||||
"jwt.namespace_mismatch", errutil.WithPublicMessage("Namespace claim doesn't allow access to requested namespace"))
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func ProvideJWT(jwtService auth.JWTVerifierService, cfg *setting.Cfg) *JWT {
|
func ProvideJWT(jwtService auth.JWTVerifierService, cfg *setting.Cfg) *JWT {
|
||||||
|
Reference in New Issue
Block a user