diff --git a/pkg/server/wire.go b/pkg/server/wire.go index 3321665bb16..e1ea693da17 100644 --- a/pkg/server/wire.go +++ b/pkg/server/wire.go @@ -43,6 +43,8 @@ import ( "github.com/grafana/grafana/pkg/services/annotations/annotationsimpl" "github.com/grafana/grafana/pkg/services/anonymous/anonimpl/anonstore" "github.com/grafana/grafana/pkg/services/apikey/apikeyimpl" + "github.com/grafana/grafana/pkg/services/auth" + "github.com/grafana/grafana/pkg/services/auth/idimpl" "github.com/grafana/grafana/pkg/services/auth/jwt" "github.com/grafana/grafana/pkg/services/authn/authnimpl" "github.com/grafana/grafana/pkg/services/cleanup" @@ -361,6 +363,8 @@ var wireBasicSet = wire.NewSet( loggermw.Provide, signingkeysimpl.ProvideEmbeddedSigningKeysService, wire.Bind(new(signingkeys.Service), new(*signingkeysimpl.Service)), + idimpl.ProvideService, + wire.Bind(new(auth.IDService), new(*idimpl.Service)), grafanaapiserver.WireSet, apiregistry.WireSet, ) diff --git a/pkg/server/wireexts_oss.go b/pkg/server/wireexts_oss.go index 66b20335ad7..9460f2d6595 100644 --- a/pkg/server/wireexts_oss.go +++ b/pkg/server/wireexts_oss.go @@ -18,6 +18,7 @@ import ( "github.com/grafana/grafana/pkg/services/anonymous/anonimpl" "github.com/grafana/grafana/pkg/services/auth" "github.com/grafana/grafana/pkg/services/auth/authimpl" + "github.com/grafana/grafana/pkg/services/auth/idimpl" "github.com/grafana/grafana/pkg/services/caching" "github.com/grafana/grafana/pkg/services/datasources/guardian" "github.com/grafana/grafana/pkg/services/encryption" @@ -91,6 +92,8 @@ var wireExtsBasicSet = wire.NewSet( wire.Bind(new(caching.CachingService), new(*caching.OSSCachingService)), secretsMigrator.ProvideSecretsMigrator, wire.Bind(new(secrets.Migrator), new(*secretsMigrator.SecretsMigrator)), + idimpl.ProvideLocalSigner, + wire.Bind(new(auth.IDSigner), new(*idimpl.LocalSigner)), ) var wireExtsSet = wire.NewSet( diff --git a/pkg/services/auth/id.go b/pkg/services/auth/id.go new file mode 100644 index 00000000000..33d505b23f7 --- /dev/null +++ b/pkg/services/auth/id.go @@ -0,0 +1,21 @@ +package auth + +import ( + "context" + + "github.com/go-jose/go-jose/v3/jwt" + "github.com/grafana/grafana/pkg/services/auth/identity" +) + +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) +} + +type IDSigner interface { + SignIDToken(ctx context.Context, claims *IDClaims) (string, error) +} + +type IDClaims struct { + jwt.Claims +} diff --git a/pkg/services/auth/idimpl/service.go b/pkg/services/auth/idimpl/service.go new file mode 100644 index 00000000000..c8607b7e822 --- /dev/null +++ b/pkg/services/auth/idimpl/service.go @@ -0,0 +1,74 @@ +package idimpl + +import ( + "context" + "fmt" + "strconv" + "time" + + "github.com/go-jose/go-jose/v3/jwt" + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/infra/remotecache" + "github.com/grafana/grafana/pkg/services/auth" + "github.com/grafana/grafana/pkg/services/auth/identity" + "github.com/grafana/grafana/pkg/setting" +) + +const ( + cachePrefix = "id-token" + tokenTTL = 1 * time.Hour + cacheTTL = 58 * time.Minute +) + +var _ auth.IDService = (*Service)(nil) + +func ProvideService(cfg *setting.Cfg, signer auth.IDSigner, cache remotecache.CacheStorage) *Service { + return &Service{cfg, log.New("id-service"), signer, cache} +} + +type Service struct { + cfg *setting.Cfg + logger log.Logger + signer auth.IDSigner + cache remotecache.CacheStorage +} + +func (s *Service) SignIdentity(ctx context.Context, id identity.Requester) (string, error) { + namespace, identifier := id.GetNamespacedID() + + cacheKey := prefixCacheKey(id.GetCacheKey()) + cachedToken, err := s.cache.Get(ctx, cacheKey) + if err == nil { + s.logger.Debug("Cached token found", "namespace", namespace, "id", identifier) + return string(cachedToken), nil + } + + s.logger.Debug("Sign new id token", "namespace", namespace, "id", identifier) + + now := time.Now() + + token, err := s.signer.SignIDToken(ctx, &auth.IDClaims{ + Claims: jwt.Claims{ + ID: identifier, + Issuer: s.cfg.AppURL, + Audience: jwt.Audience{strconv.FormatInt(id.GetOrgID(), 10)}, + Subject: fmt.Sprintf("%s:%s", namespace, identifier), + Expiry: jwt.NewNumericDate(now.Add(tokenTTL)), + IssuedAt: jwt.NewNumericDate(now), + }, + }) + + if err != nil { + return "", err + } + + if err := s.cache.Set(ctx, cacheKey, []byte(token), cacheTTL); err != nil { + s.logger.Error("failed to set cache", "error", err) + } + + return token, nil +} + +func prefixCacheKey(key string) string { + return fmt.Sprintf("%s-%s", cachePrefix, key) +} diff --git a/pkg/services/auth/idimpl/signer.go b/pkg/services/auth/idimpl/signer.go new file mode 100644 index 00000000000..0a44026b7c5 --- /dev/null +++ b/pkg/services/auth/idimpl/signer.go @@ -0,0 +1,45 @@ +package idimpl + +import ( + "context" + + "github.com/go-jose/go-jose/v3" + "github.com/go-jose/go-jose/v3/jwt" + + "github.com/grafana/grafana/pkg/services/auth" + "github.com/grafana/grafana/pkg/services/signingkeys" +) + +var _ auth.IDSigner = (*LocalSigner)(nil) + +func ProvideLocalSigner(keyService signingkeys.Service) (*LocalSigner, error) { + key := keyService.GetServerPrivateKey() // FIXME: replace with signing specific key + + signer, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: key}, &jose.SignerOptions{ + ExtraHeaders: map[jose.HeaderKey]interface{}{ + "kid": "default", // FIXME: replace with specific key id + }, + }) + if err != nil { + return nil, err + } + + return &LocalSigner{ + signer: signer, + }, nil +} + +type LocalSigner struct { + signer jose.Signer +} + +func (s *LocalSigner) SignIDToken(ctx context.Context, claims *auth.IDClaims) (string, error) { + builder := jwt.Signed(s.signer).Claims(claims.Claims) + + token, err := builder.CompactSerialize() + if err != nil { + return "", err + } + + return token, nil +}