2023-09-27 17:36:23 +08:00
|
|
|
package idimpl
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
2024-01-17 16:52:05 +08:00
|
|
|
"errors"
|
2023-09-27 17:36:23 +08:00
|
|
|
"fmt"
|
|
|
|
|
"time"
|
|
|
|
|
|
2025-09-15 18:45:15 +08:00
|
|
|
jose "github.com/go-jose/go-jose/v4"
|
|
|
|
|
"github.com/go-jose/go-jose/v4/jwt"
|
2023-10-13 20:32:53 +08:00
|
|
|
"github.com/prometheus/client_golang/prometheus"
|
2025-07-10 21:41:00 +08:00
|
|
|
"go.opentelemetry.io/otel/trace"
|
2023-10-13 20:32:53 +08:00
|
|
|
"golang.org/x/sync/singleflight"
|
|
|
|
|
|
2025-01-21 17:06:55 +08:00
|
|
|
authnlib "github.com/grafana/authlib/authn"
|
|
|
|
|
claims "github.com/grafana/authlib/types"
|
|
|
|
|
|
2024-06-13 12:11:35 +08:00
|
|
|
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
2023-09-27 17:36:23 +08:00
|
|
|
"github.com/grafana/grafana/pkg/infra/log"
|
|
|
|
|
"github.com/grafana/grafana/pkg/infra/remotecache"
|
2024-05-03 09:55:28 +08:00
|
|
|
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
|
2023-09-27 17:36:23 +08:00
|
|
|
"github.com/grafana/grafana/pkg/services/auth"
|
2023-09-28 15:22:05 +08:00
|
|
|
"github.com/grafana/grafana/pkg/services/authn"
|
2023-09-27 17:36:23 +08:00
|
|
|
"github.com/grafana/grafana/pkg/setting"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
cachePrefix = "id-token"
|
2024-01-24 20:56:44 +08:00
|
|
|
tokenTTL = 10 * time.Minute
|
|
|
|
|
cacheLeeway = 30 * time.Second
|
2023-09-27 17:36:23 +08:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
var _ auth.IDService = (*Service)(nil)
|
|
|
|
|
|
2023-09-28 15:22:05 +08:00
|
|
|
func ProvideService(
|
2024-08-21 21:30:17 +08:00
|
|
|
cfg *setting.Cfg, signer auth.IDSigner,
|
2025-07-10 21:41:00 +08:00
|
|
|
cache remotecache.CacheStorage, authnService authn.Service,
|
|
|
|
|
reg prometheus.Registerer, tracer trace.Tracer,
|
2023-09-28 15:22:05 +08:00
|
|
|
) *Service {
|
2024-01-17 16:52:05 +08:00
|
|
|
s := &Service{
|
|
|
|
|
cfg: cfg, logger: log.New("id-service"),
|
|
|
|
|
signer: signer, cache: cache,
|
2024-05-03 09:55:28 +08:00
|
|
|
metrics: newMetrics(reg),
|
2024-11-04 16:33:03 +08:00
|
|
|
nsMapper: request.GetNamespaceMapper(cfg),
|
2025-07-10 21:41:00 +08:00
|
|
|
tracer: tracer,
|
2024-01-17 16:52:05 +08:00
|
|
|
}
|
2023-09-28 15:22:05 +08:00
|
|
|
|
2025-07-10 21:41:00 +08:00
|
|
|
authnService.RegisterPostAuthHook(s.SyncIDToken, 140)
|
2023-09-28 15:22:05 +08:00
|
|
|
|
|
|
|
|
return s
|
2023-09-27 17:36:23 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type Service struct {
|
2024-05-03 09:55:28 +08:00
|
|
|
cfg *setting.Cfg
|
|
|
|
|
logger log.Logger
|
|
|
|
|
signer auth.IDSigner
|
|
|
|
|
cache remotecache.CacheStorage
|
|
|
|
|
si singleflight.Group
|
|
|
|
|
metrics *metrics
|
2025-07-10 21:41:00 +08:00
|
|
|
tracer trace.Tracer
|
2024-05-03 09:55:28 +08:00
|
|
|
nsMapper request.NamespaceMapper
|
2023-09-27 17:36:23 +08:00
|
|
|
}
|
|
|
|
|
|
2024-08-02 17:36:02 +08:00
|
|
|
func (s *Service) SignIdentity(ctx context.Context, id identity.Requester) (string, *authnlib.Claims[authnlib.IDTokenClaims], error) {
|
2025-07-10 21:41:00 +08:00
|
|
|
ctx, span := s.tracer.Start(ctx, "user.sync.SignIdentity")
|
|
|
|
|
defer span.End()
|
|
|
|
|
|
2023-10-05 15:17:40 +08:00
|
|
|
defer func(t time.Time) {
|
|
|
|
|
s.metrics.tokenSigningDurationHistogram.Observe(time.Since(t).Seconds())
|
|
|
|
|
}(time.Now())
|
|
|
|
|
|
2025-02-13 21:10:58 +08:00
|
|
|
cacheKey := getCacheKey(id)
|
2023-09-27 17:36:23 +08:00
|
|
|
|
2024-08-02 17:36:02 +08:00
|
|
|
type resultType struct {
|
|
|
|
|
token string
|
|
|
|
|
idClaims *auth.IDClaims
|
|
|
|
|
}
|
|
|
|
|
result, err, _ := s.si.Do(cacheKey, func() (any, error) {
|
2023-10-13 20:32:53 +08:00
|
|
|
cachedToken, err := s.cache.Get(ctx, cacheKey)
|
|
|
|
|
if err == nil {
|
|
|
|
|
s.metrics.tokenSigningFromCacheCounter.Inc()
|
2024-08-09 23:20:24 +08:00
|
|
|
s.logger.FromContext(ctx).Debug("Cached token found", "id", id.GetID())
|
2024-08-02 17:36:02 +08:00
|
|
|
|
|
|
|
|
tokenClaims, err := s.extractTokenClaims(string(cachedToken))
|
|
|
|
|
if err != nil {
|
|
|
|
|
return resultType{}, err
|
|
|
|
|
}
|
|
|
|
|
return resultType{token: string(cachedToken), idClaims: tokenClaims}, nil
|
2023-10-13 20:32:53 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
s.metrics.tokenSigningCounter.Inc()
|
2024-08-09 23:20:24 +08:00
|
|
|
s.logger.FromContext(ctx).Debug("Sign new id token", "id", id.GetID())
|
2023-10-13 20:32:53 +08:00
|
|
|
|
|
|
|
|
now := time.Now()
|
2024-08-29 05:49:41 +08:00
|
|
|
idClaims := &auth.IDClaims{
|
2024-12-03 22:11:17 +08:00
|
|
|
Claims: jwt.Claims{
|
2023-10-13 20:32:53 +08:00
|
|
|
Issuer: s.cfg.AppURL,
|
|
|
|
|
Audience: getAudience(id.GetOrgID()),
|
2024-08-13 16:18:28 +08:00
|
|
|
Subject: id.GetID(),
|
2023-10-13 20:32:53 +08:00
|
|
|
Expiry: jwt.NewNumericDate(now.Add(tokenTTL)),
|
|
|
|
|
IssuedAt: jwt.NewNumericDate(now),
|
|
|
|
|
},
|
2024-05-07 22:46:43 +08:00
|
|
|
Rest: authnlib.IDTokenClaims{
|
2024-08-14 16:51:44 +08:00
|
|
|
Namespace: s.nsMapper(id.GetOrgID()),
|
|
|
|
|
Identifier: id.GetRawIdentifier(),
|
|
|
|
|
Type: id.GetIdentityType(),
|
2024-05-07 22:46:43 +08:00
|
|
|
},
|
2024-01-17 16:52:05 +08:00
|
|
|
}
|
2023-10-13 20:32:53 +08:00
|
|
|
|
2024-08-29 05:49:41 +08:00
|
|
|
if id.IsIdentityType(claims.TypeUser) {
|
|
|
|
|
idClaims.Rest.Email = id.GetEmail()
|
2024-12-03 22:11:17 +08:00
|
|
|
idClaims.Rest.EmailVerified = id.GetEmailVerified()
|
2024-08-29 05:49:41 +08:00
|
|
|
idClaims.Rest.AuthenticatedBy = id.GetAuthenticatedBy()
|
|
|
|
|
idClaims.Rest.Username = id.GetLogin()
|
2024-11-26 22:29:31 +08:00
|
|
|
idClaims.Rest.DisplayName = id.GetName()
|
2024-01-17 16:52:05 +08:00
|
|
|
}
|
|
|
|
|
|
2025-02-12 21:51:29 +08:00
|
|
|
if id.GetOrgRole().IsValid() {
|
|
|
|
|
idClaims.Rest.Role = string(id.GetOrgRole())
|
|
|
|
|
}
|
|
|
|
|
|
2024-08-29 05:49:41 +08:00
|
|
|
token, err := s.signer.SignIDToken(ctx, idClaims)
|
2023-10-13 20:32:53 +08:00
|
|
|
if err != nil {
|
|
|
|
|
s.metrics.failedTokenSigningCounter.Inc()
|
2024-08-02 17:36:02 +08:00
|
|
|
return resultType{}, nil
|
2023-10-13 20:32:53 +08:00
|
|
|
}
|
|
|
|
|
|
2024-08-02 17:36:02 +08:00
|
|
|
extracted, err := s.extractTokenClaims(token)
|
2024-01-24 20:56:44 +08:00
|
|
|
if err != nil {
|
2024-08-02 17:36:02 +08:00
|
|
|
return resultType{}, err
|
2024-01-24 20:56:44 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
expires := time.Until(extracted.Expiry.Time())
|
|
|
|
|
if err := s.cache.Set(ctx, cacheKey, []byte(token), expires-cacheLeeway); err != nil {
|
2024-01-17 16:52:05 +08:00
|
|
|
s.logger.FromContext(ctx).Error("Failed to add id token to cache", "error", err)
|
2023-10-13 20:32:53 +08:00
|
|
|
}
|
|
|
|
|
|
2024-08-29 05:49:41 +08:00
|
|
|
return resultType{token: token, idClaims: idClaims}, nil
|
2023-09-27 17:36:23 +08:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
2024-08-02 17:36:02 +08:00
|
|
|
return "", nil, err
|
2023-09-27 17:36:23 +08:00
|
|
|
}
|
|
|
|
|
|
2024-08-02 17:36:02 +08:00
|
|
|
return result.(resultType).token, result.(resultType).idClaims, nil
|
2023-09-27 17:36:23 +08:00
|
|
|
}
|
|
|
|
|
|
2024-04-05 18:05:46 +08:00
|
|
|
func (s *Service) RemoveIDToken(ctx context.Context, id identity.Requester) error {
|
2025-07-10 21:41:00 +08:00
|
|
|
ctx, span := s.tracer.Start(ctx, "user.sync.RemoveIDToken")
|
|
|
|
|
defer span.End()
|
|
|
|
|
|
2025-02-13 21:10:58 +08:00
|
|
|
return s.cache.Delete(ctx, getCacheKey(id))
|
2024-04-05 18:05:46 +08:00
|
|
|
}
|
|
|
|
|
|
2025-07-10 21:41:00 +08:00
|
|
|
func (s *Service) SyncIDToken(ctx context.Context, identity *authn.Identity, _ *authn.Request) error {
|
|
|
|
|
ctx, span := s.tracer.Start(ctx, "user.sync.SyncIDToken")
|
|
|
|
|
defer span.End()
|
2023-09-28 15:22:05 +08:00
|
|
|
// FIXME(kalleep): we should probably lazy load this
|
2024-08-29 05:49:41 +08:00
|
|
|
token, idClaims, err := s.SignIdentity(ctx, identity)
|
2023-09-28 15:22:05 +08:00
|
|
|
if err != nil {
|
2024-03-26 18:36:44 +08:00
|
|
|
if shouldLogErr(err) {
|
2024-08-09 23:20:24 +08:00
|
|
|
s.logger.FromContext(ctx).Error("Failed to sign id token", "err", err, "id", identity.GetID())
|
2024-03-26 18:36:44 +08:00
|
|
|
}
|
2023-09-28 15:22:05 +08:00
|
|
|
// for now don't return error so we don't break authentication from this hook
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
identity.IDToken = token
|
2024-08-29 05:49:41 +08:00
|
|
|
identity.IDTokenClaims = idClaims
|
2023-09-28 15:22:05 +08:00
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2024-08-02 17:36:02 +08:00
|
|
|
func (s *Service) extractTokenClaims(token string) (*authnlib.Claims[authnlib.IDTokenClaims], error) {
|
2025-09-15 18:45:15 +08:00
|
|
|
parsed, err := jwt.ParseSigned(token, []jose.SignatureAlgorithm{jose.ES256})
|
2024-08-02 17:36:02 +08:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2023-10-10 20:17:16 +08:00
|
|
|
func getAudience(orgID int64) jwt.Audience {
|
|
|
|
|
return jwt.Audience{fmt.Sprintf("org:%d", orgID)}
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-13 21:10:58 +08:00
|
|
|
func getCacheKey(ident identity.Requester) string {
|
|
|
|
|
return cachePrefix + ident.GetCacheKey() + string(ident.GetOrgRole())
|
2023-09-27 17:36:23 +08:00
|
|
|
}
|
2024-03-26 18:36:44 +08:00
|
|
|
|
|
|
|
|
func shouldLogErr(err error) bool {
|
|
|
|
|
return !errors.Is(err, context.Canceled)
|
|
|
|
|
}
|