mirror of
https://github.com/grafana/grafana.git
synced 2025-09-20 09:44:26 +08:00

* Moving POC files from #64283 to a new branch Co-authored-by: Mihály Gyöngyösi <mgyongyosi@users.noreply.github.com> * Adding missing permission definition Co-authored-by: Mihály Gyöngyösi <mgyongyosi@users.noreply.github.com> * Force the service instantiation while client isn't merged Co-authored-by: Mihály Gyöngyösi <mgyongyosi@users.noreply.github.com> * Merge conf with main Co-authored-by: Mihály Gyöngyösi <mgyongyosi@users.noreply.github.com> * Leave go-sqlite3 version unchanged Co-authored-by: Mihály Gyöngyösi <mgyongyosi@users.noreply.github.com> * tidy Co-authored-by: Mihály Gyöngyösi <mgyongyosi@users.noreply.github.com> * User SearchUserPermissions instead of SearchUsersPermissions * Replace DummyKeyService with signingkeys.Service * Use user🆔<id> as subject * Fix introspection endpoint issue * Add X-Grafana-Org-Id to get_resources.bash script * Regenerate toggles_gen.go * Fix basic.go * Add GetExternalService tests * Add GetPublicKeyScopes tests * Add GetScopesOnUser tests * Add GetScopes tests * Add ParsePublicKeyPem tests * Add database test for GetByName * re-add comments * client tests added * Add GetExternalServicePublicKey tests * Add other test case to GetExternalServicePublicKey * client_credentials grant test * Add test to jwtbearer grant * Test Comments * Add handleKeyOptions tests * Add RSA key generation test * Add ECDSA by default to EmbeddedSigningKeysService * Clean up org id scope and audiences * Add audiences to the DB * Fix check on Audience * Fix double import * Add AC Store mock and align oauthserver tests * Fix test after rebase * Adding missing store function to mock * Fix double import * Add CODEOWNER * Fix some linting errors * errors don't need type assertion * Typo codeowners * use mockery for oauthserver store * Add feature toggle check * Fix db tests to handle the feature flag * Adding call to DeleteExternalServiceRole * Fix flaky test * Re-organize routes comments and plan futur work * Add client_id check to Extended JWT client * Clean up * Fix * Remove background service registry instantiation of the OAuth server * Comment cleanup * Remove unused client function * Update go.mod to use the latest ory/fosite commit * Remove oauth2_server related configs from defaults.ini * Add audiences to DTO * Fix flaky test * Remove registration endpoint and demo scripts. Document code * Rename packages * Remove the OAuthService vs OAuthServer confusion * fix incorrect import ext_jwt_test * Comments and order * Comment basic auth * Remove unecessary todo * Clean api * Moving ParsePublicKeyPem to utils * re ordering functions in service.go * Fix comment * comment on the redirect uri * Add RBAC actions, not only scopes * Fix tests * re-import featuremgmt in migrations * Fix wire * Fix scopes in test * Fix flaky test * Remove todo, the intersection should always return the minimal set * Remove unecessary check from intersection code * Allow env overrides on settings * remove the term app name * Remove app keyword for client instead and use Name instead of ExternalServiceName * LogID remove ExternalService ref * Use Name instead of ExternalServiceName * Imports order * Inline * Using ExternalService and ExternalServiceDTO * Remove xorm tags * comment * Rename client files * client -> external service * comments * Move test to correct package * slimmer test * cachedUser -> cachedExternalService * Fix aggregate store test * PluginAuthSession -> AuthSession * Revert the nil cehcks * Remove unecessary extra * Removing custom session * fix typo in test * Use constants for tests * Simplify HandleToken tests * Refactor the HandleTokenRequest test * test message * Review test * Prevent flacky test on client as well * go imports * Revert changes from 526e48ad4550fed7e2b753b9d0a0cc6097155f58 * AuthN: Change the External Service registration form (#68649) * AuthN: change the External Service registration form * Gen default permissions * Change demo script registration form * Remove unecessary comment * Nit. * Reduce cyclomatic complexity * Remove demo_scripts * Handle case with no service account * Comments * Group key gen * Nit. * Check the SaveExternalService test * Rename cachedUser to cachedClient in test * One more test case to database test * Comments * Remove last org scope Co-authored-by: Mihály Gyöngyösi <mgyongyosi@users.noreply.github.com> * Update pkg/services/oauthserver/utils/utils_test.go * Update pkg/services/sqlstore/migrations/oauthserver/migrations.go Remove comment * Update pkg/setting/setting.go Co-authored-by: Gabriel MABILLE <gamab@users.noreply.github.com> --------- Co-authored-by: Mihály Gyöngyösi <mgyongyosi@users.noreply.github.com>
231 lines
6.5 KiB
Go
231 lines
6.5 KiB
Go
package clients
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/go-jose/go-jose/v3/jwt"
|
|
"golang.org/x/exp/slices"
|
|
|
|
"github.com/grafana/grafana/pkg/infra/log"
|
|
"github.com/grafana/grafana/pkg/services/authn"
|
|
"github.com/grafana/grafana/pkg/services/oauthserver"
|
|
"github.com/grafana/grafana/pkg/services/signingkeys"
|
|
"github.com/grafana/grafana/pkg/services/user"
|
|
"github.com/grafana/grafana/pkg/setting"
|
|
)
|
|
|
|
var _ authn.Client = new(ExtendedJWT)
|
|
|
|
var (
|
|
acceptedSigningMethods = []string{"RS256", "ES256"}
|
|
timeNow = time.Now
|
|
)
|
|
|
|
const (
|
|
rfc9068ShortMediaType = "at+jwt"
|
|
rfc9068MediaType = "application/at+jwt"
|
|
)
|
|
|
|
func ProvideExtendedJWT(userService user.Service, cfg *setting.Cfg, signingKeys signingkeys.Service, oauthServer oauthserver.OAuth2Server) *ExtendedJWT {
|
|
return &ExtendedJWT{
|
|
cfg: cfg,
|
|
log: log.New(authn.ClientExtendedJWT),
|
|
userService: userService,
|
|
signingKeys: signingKeys,
|
|
oauthServer: oauthServer,
|
|
}
|
|
}
|
|
|
|
type ExtendedJWT struct {
|
|
cfg *setting.Cfg
|
|
log log.Logger
|
|
userService user.Service
|
|
signingKeys signingkeys.Service
|
|
oauthServer oauthserver.OAuth2Server
|
|
}
|
|
|
|
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"`
|
|
}
|
|
|
|
func (s *ExtendedJWT) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identity, error) {
|
|
jwtToken := s.retrieveToken(r.HTTPRequest)
|
|
|
|
claims, err := s.verifyRFC9068Token(ctx, jwtToken)
|
|
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)
|
|
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")
|
|
}
|
|
|
|
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}), nil
|
|
}
|
|
|
|
func (s *ExtendedJWT) Test(ctx context.Context, r *authn.Request) bool {
|
|
if !s.cfg.ExtendedJWTAuthEnabled {
|
|
return false
|
|
}
|
|
|
|
rawToken := s.retrieveToken(r.HTTPRequest)
|
|
if rawToken == "" {
|
|
return false
|
|
}
|
|
|
|
parsedToken, err := jwt.ParseSigned(rawToken)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
var claims jwt.Claims
|
|
if err := parsedToken.UnsafeClaimsWithoutVerification(&claims); err != nil {
|
|
return false
|
|
}
|
|
|
|
return claims.Issuer == s.cfg.ExtendedJWTExpectIssuer
|
|
}
|
|
|
|
func (s *ExtendedJWT) Name() string {
|
|
return authn.ClientExtendedJWT
|
|
}
|
|
|
|
func (s *ExtendedJWT) Priority() uint {
|
|
// This client should come before the normal JWT client, because it is more specific, because of the Issuer check
|
|
return 15
|
|
}
|
|
|
|
// retrieveToken retrieves the JWT token from the request.
|
|
func (s *ExtendedJWT) retrieveToken(httpRequest *http.Request) string {
|
|
jwtToken := httpRequest.Header.Get("Authorization")
|
|
|
|
// 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) {
|
|
parsedToken, err := jwt.ParseSigned(rawToken)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse JWT: %w", err)
|
|
}
|
|
|
|
if len(parsedToken.Headers) != 1 {
|
|
return nil, fmt.Errorf("only one header supported, got %d", len(parsedToken.Headers))
|
|
}
|
|
|
|
parsedHeader := parsedToken.Headers[0]
|
|
|
|
typeHeader := parsedHeader.ExtraHeaders["typ"]
|
|
if typeHeader == nil {
|
|
return nil, fmt.Errorf("missing 'typ' field from the header")
|
|
}
|
|
|
|
jwtType := strings.ToLower(typeHeader.(string))
|
|
if jwtType != rfc9068ShortMediaType && jwtType != rfc9068MediaType {
|
|
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, ", "))
|
|
}
|
|
|
|
var claims ExtendedJWTClaims
|
|
err = parsedToken.Claims(s.signingKeys.GetServerPublicKey(), &claims)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to verify the signature: %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")
|
|
}
|
|
|
|
if claims.IssuedAt == nil {
|
|
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")
|
|
}
|
|
|
|
if _, err := s.oauthServer.GetExternalService(ctx, claims.ClientID); err != nil {
|
|
return fmt.Errorf("invalid 'client_id' claim: %s", claims.ClientID)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *ExtendedJWT) getDefaultOrgID() int64 {
|
|
orgID := int64(1)
|
|
if s.cfg.AutoAssignOrg && s.cfg.AutoAssignOrgId > 0 {
|
|
orgID = int64(s.cfg.AutoAssignOrgId)
|
|
}
|
|
return orgID
|
|
}
|