Auth: Extended JWT client for OBO and Service Authentication (#83814)
* reenable ext-jwt-client * fixup settings struct * add user and service auth * lint up * add user auth to grafana ext * fixes * Populate token permissions Co-authored-by: jguer <joao.guerreiro@grafana.com> * fix tests * fix lint * small prealloc * small prealloc * use special namespace for access policies * fix access policy auth * fix tests * fix uncalled settings expander * add feature toggle * small feedback fixes * rename entitlements to permissions * add authlibn * allow viewing the signed in user info for non user namespace * fix invalid namespacedID * use authlib as verifier for tokens * Update pkg/services/authn/clients/ext_jwt.go Co-authored-by: Gabriel MABILLE <gamab@users.noreply.github.com> * Update pkg/services/authn/clients/ext_jwt_test.go Co-authored-by: Gabriel MABILLE <gamab@users.noreply.github.com> * fix parameter names * change asserts to normal package * add rule for assert * fix ownerships * Local diff * test and lint * Fix test * Fix ac test * Fix pluginproxy test * Revert testdata changes * Force revert on test data --------- Co-authored-by: gamab <gabriel.mabille@grafana.com> Co-authored-by: Gabriel MABILLE <gamab@users.noreply.github.com>
This commit is contained in:
co-authored by
Gabriel MABILLE
gamab
parent
ac6e51c94a
commit
5340a6e548
@@ -57,6 +57,15 @@ type ClientParams struct {
|
||||
LookUpParams login.UserLookupParams
|
||||
// SyncPermissions ensure that permissions are loaded from DB and added to the identity
|
||||
SyncPermissions bool
|
||||
// FetchPermissionsParams are the arguments used to fetch permissions from the DB
|
||||
FetchPermissionsParams FetchPermissionsParams
|
||||
}
|
||||
|
||||
type FetchPermissionsParams struct {
|
||||
// ActionsLookup will restrict the permissions to only these actions
|
||||
ActionsLookup []string
|
||||
// Roles permissions will be directly added to the identity permissions
|
||||
Roles []string
|
||||
}
|
||||
|
||||
type PostAuthHookFn func(ctx context.Context, identity *Identity, r *Request) error
|
||||
|
||||
@@ -135,10 +135,9 @@ func ProvideService(
|
||||
s.RegisterClient(clients.ProvideJWT(jwtService, cfg))
|
||||
}
|
||||
|
||||
// FIXME (gamab): Commenting that out for now as we want to re-use the client for external service auth
|
||||
// if s.cfg.ExtendedJWTAuthEnabled && features.IsEnabledGlobally(featuremgmt.FlagExternalServiceAuth) {
|
||||
// s.RegisterClient(clients.ProvideExtendedJWT(userService, cfg, signingKeysService, oauthServer))
|
||||
// }
|
||||
if s.cfg.ExtJWTAuth.Enabled && features.IsEnabledGlobally(featuremgmt.FlagAuthAPIAccessTokenAuth) {
|
||||
s.RegisterClient(clients.ProvideExtendedJWT(userService, cfg, signingKeysService))
|
||||
}
|
||||
|
||||
for name := range socialService.GetOAuthProviders() {
|
||||
clientName := authn.ClientWithPrefix(name)
|
||||
|
||||
@@ -2,6 +2,7 @@ package sync
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
@@ -34,19 +35,57 @@ func (s *RBACSync) SyncPermissionsHook(ctx context.Context, ident *authn.Identit
|
||||
return nil
|
||||
}
|
||||
|
||||
permissions, err := s.ac.GetUserPermissions(ctx, ident, accesscontrol.Options{ReloadCache: false})
|
||||
// Populate permissions from roles
|
||||
permissions, err := s.fetchPermissions(ctx, ident)
|
||||
if err != nil {
|
||||
s.log.FromContext(ctx).Error("Failed to fetch permissions from db", "error", err, "id", ident.ID)
|
||||
return errSyncPermissionsForbidden
|
||||
return err
|
||||
}
|
||||
|
||||
if ident.Permissions == nil {
|
||||
ident.Permissions = make(map[int64]map[string][]string)
|
||||
ident.Permissions = make(map[int64]map[string][]string, 1)
|
||||
}
|
||||
ident.Permissions[ident.OrgID] = accesscontrol.GroupScopesByAction(permissions)
|
||||
grouped := accesscontrol.GroupScopesByAction(permissions)
|
||||
|
||||
// Restrict access to the list of actions
|
||||
actionsLookup := ident.ClientParams.FetchPermissionsParams.ActionsLookup
|
||||
if len(actionsLookup) > 0 {
|
||||
filtered := make(map[string][]string, len(actionsLookup))
|
||||
for _, action := range actionsLookup {
|
||||
if scopes, ok := grouped[action]; ok {
|
||||
filtered[action] = scopes
|
||||
}
|
||||
}
|
||||
grouped = filtered
|
||||
}
|
||||
|
||||
ident.Permissions[ident.OrgID] = grouped
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *RBACSync) fetchPermissions(ctx context.Context, ident *authn.Identity) ([]accesscontrol.Permission, error) {
|
||||
permissions := make([]accesscontrol.Permission, 0, 8)
|
||||
roles := ident.ClientParams.FetchPermissionsParams.Roles
|
||||
if len(roles) > 0 {
|
||||
for _, role := range roles {
|
||||
roleDTO, err := s.ac.GetRoleByName(ctx, ident.GetOrgID(), role)
|
||||
if err != nil && !errors.Is(err, accesscontrol.ErrRoleNotFound) {
|
||||
s.log.FromContext(ctx).Error("Failed to fetch role from db", "error", err, "role", role)
|
||||
return nil, errSyncPermissionsForbidden
|
||||
}
|
||||
permissions = append(permissions, roleDTO.Permissions...)
|
||||
}
|
||||
|
||||
return permissions, nil
|
||||
}
|
||||
|
||||
permissions, err := s.ac.GetUserPermissions(ctx, ident, accesscontrol.Options{ReloadCache: false})
|
||||
if err != nil {
|
||||
s.log.FromContext(ctx).Error("Failed to fetch permissions from db", "error", err, "id", ident.ID)
|
||||
return nil, errSyncPermissionsForbidden
|
||||
}
|
||||
return permissions, nil
|
||||
}
|
||||
|
||||
var fixedCloudRoles = map[org.RoleType]string{
|
||||
org.RoleViewer: accesscontrol.FixedCloudViewerRole,
|
||||
org.RoleEditor: accesscontrol.FixedCloudEditorRole,
|
||||
|
||||
@@ -7,11 +7,11 @@ import (
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-jose/go-jose/v3"
|
||||
"github.com/go-jose/go-jose/v3/jwt"
|
||||
|
||||
authlib "github.com/grafana/authlib/authn"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/services/authn"
|
||||
"github.com/grafana/grafana/pkg/services/login"
|
||||
@@ -24,20 +24,29 @@ var _ authn.Client = new(ExtendedJWT)
|
||||
|
||||
var (
|
||||
acceptedSigningMethods = []string{"RS256", "ES256"}
|
||||
timeNow = time.Now
|
||||
)
|
||||
|
||||
const (
|
||||
rfc9068ShortMediaType = "at+jwt"
|
||||
rfc9068MediaType = "application/at+jwt"
|
||||
rfc9068ShortMediaType = "at+jwt"
|
||||
extJWTAuthenticationHeaderName = "X-Access-Token"
|
||||
extJWTAuthorizationHeaderName = "X-Grafana-Id"
|
||||
)
|
||||
|
||||
func ProvideExtendedJWT(userService user.Service, cfg *setting.Cfg, signingKeys signingkeys.Service) *ExtendedJWT {
|
||||
func ProvideExtendedJWT(userService user.Service, cfg *setting.Cfg,
|
||||
signingKeys signingkeys.Service) *ExtendedJWT {
|
||||
verifier := authlib.NewVerifier[ExtendedJWTClaims](authlib.IDVerifierConfig{
|
||||
SigningKeysURL: cfg.ExtJWTAuth.JWKSUrl,
|
||||
AllowedAudiences: []string{
|
||||
cfg.ExtJWTAuth.ExpectAudience,
|
||||
},
|
||||
})
|
||||
|
||||
return &ExtendedJWT{
|
||||
cfg: cfg,
|
||||
log: log.New(authn.ClientExtendedJWT),
|
||||
userService: userService,
|
||||
signingKeys: signingKeys,
|
||||
verifier: verifier,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,68 +55,97 @@ type ExtendedJWT struct {
|
||||
log log.Logger
|
||||
userService user.Service
|
||||
signingKeys signingkeys.Service
|
||||
verifier authlib.Verifier[ExtendedJWTClaims]
|
||||
}
|
||||
|
||||
type ExtendedJWTClaims struct {
|
||||
jwt.Claims
|
||||
ClientID string `json:"client_id"`
|
||||
Groups []string `json:"groups"`
|
||||
Email string `json:"email"`
|
||||
Name string `json:"name"`
|
||||
Login string `json:"login"`
|
||||
Scopes []string `json:"scope"`
|
||||
Entitlements map[string][]string `json:"entitlements"`
|
||||
// Access policy scopes
|
||||
Scopes []string `json:"scopes"`
|
||||
// Grafana roles
|
||||
Permissions []string `json:"permissions"`
|
||||
// On-behalf-of user
|
||||
DelegatedPermissions []string `json:"delegatedPermissions"`
|
||||
}
|
||||
|
||||
func (s *ExtendedJWT) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identity, error) {
|
||||
jwtToken := s.retrieveToken(r.HTTPRequest)
|
||||
jwtToken := s.retrieveAuthenticationToken(r.HTTPRequest)
|
||||
|
||||
claims, err := s.verifyRFC9068Token(ctx, jwtToken)
|
||||
claims, err := s.verifyRFC9068Token(ctx, jwtToken, rfc9068ShortMediaType)
|
||||
if err != nil {
|
||||
s.log.Error("Failed to verify JWT", "error", err)
|
||||
return nil, errJWTInvalid.Errorf("Failed to verify JWT: %w", err)
|
||||
}
|
||||
|
||||
// user:id:18
|
||||
userID, err := strconv.ParseInt(strings.TrimPrefix(claims.Subject, fmt.Sprintf("%s:id:", authn.NamespaceUser)), 10, 64)
|
||||
idToken := s.retrieveAuthorizationToken(r.HTTPRequest)
|
||||
if idToken != "" {
|
||||
idTokenClaims, err := s.verifyRFC9068Token(ctx, idToken, "jwt")
|
||||
if err != nil {
|
||||
s.log.Error("Failed to verify id token", "error", err)
|
||||
return nil, errJWTInvalid.Errorf("Failed to verify id token: %w", err)
|
||||
}
|
||||
|
||||
return s.authenticateAsUser(idTokenClaims, claims)
|
||||
}
|
||||
|
||||
return s.authenticateAsService(claims)
|
||||
}
|
||||
|
||||
func (s *ExtendedJWT) authenticateAsUser(idTokenClaims,
|
||||
accessTokenClaims *ExtendedJWTClaims) (*authn.Identity, error) {
|
||||
// Only allow access policies to impersonate
|
||||
if !strings.HasPrefix(accessTokenClaims.Subject, fmt.Sprintf("%s:", authn.NamespaceAccessPolicy)) {
|
||||
s.log.Error("Invalid subject", "subject", accessTokenClaims.Subject)
|
||||
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)
|
||||
}
|
||||
|
||||
// FIXME: support multiple organizations
|
||||
defaultOrgID := s.getDefaultOrgID()
|
||||
if r.OrgID != defaultOrgID {
|
||||
s.log.Error("Failed to verify the Organization: OrgID is not the default")
|
||||
return nil, errJWTInvalid.Errorf("Failed to verify the Organization. Only the default org is supported")
|
||||
return &authn.Identity{
|
||||
ID: idTokenClaims.Subject,
|
||||
OrgID: s.getDefaultOrgID(),
|
||||
AuthenticatedBy: login.ExtendedJWTModule,
|
||||
AuthID: accessTokenClaims.Subject,
|
||||
ClientParams: authn.ClientParams{
|
||||
SyncPermissions: true,
|
||||
FetchPermissionsParams: authn.FetchPermissionsParams{
|
||||
ActionsLookup: accessTokenClaims.DelegatedPermissions,
|
||||
},
|
||||
FetchSyncedUser: true,
|
||||
}}, nil
|
||||
}
|
||||
|
||||
func (s *ExtendedJWT) authenticateAsService(claims *ExtendedJWTClaims) (*authn.Identity, error) {
|
||||
if !strings.HasPrefix(claims.Subject, fmt.Sprintf("%s:", authn.NamespaceAccessPolicy)) {
|
||||
s.log.Error("Invalid subject", "subject", claims.Subject)
|
||||
return nil, errJWTInvalid.Errorf("Failed to parse sub: %s", "invalid subject format")
|
||||
}
|
||||
|
||||
signedInUser, err := s.userService.GetSignedInUserWithCacheCtx(ctx, &user.GetSignedInUserQuery{OrgID: defaultOrgID, UserID: userID})
|
||||
if err != nil {
|
||||
s.log.Error("Failed to get user", "error", err)
|
||||
return nil, errJWTInvalid.Errorf("Failed to get user: %w", err)
|
||||
}
|
||||
|
||||
if signedInUser.Permissions == nil {
|
||||
signedInUser.Permissions = make(map[int64]map[string][]string)
|
||||
}
|
||||
|
||||
if len(claims.Entitlements) == 0 {
|
||||
s.log.Error("Entitlements claim is missing")
|
||||
return nil, errJWTInvalid.Errorf("Entitlements claim is missing")
|
||||
}
|
||||
|
||||
signedInUser.Permissions[s.getDefaultOrgID()] = claims.Entitlements
|
||||
|
||||
return authn.IdentityFromSignedInUser(authn.NamespacedID(authn.NamespaceUser, signedInUser.UserID), signedInUser, authn.ClientParams{SyncPermissions: false}, login.ExtendedJWTModule), nil
|
||||
return &authn.Identity{
|
||||
ID: claims.Subject,
|
||||
OrgID: s.getDefaultOrgID(),
|
||||
AuthenticatedBy: login.ExtendedJWTModule,
|
||||
AuthID: claims.Subject,
|
||||
ClientParams: authn.ClientParams{
|
||||
SyncPermissions: true,
|
||||
FetchPermissionsParams: authn.FetchPermissionsParams{
|
||||
Roles: claims.Permissions,
|
||||
},
|
||||
FetchSyncedUser: false,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *ExtendedJWT) Test(ctx context.Context, r *authn.Request) bool {
|
||||
if !s.cfg.ExtendedJWTAuthEnabled {
|
||||
if !s.cfg.ExtJWTAuth.Enabled {
|
||||
return false
|
||||
}
|
||||
|
||||
rawToken := s.retrieveToken(r.HTTPRequest)
|
||||
rawToken := s.retrieveAuthenticationToken(r.HTTPRequest)
|
||||
if rawToken == "" {
|
||||
return false
|
||||
}
|
||||
@@ -122,7 +160,7 @@ func (s *ExtendedJWT) Test(ctx context.Context, r *authn.Request) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
return claims.Issuer == s.cfg.ExtendedJWTExpectIssuer
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *ExtendedJWT) Name() string {
|
||||
@@ -134,16 +172,24 @@ func (s *ExtendedJWT) Priority() uint {
|
||||
return 15
|
||||
}
|
||||
|
||||
// retrieveToken retrieves the JWT token from the request.
|
||||
func (s *ExtendedJWT) retrieveToken(httpRequest *http.Request) string {
|
||||
jwtToken := httpRequest.Header.Get("Authorization")
|
||||
// retrieveAuthenticationToken retrieves the JWT token from the request.
|
||||
func (s *ExtendedJWT) retrieveAuthenticationToken(httpRequest *http.Request) string {
|
||||
jwtToken := httpRequest.Header.Get(extJWTAuthenticationHeaderName)
|
||||
|
||||
// Strip the 'Bearer' prefix if it exists.
|
||||
return strings.TrimPrefix(jwtToken, "Bearer ")
|
||||
}
|
||||
|
||||
// retrieveAuthorizationToken retrieves the JWT token from the request.
|
||||
func (s *ExtendedJWT) retrieveAuthorizationToken(httpRequest *http.Request) string {
|
||||
jwtToken := httpRequest.Header.Get(extJWTAuthorizationHeaderName)
|
||||
|
||||
// Strip the 'Bearer' prefix if it exists.
|
||||
return strings.TrimPrefix(jwtToken, "Bearer ")
|
||||
}
|
||||
|
||||
// verifyRFC9068Token verifies the token against the RFC 9068 specification.
|
||||
func (s *ExtendedJWT) verifyRFC9068Token(ctx context.Context, rawToken string) (*ExtendedJWTClaims, error) {
|
||||
func (s *ExtendedJWT) verifyRFC9068Token(ctx context.Context, rawToken string, typ string) (*ExtendedJWTClaims, error) {
|
||||
parsedToken, err := jwt.ParseSigned(rawToken)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse JWT: %w", err)
|
||||
@@ -161,34 +207,29 @@ func (s *ExtendedJWT) verifyRFC9068Token(ctx context.Context, rawToken string) (
|
||||
}
|
||||
|
||||
jwtType := strings.ToLower(typeHeader.(string))
|
||||
if jwtType != rfc9068ShortMediaType && jwtType != rfc9068MediaType {
|
||||
if !strings.EqualFold(jwtType, typ) {
|
||||
return nil, fmt.Errorf("invalid JWT type: %s", jwtType)
|
||||
}
|
||||
|
||||
if !slices.Contains(acceptedSigningMethods, parsedHeader.Algorithm) {
|
||||
return nil, fmt.Errorf("invalid algorithm: %s. Accepted algorithms: %s", parsedHeader.Algorithm, strings.Join(acceptedSigningMethods, ", "))
|
||||
return nil, fmt.Errorf("invalid algorithm: %s. Accepted algorithms: %s",
|
||||
parsedHeader.Algorithm, strings.Join(acceptedSigningMethods, ", "))
|
||||
}
|
||||
|
||||
var claims ExtendedJWTClaims
|
||||
_, key, err := s.signingKeys.GetOrCreatePrivateKey(ctx,
|
||||
signingkeys.ServerPrivateKeyID, jose.ES256)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get public key: %w", err)
|
||||
keyID := parsedHeader.KeyID
|
||||
if keyID == "" {
|
||||
return nil, fmt.Errorf("missing 'kid' field from the header")
|
||||
}
|
||||
|
||||
err = parsedToken.Claims(key.Public(), &claims)
|
||||
claims, err := s.verifier.Verify(ctx, rawToken)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to verify the signature: %w", err)
|
||||
return nil, fmt.Errorf("failed to verify JWT: %w", err)
|
||||
}
|
||||
|
||||
if claims.Expiry == nil {
|
||||
return nil, fmt.Errorf("missing 'exp' claim")
|
||||
}
|
||||
|
||||
if claims.ID == "" {
|
||||
return nil, fmt.Errorf("missing 'jti' claim")
|
||||
}
|
||||
|
||||
if claims.Subject == "" {
|
||||
return nil, fmt.Errorf("missing 'sub' claim")
|
||||
}
|
||||
@@ -197,29 +238,7 @@ func (s *ExtendedJWT) verifyRFC9068Token(ctx context.Context, rawToken string) (
|
||||
return nil, fmt.Errorf("missing 'iat' claim")
|
||||
}
|
||||
|
||||
err = claims.ValidateWithLeeway(jwt.Expected{
|
||||
Issuer: s.cfg.ExtendedJWTExpectIssuer,
|
||||
Audience: jwt.Audience{s.cfg.ExtendedJWTExpectAudience},
|
||||
Time: timeNow(),
|
||||
}, 0)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to validate JWT: %w", err)
|
||||
}
|
||||
|
||||
if err := s.validateClientIdClaim(ctx, claims); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &claims, nil
|
||||
}
|
||||
|
||||
func (s *ExtendedJWT) validateClientIdClaim(ctx context.Context, claims ExtendedJWTClaims) error {
|
||||
if claims.ClientID == "" {
|
||||
return fmt.Errorf("missing 'client_id' claim")
|
||||
}
|
||||
|
||||
return nil
|
||||
return &claims.Rest, nil
|
||||
}
|
||||
|
||||
func (s *ExtendedJWT) getDefaultOrgID() int64 {
|
||||
|
||||
@@ -11,11 +11,15 @@ import (
|
||||
|
||||
"github.com/go-jose/go-jose/v3"
|
||||
"github.com/go-jose/go-jose/v3/jwt"
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
authlib "github.com/grafana/authlib/authn"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models/roletype"
|
||||
"github.com/grafana/grafana/pkg/models/usertoken"
|
||||
"github.com/grafana/grafana/pkg/services/authn"
|
||||
"github.com/grafana/grafana/pkg/services/login"
|
||||
"github.com/grafana/grafana/pkg/services/signingkeys"
|
||||
@@ -29,28 +33,45 @@ var (
|
||||
validPayload = ExtendedJWTClaims{
|
||||
Claims: jwt.Claims{
|
||||
Issuer: "http://localhost:3000",
|
||||
Subject: "user:id:2",
|
||||
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)),
|
||||
},
|
||||
ClientID: "grafana",
|
||||
Scopes: []string{"profile", "groups"},
|
||||
Entitlements: map[string][]string{
|
||||
"dashboards:create": {
|
||||
"folders:uid:general",
|
||||
},
|
||||
"folders:read": {
|
||||
"folders:uid:general",
|
||||
},
|
||||
"datasources:explore": nil,
|
||||
"datasources.insights:read": {},
|
||||
Scopes: []string{"profile", "groups"},
|
||||
DelegatedPermissions: []string{"dashboards:create", "folders:read", "datasources:explore", "datasources.insights:read"},
|
||||
Permissions: []string{"fixed:folders:reader"},
|
||||
}
|
||||
validIDPayload = ExtendedJWTClaims{
|
||||
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)),
|
||||
},
|
||||
Scopes: []string{"profile", "groups"},
|
||||
}
|
||||
pk, _ = rsa.GenerateKey(rand.Reader, 4096)
|
||||
)
|
||||
|
||||
type mockVerifier struct {
|
||||
Claims []ExtendedJWTClaims
|
||||
Error error
|
||||
counter int
|
||||
}
|
||||
|
||||
func (m *mockVerifier) Verify(ctx context.Context, token string) (*authlib.Claims[ExtendedJWTClaims], error) {
|
||||
m.counter++
|
||||
claims := m.Claims[m.counter-1]
|
||||
return &authlib.Claims[ExtendedJWTClaims]{
|
||||
Claims: &claims.Claims,
|
||||
Rest: claims,
|
||||
}, m.Error
|
||||
}
|
||||
|
||||
func TestExtendedJWT_Test(t *testing.T) {
|
||||
type testCase struct {
|
||||
name string
|
||||
@@ -63,7 +84,9 @@ func TestExtendedJWT_Test(t *testing.T) {
|
||||
{
|
||||
name: "should return false when extended jwt is disabled",
|
||||
cfg: &setting.Cfg{
|
||||
ExtendedJWTAuthEnabled: false,
|
||||
ExtJWTAuth: setting.ExtJWTSettings{
|
||||
Enabled: false,
|
||||
},
|
||||
},
|
||||
authHeaderFunc: func() string { return "eyJ" },
|
||||
want: false,
|
||||
@@ -71,13 +94,13 @@ func TestExtendedJWT_Test(t *testing.T) {
|
||||
{
|
||||
name: "should return true when Authorization header contains Bearer prefix",
|
||||
cfg: nil,
|
||||
authHeaderFunc: func() string { return "Bearer " + generateToken(validPayload, pk, jose.RS256) },
|
||||
authHeaderFunc: func() string { return "Bearer " + generateToken(validPayload, pk, jose.RS256, "at+jwt") },
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "should return true when Authorization header only contains the token",
|
||||
cfg: nil,
|
||||
authHeaderFunc: func() string { return generateToken(validPayload, pk, jose.RS256) },
|
||||
authHeaderFunc: func() string { return generateToken(validPayload, pk, jose.RS256, "at+jwt") },
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
@@ -95,23 +118,25 @@ func TestExtendedJWT_Test(t *testing.T) {
|
||||
{
|
||||
name: "should return false when the issuer does not match the configured issuer",
|
||||
cfg: &setting.Cfg{
|
||||
ExtendedJWTExpectIssuer: "http://localhost:3000",
|
||||
ExtJWTAuth: setting.ExtJWTSettings{
|
||||
ExpectIssuer: "http://localhost:3000",
|
||||
},
|
||||
},
|
||||
authHeaderFunc: func() string {
|
||||
payload := validPayload
|
||||
payload.Issuer = "http://unknown-issuer"
|
||||
return generateToken(payload, pk, jose.RS256)
|
||||
return generateToken(payload, pk, jose.RS256, "at+jwt")
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
env := setupTestCtx(t, tc.cfg)
|
||||
env := setupTestCtx(tc.cfg)
|
||||
|
||||
validHTTPReq := &http.Request{
|
||||
Header: map[string][]string{
|
||||
"Authorization": {tc.authHeaderFunc()},
|
||||
"X-Access-Token": {tc.authHeaderFunc()},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -129,16 +154,39 @@ func TestExtendedJWT_Authenticate(t *testing.T) {
|
||||
type testCase struct {
|
||||
name string
|
||||
payload ExtendedJWTClaims
|
||||
idPayload *ExtendedJWTClaims
|
||||
orgID int64
|
||||
want *authn.Identity
|
||||
initTestEnv func(env *testEnv)
|
||||
wantErr bool
|
||||
wantErr error
|
||||
}
|
||||
testCases := []testCase{
|
||||
{
|
||||
name: "successful authentication",
|
||||
name: "successful authentication as service",
|
||||
payload: validPayload,
|
||||
orgID: 1,
|
||||
want: &authn.Identity{OrgID: 1, OrgName: "",
|
||||
OrgRoles: map[int64]roletype.RoleType(nil),
|
||||
ID: "access-policy:this-uid", Login: "", Name: "", Email: "",
|
||||
IsGrafanaAdmin: (*bool)(nil), AuthenticatedBy: "extendedjwt",
|
||||
AuthID: "access-policy:this-uid", IsDisabled: false, HelpFlags1: 0x0,
|
||||
LastSeenAt: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC),
|
||||
Teams: []int64(nil), Groups: []string(nil),
|
||||
OAuthToken: (*oauth2.Token)(nil), SessionToken: (*usertoken.UserToken)(nil),
|
||||
ClientParams: authn.ClientParams{SyncUser: false,
|
||||
AllowSignUp: false, EnableUser: false, FetchSyncedUser: false,
|
||||
SyncTeams: false, SyncOrgRoles: false, CacheAuthProxyKey: "",
|
||||
LookUpParams: login.UserLookupParams{UserID: (*int64)(nil),
|
||||
Email: (*string)(nil), Login: (*string)(nil)}, SyncPermissions: true,
|
||||
FetchPermissionsParams: authn.FetchPermissionsParams{ActionsLookup: []string(nil), Roles: []string{"fixed:folders:reader"}}},
|
||||
Permissions: map[int64]map[string][]string(nil), IDToken: ""},
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "successful authentication as user",
|
||||
payload: validPayload,
|
||||
idPayload: &validIDPayload,
|
||||
orgID: 1,
|
||||
initTestEnv: func(env *testEnv) {
|
||||
env.userSvc.ExpectedSignedInUser = &user.SignedInUser{
|
||||
UserID: 2,
|
||||
@@ -149,50 +197,26 @@ func TestExtendedJWT_Authenticate(t *testing.T) {
|
||||
Login: "johndoe",
|
||||
}
|
||||
},
|
||||
want: &authn.Identity{
|
||||
OrgID: 1,
|
||||
OrgName: "",
|
||||
OrgRoles: map[int64]roletype.RoleType{1: roletype.RoleAdmin},
|
||||
ID: "user:2",
|
||||
Login: "johndoe",
|
||||
Name: "John Doe",
|
||||
Email: "johndoe@grafana.com",
|
||||
IsGrafanaAdmin: boolPtr(false),
|
||||
AuthenticatedBy: login.ExtendedJWTModule,
|
||||
AuthID: "",
|
||||
IsDisabled: false,
|
||||
HelpFlags1: 0,
|
||||
Permissions: map[int64]map[string][]string{
|
||||
1: {
|
||||
"dashboards:create": {
|
||||
"folders:uid:general",
|
||||
},
|
||||
"folders:read": {
|
||||
"folders:uid:general",
|
||||
},
|
||||
"datasources:explore": nil,
|
||||
"datasources.insights:read": []string{},
|
||||
},
|
||||
},
|
||||
ClientParams: authn.ClientParams{
|
||||
SyncUser: false,
|
||||
AllowSignUp: false,
|
||||
FetchSyncedUser: false,
|
||||
EnableUser: false,
|
||||
SyncOrgRoles: false,
|
||||
SyncTeams: false,
|
||||
SyncPermissions: false,
|
||||
LookUpParams: login.UserLookupParams{
|
||||
UserID: nil,
|
||||
Email: nil,
|
||||
Login: nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
want: &authn.Identity{OrgID: 1, OrgName: "",
|
||||
OrgRoles: map[int64]roletype.RoleType(nil), ID: "user:2",
|
||||
Login: "", Name: "", Email: "",
|
||||
IsGrafanaAdmin: (*bool)(nil), AuthenticatedBy: "extendedjwt",
|
||||
AuthID: "access-policy:this-uid", IsDisabled: false, HelpFlags1: 0x0,
|
||||
LastSeenAt: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC),
|
||||
Teams: []int64(nil), Groups: []string(nil),
|
||||
OAuthToken: (*oauth2.Token)(nil), SessionToken: (*usertoken.UserToken)(nil),
|
||||
ClientParams: authn.ClientParams{SyncUser: false, AllowSignUp: false,
|
||||
EnableUser: false, FetchSyncedUser: true, SyncTeams: false,
|
||||
SyncOrgRoles: false, CacheAuthProxyKey: "",
|
||||
LookUpParams: login.UserLookupParams{UserID: (*int64)(nil), Email: (*string)(nil), Login: (*string)(nil)},
|
||||
SyncPermissions: true,
|
||||
FetchPermissionsParams: authn.FetchPermissionsParams{ActionsLookup: []string{"dashboards:create",
|
||||
"folders:read", "datasources:explore", "datasources.insights:read"},
|
||||
Roles: []string(nil)}}, Permissions: map[int64]map[string][]string(nil), IDToken: ""},
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "should return error when the user cannot be parsed from the Subject claim",
|
||||
name: "should return error when the subject is not an access-policy",
|
||||
payload: ExtendedJWTClaims{
|
||||
Claims: jwt.Claims{
|
||||
Issuer: "http://localhost:3000",
|
||||
@@ -202,94 +226,40 @@ func TestExtendedJWT_Authenticate(t *testing.T) {
|
||||
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)),
|
||||
},
|
||||
ClientID: "grafana",
|
||||
Scopes: []string{"profile", "groups"},
|
||||
Permissions: []string{"fixed:folders:reader"},
|
||||
},
|
||||
orgID: 1,
|
||||
want: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "should return error when the OrgId is not the ID of the default org",
|
||||
payload: ExtendedJWTClaims{
|
||||
Claims: jwt.Claims{
|
||||
Issuer: "http://localhost:3000",
|
||||
Subject: "user:id: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)),
|
||||
},
|
||||
ClientID: "grafana",
|
||||
Scopes: []string{"profile", "groups"},
|
||||
},
|
||||
orgID: 0,
|
||||
want: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "should return error when the user cannot be found",
|
||||
payload: ExtendedJWTClaims{
|
||||
Claims: jwt.Claims{
|
||||
Issuer: "http://localhost:3000",
|
||||
Subject: "user:id: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)),
|
||||
},
|
||||
ClientID: "grafana",
|
||||
Scopes: []string{"profile", "groups"},
|
||||
},
|
||||
orgID: 1,
|
||||
want: nil,
|
||||
initTestEnv: func(env *testEnv) {
|
||||
env.userSvc.ExpectedError = user.ErrUserNotFound
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "should return error when entitlements claim is missing",
|
||||
payload: ExtendedJWTClaims{
|
||||
Claims: jwt.Claims{
|
||||
Issuer: "http://localhost:3000",
|
||||
Subject: "user:id: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)),
|
||||
},
|
||||
ClientID: "grafana",
|
||||
Scopes: []string{"profile", "groups"},
|
||||
},
|
||||
orgID: 1,
|
||||
want: nil,
|
||||
wantErr: true,
|
||||
wantErr: errJWTInvalid.Errorf("Failed to parse sub: %s", "invalid subject format"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
env := setupTestCtx(t, nil)
|
||||
env := setupTestCtx(nil)
|
||||
if tc.initTestEnv != nil {
|
||||
tc.initTestEnv(env)
|
||||
}
|
||||
|
||||
validHTTPReq := &http.Request{
|
||||
Header: map[string][]string{
|
||||
"Authorization": {generateToken(tc.payload, pk, jose.RS256)},
|
||||
"X-Access-Token": {generateToken(tc.payload, pk, jose.RS256, "at+jwt")},
|
||||
},
|
||||
}
|
||||
|
||||
mockTimeNow(time.Date(2023, 5, 2, 0, 1, 0, 0, time.UTC))
|
||||
env.s.verifier = &mockVerifier{Claims: []ExtendedJWTClaims{tc.payload}}
|
||||
if tc.idPayload != nil {
|
||||
env.s.verifier = &mockVerifier{Claims: []ExtendedJWTClaims{tc.payload, *tc.idPayload}}
|
||||
validHTTPReq.Header.Add(extJWTAuthorizationHeaderName, generateToken(*tc.idPayload, pk, jose.RS256, "jwt"))
|
||||
}
|
||||
|
||||
id, err := env.s.Authenticate(context.Background(), &authn.Request{
|
||||
OrgID: tc.orgID,
|
||||
HTTPRequest: validHTTPReq,
|
||||
Resp: nil,
|
||||
})
|
||||
if tc.wantErr {
|
||||
require.Error(t, err)
|
||||
if tc.wantErr != nil {
|
||||
require.ErrorIs(t, err, tc.wantErr)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
assert.EqualValues(t, tc.want, id, fmt.Sprintf("%+v", id))
|
||||
@@ -304,6 +274,7 @@ func TestVerifyRFC9068TokenFailureScenarios(t *testing.T) {
|
||||
name string
|
||||
payload ExtendedJWTClaims
|
||||
alg jose.SignatureAlgorithm
|
||||
typ string
|
||||
}
|
||||
|
||||
testCases := []testCase{
|
||||
@@ -311,14 +282,13 @@ func TestVerifyRFC9068TokenFailureScenarios(t *testing.T) {
|
||||
name: "missing iss",
|
||||
payload: ExtendedJWTClaims{
|
||||
Claims: jwt.Claims{
|
||||
Subject: "user:id:2",
|
||||
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)),
|
||||
},
|
||||
ClientID: "grafana",
|
||||
Scopes: []string{"profile", "groups"},
|
||||
Scopes: []string{"profile", "groups"},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -326,13 +296,12 @@ func TestVerifyRFC9068TokenFailureScenarios(t *testing.T) {
|
||||
payload: ExtendedJWTClaims{
|
||||
Claims: jwt.Claims{
|
||||
Issuer: "http://localhost:3000",
|
||||
Subject: "user:id:2",
|
||||
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)),
|
||||
},
|
||||
ClientID: "grafana",
|
||||
Scopes: []string{"profile", "groups"},
|
||||
Scopes: []string{"profile", "groups"},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -340,14 +309,13 @@ func TestVerifyRFC9068TokenFailureScenarios(t *testing.T) {
|
||||
payload: ExtendedJWTClaims{
|
||||
Claims: jwt.Claims{
|
||||
Issuer: "http://localhost:3000",
|
||||
Subject: "user:id:2",
|
||||
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)),
|
||||
},
|
||||
ClientID: "grafana",
|
||||
Scopes: []string{"profile", "groups"},
|
||||
Scopes: []string{"profile", "groups"},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -355,13 +323,12 @@ func TestVerifyRFC9068TokenFailureScenarios(t *testing.T) {
|
||||
payload: ExtendedJWTClaims{
|
||||
Claims: jwt.Claims{
|
||||
Issuer: "http://localhost:3000",
|
||||
Subject: "user:id:2",
|
||||
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)),
|
||||
},
|
||||
ClientID: "grafana",
|
||||
Scopes: []string{"profile", "groups"},
|
||||
Scopes: []string{"profile", "groups"},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -369,16 +336,30 @@ func TestVerifyRFC9068TokenFailureScenarios(t *testing.T) {
|
||||
payload: ExtendedJWTClaims{
|
||||
Claims: jwt.Claims{
|
||||
Issuer: "http://localhost:3000",
|
||||
Subject: "user:id:2",
|
||||
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)),
|
||||
},
|
||||
ClientID: "grafana",
|
||||
Scopes: []string{"profile", "groups"},
|
||||
Scopes: []string{"profile", "groups"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "wrong typ",
|
||||
payload: ExtendedJWTClaims{
|
||||
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)),
|
||||
},
|
||||
Scopes: []string{"profile", "groups"},
|
||||
},
|
||||
typ: "jwt",
|
||||
},
|
||||
{
|
||||
name: "missing sub",
|
||||
payload: ExtendedJWTClaims{
|
||||
@@ -389,21 +370,6 @@ func TestVerifyRFC9068TokenFailureScenarios(t *testing.T) {
|
||||
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)),
|
||||
},
|
||||
ClientID: "grafana",
|
||||
Scopes: []string{"profile", "groups"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "missing client_id",
|
||||
payload: ExtendedJWTClaims{
|
||||
Claims: jwt.Claims{
|
||||
Issuer: "http://localhost:3000",
|
||||
Subject: "user:id: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)),
|
||||
},
|
||||
Scopes: []string{"profile", "groups"},
|
||||
},
|
||||
},
|
||||
@@ -412,13 +378,12 @@ func TestVerifyRFC9068TokenFailureScenarios(t *testing.T) {
|
||||
payload: ExtendedJWTClaims{
|
||||
Claims: jwt.Claims{
|
||||
Issuer: "http://localhost:3000",
|
||||
Subject: "user:id:2",
|
||||
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)),
|
||||
},
|
||||
ClientID: "grafana",
|
||||
Scopes: []string{"profile", "groups"},
|
||||
Scopes: []string{"profile", "groups"},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -426,28 +391,13 @@ func TestVerifyRFC9068TokenFailureScenarios(t *testing.T) {
|
||||
payload: ExtendedJWTClaims{
|
||||
Claims: jwt.Claims{
|
||||
Issuer: "http://localhost:3000",
|
||||
Subject: "user:id:2",
|
||||
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)),
|
||||
},
|
||||
ClientID: "grafana",
|
||||
Scopes: []string{"profile", "groups"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "missing jti",
|
||||
payload: ExtendedJWTClaims{
|
||||
Claims: jwt.Claims{
|
||||
Issuer: "http://localhost:3000",
|
||||
Subject: "user:id:2",
|
||||
Audience: jwt.Audience{"http://localhost:3000"},
|
||||
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)),
|
||||
},
|
||||
ClientID: "grafana",
|
||||
Scopes: []string{"profile", "groups"},
|
||||
Scopes: []string{"profile", "groups"},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -455,40 +405,40 @@ func TestVerifyRFC9068TokenFailureScenarios(t *testing.T) {
|
||||
payload: ExtendedJWTClaims{
|
||||
Claims: jwt.Claims{
|
||||
Issuer: "http://localhost:3000",
|
||||
Subject: "user:id:2",
|
||||
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)),
|
||||
},
|
||||
ClientID: "grafana",
|
||||
Scopes: []string{"profile", "groups"},
|
||||
Scopes: []string{"profile", "groups"},
|
||||
},
|
||||
alg: jose.RS384,
|
||||
},
|
||||
}
|
||||
|
||||
env := setupTestCtx(t, nil)
|
||||
mockTimeNow(time.Date(2023, 5, 2, 0, 1, 0, 0, time.UTC))
|
||||
env := setupTestCtx(nil)
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if tc.alg == "" {
|
||||
tc.alg = jose.RS256
|
||||
}
|
||||
tokenToTest := generateToken(tc.payload, pk, tc.alg)
|
||||
_, err := env.s.verifyRFC9068Token(context.Background(), tokenToTest)
|
||||
tokenToTest := generateToken(tc.payload, pk, tc.alg, "at+jwt")
|
||||
_, err := env.s.verifyRFC9068Token(context.Background(), tokenToTest, rfc9068ShortMediaType)
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func setupTestCtx(t *testing.T, cfg *setting.Cfg) *testEnv {
|
||||
func setupTestCtx(cfg *setting.Cfg) *testEnv {
|
||||
if cfg == nil {
|
||||
cfg = &setting.Cfg{
|
||||
ExtendedJWTAuthEnabled: true,
|
||||
ExtendedJWTExpectIssuer: "http://localhost:3000",
|
||||
ExtendedJWTExpectAudience: "http://localhost:3000",
|
||||
ExtJWTAuth: setting.ExtJWTSettings{
|
||||
Enabled: true,
|
||||
ExpectIssuer: "http://localhost:3000",
|
||||
ExpectAudience: "http://localhost:3000",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -512,18 +462,13 @@ type testEnv struct {
|
||||
s *ExtendedJWT
|
||||
}
|
||||
|
||||
func generateToken(payload ExtendedJWTClaims, signingKey any, alg jose.SignatureAlgorithm) string {
|
||||
func generateToken(payload ExtendedJWTClaims, signingKey any, alg jose.SignatureAlgorithm, typ string) string {
|
||||
signer, _ := jose.NewSigner(jose.SigningKey{Algorithm: alg, Key: signingKey}, &jose.SignerOptions{
|
||||
ExtraHeaders: map[jose.HeaderKey]any{
|
||||
jose.HeaderType: "at+jwt",
|
||||
jose.HeaderType: typ,
|
||||
"kid": "default",
|
||||
}})
|
||||
|
||||
result, _ := jwt.Signed(signer).Claims(payload).CompactSerialize()
|
||||
return result
|
||||
}
|
||||
|
||||
func mockTimeNow(timeSeed time.Time) {
|
||||
timeNow = func() time.Time {
|
||||
return timeSeed
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ const (
|
||||
NamespaceServiceAccount = identity.NamespaceServiceAccount
|
||||
NamespaceAnonymous = identity.NamespaceAnonymous
|
||||
NamespaceRenderService = identity.NamespaceRenderService
|
||||
NamespaceAccessPolicy = identity.NamespaceAccessPolicy
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -230,6 +231,7 @@ func (i *Identity) SignedInUser() *user.SignedInUser {
|
||||
Teams: i.Teams,
|
||||
Permissions: i.Permissions,
|
||||
IDToken: i.IDToken,
|
||||
NamespacedID: i.ID,
|
||||
}
|
||||
|
||||
if namespace == NamespaceAPIKey {
|
||||
|
||||
Reference in New Issue
Block a user