Auth: implement auto_sign_up for auth.jwt (#43502)

Co-authored-by: James Brown <jbrown@easypost.com>
pull/44055/head
Emil Tullstedt 4 years ago committed by GitHub
parent 45287b4129
commit 25736b6afb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      conf/defaults.ini
  2. 1
      conf/sample.ini
  3. 5
      docs/sources/auth/jwt.md
  4. 77
      pkg/middleware/middleware_jwt_auth_test.go
  5. 34
      pkg/services/contexthandler/auth_jwt.go
  6. 2
      pkg/services/searchusers/searchusers.go
  7. 2
      pkg/setting/setting.go

@ -564,6 +564,7 @@ jwk_set_file =
cache_ttl = 60m
expected_claims = {}
key_file =
auto_sign_up = false
#################################### Auth LDAP ###########################
[auth.ldap]

@ -548,6 +548,7 @@
;cache_ttl = 60m
;expected_claims = {"aud": ["foo", "bar"]}
;key_file = /path/to/key/file
;auto_sign_up = false
#################################### Auth LDAP ##########################
[auth.ldap]

@ -44,8 +44,13 @@ username_claim = sub
# Specify a claim to use as an email to sign in.
email_claim = sub
# auto-create users if they are not already matched
# auto_sign_up = true
```
If `auto_sign_up` is enabled, then the `sub` claim is used as the "external Auth ID". The `name` claim is used as the user's full name if it is present.
## Signature verification
JSON web token integrity needs to be verified so cryptographic signature is used for this purpose. So we expect that every token must be signed with some known cryptographic key.

@ -5,11 +5,12 @@ import (
"errors"
"testing"
"github.com/stretchr/testify/assert"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/contexthandler"
"github.com/grafana/grafana/pkg/setting"
"github.com/stretchr/testify/assert"
)
func TestMiddlewareJWTAuth(t *testing.T) {
@ -29,6 +30,10 @@ func TestMiddlewareJWTAuth(t *testing.T) {
cfg.JWTAuthEmailClaim = "foo-email"
}
configureAutoSignUp := func(cfg *setting.Cfg) {
cfg.JWTAuthAutoSignUp = true
}
token := "some-token"
middlewareScenario(t, "Valid token with valid login claim", func(t *testing.T, sc *scenarioContext) {
@ -37,6 +42,7 @@ func TestMiddlewareJWTAuth(t *testing.T) {
sc.jwtAuthService.VerifyProvider = func(ctx context.Context, token string) (models.JWTClaims, error) {
verifiedToken = token
return models.JWTClaims{
"sub": myUsername,
"foo-username": myUsername,
}, nil
}
@ -64,6 +70,7 @@ func TestMiddlewareJWTAuth(t *testing.T) {
sc.jwtAuthService.VerifyProvider = func(ctx context.Context, token string) (models.JWTClaims, error) {
verifiedToken = token
return models.JWTClaims{
"sub": myEmail,
"foo-email": myEmail,
}, nil
}
@ -85,11 +92,72 @@ func TestMiddlewareJWTAuth(t *testing.T) {
assert.Equal(t, myEmail, sc.context.Email)
}, configure, configureEmailClaim)
middlewareScenario(t, "Valid token with no user and auto_sign_up disabled", func(t *testing.T, sc *scenarioContext) {
myEmail := "vladimir@example.com"
var verifiedToken string
sc.jwtAuthService.VerifyProvider = func(ctx context.Context, token string) (models.JWTClaims, error) {
verifiedToken = token
return models.JWTClaims{
"sub": myEmail,
"name": "Vladimir Example",
"foo-email": myEmail,
}, nil
}
bus.AddHandler("get-sign-user", func(ctx context.Context, query *models.GetSignedInUserQuery) error {
return models.ErrUserNotFound
})
sc.fakeReq("GET", "/").withJWTAuthHeader(token).exec()
assert.Equal(t, verifiedToken, token)
assert.Equal(t, 401, sc.resp.Code)
assert.Equal(t, contexthandler.UserNotFound, sc.respJson["message"])
}, configure, configureEmailClaim)
middlewareScenario(t, "Valid token with no user and auto_sign_up enabled", func(t *testing.T, sc *scenarioContext) {
myEmail := "vladimir@example.com"
var verifiedToken string
sc.jwtAuthService.VerifyProvider = func(ctx context.Context, token string) (models.JWTClaims, error) {
verifiedToken = token
return models.JWTClaims{
"sub": myEmail,
"name": "Vladimir Example",
"foo-email": myEmail,
}, nil
}
bus.AddHandler("get-sign-user", func(ctx context.Context, query *models.GetSignedInUserQuery) error {
query.Result = &models.SignedInUser{
UserId: id,
OrgId: orgID,
Email: query.Email,
}
return nil
})
bus.AddHandler("upsert-user", func(ctx context.Context, command *models.UpsertUserCommand) error {
command.Result = &models.User{
Id: id,
Name: command.ExternalUser.Name,
Email: command.ExternalUser.Email,
}
return nil
})
sc.fakeReq("GET", "/").withJWTAuthHeader(token).exec()
assert.Equal(t, verifiedToken, token)
assert.Equal(t, 200, sc.resp.Code)
assert.True(t, sc.context.IsSignedIn)
assert.Equal(t, orgID, sc.context.OrgId)
assert.Equal(t, id, sc.context.UserId)
assert.Equal(t, myEmail, sc.context.Email)
}, configure, configureEmailClaim, configureAutoSignUp)
middlewareScenario(t, "Valid token without a login claim", func(t *testing.T, sc *scenarioContext) {
var verifiedToken string
sc.jwtAuthService.VerifyProvider = func(ctx context.Context, token string) (models.JWTClaims, error) {
verifiedToken = token
return models.JWTClaims{"foo": "bar"}, nil
return models.JWTClaims{
"sub": "baz",
"foo": "bar",
}, nil
}
sc.fakeReq("GET", "/").withJWTAuthHeader(token).exec()
@ -102,7 +170,10 @@ func TestMiddlewareJWTAuth(t *testing.T) {
var verifiedToken string
sc.jwtAuthService.VerifyProvider = func(ctx context.Context, token string) (models.JWTClaims, error) {
verifiedToken = token
return models.JWTClaims{"foo": "bar"}, nil
return models.JWTClaims{
"sub": "baz",
"foo": "bar",
}, nil
}
sc.fakeReq("GET", "/").withJWTAuthHeader(token).exec()

@ -9,6 +9,7 @@ import (
)
const InvalidJWT = "Invalid JWT"
const UserNotFound = "User not found"
func (h *ContextHandler) initContextWithJWT(ctx *models.ReqContext, orgId int64) bool {
if !h.Cfg.JWTAuthEnabled || h.Cfg.JWTAuthHeaderName == "" {
@ -29,11 +30,29 @@ func (h *ContextHandler) initContextWithJWT(ctx *models.ReqContext, orgId int64)
query := models.GetSignedInUserQuery{OrgId: orgId}
sub, _ := claims["sub"].(string)
if sub == "" {
ctx.Logger.Warn("Got a JWT without the mandatory 'sub' claim", "error", err)
ctx.JsonApiErr(401, InvalidJWT, err)
return true
}
extUser := &models.ExternalUserInfo{
AuthModule: "jwt",
AuthId: sub,
}
if key := h.Cfg.JWTAuthUsernameClaim; key != "" {
query.Login, _ = claims[key].(string)
extUser.Login, _ = claims[key].(string)
}
if key := h.Cfg.JWTAuthEmailClaim; key != "" {
query.Email, _ = claims[key].(string)
extUser.Email, _ = claims[key].(string)
}
if name, _ := claims["name"].(string); name != "" {
extUser.Name = name
}
if query.Login == "" && query.Email == "" {
@ -42,6 +61,18 @@ func (h *ContextHandler) initContextWithJWT(ctx *models.ReqContext, orgId int64)
return true
}
if h.Cfg.JWTAuthAutoSignUp {
upsert := &models.UpsertUserCommand{
ReqContext: ctx,
SignupAllowed: h.Cfg.JWTAuthAutoSignUp,
ExternalUser: extUser,
}
if err := bus.Dispatch(ctx.Req.Context(), upsert); err != nil {
ctx.Logger.Error("Failed to upsert JWT user", "error", err)
return false
}
}
if err := bus.Dispatch(ctx.Req.Context(), &query); err != nil {
if errors.Is(err, models.ErrUserNotFound) {
ctx.Logger.Debug(
@ -50,10 +81,11 @@ func (h *ContextHandler) initContextWithJWT(ctx *models.ReqContext, orgId int64)
"username_claim", query.Login,
)
err = login.ErrInvalidCredentials
ctx.JsonApiErr(401, UserNotFound, err)
} else {
ctx.Logger.Error("Failed to get signed in user", "error", err)
ctx.JsonApiErr(401, InvalidJWT, err)
}
ctx.JsonApiErr(401, InvalidJWT, err)
return true
}

@ -96,6 +96,8 @@ func GetAuthProviderLabel(authModule string) string {
return "SAML"
case "ldap", "":
return "LDAP"
case "jwt":
return "JWT"
default:
return "OAuth"
}

@ -319,6 +319,7 @@ type Cfg struct {
JWTAuthCacheTTL time.Duration
JWTAuthKeyFile string
JWTAuthJWKSetFile string
JWTAuthAutoSignUp bool
// Dataproxy
SendUserHeader bool
@ -1308,6 +1309,7 @@ func readAuthSettings(iniFile *ini.File, cfg *Cfg) (err error) {
cfg.JWTAuthCacheTTL = authJWT.Key("cache_ttl").MustDuration(time.Minute * 60)
cfg.JWTAuthKeyFile = valueAsString(authJWT, "key_file", "")
cfg.JWTAuthJWKSetFile = valueAsString(authJWT, "jwk_set_file", "")
cfg.JWTAuthAutoSignUp = authJWT.Key("auto_sign_up").MustBool(false)
authProxy := iniFile.Section("auth.proxy")
AuthProxyEnabled = authProxy.Key("enabled").MustBool(false)

Loading…
Cancel
Save