grafana/pkg/services/authn/clients/jwt.go
2023-01-26 17:32:20 +01:00

212 lines
5.8 KiB
Go

package clients
import (
"context"
"errors"
"fmt"
"net/http"
"strings"
"github.com/jmespath/go-jmespath"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models/roletype"
"github.com/grafana/grafana/pkg/services/auth"
authJWT "github.com/grafana/grafana/pkg/services/auth/jwt"
"github.com/grafana/grafana/pkg/services/authn"
"github.com/grafana/grafana/pkg/services/login"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util/errutil"
)
var _ authn.ContextAwareClient = new(JWT)
var (
ErrJWTInvalid = errutil.NewBase(errutil.StatusUnauthorized,
"jwt.invalid", errutil.WithPublicMessage("Failed to verify JWT"))
ErrJWTMissingClaim = errutil.NewBase(errutil.StatusUnauthorized,
"jwt.missing_claim", errutil.WithPublicMessage("Missing mandatory claim in JWT"))
ErrJWTInvalidRole = errutil.NewBase(errutil.StatusForbidden,
"jwt.invalid_role", errutil.WithPublicMessage("Invalid Role in claim"))
)
func ProvideJWT(jwtService auth.JWTVerifierService, cfg *setting.Cfg) *JWT {
return &JWT{
cfg: cfg,
log: log.New(authn.ClientJWT),
jwtService: jwtService,
}
}
type JWT struct {
cfg *setting.Cfg
log log.Logger
jwtService auth.JWTVerifierService
}
func (s *JWT) Name() string {
return authn.ClientJWT
}
func (s *JWT) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identity, error) {
jwtToken := s.retrieveToken(r.HTTPRequest)
claims, err := s.jwtService.Verify(ctx, jwtToken)
if err != nil {
s.log.Debug("Failed to verify JWT", "error", err)
return nil, ErrJWTInvalid.Errorf("failed to verify JWT: %w", err)
}
sub, _ := claims["sub"].(string)
if sub == "" {
s.log.Warn("Got a JWT without the mandatory 'sub' claim", "error", err)
return nil, ErrJWTMissingClaim.Errorf("missing mandatory 'sub' claim in JWT")
}
id := &authn.Identity{
AuthModule: login.JWTModule,
AuthID: sub,
OrgRoles: map[int64]org.RoleType{},
ClientParams: authn.ClientParams{
SyncUser: true,
SyncTeamMembers: true,
AllowSignUp: s.cfg.JWTAuthAutoSignUp,
EnableDisabledUsers: false,
}}
if key := s.cfg.JWTAuthUsernameClaim; key != "" {
id.Login, _ = claims[key].(string)
id.ClientParams.LookUpParams.Login = &id.Login
}
if key := s.cfg.JWTAuthEmailClaim; key != "" {
id.Email, _ = claims[key].(string)
id.ClientParams.LookUpParams.Email = &id.Email
}
if name, _ := claims["name"].(string); name != "" {
id.Name = name
}
var role roletype.RoleType
var grafanaAdmin bool
if !s.cfg.JWTAuthSkipOrgRoleSync {
role, grafanaAdmin = s.extractRoleAndAdmin(claims)
if s.cfg.JWTAuthRoleAttributeStrict && !role.IsValid() {
s.log.Warn("extracted Role is invalid", "role", role, "auth_id", id.AuthID)
return nil, ErrJWTInvalidRole.Errorf("invalid role claim in JWT: %s", role)
}
if role.IsValid() {
var orgID int64
// FIXME (jguer): GetIDForNewUser already has the auto assign information
// just needs the org role. Find a meaningful way to pass this default
// role to it (that doesn't involve id.OrgRoles[0] = role)
if s.cfg.AutoAssignOrg && s.cfg.AutoAssignOrgId > 0 {
orgID = int64(s.cfg.AutoAssignOrgId)
s.log.Debug("The user has a role assignment and organization membership is auto-assigned",
"role", role, "orgId", orgID)
} else {
orgID = int64(1)
s.log.Debug("The user has a role assignment and organization membership is not auto-assigned",
"role", role, "orgId", orgID)
}
id.OrgRoles[orgID] = role
if s.cfg.JWTAuthAllowAssignGrafanaAdmin {
id.IsGrafanaAdmin = &grafanaAdmin
}
}
}
if id.Login == "" && id.Email == "" {
s.log.Debug("Failed to get an authentication claim from JWT",
"login", id.Login, "email", id.Email)
return nil, ErrJWTMissingClaim.Errorf("missing login and email claim in JWT")
}
return id, nil
}
// retrieveToken retrieves the JWT token from the request.
func (s *JWT) retrieveToken(httpRequest *http.Request) string {
jwtToken := httpRequest.Header.Get(s.cfg.JWTAuthHeaderName)
if jwtToken == "" && s.cfg.JWTAuthURLLogin {
jwtToken = httpRequest.URL.Query().Get("auth_token")
}
// Strip the 'Bearer' prefix if it exists.
return strings.TrimPrefix(jwtToken, "Bearer ")
}
func (s *JWT) Test(ctx context.Context, r *authn.Request) bool {
if !s.cfg.JWTAuthEnabled || s.cfg.JWTAuthHeaderName == "" {
return false
}
jwtToken := s.retrieveToken(r.HTTPRequest)
if jwtToken == "" {
return false
}
// If the "sub" claim is missing or empty then pass the control to the next handler
if !authJWT.HasSubClaim(jwtToken) {
return false
}
return true
}
func (s *JWT) Priority() uint {
return 20
}
const roleGrafanaAdmin = "GrafanaAdmin"
func (s *JWT) extractRoleAndAdmin(claims map[string]interface{}) (org.RoleType, bool) {
if s.cfg.JWTAuthRoleAttributePath == "" {
return "", false
}
role, err := searchClaimsForStringAttr(s.cfg.JWTAuthRoleAttributePath, claims)
if err != nil || role == "" {
return "", false
}
if role == roleGrafanaAdmin {
return org.RoleAdmin, true
}
return org.RoleType(role), false
}
func searchClaimsForStringAttr(attributePath string, claims map[string]interface{}) (string, error) {
val, err := searchClaimsForAttr(attributePath, claims)
if err != nil {
return "", err
}
strVal, ok := val.(string)
if ok {
return strVal, nil
}
return "", nil
}
func searchClaimsForAttr(attributePath string, claims map[string]interface{}) (interface{}, error) {
if attributePath == "" {
return "", errors.New("no attribute path specified")
}
if len(claims) == 0 {
return "", errors.New("empty claims provided")
}
val, err := jmespath.Search(attributePath, claims)
if err != nil {
return "", fmt.Errorf("failed to search claims with provided path: %q: %w", attributePath, err)
}
return val, nil
}