[v11.0.x] JWT: Find login and email claims with JMESPATH (#86357)

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

(cherry picked from commit e4250a72db)

Co-authored-by: linoman <2051016+linoman@users.noreply.github.com>
This commit is contained in:
grafana-delivery-bot[bot]
2024-04-16 16:25:18 +01:00
committed by GitHub
co-authored by linoman
parent ca94886e18
commit 6f6667d89c
9 changed files with 122 additions and 21 deletions
+2 -2
View File
@@ -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
+5 -9
View File
@@ -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
},
}
}
+1 -1
View File
@@ -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 {
+13
View File
@@ -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 != "" {
+67 -9
View File
@@ -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)
}
+4
View File
@@ -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
}