The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/pkg/services/authn/clients/jwt_test.go

307 lines
8.1 KiB

package clients
import (
"context"
"fmt"
"net/http"
"net/url"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/models/roletype"
"github.com/grafana/grafana/pkg/services/auth/jwt"
"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) {
jwtService := &jwt.FakeJWTService{
VerifyProvider: func(context.Context, string) (jwt.JWTClaims, error) {
return jwt.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{
SyncTeamMembers: false,
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))
}
func TestJWTClaimConfig(t *testing.T) {
jwtService := &jwt.FakeJWTService{
VerifyProvider: func(context.Context, string) (jwt.JWTClaims, error) {
return jwt.JWTClaims{
"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)
}
}
}
func TestJWTTest(t *testing.T) {
jwtService := &jwt.FakeJWTService{}
jwtHeaderName := "X-Forwarded-User"
// #nosec G101 -- This is dummy/test token
validFormatToken := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.XbPfbIHMI6arZ3Y922BhjWgQzWXcXNrz0ogtVhfEd2o"
invalidFormatToken := "sampletokeninvalid"
// #nosec G101 -- This is dummy/test token
missingSubToken := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSm9obiBEb2UiLCJpYXQiOjE1MTYyMzkwMjJ9.8nYFUX869Y1mnDDDU4yL11aANgVRuifoxrE8BHZY1iE"
// #nosec G101 -- This is dummy/test token
emptySubToken := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSm9obiBEb2UiLCJzdWIiOiIiLCJpYXQiOjE1MTYyMzkwMjJ9.tnwtOHK58d47dO4DHW4b9MzeToxa1kGiko5Oo887Rqc"
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,
},
{
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,
},
}
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)
})
}
}