[authn]: add GetIDClaims() to Requester (#91387)

* authn: add GetIDClaims() to Requester

Co-Authored-By: Gabriel MABILLE <gamab@users.noreply.github.com>

* authn: update StaticRequester

Co-Authored-By: Gabriel MABILLE <gamab@users.noreply.github.com>

* update auth/idtest/mock

Co-Authored-By: Gabriel MABILLE <gamab@users.noreply.github.com>

* Fix test

Co-authored-by: Claudiu Dragalina-Paraipan <claudiu.dragalina@grafana.com>

---------

Co-authored-by: Gabriel MABILLE <gamab@users.noreply.github.com>
Co-authored-by: gamab <gabriel.mabille@grafana.com>
This commit is contained in:
Claudiu Dragalina-Paraipan
2024-08-02 12:36:02 +03:00
committed by GitHub
co-authored by Gabriel MABILLE Claudiu Dragalina-Paraipan gamab
parent a940bb87be
commit e2435f92f1
10 changed files with 112 additions and 32 deletions
+1 -1
View File
@@ -10,7 +10,7 @@ import (
type IDService interface {
// SignIdentity signs a id token for provided identity that can be forwarded to plugins and external services
SignIdentity(ctx context.Context, identity identity.Requester) (string, error)
SignIdentity(ctx context.Context, id identity.Requester) (string, *authnlib.Claims[authnlib.IDTokenClaims], error)
// RemoveIDToken removes any locally stored id tokens for key
RemoveIDToken(ctx context.Context, identity identity.Requester) error
+38 -19
View File
@@ -58,21 +58,30 @@ type Service struct {
nsMapper request.NamespaceMapper
}
func (s *Service) SignIdentity(ctx context.Context, id identity.Requester) (string, error) {
func (s *Service) SignIdentity(ctx context.Context, id identity.Requester) (string, *authnlib.Claims[authnlib.IDTokenClaims], error) {
defer func(t time.Time) {
s.metrics.tokenSigningDurationHistogram.Observe(time.Since(t).Seconds())
}(time.Now())
cacheKey := prefixCacheKey(id.GetCacheKey())
result, err, _ := s.si.Do(cacheKey, func() (interface{}, error) {
type resultType struct {
token string
idClaims *auth.IDClaims
}
result, err, _ := s.si.Do(cacheKey, func() (any, error) {
namespace, identifier := id.GetTypedID()
cachedToken, err := s.cache.Get(ctx, cacheKey)
if err == nil {
s.metrics.tokenSigningFromCacheCounter.Inc()
s.logger.FromContext(ctx).Debug("Cached token found", "namespace", namespace, "id", identifier)
return string(cachedToken), nil
tokenClaims, err := s.extractTokenClaims(string(cachedToken))
if err != nil {
return resultType{}, err
}
return resultType{token: string(cachedToken), idClaims: tokenClaims}, nil
}
s.metrics.tokenSigningCounter.Inc()
@@ -104,21 +113,12 @@ func (s *Service) SignIdentity(ctx context.Context, id identity.Requester) (stri
token, err := s.signer.SignIDToken(ctx, claims)
if err != nil {
s.metrics.failedTokenSigningCounter.Inc()
return "", err
return resultType{}, nil
}
parsed, err := jwt.ParseSigned(token)
extracted, err := s.extractTokenClaims(token)
if err != nil {
s.metrics.failedTokenSigningCounter.Inc()
return "", err
}
extracted := auth.IDClaims{}
// We don't need to verify the signature here, we are only interested in checking
// when the token expires.
if err := parsed.UnsafeClaimsWithoutVerification(&extracted); err != nil {
s.metrics.failedTokenSigningCounter.Inc()
return "", err
return resultType{}, err
}
expires := time.Until(extracted.Expiry.Time())
@@ -126,14 +126,14 @@ func (s *Service) SignIdentity(ctx context.Context, id identity.Requester) (stri
s.logger.FromContext(ctx).Error("Failed to add id token to cache", "error", err)
}
return token, nil
return resultType{token: token, idClaims: claims}, nil
})
if err != nil {
return "", err
return "", nil, err
}
return result.(string), nil
return result.(resultType).token, result.(resultType).idClaims, nil
}
func (s *Service) RemoveIDToken(ctx context.Context, id identity.Requester) error {
@@ -142,7 +142,7 @@ func (s *Service) RemoveIDToken(ctx context.Context, id identity.Requester) erro
func (s *Service) hook(ctx context.Context, identity *authn.Identity, _ *authn.Request) error {
// FIXME(kalleep): we should probably lazy load this
token, err := s.SignIdentity(ctx, identity)
token, claims, err := s.SignIdentity(ctx, identity)
if err != nil {
if shouldLogErr(err) {
namespace, id := identity.GetTypedID()
@@ -153,9 +153,28 @@ func (s *Service) hook(ctx context.Context, identity *authn.Identity, _ *authn.R
}
identity.IDToken = token
identity.IDTokenClaims = claims
return nil
}
func (s *Service) extractTokenClaims(token string) (*authnlib.Claims[authnlib.IDTokenClaims], error) {
parsed, err := jwt.ParseSigned(token)
if err != nil {
s.metrics.failedTokenSigningCounter.Inc()
return nil, err
}
extracted := authnlib.Claims[authnlib.IDTokenClaims]{}
// We don't need to verify the signature here, we are only interested in checking
// when the token expires.
if err := parsed.UnsafeClaimsWithoutVerification(&extracted); err != nil {
s.metrics.failedTokenSigningCounter.Inc()
return nil, err
}
return &extracted, nil
}
func getAudience(orgID int64) jwt.Audience {
return jwt.Audience{fmt.Sprintf("org:%d", orgID)}
}
+20 -2
View File
@@ -70,7 +70,7 @@ func TestService_SignIdentity(t *testing.T) {
featuremgmt.WithFeatures(featuremgmt.FlagIdForwarding),
&authntest.FakeService{}, nil,
)
token, err := s.SignIdentity(context.Background(), &authn.Identity{ID: identity.MustParseTypedID("user:1")})
token, _, err := s.SignIdentity(context.Background(), &authn.Identity{ID: identity.MustParseTypedID("user:1")})
require.NoError(t, err)
require.NotEmpty(t, token)
})
@@ -81,7 +81,7 @@ func TestService_SignIdentity(t *testing.T) {
featuremgmt.WithFeatures(featuremgmt.FlagIdForwarding),
&authntest.FakeService{}, nil,
)
token, err := s.SignIdentity(context.Background(), &authn.Identity{
token, _, err := s.SignIdentity(context.Background(), &authn.Identity{
ID: identity.MustParseTypedID("user:1"),
AuthenticatedBy: login.AzureADAuthModule,
Login: "U1",
@@ -97,4 +97,22 @@ func TestService_SignIdentity(t *testing.T) {
assert.Equal(t, "U1", claims.Rest.Username)
assert.Equal(t, "user:edpu3nnt61se8e", claims.Rest.UID)
})
t.Run("should sign identity with authenticated by if user is externally authenticated", func(t *testing.T) {
s := ProvideService(
setting.NewCfg(), signer, remotecache.NewFakeCacheStorage(),
featuremgmt.WithFeatures(featuremgmt.FlagIdForwarding),
&authntest.FakeService{}, nil,
)
_, gotClaims, err := s.SignIdentity(context.Background(), &authn.Identity{
ID: identity.MustParseTypedID("user:1"),
AuthenticatedBy: login.AzureADAuthModule,
Login: "U1",
UID: identity.NewTypedIDString(identity.TypeUser, "edpu3nnt61se8e")})
require.NoError(t, err)
assert.Equal(t, login.AzureADAuthModule, gotClaims.Rest.AuthenticatedBy)
assert.Equal(t, "U1", gotClaims.Rest.Username)
assert.Equal(t, "user:edpu3nnt61se8e", gotClaims.Rest.UID)
})
}
+5 -3
View File
@@ -3,6 +3,8 @@ package idtest
import (
"context"
authnlib "github.com/grafana/authlib/authn"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/services/auth"
)
@@ -10,15 +12,15 @@ import (
var _ auth.IDService = (*MockService)(nil)
type MockService struct {
SignIdentityFn func(ctx context.Context, identity identity.Requester) (string, error)
SignIdentityFn func(ctx context.Context, identity identity.Requester) (string, *authnlib.Claims[authnlib.IDTokenClaims], error)
RemoveIDTokenFn func(ctx context.Context, identity identity.Requester) error
}
func (m *MockService) SignIdentity(ctx context.Context, identity identity.Requester) (string, error) {
func (m *MockService) SignIdentity(ctx context.Context, identity identity.Requester) (string, *authnlib.Claims[authnlib.IDTokenClaims], error) {
if m.SignIdentityFn != nil {
return m.SignIdentityFn(ctx, identity)
}
return "", nil
return "", nil, nil
}
func (m *MockService) RemoveIDToken(ctx context.Context, identity identity.Requester) error {
+7 -1
View File
@@ -4,6 +4,7 @@ import (
"fmt"
"time"
"github.com/grafana/authlib/authn"
"golang.org/x/oauth2"
"github.com/grafana/grafana/pkg/apimachinery/identity"
@@ -69,7 +70,8 @@ type Identity struct {
Permissions map[int64]map[string][]string
// IDToken is a signed token representing the identity that can be forwarded to plugins and external services.
// Will only be set when featuremgmt.FlagIdForwarding is enabled.
IDToken string
IDToken string
IDTokenClaims *authn.Claims[authn.IDTokenClaims]
}
// GetRawIdentifier implements Requester.
@@ -156,6 +158,10 @@ func (i *Identity) GetIDToken() string {
return i.IDToken
}
func (i *Identity) GetIDClaims() *authn.Claims[authn.IDTokenClaims] {
return i.IDTokenClaims
}
func (i *Identity) GetIsGrafanaAdmin() bool {
return i.IsGrafanaAdmin != nil && *i.IsGrafanaAdmin
}
+9 -1
View File
@@ -5,6 +5,8 @@ import (
"strconv"
"time"
authnlib "github.com/grafana/authlib/authn"
"github.com/grafana/grafana/pkg/apimachinery/identity"
)
@@ -40,9 +42,11 @@ type SignedInUser struct {
Teams []int64
// Permissions grouped by orgID and actions
Permissions map[int64]map[string][]string `json:"-"`
// IDToken is a signed token representing the identity that can be forwarded to plugins and external services.
// Will only be set when featuremgmt.FlagIdForwarding is enabled.
IDToken string `json:"-" xorm:"-"`
IDToken string `json:"-" xorm:"-"`
IDTokenClaims *authnlib.Claims[authnlib.IDTokenClaims] `json:"-" xorm:"-"`
// When other settings are not deterministic, this value is used
FallbackType identity.IdentityType
@@ -309,3 +313,7 @@ func (u *SignedInUser) GetDisplayName() string {
func (u *SignedInUser) GetIDToken() string {
return u.IDToken
}
func (u *SignedInUser) GetIDClaims() *authnlib.Claims[authnlib.IDTokenClaims] {
return u.IDTokenClaims
}