| 
									
										
										
										
											2023-01-10 22:08:52 +08:00
										 |  |  | package clients | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import ( | 
					
						
							|  |  |  | 	"context" | 
					
						
							|  |  |  | 	"fmt" | 
					
						
							|  |  |  | 	"net/http" | 
					
						
							|  |  |  | 	"net/url" | 
					
						
							|  |  |  | 	"testing" | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-01-16 17:50:34 +08:00
										 |  |  | 	"github.com/stretchr/testify/assert" | 
					
						
							|  |  |  | 	"github.com/stretchr/testify/require" | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-01-10 22:08:52 +08:00
										 |  |  | 	"github.com/grafana/grafana/pkg/models" | 
					
						
							|  |  |  | 	"github.com/grafana/grafana/pkg/models/roletype" | 
					
						
							| 
									
										
										
										
											2023-01-21 02:11:06 +08:00
										 |  |  | 	"github.com/grafana/grafana/pkg/services/auth/jwt" | 
					
						
							| 
									
										
										
										
											2023-01-10 22:08:52 +08:00
										 |  |  | 	"github.com/grafana/grafana/pkg/services/authn" | 
					
						
							|  |  |  | 	"github.com/grafana/grafana/pkg/setting" | 
					
						
							|  |  |  | ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func stringPtr(s string) *string { | 
					
						
							|  |  |  | 	return &s | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func TestAuthenticateJWT(t *testing.T) { | 
					
						
							| 
									
										
										
										
											2023-01-21 02:11:06 +08:00
										 |  |  | 	jwtService := &jwt.FakeJWTService{ | 
					
						
							|  |  |  | 		VerifyProvider: func(context.Context, string) (jwt.JWTClaims, error) { | 
					
						
							|  |  |  | 			return jwt.JWTClaims{ | 
					
						
							| 
									
										
										
										
											2023-01-10 22:08:52 +08:00
										 |  |  | 				"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{ | 
					
						
							| 
									
										
										
										
											2023-01-27 23:05:25 +08:00
										 |  |  | 			SyncTeamMembers: false, | 
					
						
							| 
									
										
										
										
											2023-01-10 22:08:52 +08:00
										 |  |  | 			SyncUser:        true, | 
					
						
							|  |  |  | 			AllowSignUp:     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)) | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-01-19 23:03:09 +08:00
										 |  |  | func TestJWTClaimConfig(t *testing.T) { | 
					
						
							| 
									
										
										
										
											2023-01-21 02:11:06 +08:00
										 |  |  | 	jwtService := &jwt.FakeJWTService{ | 
					
						
							|  |  |  | 		VerifyProvider: func(context.Context, string) (jwt.JWTClaims, error) { | 
					
						
							|  |  |  | 			return jwt.JWTClaims{ | 
					
						
							| 
									
										
										
										
											2023-01-19 23:03:09 +08:00
										 |  |  | 				"sub":                "1234567890", | 
					
						
							|  |  |  | 				"email":              "eai.doe@cor.po", | 
					
						
							|  |  |  | 				"preferred_username": "eai-doe", | 
					
						
							|  |  |  | 				"name":               "Eai Doe", | 
					
						
							|  |  |  | 				"roles":              "Admin", | 
					
						
							|  |  |  | 			}, nil | 
					
						
							|  |  |  | 		}, | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	jwtHeaderName := "X-Forwarded-User" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	cfg := &setting.Cfg{ | 
					
						
							|  |  |  | 		JWTAuthEnabled:                 true, | 
					
						
							|  |  |  | 		JWTAuthHeaderName:              jwtHeaderName, | 
					
						
							|  |  |  | 		JWTAuthAutoSignUp:              true, | 
					
						
							|  |  |  | 		JWTAuthAllowAssignGrafanaAdmin: true, | 
					
						
							|  |  |  | 		JWTAuthRoleAttributeStrict:     true, | 
					
						
							|  |  |  | 		JWTAuthRoleAttributePath:       "roles", | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// #nosec G101 -- This is a dummy/test token
 | 
					
						
							|  |  |  | 	token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.XbPfbIHMI6arZ3Y922BhjWgQzWXcXNrz0ogtVhfEd2o" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	type Dictionary map[string]interface{} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	type testCase struct { | 
					
						
							|  |  |  | 		desc                 string | 
					
						
							|  |  |  | 		claimsConfigurations []Dictionary | 
					
						
							|  |  |  | 		valid                bool | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	testCases := []testCase{ | 
					
						
							|  |  |  | 		{ | 
					
						
							|  |  |  | 			desc: "JWT configuration with email and username claims", | 
					
						
							|  |  |  | 			claimsConfigurations: []Dictionary{ | 
					
						
							|  |  |  | 				{ | 
					
						
							|  |  |  | 					"JWTAuthEmailClaim":    true, | 
					
						
							|  |  |  | 					"JWTAuthUsernameClaim": true, | 
					
						
							|  |  |  | 				}, | 
					
						
							|  |  |  | 			}, | 
					
						
							|  |  |  | 			valid: true, | 
					
						
							|  |  |  | 		}, | 
					
						
							|  |  |  | 		{ | 
					
						
							|  |  |  | 			desc: "JWT configuration with email claim", | 
					
						
							|  |  |  | 			claimsConfigurations: []Dictionary{ | 
					
						
							|  |  |  | 				{ | 
					
						
							|  |  |  | 					"JWTAuthEmailClaim":    true, | 
					
						
							|  |  |  | 					"JWTAuthUsernameClaim": false, | 
					
						
							|  |  |  | 				}, | 
					
						
							|  |  |  | 			}, | 
					
						
							|  |  |  | 			valid: true, | 
					
						
							|  |  |  | 		}, | 
					
						
							|  |  |  | 		{ | 
					
						
							|  |  |  | 			desc: "JWT configuration with username claim", | 
					
						
							|  |  |  | 			claimsConfigurations: []Dictionary{ | 
					
						
							|  |  |  | 				{ | 
					
						
							|  |  |  | 					"JWTAuthEmailClaim":    false, | 
					
						
							|  |  |  | 					"JWTAuthUsernameClaim": true, | 
					
						
							|  |  |  | 				}, | 
					
						
							|  |  |  | 			}, | 
					
						
							|  |  |  | 			valid: true, | 
					
						
							|  |  |  | 		}, | 
					
						
							|  |  |  | 		{ | 
					
						
							|  |  |  | 			desc: "JWT configuration without email and username claims", | 
					
						
							|  |  |  | 			claimsConfigurations: []Dictionary{ | 
					
						
							|  |  |  | 				{ | 
					
						
							|  |  |  | 					"JWTAuthEmailClaim":    false, | 
					
						
							|  |  |  | 					"JWTAuthUsernameClaim": false, | 
					
						
							|  |  |  | 				}, | 
					
						
							|  |  |  | 			}, | 
					
						
							|  |  |  | 			valid: false, | 
					
						
							|  |  |  | 		}, | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	for _, tc := range testCases { | 
					
						
							|  |  |  | 		t.Run(tc.desc, func(t *testing.T) { | 
					
						
							|  |  |  | 			for _, claims := range tc.claimsConfigurations { | 
					
						
							|  |  |  | 				cfg.JWTAuthEmailClaim = "" | 
					
						
							|  |  |  | 				cfg.JWTAuthUsernameClaim = "" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				if claims["JWTAuthEmailClaim"] == true { | 
					
						
							|  |  |  | 					cfg.JWTAuthEmailClaim = "email" | 
					
						
							|  |  |  | 				} | 
					
						
							|  |  |  | 				if claims["JWTAuthUsernameClaim"] == true { | 
					
						
							|  |  |  | 					cfg.JWTAuthUsernameClaim = "preferred_username" | 
					
						
							|  |  |  | 				} | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		}) | 
					
						
							|  |  |  | 		httpReq := &http.Request{ | 
					
						
							|  |  |  | 			URL: &url.URL{RawQuery: "auth_token=" + token}, | 
					
						
							|  |  |  | 			Header: map[string][]string{ | 
					
						
							|  |  |  | 				jwtHeaderName: {token}}, | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		jwtClient := ProvideJWT(jwtService, cfg) | 
					
						
							|  |  |  | 		_, err := jwtClient.Authenticate(context.Background(), &authn.Request{ | 
					
						
							|  |  |  | 			OrgID:       1, | 
					
						
							|  |  |  | 			HTTPRequest: httpReq, | 
					
						
							|  |  |  | 			Resp:        nil, | 
					
						
							|  |  |  | 		}) | 
					
						
							|  |  |  | 		if tc.valid { | 
					
						
							|  |  |  | 			require.NoError(t, err) | 
					
						
							|  |  |  | 		} else { | 
					
						
							|  |  |  | 			require.Error(t, err) | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-01-10 22:08:52 +08:00
										 |  |  | func TestJWTTest(t *testing.T) { | 
					
						
							| 
									
										
										
										
											2023-01-21 02:11:06 +08:00
										 |  |  | 	jwtService := &jwt.FakeJWTService{} | 
					
						
							| 
									
										
										
										
											2023-01-10 22:08:52 +08:00
										 |  |  | 	jwtHeaderName := "X-Forwarded-User" | 
					
						
							| 
									
										
										
										
											2023-01-16 17:50:34 +08:00
										 |  |  | 	// #nosec G101 -- This is dummy/test token
 | 
					
						
							|  |  |  | 	validFormatToken := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.XbPfbIHMI6arZ3Y922BhjWgQzWXcXNrz0ogtVhfEd2o" | 
					
						
							| 
									
										
										
										
											2023-01-10 22:08:52 +08:00
										 |  |  | 	invalidFormatToken := "sampletokeninvalid" | 
					
						
							| 
									
										
										
										
											2023-01-16 17:50:34 +08:00
										 |  |  | 	// #nosec G101 -- This is dummy/test token
 | 
					
						
							|  |  |  | 	missingSubToken := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSm9obiBEb2UiLCJpYXQiOjE1MTYyMzkwMjJ9.8nYFUX869Y1mnDDDU4yL11aANgVRuifoxrE8BHZY1iE" | 
					
						
							|  |  |  | 	// #nosec G101 -- This is dummy/test token
 | 
					
						
							|  |  |  | 	emptySubToken := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSm9obiBEb2UiLCJzdWIiOiIiLCJpYXQiOjE1MTYyMzkwMjJ9.tnwtOHK58d47dO4DHW4b9MzeToxa1kGiko5Oo887Rqc" | 
					
						
							| 
									
										
										
										
											2023-01-10 22:08:52 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	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, | 
					
						
							|  |  |  | 		}, | 
					
						
							| 
									
										
										
										
											2023-01-16 17:50:34 +08:00
										 |  |  | 		{ | 
					
						
							|  |  |  | 			desc:          "token without a sub claim", | 
					
						
							|  |  |  | 			reqHeaderName: "Authorization", | 
					
						
							|  |  |  | 			cfgHeaderName: "Authorization", | 
					
						
							|  |  |  | 			token:         missingSubToken, | 
					
						
							|  |  |  | 			want:          false, | 
					
						
							|  |  |  | 		}, | 
					
						
							|  |  |  | 		{ | 
					
						
							|  |  |  | 			desc:          "token with an empty sub claim", | 
					
						
							|  |  |  | 			reqHeaderName: "Authorization", | 
					
						
							|  |  |  | 			cfgHeaderName: "Authorization", | 
					
						
							|  |  |  | 			token:         emptySubToken, | 
					
						
							|  |  |  | 			want:          false, | 
					
						
							|  |  |  | 		}, | 
					
						
							| 
									
										
										
										
											2023-01-10 22:08:52 +08:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	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) | 
					
						
							|  |  |  | 		}) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | } |