Authn: JWT client (#61157)

* add jwt client

* alias JWT verifier

* debug implementation

* add tests for jwt client

* add constant for JWT module

* Feedback

Co-authored-by: Kalle Persson <kalle.persson@grafana.com>
Co-authored-by: Mihály Gyöngyösi <mgyongyosi@users.noreply.github.com>

Co-authored-by: Kalle Persson <kalle.persson@grafana.com>
Co-authored-by: Mihály Gyöngyösi <mgyongyosi@users.noreply.github.com>
pull/51083/head
Jo 2 years ago committed by GitHub
parent 2de72c1c39
commit 0c8ad80575
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 7
      devenv/docker/blocks/auth/oauth/readme.md
  2. 3
      pkg/services/auth/auth.go
  3. 20
      pkg/services/authn/authn.go
  4. 6
      pkg/services/authn/authnimpl/service.go
  5. 20
      pkg/services/authn/authnimpl/usersync/orgsync_test.go
  6. 4
      pkg/services/authn/authnimpl/usersync/usersync.go
  7. 152
      pkg/services/authn/authnimpl/usersync/usersync_test.go
  8. 208
      pkg/services/authn/clients/jwt.go
  9. 175
      pkg/services/authn/clients/jwt_test.go
  10. 10
      pkg/services/authn/clients/ldap.go
  11. 8
      pkg/services/authn/clients/ldap_test.go
  12. 2
      pkg/services/authn/clients/session_test.go
  13. 23
      pkg/services/contexthandler/auth_jwt.go
  14. 2
      pkg/services/contexthandler/contexthandler.go
  15. 3
      pkg/services/login/authinfo.go
  16. 6
      pkg/services/user/userimpl/store.go

@ -44,12 +44,15 @@ Here is the conf you need to add to your configuration file (conf/custom.ini):
[auth.jwt]
enabled = true
header_name = X-JWT-Assertion
username_claim = login
username_claim = preferred_username
email_claim = email
jwk_set_file = devenv/docker/blocks/auth/oauth/jwks.json
cache_ttl = 60m
expect_claims = {"iss": "http://localhost:8087/auth/realms/grafana", "azp": "grafana-oauth"}
expect_claims = {"iss": "http://localhost:8087/realms/grafana", "azp": "grafana-oauth"}
auto_sign_up = true
role_attribute_path = contains(roles[*], 'grafanaadmin') && 'GrafanaAdmin' || contains(roles[*], 'admin') && 'Admin' || contains(roles[*], 'editor') && 'Editor' || 'Viewer'
role_attribute_strict = true
allow_assign_grafana_admin = true
```
You can obtain a jwt token by using the following command for oauth-admin:

@ -6,6 +6,7 @@ import (
"fmt"
"net"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/models/usertoken"
"github.com/grafana/grafana/pkg/registry"
"github.com/grafana/grafana/pkg/services/quota"
@ -72,3 +73,5 @@ type UserTokenService interface {
type UserTokenBackgroundService interface {
registry.BackgroundService
}
type JWTVerifierService = models.JWTService

@ -18,17 +18,26 @@ import (
const (
ClientAPIKey = "auth.client.api-key" // #nosec G101
ClientSession = "auth.client.session"
ClientAnonymous = "auth.client.anonymous"
ClientBasic = "auth.client.basic"
ClientJWT = "auth.client.jwt"
ClientRender = "auth.client.render"
ClientSession = "auth.client.session"
)
// ClientParams are hints to the auth service about how to handle the identity management
// from the authenticating client.
type ClientParams struct {
SyncUser bool
SyncTeamMembers bool
AllowSignUp bool
// Update the internal representation of the entity from the identity provided
SyncUser bool
// Add entity to teams
SyncTeamMembers bool
// Create entity in the DB if it doesn't exist
AllowSignUp bool
// EnableDisabledUsers is a hint to the auth service that it should reenable disabled users
EnableDisabledUsers bool
// LookUpParams are the arguments used to look up the entity in the DB.
LookUpParams models.UserLookupParams
}
type PostAuthHookFn func(ctx context.Context, identity *Identity, r *Request) error
@ -98,9 +107,6 @@ type Identity struct {
// AuthId is the unique identifier for the entity in the external system.
// Empty if the identity is provided by Grafana.
AuthID string
// LookUpParams are the arguments used to look up the entity in the DB.
// Empty if the identity is provided by Grafana. TODO: move to client params
LookUpParams models.UserLookupParams
// IsDisabled is true if the entity is disabled.
IsDisabled bool
// HelpFlags1 is the help flags for the entity.

@ -34,6 +34,7 @@ func ProvideService(
orgService org.Service, sessionService auth.UserTokenService,
accessControlService accesscontrol.Service,
apikeyService apikey.Service, userService user.Service,
jwtService auth.JWTVerifierService,
loginAttempts loginattempt.Service, quotaService quota.Service,
authInfoService login.AuthInfoService, renderService rendering.Service,
) *Service {
@ -72,6 +73,10 @@ func ProvideService(
s.clients[authn.ClientBasic] = clients.ProvideBasic(loginAttempts, passwordClients...)
}
if s.cfg.JWTAuthEnabled {
s.clients[authn.ClientJWT] = clients.ProvideJWT(jwtService, cfg)
}
// FIXME (jguer): move to User package
userSyncService := sync.ProvideUserSync(userService, authInfoService, quotaService)
orgUserSyncService := sync.ProvideOrgSync(userService, orgService, accessControlService)
@ -117,6 +122,7 @@ func (s *Service) Authenticate(ctx context.Context, client string, r *authn.Requ
for _, hook := range s.postAuthHooks {
if err := hook(ctx, identity, r); err != nil {
s.log.FromContext(ctx).Warn("post auth hook failed", "error", err, "id", identity)
return nil, false, err
}
}

@ -77,13 +77,13 @@ func TestOrgSync_SyncOrgUser(t *testing.T) {
Email: "test",
OrgRoles: map[int64]roletype.RoleType{1: org.RoleAdmin, 2: org.RoleEditor},
IsGrafanaAdmin: ptrBool(false),
LookUpParams: models.UserLookupParams{
UserID: nil,
Email: ptrString("test"),
Login: nil,
},
ClientParams: authn.ClientParams{
SyncUser: true,
LookUpParams: models.UserLookupParams{
UserID: nil,
Email: ptrString("test"),
Login: nil,
},
},
},
},
@ -95,13 +95,13 @@ func TestOrgSync_SyncOrgUser(t *testing.T) {
OrgRoles: map[int64]roletype.RoleType{1: org.RoleAdmin, 2: org.RoleEditor},
OrgID: 1, //set using org
IsGrafanaAdmin: ptrBool(false),
LookUpParams: models.UserLookupParams{
UserID: nil,
Email: ptrString("test"),
Login: nil,
},
ClientParams: authn.ClientParams{
SyncUser: true,
LookUpParams: models.UserLookupParams{
UserID: nil,
Email: ptrString("test"),
Login: nil,
},
},
},
wantErr: false,

@ -32,7 +32,7 @@ func (s *UserSync) SyncUser(ctx context.Context, id *authn.Identity, _ *authn.Re
}
// Does user exist in the database?
usr, errUserInDB := s.UserInDB(ctx, &id.AuthModule, &id.AuthID, id.LookUpParams)
usr, errUserInDB := s.UserInDB(ctx, &id.AuthModule, &id.AuthID, id.ClientParams.LookUpParams)
if errUserInDB != nil && !errors.Is(errUserInDB, user.ErrUserNotFound) {
return errUserInDB
}
@ -244,7 +244,7 @@ func (s *UserSync) LookupByOneOf(ctx context.Context, params *models.UserLookupP
}
}
if usr == nil {
if usr == nil || usr.ID == 0 { // id check as safeguard against returning empty user
return nil, user.ErrUserNotFound
}

@ -110,12 +110,13 @@ func TestUserSync_SyncUser(t *testing.T) {
Login: "test",
Name: "test",
Email: "test",
LookUpParams: models.UserLookupParams{
UserID: nil,
Email: ptrString("test"),
Login: nil,
ClientParams: authn.ClientParams{
LookUpParams: models.UserLookupParams{
UserID: nil,
Email: ptrString("test"),
Login: nil,
},
},
ClientParams: authn.ClientParams{},
},
},
wantErr: false,
@ -124,12 +125,13 @@ func TestUserSync_SyncUser(t *testing.T) {
Login: "test",
Name: "test",
Email: "test",
LookUpParams: models.UserLookupParams{
UserID: nil,
Email: ptrString("test"),
Login: nil,
ClientParams: authn.ClientParams{
LookUpParams: models.UserLookupParams{
UserID: nil,
Email: ptrString("test"),
Login: nil,
},
},
ClientParams: authn.ClientParams{},
},
},
{
@ -147,13 +149,13 @@ func TestUserSync_SyncUser(t *testing.T) {
Login: "test",
Name: "test",
Email: "test",
LookUpParams: models.UserLookupParams{
UserID: nil,
Email: ptrString("test"),
Login: nil,
},
ClientParams: authn.ClientParams{
SyncUser: true,
LookUpParams: models.UserLookupParams{
UserID: nil,
Email: ptrString("test"),
Login: nil,
},
},
},
},
@ -164,13 +166,13 @@ func TestUserSync_SyncUser(t *testing.T) {
Name: "test",
Email: "test",
IsGrafanaAdmin: ptrBool(false),
LookUpParams: models.UserLookupParams{
UserID: nil,
Email: ptrString("test"),
Login: nil,
},
ClientParams: authn.ClientParams{
SyncUser: true,
LookUpParams: models.UserLookupParams{
UserID: nil,
Email: ptrString("test"),
Login: nil,
},
},
},
},
@ -189,13 +191,13 @@ func TestUserSync_SyncUser(t *testing.T) {
Login: "test",
Name: "test",
Email: "test",
LookUpParams: models.UserLookupParams{
UserID: nil,
Email: nil,
Login: ptrString("test"),
},
ClientParams: authn.ClientParams{
SyncUser: true,
LookUpParams: models.UserLookupParams{
UserID: nil,
Email: nil,
Login: ptrString("test"),
},
},
},
},
@ -206,12 +208,12 @@ func TestUserSync_SyncUser(t *testing.T) {
Name: "test",
Email: "test",
IsGrafanaAdmin: ptrBool(false),
LookUpParams: models.UserLookupParams{
UserID: nil,
Email: nil,
Login: ptrString("test"),
},
ClientParams: authn.ClientParams{
LookUpParams: models.UserLookupParams{
UserID: nil,
Email: nil,
Login: ptrString("test"),
},
SyncUser: true,
},
},
@ -231,13 +233,13 @@ func TestUserSync_SyncUser(t *testing.T) {
Login: "test",
Name: "test",
Email: "test",
LookUpParams: models.UserLookupParams{
UserID: ptrInt64(1),
Email: nil,
Login: nil,
},
ClientParams: authn.ClientParams{
SyncUser: true,
LookUpParams: models.UserLookupParams{
UserID: ptrInt64(1),
Email: nil,
Login: nil,
},
},
},
},
@ -248,13 +250,13 @@ func TestUserSync_SyncUser(t *testing.T) {
Name: "test",
Email: "test",
IsGrafanaAdmin: ptrBool(false),
LookUpParams: models.UserLookupParams{
UserID: ptrInt64(1),
Email: nil,
Login: nil,
},
ClientParams: authn.ClientParams{
SyncUser: true,
LookUpParams: models.UserLookupParams{
UserID: ptrInt64(1),
Email: nil,
Login: nil,
},
},
},
},
@ -274,13 +276,13 @@ func TestUserSync_SyncUser(t *testing.T) {
Login: "test",
Name: "test",
Email: "test",
LookUpParams: models.UserLookupParams{
UserID: nil,
Email: nil,
Login: nil,
},
ClientParams: authn.ClientParams{
SyncUser: true,
LookUpParams: models.UserLookupParams{
UserID: nil,
Email: nil,
Login: nil,
},
},
},
},
@ -291,13 +293,13 @@ func TestUserSync_SyncUser(t *testing.T) {
Name: "test",
Email: "test",
IsGrafanaAdmin: ptrBool(false),
LookUpParams: models.UserLookupParams{
UserID: nil,
Email: nil,
Login: nil,
},
ClientParams: authn.ClientParams{
SyncUser: true,
LookUpParams: models.UserLookupParams{
UserID: nil,
Email: nil,
Login: nil,
},
},
},
},
@ -318,13 +320,13 @@ func TestUserSync_SyncUser(t *testing.T) {
Email: "test",
AuthModule: "oauth",
AuthID: "2032",
LookUpParams: models.UserLookupParams{
UserID: nil,
Email: nil,
Login: nil,
},
ClientParams: authn.ClientParams{
SyncUser: true,
LookUpParams: models.UserLookupParams{
UserID: nil,
Email: nil,
Login: nil,
},
},
},
},
@ -348,15 +350,15 @@ func TestUserSync_SyncUser(t *testing.T) {
Email: "test_create",
AuthModule: "oauth",
AuthID: "2032",
LookUpParams: models.UserLookupParams{
UserID: nil,
Email: ptrString("test_create"),
Login: nil,
},
ClientParams: authn.ClientParams{
SyncUser: true,
AllowSignUp: true,
EnableDisabledUsers: true,
LookUpParams: models.UserLookupParams{
UserID: nil,
Email: ptrString("test_create"),
Login: nil,
},
},
},
},
@ -369,15 +371,15 @@ func TestUserSync_SyncUser(t *testing.T) {
AuthModule: "oauth",
AuthID: "2032",
IsGrafanaAdmin: ptrBool(true),
LookUpParams: models.UserLookupParams{
UserID: nil,
Email: ptrString("test_create"),
Login: nil,
},
ClientParams: authn.ClientParams{
SyncUser: true,
AllowSignUp: true,
EnableDisabledUsers: true,
LookUpParams: models.UserLookupParams{
UserID: nil,
Email: ptrString("test_create"),
Login: nil,
},
},
},
},
@ -398,14 +400,14 @@ func TestUserSync_SyncUser(t *testing.T) {
Email: "test_mod",
IsDisabled: false,
IsGrafanaAdmin: ptrBool(true),
LookUpParams: models.UserLookupParams{
UserID: ptrInt64(3),
Email: nil,
Login: nil,
},
ClientParams: authn.ClientParams{
SyncUser: true,
EnableDisabledUsers: true,
LookUpParams: models.UserLookupParams{
UserID: ptrInt64(3),
Email: nil,
Login: nil,
},
},
},
},
@ -417,14 +419,14 @@ func TestUserSync_SyncUser(t *testing.T) {
Email: "test_mod",
IsDisabled: false,
IsGrafanaAdmin: ptrBool(true),
LookUpParams: models.UserLookupParams{
UserID: ptrInt64(3),
Email: nil,
Login: nil,
},
ClientParams: authn.ClientParams{
SyncUser: true,
EnableDisabledUsers: true,
LookUpParams: models.UserLookupParams{
UserID: ptrInt64(3),
Email: nil,
Login: nil,
},
},
},
},

@ -0,0 +1,208 @@
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/services/auth"
"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.Client = 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) 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: false,
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
}
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 neeeds 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 or email claim in JWT")
}
if s.cfg.JWTAuthAutoSignUp {
id.ClientParams.AllowSignUp = true
}
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
}
// The header is Authorization and the token does not look like a JWT,
// this is likely an API key. Pass it on.
if s.cfg.JWTAuthHeaderName == "Authorization" && !looksLikeJWT(jwtToken) {
return false
}
return true
}
func looksLikeJWT(token string) bool {
// A JWT must have 3 parts separated by `.`.
parts := strings.Split(token, ".")
return len(parts) == 3
}
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
}

@ -0,0 +1,175 @@
package clients
import (
"context"
"fmt"
"net/http"
"net/url"
"testing"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/models/roletype"
"github.com/grafana/grafana/pkg/services/authn"
"github.com/grafana/grafana/pkg/setting"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func stringPtr(s string) *string {
return &s
}
func TestAuthenticateJWT(t *testing.T) {
jwtService := &models.FakeJWTService{
VerifyProvider: func(context.Context, string) (models.JWTClaims, error) {
return models.JWTClaims{
"sub": "1234567890",
"email": "eai.doe@cor.po",
"preferred_username": "eai-doe",
"name": "Eai Doe",
"roles": "Admin",
}, nil
},
}
jwtHeaderName := "X-Forwarded-User"
wantID := &authn.Identity{
OrgID: 0,
OrgCount: 0,
OrgName: "",
OrgRoles: map[int64]roletype.RoleType{1: roletype.RoleAdmin},
ID: "",
Login: "eai-doe",
Name: "Eai Doe",
Email: "eai.doe@cor.po",
IsGrafanaAdmin: boolPtr(false),
AuthModule: "jwt",
AuthID: "1234567890",
IsDisabled: false,
HelpFlags1: 0,
ClientParams: authn.ClientParams{
SyncUser: true,
AllowSignUp: true,
SyncTeamMembers: true,
LookUpParams: models.UserLookupParams{
UserID: nil,
Email: stringPtr("eai.doe@cor.po"),
Login: stringPtr("eai-doe"),
},
},
}
cfg := &setting.Cfg{
JWTAuthEnabled: true,
JWTAuthHeaderName: jwtHeaderName,
JWTAuthEmailClaim: "email",
JWTAuthUsernameClaim: "preferred_username",
JWTAuthAutoSignUp: true,
JWTAuthAllowAssignGrafanaAdmin: true,
JWTAuthRoleAttributeStrict: true,
JWTAuthRoleAttributePath: "roles",
}
jwtClient := ProvideJWT(jwtService, cfg)
validHTTPReq := &http.Request{
Header: map[string][]string{
jwtHeaderName: {"sample-token"}},
}
id, err := jwtClient.Authenticate(context.Background(), &authn.Request{
OrgID: 1,
HTTPRequest: validHTTPReq,
Resp: nil,
})
require.NoError(t, err)
assert.EqualValues(t, wantID, id, fmt.Sprintf("%+v", id))
}
func TestJWTTest(t *testing.T) {
jwtService := &models.FakeJWTService{}
jwtHeaderName := "X-Forwarded-User"
validFormatToken := "sample.token.valid"
invalidFormatToken := "sampletokeninvalid"
type testCase struct {
desc string
reqHeaderName string
cfgHeaderName string
urlLogin bool
token string
want bool
}
testCases := []testCase{
{
desc: "valid",
reqHeaderName: jwtHeaderName,
cfgHeaderName: jwtHeaderName,
token: validFormatToken,
want: true,
},
{
desc: "not in the right header",
reqHeaderName: "other-header",
cfgHeaderName: jwtHeaderName,
token: validFormatToken,
want: false,
},
{
desc: "valid format in Authorization",
reqHeaderName: "Authorization",
cfgHeaderName: "Authorization",
token: validFormatToken,
want: true,
},
{
desc: "invalid format in Authorization",
reqHeaderName: "Authorization",
cfgHeaderName: "Authorization",
token: invalidFormatToken,
want: false,
},
{
desc: "url login enabled",
reqHeaderName: "other-header",
cfgHeaderName: jwtHeaderName,
urlLogin: true,
token: validFormatToken,
want: true,
},
{
desc: "url login enabled",
reqHeaderName: "other-header",
cfgHeaderName: jwtHeaderName,
urlLogin: false,
token: validFormatToken,
want: false,
},
}
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
cfg := &setting.Cfg{
JWTAuthEnabled: true,
JWTAuthURLLogin: tc.urlLogin,
JWTAuthHeaderName: tc.cfgHeaderName,
JWTAuthAutoSignUp: true,
JWTAuthAllowAssignGrafanaAdmin: true,
JWTAuthRoleAttributeStrict: true,
}
jwtClient := ProvideJWT(jwtService, cfg)
httpReq := &http.Request{
URL: &url.URL{RawQuery: "auth_token=" + tc.token},
Header: map[string][]string{
tc.reqHeaderName: {tc.token}},
}
got := jwtClient.Test(context.Background(), &authn.Request{
OrgID: 1,
HTTPRequest: httpReq,
Resp: nil,
})
require.Equal(t, tc.want, got)
})
}
}

@ -48,16 +48,16 @@ func (c *LDAP) AuthenticatePassword(ctx context.Context, orgID int64, username,
IsGrafanaAdmin: info.IsGrafanaAdmin,
AuthModule: info.AuthModule,
AuthID: info.AuthId,
LookUpParams: models.UserLookupParams{
Login: &info.Login,
Email: &info.Email,
},
Groups: info.Groups,
Groups: info.Groups,
ClientParams: authn.ClientParams{
SyncUser: true,
SyncTeamMembers: true,
AllowSignUp: c.cfg.LDAPAllowSignup,
EnableDisabledUsers: true,
LookUpParams: models.UserLookupParams{
Login: &info.Login,
Email: &info.Email,
},
},
}, nil
}

@ -52,10 +52,10 @@ func TestLDAP_AuthenticatePassword(t *testing.T) {
SyncTeamMembers: true,
AllowSignUp: false,
EnableDisabledUsers: true,
},
LookUpParams: models.UserLookupParams{
Email: strPtr("test@test.com"),
Login: strPtr("test"),
LookUpParams: models.UserLookupParams{
Email: strPtr("test@test.com"),
Login: strPtr("test"),
},
},
},
},

@ -7,7 +7,6 @@ import (
"testing"
"time"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/models/roletype"
"github.com/grafana/grafana/pkg/models/usertoken"
"github.com/grafana/grafana/pkg/services/auth"
@ -104,7 +103,6 @@ func TestSession_Authenticate(t *testing.T) {
Email: "sample_user@samples.iwz",
OrgID: 1,
OrgRoles: map[int64]roletype.RoleType{1: roletype.RoleEditor},
LookUpParams: models.UserLookupParams{},
IsGrafanaAdmin: boolPtr(false),
},
wantErr: false,

@ -8,6 +8,8 @@ import (
"github.com/grafana/grafana/pkg/login"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/authn"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/user"
"github.com/jmespath/go-jmespath"
@ -20,6 +22,27 @@ const (
)
func (h *ContextHandler) initContextWithJWT(ctx *models.ReqContext, orgId int64) bool {
if h.features.IsEnabled(featuremgmt.FlagAuthnService) {
identity, ok, err := h.authnService.Authenticate(ctx.Req.Context(),
authn.ClientJWT,
&authn.Request{HTTPRequest: ctx.Req, Resp: ctx.Resp, OrgID: orgId})
if !ok {
return false
}
newCtx := WithAuthHTTPHeader(ctx.Req.Context(), h.Cfg.JWTAuthHeaderName)
*ctx.Req = *ctx.Req.WithContext(newCtx)
if err != nil {
writeErr(ctx, err)
return true
}
ctx.SignedInUser = identity.SignedInUser()
ctx.IsSignedIn = true
return true
}
if !h.Cfg.JWTAuthEnabled || h.Cfg.JWTAuthHeaderName == "" {
return false
}

@ -77,7 +77,7 @@ func ProvideService(cfg *setting.Cfg, tokenService auth.UserTokenService, jwtSer
type ContextHandler struct {
Cfg *setting.Cfg
AuthTokenService auth.UserTokenService
JWTAuthService models.JWTService
JWTAuthService auth.JWTVerifierService
RemoteCache *remotecache.RemoteCache
RenderService rendering.Service
SQLStore db.DB

@ -21,6 +21,7 @@ const (
SAMLAuthModule = "auth.saml"
LDAPAuthModule = "ldap"
AuthProxyAuthModule = "authproxy"
JWTModule = "jwt"
)
func GetAuthProviderLabel(authModule string) string {
@ -39,7 +40,7 @@ func GetAuthProviderLabel(authModule string) string {
return "SAML"
case LDAPAuthModule, "": // FIXME: verify this situation doesn't exist anymore
return "LDAP"
case "jwt":
case JWTModule:
return "JWT"
case AuthProxyAuthModule:
return "Auth Proxy"

@ -217,7 +217,11 @@ func (ss *sqlStore) GetByLogin(ctx context.Context, query *user.GetUserByLoginQu
return nil
})
return usr, err
if err != nil {
return nil, err
}
return usr, nil
}
func (ss *sqlStore) GetByEmail(ctx context.Context, query *user.GetUserByEmailQuery) (*user.User, error) {

Loading…
Cancel
Save