mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
JWT: Find login and email claims with JMESPATH (#85305)
* add function to static function to static service * find email and login claims with jmespath * rename configuration files * Replace JWTClaims struct for map * check for subclaims error
This commit is contained in:
parent
18f3c7188b
commit
e4250a72db
@ -852,6 +852,8 @@ enable_login_token = false
|
||||
header_name =
|
||||
email_claim =
|
||||
username_claim =
|
||||
email_attribute_path =
|
||||
username_attribute_path =
|
||||
jwk_set_url =
|
||||
jwk_set_file =
|
||||
cache_ttl = 60m
|
||||
|
@ -775,6 +775,8 @@
|
||||
;header_name = X-JWT-Assertion
|
||||
;email_claim = sub
|
||||
;username_claim = sub
|
||||
;email_attribute_path = jmespath.email
|
||||
;username_attribute_path = jmespath.username
|
||||
;jwk_set_url = https://foo.bar/.well-known/jwks.json
|
||||
;jwk_set_file = /path/to/jwks.json
|
||||
;cache_ttl = 60m
|
||||
|
@ -62,6 +62,32 @@ email_claim = sub
|
||||
|
||||
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.
|
||||
|
||||
Additionally, if the login username or the email claims are nested inside the JWT structure, you can specify the path to the attributes using the `username_attribute_path` and `email_attribute_path` configuration options using the JMESPath syntax.
|
||||
|
||||
JWT structure example.
|
||||
|
||||
```json
|
||||
{
|
||||
"user": {
|
||||
"UID": "1234567890",
|
||||
"name": "John Doe",
|
||||
"username": "johndoe",
|
||||
"emails": ["personal@email.com", "professional@email.com"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```ini
|
||||
# [auth.jwt]
|
||||
# ...
|
||||
|
||||
# Specify a nested attribute to use as a username to sign in.
|
||||
username_attribute_path = user.username # user's login is johndoe
|
||||
|
||||
# Specify a nested attribute to use as an email to sign in.
|
||||
email_attribute_path = user.emails[1] # user's email is professional@email.com
|
||||
```
|
||||
|
||||
## Iframe Embedding
|
||||
|
||||
If you want to embed Grafana in an iframe while maintaining user identity and role checks,
|
||||
|
@ -65,7 +65,7 @@ func sanitizeJWT(jwtToken string) string {
|
||||
return strings.ReplaceAll(jwtToken, string(base64.StdPadding), "")
|
||||
}
|
||||
|
||||
func (s *AuthService) Verify(ctx context.Context, strToken string) (JWTClaims, error) {
|
||||
func (s *AuthService) Verify(ctx context.Context, strToken string) (map[string]any, error) {
|
||||
s.log.Debug("Parsing JSON Web Token")
|
||||
|
||||
strToken = sanitizeJWT(strToken)
|
||||
@ -84,7 +84,7 @@ func (s *AuthService) Verify(ctx context.Context, strToken string) (JWTClaims, e
|
||||
|
||||
s.log.Debug("Trying to verify JSON Web Token using a key")
|
||||
|
||||
var claims JWTClaims
|
||||
var claims map[string]any
|
||||
for _, key := range keys {
|
||||
if err = token.Claims(key, &claims); err == nil {
|
||||
break
|
||||
|
@ -2,28 +2,24 @@ package jwt
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
type JWTClaims util.DynMap
|
||||
|
||||
type JWTService interface {
|
||||
Verify(ctx context.Context, strToken string) (JWTClaims, error)
|
||||
Verify(ctx context.Context, strToken string) (map[string]any, error)
|
||||
}
|
||||
|
||||
type FakeJWTService struct {
|
||||
VerifyProvider func(context.Context, string) (JWTClaims, error)
|
||||
VerifyProvider func(context.Context, string) (map[string]any, error)
|
||||
}
|
||||
|
||||
func (s *FakeJWTService) Verify(ctx context.Context, token string) (JWTClaims, error) {
|
||||
func (s *FakeJWTService) Verify(ctx context.Context, token string) (map[string]any, error) {
|
||||
return s.VerifyProvider(ctx, token)
|
||||
}
|
||||
|
||||
func NewFakeJWTService() *FakeJWTService {
|
||||
return &FakeJWTService{
|
||||
VerifyProvider: func(ctx context.Context, token string) (JWTClaims, error) {
|
||||
return JWTClaims{}, nil
|
||||
VerifyProvider: func(ctx context.Context, token string) (map[string]any, error) {
|
||||
return map[string]any{}, nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -52,7 +52,7 @@ func (s *AuthService) initClaimExpectations() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *AuthService) validateClaims(claims JWTClaims) error {
|
||||
func (s *AuthService) validateClaims(claims map[string]any) error {
|
||||
var registeredClaims jwt.Claims
|
||||
for key, value := range claims {
|
||||
switch key {
|
||||
|
@ -78,10 +78,23 @@ func (s *JWT) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identi
|
||||
if key := s.cfg.JWTAuth.UsernameClaim; key != "" {
|
||||
id.Login, _ = claims[key].(string)
|
||||
id.ClientParams.LookUpParams.Login = &id.Login
|
||||
} else if key := s.cfg.JWTAuth.UsernameAttributePath; key != "" {
|
||||
id.Login, err = util.SearchJSONForStringAttr(s.cfg.JWTAuth.UsernameAttributePath, claims)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
id.ClientParams.LookUpParams.Login = &id.Login
|
||||
}
|
||||
|
||||
if key := s.cfg.JWTAuth.EmailClaim; key != "" {
|
||||
id.Email, _ = claims[key].(string)
|
||||
id.ClientParams.LookUpParams.Email = &id.Email
|
||||
} else if key := s.cfg.JWTAuth.EmailAttributePath; key != "" {
|
||||
id.Email, err = util.SearchJSONForStringAttr(s.cfg.JWTAuth.EmailAttributePath, claims)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
id.ClientParams.LookUpParams.Email = &id.Email
|
||||
}
|
||||
|
||||
if name, _ := claims["name"].(string); name != "" {
|
||||
|
@ -30,7 +30,7 @@ func TestAuthenticateJWT(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
wantID *authn.Identity
|
||||
verifyProvider func(context.Context, string) (jwt.JWTClaims, error)
|
||||
verifyProvider func(context.Context, string) (map[string]any, error)
|
||||
cfg *setting.Cfg
|
||||
}{
|
||||
{
|
||||
@ -63,8 +63,8 @@ func TestAuthenticateJWT(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
verifyProvider: func(context.Context, string) (jwt.JWTClaims, error) {
|
||||
return jwt.JWTClaims{
|
||||
verifyProvider: func(context.Context, string) (map[string]any, error) {
|
||||
return map[string]any{
|
||||
"sub": "1234567890",
|
||||
"email": "eai.doe@cor.po",
|
||||
"preferred_username": "eai-doe",
|
||||
@ -117,8 +117,8 @@ func TestAuthenticateJWT(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
verifyProvider: func(context.Context, string) (jwt.JWTClaims, error) {
|
||||
return jwt.JWTClaims{
|
||||
verifyProvider: func(context.Context, string) (map[string]any, error) {
|
||||
return map[string]any{
|
||||
"sub": "1234567890",
|
||||
"email": "eai.doe@cor.po",
|
||||
"preferred_username": "eai-doe",
|
||||
@ -171,8 +171,8 @@ func TestAuthenticateJWT(t *testing.T) {
|
||||
func TestJWTClaimConfig(t *testing.T) {
|
||||
t.Parallel()
|
||||
jwtService := &jwt.FakeJWTService{
|
||||
VerifyProvider: func(context.Context, string) (jwt.JWTClaims, error) {
|
||||
return jwt.JWTClaims{
|
||||
VerifyProvider: func(context.Context, string) (map[string]any, error) {
|
||||
return map[string]any{
|
||||
"sub": "1234567890",
|
||||
"email": "eai.doe@cor.po",
|
||||
"preferred_username": "eai-doe",
|
||||
@ -399,8 +399,8 @@ func TestJWTTest(t *testing.T) {
|
||||
func TestJWTStripParam(t *testing.T) {
|
||||
t.Parallel()
|
||||
jwtService := &jwt.FakeJWTService{
|
||||
VerifyProvider: func(context.Context, string) (jwt.JWTClaims, error) {
|
||||
return jwt.JWTClaims{
|
||||
VerifyProvider: func(context.Context, string) (map[string]any, error) {
|
||||
return map[string]any{
|
||||
"sub": "1234567890",
|
||||
"email": "eai.doe@cor.po",
|
||||
"preferred_username": "eai-doe",
|
||||
@ -442,3 +442,61 @@ func TestJWTStripParam(t *testing.T) {
|
||||
// auth_token should be removed from the query string
|
||||
assert.Equal(t, "other_param=other_value", httpReq.URL.RawQuery)
|
||||
}
|
||||
|
||||
func TestJWTSubClaimsConfig(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// #nosec G101 -- This is a dummy/test token
|
||||
token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ2ZXIiOiIxLjAiLCJpc3MiOiJodHRwczovL2F6dXJlZG9tYWlubmFtZS5iMmNsb2dpbi5jb20vNjIwYjI2MzQtYmI4OC00MzdiLTgwYWQtYWM0YTkwZGZkZTkxL3YyLjAvIiwic3ViIjoiOWI4OTg5MDgtMWFlYy00NDc1LTljNDgtNzg1MWQyNjVkZGIxIiwiYXVkIjoiYmEyNzM0NDktMmZiNS00YTRhLTlmODItYTA2MTRhM2MxODQ1IiwiZXhwIjoxNzExNTYwMDcxLCJub25jZSI6ImRlZmF1bHROb25jZSIsImlhdCI6MTcxMTU1NjQ3MSwiYXV0aF90aW1lIjoxNzExNTU2NDcxLCJuYW1lIjoibmFtZV9vZl90aGVfdXNlciIsImdpdmVuX25hbWUiOiJVc2VyTmFtZSIsImZhbWlseV9uYW1lIjoiVXNlclN1cm5hbWUiLCJlbWFpbHMiOlsibWFpbmVtYWlsK2V4dHJhZW1haWwwNUBnbWFpbC5jb20iLCJtYWluZW1haWwrZXh0cmFlbWFpbDA0QGdtYWlsLmNvbSIsIm1haW5lbWFpbCtleHRyYWVtYWlsMDNAZ21haWwuY29tIiwibWFpbmVtYWlsK2V4dHJhZW1haWwwMkBnbWFpbC5jb20iLCJtYWluZW1haWwrZXh0cmFlbWFpbDAxQGdtYWlsLmNvbSIsIm1haW5lbWFpbEBnbWFpbC5jb20iXSwidGZwIjoiQjJDXzFfdXNlcmZsb3ciLCJuYmYiOjE3MTE1NTY0NzF9.qpN3upxUB5CTJ7kmYPHFuhlwG95vdQqJaDDC_8KJFZ8"
|
||||
jwtHeaderName := "X-Forwarded-User"
|
||||
response := map[string]any{
|
||||
"ver": "1.0",
|
||||
"iss": "https://azuredomainname.b2clogin.com/620b2634-bb88-437b-80ad-ac4a90dfde91/v2.0/",
|
||||
"sub": "9b898908-1aec-4475-9c48-7851d265ddb1",
|
||||
"aud": "ba273449-2fb5-4a4a-9f82-a0614a3c1845",
|
||||
"exp": 1711560071,
|
||||
"nonce": "defaultNonce",
|
||||
"iat": 1711556471,
|
||||
"auth_time": 1711556471,
|
||||
"name": "name_of_the_user",
|
||||
"given_name": "UserName",
|
||||
"family_name": "UserSurname",
|
||||
"emails": []string{
|
||||
"mainemail+extraemail04@gmail.com",
|
||||
"mainemail+extraemail03@gmail.com",
|
||||
"mainemail+extraemail02@gmail.com",
|
||||
"mainemail+extraemail01@gmail.com",
|
||||
"mainemail@gmail.com",
|
||||
},
|
||||
"tfp": "B2C_1_userflow",
|
||||
"nbf": 1711556471,
|
||||
}
|
||||
cfg := &setting.Cfg{
|
||||
JWTAuth: setting.AuthJWTSettings{
|
||||
HeaderName: jwtHeaderName,
|
||||
EmailAttributePath: "emails[2]",
|
||||
UsernameAttributePath: "name",
|
||||
},
|
||||
}
|
||||
httpReq := &http.Request{
|
||||
URL: &url.URL{RawQuery: "auth_token=" + token},
|
||||
Header: map[string][]string{
|
||||
jwtHeaderName: {token}},
|
||||
}
|
||||
jwtService := &jwt.FakeJWTService{
|
||||
VerifyProvider: func(context.Context, string) (map[string]any, error) {
|
||||
return response, nil
|
||||
},
|
||||
}
|
||||
|
||||
jwtClient := ProvideJWT(jwtService, cfg)
|
||||
identity, err := jwtClient.Authenticate(context.Background(), &authn.Request{
|
||||
OrgID: 1,
|
||||
HTTPRequest: httpReq,
|
||||
Resp: nil,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "mainemail+extraemail02@gmail.com", identity.Email)
|
||||
require.Equal(t, "name_of_the_user", identity.Name)
|
||||
fmt.Println("identity.Email", identity.Email)
|
||||
}
|
||||
|
@ -21,6 +21,8 @@ type AuthJWTSettings struct {
|
||||
AllowAssignGrafanaAdmin bool
|
||||
SkipOrgRoleSync bool
|
||||
GroupsAttributePath string
|
||||
EmailAttributePath string
|
||||
UsernameAttributePath string
|
||||
}
|
||||
|
||||
func (cfg *Cfg) readAuthJWTSettings() {
|
||||
@ -43,6 +45,8 @@ func (cfg *Cfg) readAuthJWTSettings() {
|
||||
jwtSettings.AllowAssignGrafanaAdmin = authJWT.Key("allow_assign_grafana_admin").MustBool(false)
|
||||
jwtSettings.SkipOrgRoleSync = authJWT.Key("skip_org_role_sync").MustBool(false)
|
||||
jwtSettings.GroupsAttributePath = valueAsString(authJWT, "groups_attribute_path", "")
|
||||
jwtSettings.EmailAttributePath = valueAsString(authJWT, "email_attribute_path", "")
|
||||
jwtSettings.UsernameAttributePath = valueAsString(authJWT, "username_attribute_path", "")
|
||||
|
||||
cfg.JWTAuth = jwtSettings
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user