Auth: Add sub claim check to JWT Auth pre-checks (#61417)

* Auth: Add sub claim check to JWT Auth pre-checks

* Add #nosec annotation to the test tokens
This commit is contained in:
Misi
2023-01-16 10:50:34 +01:00
committed by GitHub
parent e481673b77
commit b8b08ea292
6 changed files with 97 additions and 26 deletions
+18 -1
View File
@@ -6,11 +6,12 @@ import (
"errors"
"strings"
"gopkg.in/square/go-jose.v2/jwt"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/remotecache"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
"gopkg.in/square/go-jose.v2/jwt"
)
const ServiceName = "AuthService"
@@ -102,3 +103,19 @@ func (s *AuthService) Verify(ctx context.Context, strToken string) (models.JWTCl
return claims, nil
}
// HasSubClaim checks if the provided JWT token contains a non-empty "sub" claim.
// Returns true if it contains, otherwise returns false.
func HasSubClaim(jwtToken string) bool {
parsed, err := jwt.ParseSigned(sanitizeJWT(jwtToken))
if err != nil {
return false
}
var claims jwt.Claims
if err := parsed.UnsafeClaimsWithoutVerification(&claims); err != nil {
return false
}
return claims.Subject != ""
}
+3 -9
View File
@@ -11,6 +11,7 @@ import (
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/auth"
authJWT "github.com/grafana/grafana/pkg/services/auth/jwt"
"github.com/grafana/grafana/pkg/services/authn"
"github.com/grafana/grafana/pkg/services/login"
"github.com/grafana/grafana/pkg/services/org"
@@ -143,21 +144,14 @@ func (s *JWT) Test(ctx context.Context, r *authn.Request) bool {
return false
}
// The header is Authorization and the token does not look like a JWT,
// this is likely an API key. Pass it on.
if s.cfg.JWTAuthHeaderName == "Authorization" && !looksLikeJWT(jwtToken) {
// If the "sub" claim is missing or empty then pass the control to the next handler
if !authJWT.HasSubClaim(jwtToken) {
return false
}
return true
}
func looksLikeJWT(token string) bool {
// A JWT must have 3 parts separated by `.`.
parts := strings.Split(token, ".")
return len(parts) == 3
}
const roleGrafanaAdmin = "GrafanaAdmin"
func (s *JWT) extractRoleAndAdmin(claims map[string]interface{}) (org.RoleType, bool) {
+23 -3
View File
@@ -7,12 +7,13 @@ import (
"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/authn"
"github.com/grafana/grafana/pkg/setting"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func stringPtr(s string) *string {
@@ -87,8 +88,13 @@ func TestAuthenticateJWT(t *testing.T) {
func TestJWTTest(t *testing.T) {
jwtService := &models.FakeJWTService{}
jwtHeaderName := "X-Forwarded-User"
validFormatToken := "sample.token.valid"
// #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
@@ -144,6 +150,20 @@ func TestJWTTest(t *testing.T) {
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 {
+5 -10
View File
@@ -6,13 +6,15 @@ import (
"net/http"
"strings"
"github.com/jmespath/go-jmespath"
"github.com/grafana/grafana/pkg/login"
"github.com/grafana/grafana/pkg/models"
authJWT "github.com/grafana/grafana/pkg/services/auth/jwt"
"github.com/grafana/grafana/pkg/services/authn"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/user"
"github.com/jmespath/go-jmespath"
)
const (
@@ -59,9 +61,8 @@ func (h *ContextHandler) initContextWithJWT(ctx *models.ReqContext, orgId int64)
// Strip the 'Bearer' prefix if it exists.
jwtToken = strings.TrimPrefix(jwtToken, "Bearer ")
// The header is Authorization and the token does not look like a JWT,
// this is likely an API key. Pass it on.
if h.Cfg.JWTAuthHeaderName == "Authorization" && !looksLikeJWT(jwtToken) {
// If the "sub" claim is missing or empty then pass the control to the next handler
if !authJWT.HasSubClaim(jwtToken) {
return false
}
@@ -222,9 +223,3 @@ func searchClaimsForStringAttr(attributePath string, claims map[string]interface
return "", nil
}
func looksLikeJWT(token string) bool {
// A JWT must have 3 parts separated by `.`.
parts := strings.Split(token, ".")
return len(parts) == 3
}