diff --git a/Gopkg.lock b/Gopkg.lock index f6bfc65..d91aeaa 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -2,9 +2,13 @@ [[projects]] - name = "github.com/auth0-community/go-auth0" - packages = ["."] - revision = "3de339a55934c49fa256181e63b9ecf0234c15b7" + name = "github.com/allegro/bigcache" + packages = [ + ".", + "queue" + ] + revision = "aa76879d59fa5d93c43680238227dbf7a53f5c28" + version = "v1.0.0" [[projects]] name = "github.com/davecgh/go-spew" @@ -19,15 +23,14 @@ [[projects]] name = "github.com/gin-gonic/gin" - packages = [".","binding","render"] + packages = [ + ".", + "binding", + "render" + ] revision = "d459835d2b077e44f7c9b453505ee29881d5d12d" version = "v1.2" -[[projects]] - name = "github.com/go-errors/errors" - packages = ["."] - revision = "8fa88b06e5974e97fbf9899a7f86a344bfd1f105" - [[projects]] name = "github.com/golang/protobuf" packages = ["proto"] @@ -36,7 +39,10 @@ [[projects]] branch = "master" name = "github.com/hashicorp/golang-lru" - packages = [".","simplelru"] + packages = [ + ".", + "simplelru" + ] revision = "0a025b7e63adc15a622f29b0b2c4c3848243bbf6" [[projects]] @@ -46,7 +52,11 @@ [[projects]] name = "github.com/ory/ladon" - packages = [".","compiler","manager/memory"] + packages = [ + ".", + "compiler", + "manager/memory" + ] revision = "8128aeca0a774620b4f3be9fb2a1f7c53eac662c" version = "v0.8.5" @@ -80,7 +90,11 @@ [[projects]] name = "github.com/stretchr/testify" - packages = ["assert","mock","require"] + packages = [ + "assert", + "mock", + "require" + ] revision = "69483b4bd14f5845b5a1e55bca19e954e827f1d0" version = "v1.1.4" @@ -97,12 +111,19 @@ [[projects]] name = "golang.org/x/crypto" - packages = ["ed25519","ed25519/internal/edwards25519","ssh/terminal"] + packages = [ + "ed25519", + "ed25519/internal/edwards25519", + "ssh/terminal" + ] revision = "9419663f5a44be8b34ca85f08abc5fe1be11f8a3" [[projects]] name = "golang.org/x/sys" - packages = ["unix","windows"] + packages = [ + "unix", + "windows" + ] revision = "6faef541c73732f438fb660a212750a9ba9f9362" [[projects]] @@ -113,7 +134,12 @@ [[projects]] name = "gopkg.in/square/go-jose.v2" - packages = [".","cipher","json","jwt"] + packages = [ + ".", + "cipher", + "json", + "jwt" + ] revision = "f8f38de21b4dcd69d0413faf231983f5fd6634b1" version = "v2.1.3" @@ -125,6 +151,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "07a2200fcb41c7086e2701672735d5624f415b92ef410fbca16ef98a5169ec46" + inputs-digest = "701ae34e1b1d0cb8a416648b360e0c919e1e130768fc074bf1d3304f55dd8387" solver-name = "gps-cdcl" solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index 481f003..34c698f 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -43,3 +43,11 @@ [[constraint]] name = "gopkg.in/square/go-jose.v2" version = "2.1.3" + +[[constraint]] + name = "github.com/allegro/bigcache" + version = "1.0.0" + +[[constraint]] + name = "github.com/pkg/errors" + version = "0.8.0" diff --git a/doorman/doorman_ladon_test.go b/doorman/doorman_ladon_test.go index f26aa46..be677f4 100644 --- a/doorman/doorman_ladon_test.go +++ b/doorman/doorman_ladon_test.go @@ -154,7 +154,7 @@ func TestBadServicesConfig(t *testing.T) { // Bad JWT issuer err = d.LoadPolicies(ServicesConfig{ ServiceConfig{ - JWTIssuer: "https://perlin-pinpin", + JWTIssuer: "http://perlin-pinpin", }, }) assert.NotNil(t, err) @@ -192,10 +192,10 @@ func TestLoadPoliciesTwice(t *testing.T) { // Load bad policies, does not affect existing. err := doorman.LoadPolicies(ServicesConfig{ ServiceConfig{ - JWTIssuer: "https://perlin-pinpin", + JWTIssuer: "http://perlin-pinpin", }, }) - assert.Contains(t, err.Error(), "issuer \"https://perlin-pinpin\" not supported or has bad format") + assert.Contains(t, err.Error(), "issuer \"http://perlin-pinpin\" not supported or has bad format") _, ok := doorman.ladons["https://sample.yaml"] assert.True(t, ok) } diff --git a/doorman/handler_test.go b/doorman/handler_test.go index 36bc066..72eab11 100644 --- a/doorman/handler_test.go +++ b/doorman/handler_test.go @@ -71,7 +71,7 @@ func TestAllowedVerifiesJWT(t *testing.T) { var response ErrorResponse // Missing Authorization header. performAllowed(t, r, body, http.StatusUnauthorized, &response) - assert.Equal(t, "Token not found", response.Message) + assert.Equal(t, "token not found", response.Message) } func TestAllowedHandlerBadRequest(t *testing.T) { diff --git a/doorman/jwt.go b/doorman/jwt.go index 6146a86..5a6eb28 100644 --- a/doorman/jwt.go +++ b/doorman/jwt.go @@ -1,24 +1,14 @@ package doorman import ( + "fmt" "net/http" "strings" - - jwt "gopkg.in/square/go-jose.v2/jwt" ) -// Claims is the set of information we extract from the JWT payload. -type Claims struct { - Subject string `json:"sub,omitempty"` - Audience jwt.Audience `json:"aud,omitempty"` - Email string `json:"email,omitempty"` - Groups []string `json:"groups,omitempty"` -} - // JWTValidator is the interface in charge of extracting JWT claims from request. type JWTValidator interface { - Initialize() error - ExtractClaims(*http.Request) (*Claims, error) + ValidateRequest(*http.Request) (*Claims, error) } var jwtValidators map[string]JWTValidator @@ -29,24 +19,14 @@ func init() { // NewJWTValidator instantiates a JWT validator for the specified issuer. func NewJWTValidator(issuer string) (JWTValidator, error) { + if !strings.HasPrefix(issuer, "https://") { + return nil, fmt.Errorf("issuer %q not supported or has bad format", issuer) + } + // Reuse JWT validators instances among configs if they are for the same issuer. v, ok := jwtValidators[issuer] if !ok { - if strings.Contains(issuer, "mozilla.auth0.com") { - v = &MozillaAuth0Validator{ - Issuer: issuer, - } - } else { - // Fallback on basic Auth0. - // XXX: Here is where we can add other Identity providers. - v = &Auth0Validator{ - Issuer: issuer, - } - } - err := v.Initialize() - if err != nil { - return nil, err - } + v = newJWTGenericValidator(issuer) jwtValidators[issuer] = v } return v, nil diff --git a/doorman/jwt_auth0.go b/doorman/jwt_auth0.go deleted file mode 100644 index 467c3b8..0000000 --- a/doorman/jwt_auth0.go +++ /dev/null @@ -1,51 +0,0 @@ -package doorman - -import ( - "fmt" - "net/http" - "strings" - - auth0 "github.com/auth0-community/go-auth0" - log "github.com/sirupsen/logrus" - jose "gopkg.in/square/go-jose.v2" -) - -// Auth0Validator is the implementation of JWTValidator for Auth0. -type Auth0Validator struct { - Issuer string - validator *auth0.JWTValidator -} - -// Initialize will fetch Auth0 public keys and instantiate a validator. -func (v *Auth0Validator) Initialize() error { - validator, err := auth0Validator(v.Issuer) - if err != nil { - return err - } - v.validator = validator - return nil -} - -// ExtractClaims validates the token from request, and returns the JWT claims. -func (v *Auth0Validator) ExtractClaims(request *http.Request) (*Claims, error) { - token, err := v.validator.ValidateRequest(request) - claims := Claims{} - err = v.validator.Claims(request, token, &claims) - if err != nil { - return nil, err - } - return &claims, nil -} - -func auth0Validator(issuer string) (*auth0.JWTValidator, error) { - if !strings.HasPrefix(issuer, "https://") || !strings.HasSuffix(issuer, "auth0.com/") { - return nil, fmt.Errorf("issuer %q not supported or has bad format", issuer) - } - jwksURI := fmt.Sprintf("%s.well-known/jwks.json", issuer) - log.Infof("JWT keys: %s", jwksURI) - // Will check audience only when request comes in, leave empty for now. - audience := []string{} - client := auth0.NewJWKClient(auth0.JWKClientOptions{URI: jwksURI}) - config := auth0.NewConfiguration(client, audience, issuer, jose.RS256) - return auth0.NewValidator(config), nil -} diff --git a/doorman/jwt_auth0_mozilla_test.go b/doorman/jwt_auth0_mozilla_test.go deleted file mode 100644 index 069b12b..0000000 --- a/doorman/jwt_auth0_mozilla_test.go +++ /dev/null @@ -1,43 +0,0 @@ -package doorman - -import ( - "net/http" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestMozillaAuth0Initialize(t *testing.T) { - // No trailing slash - validator := MozillaAuth0Validator{"https://auth.mozilla.auth0.com", nil} - err := validator.Initialize() - assert.NotNil(t, err) - - validator = MozillaAuth0Validator{"https://auth.mozilla.auth0.com/", nil} - err = validator.Initialize() - assert.Nil(t, err) -} - -func TestMozillaAuth0ExtractClaims(t *testing.T) { - var err error - - validator := MozillaAuth0Validator{"https://auth.mozilla.auth0.com/", nil} - err = validator.Initialize() - require.Nil(t, err) - - r, _ := http.NewRequest("GET", "/", nil) - r.Header.Set("Authorization", "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ik1rWkRORGN5UmtOR1JURkROamxCTmpaRk9FSkJOMFpCTnpKQlFUTkVNRGhDTUVFd05rRkdPQSJ9.eyJuYW1lIjoiTWF0aGlldSBMZXBsYXRyZSIsImdpdmVuX25hbWUiOiJNYXRoaWV1IiwiZmFtaWx5X25hbWUiOiJMZXBsYXRyZSIsIm5pY2tuYW1lIjoiTWF0aGlldSBMZXBsYXRyZSIsInBpY3R1cmUiOiJodHRwczovL3MuZ3JhdmF0YXIuY29tL2F2YXRhci85NzE5N2YwMTFhM2Q5ZDQ5NGFlODEzNTY2ZjI0Njc5YT9zPTQ4MCZyPXBnJmQ9aHR0cHMlM0ElMkYlMkZjZG4uYXV0aDAuY29tJTJGYXZhdGFycyUyRm1sLnBuZyIsInVwZGF0ZWRfYXQiOiIyMDE3LTEyLTA0VDE1OjUyOjMzLjc2MVoiLCJpc3MiOiJodHRwczovL2F1dGgubW96aWxsYS5hdXRoMC5jb20vIiwic3ViIjoiYWR8TW96aWxsYS1MREFQfG1sZXBsYXRyZSIsImF1ZCI6IlNMb2NmN1NhMWliZDVHTkpNTXFPNTM5ZzdjS3ZXQk9JIiwiZXhwIjoxNTEzMDA3NTcwLCJpYXQiOjE1MTI0MDI3NzAsImFtciI6WyJtZmEiXSwiYWNyIjoiaHR0cDovL3NjaGVtYXMub3BlbmlkLm5ldC9wYXBlL3BvbGljaWVzLzIwMDcvMDYvbXVsdGktZmFjdG9yIiwibm9uY2UiOiJQRkxyLmxtYWhCQWRYaEVSWm0zYVFxc2ZuWjhwcWt0VSIsImF0X2hhc2giOiJTN0Rha1BrZVA0Tnk4SWpTOGxnMHJBIiwiaHR0cHM6Ly9zc28ubW96aWxsYS5jb20vY2xhaW0vZ3JvdXBzIjpbIkludHJhbmV0V2lraSIsIlN0YXRzRGFzaGJvYXJkIiwicGhvbmVib29rX2FjY2VzcyIsImNvcnAtdnBuIiwidnBuX2NvcnAiLCJ2cG5fZGVmYXVsdCIsIkNsb3Vkc2VydmljZXNXaWtpIiwidGVhbV9tb2NvIiwiaXJjY2xvdWQiLCJva3RhX21mYSIsImNsb3Vkc2VydmljZXNfZGV2IiwidnBuX2tpbnRvMV9zdGFnZSIsInZwbl9raW50bzFfcHJvZCIsImVnZW5jaWFfZGUiLCJhY3RpdmVfc2NtX2xldmVsXzEiLCJhbGxfc2NtX2xldmVsXzEiLCJzZXJ2aWNlX3NhZmFyaWJvb2tzIl0sImh0dHBzOi8vc3NvLm1vemlsbGEuY29tL2NsYWltL2VtYWlscyI6WyJtbGVwbGF0cmVAbW96aWxsYS5jb20iLCJtYXRoaWV1QG1vemlsbGEuY29tIiwibWF0aGlldS5sZXBsYXRyZUBtb3ppbGxhLmNvbSJdLCJodHRwczovL3Nzby5tb3ppbGxhLmNvbS9jbGFpbS9kbiI6Im1haWw9bWxlcGxhdHJlQG1vemlsbGEuY29tLG89Y29tLGRjPW1vemlsbGEiLCJodHRwczovL3Nzby5tb3ppbGxhLmNvbS9jbGFpbS9vcmdhbml6YXRpb25Vbml0cyI6Im1haWw9bWxlcGxhdHJlQG1vemlsbGEuY29tLG89Y29tLGRjPW1vemlsbGEiLCJodHRwczovL3Nzby5tb3ppbGxhLmNvbS9jbGFpbS9lbWFpbF9hbGlhc2VzIjpbIm1hdGhpZXVAbW96aWxsYS5jb20iLCJtYXRoaWV1LmxlcGxhdHJlQG1vemlsbGEuY29tIl0sImh0dHBzOi8vc3NvLm1vemlsbGEuY29tL2NsYWltL19IUkRhdGEiOnsicGxhY2Vob2xkZXIiOiJlbXB0eSJ9fQ.MK3Z1Nj15MfbM2TcO4FWVTTYPqAbUhL26pYOFa92mPnEUR2W_oJhwoZ8Vwq7dJcvTZfPq-aZKBnqHoPHHYlQbtaqfflhHmY9iRH0aPlxLQed_WVem4YqMn9xw0az4xHnf0UlzLU58kI97bqUFvvzs0fg_OTdDdO3owVUcaZrG8-xalCqQGQqwTfiH514gxeZ_Ki6610HSVDvpPvmODWPz87IDdgS6WkyM-SyAc3aYukP38aqRo-PUjEdpGbOtV_T_W2x8A3yQDxu0Bcq0WJz-FUEu2BHq1Vn6rmLm7BVYjDD6rYseusp8M0bvTfvXA-9OhJWGAAh6KrN9fnw7r30LQ") - claims, err := validator.ExtractClaims(r) - require.Nil(t, err) - assert.Contains(t, claims.Subject, "|Mozilla-LDAP|") - assert.Contains(t, claims.Email, "@mozilla.com") - assert.Contains(t, claims.Groups, "cloudservices_dev", "irccloud") - - // Email provided in `email` field instead of https://sso.../emails list - r, _ = http.NewRequest("GET", "/", nil) - r.Header.Set("Authorization", "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ik1rWkRORGN5UmtOR1JURkROamxCTmpaRk9FSkJOMFpCTnpKQlFUTkVNRGhDTUVFd05rRkdPQSJ9.eyJuYW1lIjoiTWF0aGlldSBMZXBsYXRyZSIsImdpdmVuX25hbWUiOiJNYXRoaWV1IiwiZmFtaWx5X25hbWUiOiJMZXBsYXRyZSIsIm5pY2tuYW1lIjoiTWF0aGlldSBMZXBsYXRyZSIsInBpY3R1cmUiOiJodHRwczovL3MuZ3JhdmF0YXIuY29tL2F2YXRhci85NzE5N2YwMTFhM2Q5ZDQ5NGFlODEzNTY2ZjI0Njc5YT9zPTQ4MCZyPXBnJmQ9aHR0cHMlM0ElMkYlMkZjZG4uYXV0aDAuY29tJTJGYXZhdGFycyUyRm1sLnBuZyIsInVwZGF0ZWRfYXQiOiIyMDE3LTEyLTEzVDIzOjE0OjQ0LjUzOVoiLCJlbWFpbCI6Im1sZXBsYXRyZUBtb3ppbGxhLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJpc3MiOiJodHRwczovL2F1dGgubW96aWxsYS5hdXRoMC5jb20vIiwic3ViIjoiYWR8TW96aWxsYS1MREFQfG1sZXBsYXRyZSIsImF1ZCI6IlNMb2NmN1NhMWliZDVHTkpNTXFPNTM5ZzdjS3ZXQk9JIiwiZXhwIjoxNTEzODExNjg0LCJpYXQiOjE1MTMyMDY4ODQsIm5vbmNlIjoickhOSXF5bGM3SE54MmFhNjktay1SbVA1Y3VqVWNudUkiLCJhdF9oYXNoIjoiZllPZzB6elNHSk1ZWlZTNFRsLXV3dyIsImh0dHBzOi8vc3NvLm1vemlsbGEuY29tL2NsYWltL2dyb3VwcyI6WyJJbnRyYW5ldFdpa2kiLCJTdGF0c0Rhc2hib2FyZCIsInBob25lYm9va19hY2Nlc3MiLCJjb3JwLXZwbiIsInZwbl9jb3JwIiwidnBuX2RlZmF1bHQiLCJDbG91ZHNlcnZpY2VzV2lraSIsInRlYW1fbW9jbyIsImlyY2Nsb3VkIiwib2t0YV9tZmEiLCJjbG91ZHNlcnZpY2VzX2RldiIsInZwbl9raW50bzFfc3RhZ2UiLCJ2cG5fa2ludG8xX3Byb2QiLCJlZ2VuY2lhX2RlIiwiYWN0aXZlX3NjbV9sZXZlbF8xIiwiYWxsX3NjbV9sZXZlbF8xIiwic2VydmljZV9zYWZhcmlib29rcyIsImV2ZXJ5b25lIl0sImh0dHBzOi8vc3NvLm1vemlsbGEuY29tL2NsYWltL1JFQURNRV9GSVJTVCI6IlBsZWFzZSByZWZlciB0byBodHRwczovL2dpdGh1Yi5jb20vbW96aWxsYS1pYW0vcGVyc29uLWFwaSBpbiBvcmRlciB0byBxdWVyeSBNb3ppbGxhIElBTSBDSVMgdXNlciBwcm9maWxlIGRhdGEifQ.EnF3oPHm90ZXnJ4egJqr-4eTaHMw-16beuZlvC66UsIehX7nBooP4VRfMW7KLwOHEnVVGV8jlxgn5p3Dnv1V_W6Yx4PLw7loeKrfhnEKw9onaH3frR_Vo0Y0-MgH4VnCbTwtGHsAfl32j2EoDljXYCqPhYCXD4H25o51lemAoKU3xWamF629FjooyhFTZPVI6JzKkOt39dQjALtXL9EVYRk0ameohHzOT0ZHA57H83FTrPmY_Jy5MWxv1aswcbzcENU1HsFEEkxkRCnGiosxYkStmDo957OQ0IXgNxdNe4VVXzuy5YiNmsjN-IF4tOADLFK5KnLHi4OBOGYiiRiJcQ") - claims, err = validator.ExtractClaims(r) - require.Nil(t, err) - assert.Contains(t, claims.Email, "@mozilla.com") -} diff --git a/doorman/jwt_auth0_test.go b/doorman/jwt_auth0_test.go deleted file mode 100644 index 371ba9e..0000000 --- a/doorman/jwt_auth0_test.go +++ /dev/null @@ -1,47 +0,0 @@ -package doorman - -import ( - "net/http" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestAuth0Initialize(t *testing.T) { - validator := Auth0Validator{"https://demo.oath-zero.com/", nil} - err := validator.Initialize() - assert.NotNil(t, err) - - validator = Auth0Validator{"http://demo.oauth0.com/", nil} - err = validator.Initialize() - assert.NotNil(t, err) -} - -func TestAuth0ExtractClaims(t *testing.T) { - var err error - - validator := Auth0Validator{"https://minimal-demo-iam.auth0.com/", nil} - validator.Initialize() - - r, _ := http.NewRequest("GET", "/", nil) - - _, err = validator.ExtractClaims(r) - require.NotNil(t, err) - assert.Equal(t, "Token not found", err.Error()) - - r.Header.Set("Authorization", "Basic abc") - _, err = validator.ExtractClaims(r) - require.NotNil(t, err) - assert.Equal(t, "Token not found", err.Error()) - - r.Header.Set("Authorization", "Bearer abc zy") - _, err = validator.ExtractClaims(r) - require.NotNil(t, err) - assert.Equal(t, "square/go-jose: compact JWS format must have three parts", err.Error()) - - r.Header.Set("Authorization", "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ik5EZzFOemczTlRFeVEwVTFNMEZCTnpCQlFqa3hOVVk1UTBVMU9USXpOalEzUXpVek5UWkRNQSJ9.eyJpc3MiOiJodHRwczovL21pbmltYWwtZGVtby1pYW0uYXV0aDAuY29tLyIsInN1YiI6Imdvb2dsZS1vYXV0aDJ8MTA0MTAyMzA2MTExMzUwNTc2NjI4IiwiYXVkIjpbImh0dHA6Ly9taW5pbWFsLWRlbW8taWFtLmxvY2FsaG9zdDo4MDAwIiwiaHR0cHM6Ly9taW5pbWFsLWRlbW8taWFtLmF1dGgwLmNvbS91c2VyaW5mbyJdLCJpYXQiOjE1MDY2MDQzMTMsImV4cCI6MTUwNjYxMTUxMywiYXpwIjoiV1lSWXBKeVM1RG5EeXhMVFJWR0NRR0NXR28yS05RTE4iLCJzY29wZSI6Im9wZW5pZCBwcm9maWxlIn0.JmfQajLJ6UMU8sGwv-4FyN0hAPjlLnixoVXAJwn9-985Y4jnMNiG22RWAk5qsdhxVKjIsyQFGA2oHuKELfcrI-LEHX3dxePxx9jSGUdC1wzk3p2q3YCRwIV3DUFEtBVeml8gdB9V7tVBE6XDivfq7RphiC8c5zz28_vlB2iPPaAwfucJLc1d5t83xlBaSYU9-hWDet3HbgjQg4zvFat6C2-CuKkCuQEG92tsOdoD8RIJtlWmLiMVUhCFgr3pGa7_ZNiKmMFkgZiDsX2qqD107CfOLG3IutcLGCqlpHxOuVltGZNp3QCXwtjIoZSV-5IXssXKLYuz-75GpfEAmUB5fg") - claims, err := validator.ExtractClaims(r) - require.Nil(t, err) - assert.Equal(t, "google-oauth2|104102306111350576628", claims.Subject) -} diff --git a/doorman/jwt_claims.go b/doorman/jwt_claims.go new file mode 100644 index 0000000..6502eeb --- /dev/null +++ b/doorman/jwt_claims.go @@ -0,0 +1,32 @@ +package doorman + +import ( + jose "gopkg.in/square/go-jose.v2" + jwt "gopkg.in/square/go-jose.v2/jwt" +) + +// Claims is the set of information we extract from the JWT payload. +type Claims struct { + Subject string `json:"sub,omitempty"` + Audience jwt.Audience `json:"aud,omitempty"` + Email string `json:"email,omitempty"` + Groups []string `json:"groups,omitempty"` +} + +// ClaimExtractor is in charge of extracting meaningful info from JWT payload. +type ClaimExtractor interface { + Extract(*jwt.JSONWebToken, *jose.JSONWebKey) (*Claims, error) +} + +type defaultClaimExtractor struct{} + +func (*defaultClaimExtractor) Extract(token *jwt.JSONWebToken, key *jose.JSONWebKey) (*Claims, error) { + claims := &Claims{} + err := token.Claims(key, claims) + if err != nil { + return nil, err + } + return claims, nil +} + +var defaultExtractor = &defaultClaimExtractor{} diff --git a/doorman/jwt_auth0_mozilla.go b/doorman/jwt_claims_mozilla.go similarity index 52% rename from doorman/jwt_auth0_mozilla.go rename to doorman/jwt_claims_mozilla.go index ad8dd1c..163a09b 100644 --- a/doorman/jwt_auth0_mozilla.go +++ b/doorman/jwt_claims_mozilla.go @@ -1,9 +1,7 @@ package doorman import ( - "net/http" - - auth0 "github.com/auth0-community/go-auth0" + jose "gopkg.in/square/go-jose.v2" jwt "gopkg.in/square/go-jose.v2/jwt" ) @@ -16,27 +14,11 @@ type MozillaClaims struct { Groups []string `json:"https://sso.mozilla.com/claim/groups"` } -// MozillaAuth0Validator is the implementation of JWTValidator for Auth0. -type MozillaAuth0Validator struct { - Issuer string - validator *auth0.JWTValidator -} +type mozillaClaimExtractor struct{} -// Initialize will fetch Auth0 public keys and instantiate a validator. -func (v *MozillaAuth0Validator) Initialize() error { - validator, err := auth0Validator(v.Issuer) - if err != nil { - return err - } - v.validator = validator - return nil -} - -// ExtractClaims validates the token from request, and returns the JWT claims. -func (v *MozillaAuth0Validator) ExtractClaims(request *http.Request) (*Claims, error) { - token, err := v.validator.ValidateRequest(request) +func (*mozillaClaimExtractor) Extract(token *jwt.JSONWebToken, key *jose.JSONWebKey) (*Claims, error) { mozclaims := MozillaClaims{} - err = v.validator.Claims(request, token, &mozclaims) + err := token.Claims(key, &mozclaims) if err != nil { return nil, err } @@ -53,5 +35,8 @@ func (v *MozillaAuth0Validator) ExtractClaims(request *http.Request) (*Claims, e Email: email, Groups: mozclaims.Groups, } + return &claims, nil } + +var mozillaExtractor = &mozillaClaimExtractor{} diff --git a/doorman/jwt_claims_mozilla_test.go b/doorman/jwt_claims_mozilla_test.go new file mode 100644 index 0000000..925020e --- /dev/null +++ b/doorman/jwt_claims_mozilla_test.go @@ -0,0 +1,32 @@ +package doorman + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + jwt "gopkg.in/square/go-jose.v2/jwt" +) + +func TestMozillaClaimsExtractor(t *testing.T) { + token, err := jwt.ParseSigned("eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ik1rWkRORGN5UmtOR1JURkROamxCTmpaRk9FSkJOMFpCTnpKQlFUTkVNRGhDTUVFd05rRkdPQSJ9.eyJuYW1lIjoiTWF0aGlldSBMZXBsYXRyZSIsImdpdmVuX25hbWUiOiJNYXRoaWV1IiwiZmFtaWx5X25hbWUiOiJMZXBsYXRyZSIsIm5pY2tuYW1lIjoiTWF0aGlldSBMZXBsYXRyZSIsInBpY3R1cmUiOiJodHRwczovL3MuZ3JhdmF0YXIuY29tL2F2YXRhci85NzE5N2YwMTFhM2Q5ZDQ5NGFlODEzNTY2ZjI0Njc5YT9zPTQ4MCZyPXBnJmQ9aHR0cHMlM0ElMkYlMkZjZG4uYXV0aDAuY29tJTJGYXZhdGFycyUyRm1sLnBuZyIsInVwZGF0ZWRfYXQiOiIyMDE3LTEyLTA0VDE1OjUyOjMzLjc2MVoiLCJpc3MiOiJodHRwczovL2F1dGgubW96aWxsYS5hdXRoMC5jb20vIiwic3ViIjoiYWR8TW96aWxsYS1MREFQfG1sZXBsYXRyZSIsImF1ZCI6IlNMb2NmN1NhMWliZDVHTkpNTXFPNTM5ZzdjS3ZXQk9JIiwiZXhwIjoxNTEzMDA3NTcwLCJpYXQiOjE1MTI0MDI3NzAsImFtciI6WyJtZmEiXSwiYWNyIjoiaHR0cDovL3NjaGVtYXMub3BlbmlkLm5ldC9wYXBlL3BvbGljaWVzLzIwMDcvMDYvbXVsdGktZmFjdG9yIiwibm9uY2UiOiJQRkxyLmxtYWhCQWRYaEVSWm0zYVFxc2ZuWjhwcWt0VSIsImF0X2hhc2giOiJTN0Rha1BrZVA0Tnk4SWpTOGxnMHJBIiwiaHR0cHM6Ly9zc28ubW96aWxsYS5jb20vY2xhaW0vZ3JvdXBzIjpbIkludHJhbmV0V2lraSIsIlN0YXRzRGFzaGJvYXJkIiwicGhvbmVib29rX2FjY2VzcyIsImNvcnAtdnBuIiwidnBuX2NvcnAiLCJ2cG5fZGVmYXVsdCIsIkNsb3Vkc2VydmljZXNXaWtpIiwidGVhbV9tb2NvIiwiaXJjY2xvdWQiLCJva3RhX21mYSIsImNsb3Vkc2VydmljZXNfZGV2IiwidnBuX2tpbnRvMV9zdGFnZSIsInZwbl9raW50bzFfcHJvZCIsImVnZW5jaWFfZGUiLCJhY3RpdmVfc2NtX2xldmVsXzEiLCJhbGxfc2NtX2xldmVsXzEiLCJzZXJ2aWNlX3NhZmFyaWJvb2tzIl0sImh0dHBzOi8vc3NvLm1vemlsbGEuY29tL2NsYWltL2VtYWlscyI6WyJtbGVwbGF0cmVAbW96aWxsYS5jb20iLCJtYXRoaWV1QG1vemlsbGEuY29tIiwibWF0aGlldS5sZXBsYXRyZUBtb3ppbGxhLmNvbSJdLCJodHRwczovL3Nzby5tb3ppbGxhLmNvbS9jbGFpbS9kbiI6Im1haWw9bWxlcGxhdHJlQG1vemlsbGEuY29tLG89Y29tLGRjPW1vemlsbGEiLCJodHRwczovL3Nzby5tb3ppbGxhLmNvbS9jbGFpbS9vcmdhbml6YXRpb25Vbml0cyI6Im1haWw9bWxlcGxhdHJlQG1vemlsbGEuY29tLG89Y29tLGRjPW1vemlsbGEiLCJodHRwczovL3Nzby5tb3ppbGxhLmNvbS9jbGFpbS9lbWFpbF9hbGlhc2VzIjpbIm1hdGhpZXVAbW96aWxsYS5jb20iLCJtYXRoaWV1LmxlcGxhdHJlQG1vemlsbGEuY29tIl0sImh0dHBzOi8vc3NvLm1vemlsbGEuY29tL2NsYWltL19IUkRhdGEiOnsicGxhY2Vob2xkZXIiOiJlbXB0eSJ9fQ.MK3Z1Nj15MfbM2TcO4FWVTTYPqAbUhL26pYOFa92mPnEUR2W_oJhwoZ8Vwq7dJcvTZfPq-aZKBnqHoPHHYlQbtaqfflhHmY9iRH0aPlxLQed_WVem4YqMn9xw0az4xHnf0UlzLU58kI97bqUFvvzs0fg_OTdDdO3owVUcaZrG8-xalCqQGQqwTfiH514gxeZ_Ki6610HSVDvpPvmODWPz87IDdgS6WkyM-SyAc3aYukP38aqRo-PUjEdpGbOtV_T_W2x8A3yQDxu0Bcq0WJz-FUEu2BHq1Vn6rmLm7BVYjDD6rYseusp8M0bvTfvXA-9OhJWGAAh6KrN9fnw7r30LQ") + require.Nil(t, err) + + validator := newJWTGenericValidator("https://auth.mozilla.auth0.com") + jwks, err := validator.jwks() + require.Nil(t, err) + key := &jwks.Keys[0] + + claims, err := mozillaExtractor.Extract(token, key) + require.Nil(t, err) + assert.Contains(t, claims.Subject, "|Mozilla-LDAP|") + assert.Contains(t, claims.Email, "@mozilla.com") + assert.Contains(t, claims.Groups, "cloudservices_dev", "irccloud") + + // Email provided in `email` field instead of https://sso.../emails list + token, err = jwt.ParseSigned("eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ik1rWkRORGN5UmtOR1JURkROamxCTmpaRk9FSkJOMFpCTnpKQlFUTkVNRGhDTUVFd05rRkdPQSJ9.eyJuYW1lIjoiTWF0aGlldSBMZXBsYXRyZSIsImdpdmVuX25hbWUiOiJNYXRoaWV1IiwiZmFtaWx5X25hbWUiOiJMZXBsYXRyZSIsIm5pY2tuYW1lIjoiTWF0aGlldSBMZXBsYXRyZSIsInBpY3R1cmUiOiJodHRwczovL3MuZ3JhdmF0YXIuY29tL2F2YXRhci85NzE5N2YwMTFhM2Q5ZDQ5NGFlODEzNTY2ZjI0Njc5YT9zPTQ4MCZyPXBnJmQ9aHR0cHMlM0ElMkYlMkZjZG4uYXV0aDAuY29tJTJGYXZhdGFycyUyRm1sLnBuZyIsInVwZGF0ZWRfYXQiOiIyMDE3LTEyLTEzVDIzOjE0OjQ0LjUzOVoiLCJlbWFpbCI6Im1sZXBsYXRyZUBtb3ppbGxhLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJpc3MiOiJodHRwczovL2F1dGgubW96aWxsYS5hdXRoMC5jb20vIiwic3ViIjoiYWR8TW96aWxsYS1MREFQfG1sZXBsYXRyZSIsImF1ZCI6IlNMb2NmN1NhMWliZDVHTkpNTXFPNTM5ZzdjS3ZXQk9JIiwiZXhwIjoxNTEzODExNjg0LCJpYXQiOjE1MTMyMDY4ODQsIm5vbmNlIjoickhOSXF5bGM3SE54MmFhNjktay1SbVA1Y3VqVWNudUkiLCJhdF9oYXNoIjoiZllPZzB6elNHSk1ZWlZTNFRsLXV3dyIsImh0dHBzOi8vc3NvLm1vemlsbGEuY29tL2NsYWltL2dyb3VwcyI6WyJJbnRyYW5ldFdpa2kiLCJTdGF0c0Rhc2hib2FyZCIsInBob25lYm9va19hY2Nlc3MiLCJjb3JwLXZwbiIsInZwbl9jb3JwIiwidnBuX2RlZmF1bHQiLCJDbG91ZHNlcnZpY2VzV2lraSIsInRlYW1fbW9jbyIsImlyY2Nsb3VkIiwib2t0YV9tZmEiLCJjbG91ZHNlcnZpY2VzX2RldiIsInZwbl9raW50bzFfc3RhZ2UiLCJ2cG5fa2ludG8xX3Byb2QiLCJlZ2VuY2lhX2RlIiwiYWN0aXZlX3NjbV9sZXZlbF8xIiwiYWxsX3NjbV9sZXZlbF8xIiwic2VydmljZV9zYWZhcmlib29rcyIsImV2ZXJ5b25lIl0sImh0dHBzOi8vc3NvLm1vemlsbGEuY29tL2NsYWltL1JFQURNRV9GSVJTVCI6IlBsZWFzZSByZWZlciB0byBodHRwczovL2dpdGh1Yi5jb20vbW96aWxsYS1pYW0vcGVyc29uLWFwaSBpbiBvcmRlciB0byBxdWVyeSBNb3ppbGxhIElBTSBDSVMgdXNlciBwcm9maWxlIGRhdGEifQ.EnF3oPHm90ZXnJ4egJqr-4eTaHMw-16beuZlvC66UsIehX7nBooP4VRfMW7KLwOHEnVVGV8jlxgn5p3Dnv1V_W6Yx4PLw7loeKrfhnEKw9onaH3frR_Vo0Y0-MgH4VnCbTwtGHsAfl32j2EoDljXYCqPhYCXD4H25o51lemAoKU3xWamF629FjooyhFTZPVI6JzKkOt39dQjALtXL9EVYRk0ameohHzOT0ZHA57H83FTrPmY_Jy5MWxv1aswcbzcENU1HsFEEkxkRCnGiosxYkStmDo957OQ0IXgNxdNe4VVXzuy5YiNmsjN-IF4tOADLFK5KnLHi4OBOGYiiRiJcQ") + require.Nil(t, err) + claims, err = mozillaExtractor.Extract(token, key) + require.Nil(t, err) + assert.Contains(t, claims.Email, "@mozilla.com") +} diff --git a/doorman/jwt_claims_test.go b/doorman/jwt_claims_test.go new file mode 100644 index 0000000..8fd9608 --- /dev/null +++ b/doorman/jwt_claims_test.go @@ -0,0 +1,23 @@ +package doorman + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + jwt "gopkg.in/square/go-jose.v2/jwt" +) + +func TestDefaultClaimsExtractor(t *testing.T) { + token, err := jwt.ParseSigned("eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ik5EZzFOemczTlRFeVEwVTFNMEZCTnpCQlFqa3hOVVk1UTBVMU9USXpOalEzUXpVek5UWkRNQSJ9.eyJpc3MiOiJodHRwczovL21pbmltYWwtZGVtby1pYW0uYXV0aDAuY29tLyIsInN1YiI6Imdvb2dsZS1vYXV0aDJ8MTA0MTAyMzA2MTExMzUwNTc2NjI4IiwiYXVkIjpbImh0dHA6Ly9taW5pbWFsLWRlbW8taWFtLmxvY2FsaG9zdDo4MDAwIiwiaHR0cHM6Ly9taW5pbWFsLWRlbW8taWFtLmF1dGgwLmNvbS91c2VyaW5mbyJdLCJpYXQiOjE1MDY2MDQzMTMsImV4cCI6MTUwNjYxMTUxMywiYXpwIjoiV1lSWXBKeVM1RG5EeXhMVFJWR0NRR0NXR28yS05RTE4iLCJzY29wZSI6Im9wZW5pZCBwcm9maWxlIn0.JmfQajLJ6UMU8sGwv-4FyN0hAPjlLnixoVXAJwn9-985Y4jnMNiG22RWAk5qsdhxVKjIsyQFGA2oHuKELfcrI-LEHX3dxePxx9jSGUdC1wzk3p2q3YCRwIV3DUFEtBVeml8gdB9V7tVBE6XDivfq7RphiC8c5zz28_vlB2iPPaAwfucJLc1d5t83xlBaSYU9-hWDet3HbgjQg4zvFat6C2-CuKkCuQEG92tsOdoD8RIJtlWmLiMVUhCFgr3pGa7_ZNiKmMFkgZiDsX2qqD107CfOLG3IutcLGCqlpHxOuVltGZNp3QCXwtjIoZSV-5IXssXKLYuz-75GpfEAmUB5fg") + require.Nil(t, err) + + validator := newJWTGenericValidator("https://minimal-demo-iam.auth0.com") + jwks, err := validator.jwks() + require.Nil(t, err) + key := &jwks.Keys[0] + + claims, err := defaultExtractor.Extract(token, key) + require.Nil(t, err) + assert.Equal(t, "google-oauth2|104102306111350576628", claims.Subject) +} diff --git a/doorman/jwt_generic.go b/doorman/jwt_generic.go new file mode 100644 index 0000000..175effa --- /dev/null +++ b/doorman/jwt_generic.go @@ -0,0 +1,190 @@ +package doorman + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "strings" + "time" + + "github.com/pkg/errors" + "github.com/allegro/bigcache" + jose "gopkg.in/square/go-jose.v2" + jwt "gopkg.in/square/go-jose.v2/jwt" + log "github.com/sirupsen/logrus" +) + +// OpenIDConfiguration is the OpenID provider metadata about endpoints etc. +type OpenIDConfiguration struct { + JWKSUri string `json:"jwks_uri"` +} + +// JWKS are the JWT public keys +type JWKS struct { + Keys []jose.JSONWebKey `json:"keys"` +} + +type jwtGenericValidator struct { + Issuer string + ClaimExtractor ClaimExtractor + SignatureAlgorithm jose.SignatureAlgorithm + cache *bigcache.BigCache +} + +// newJWTGenericValidator returns a generic JWT validator of this issuer. +func newJWTGenericValidator(issuer string) *jwtGenericValidator { + cache, _ := bigcache.NewBigCache(bigcache.DefaultConfig(1 * time.Hour)) + + var extractor ClaimExtractor = defaultExtractor + if strings.Contains(issuer, "mozilla.auth0.com") { + extractor = mozillaExtractor + } + return &jwtGenericValidator{ + Issuer: issuer, + ClaimExtractor: extractor, + SignatureAlgorithm: jose.RS256, + cache: cache, + } +} + +func (v *jwtGenericValidator) config() (*OpenIDConfiguration, error) { + cacheKey := "config:" + v.Issuer + data, err := v.cache.Get(cacheKey) + + // Cache is empty or expired: fetch again. + if err != nil { + uri := strings.TrimRight(v.Issuer, "/") + "/.well-known/openid-configuration" + log.Debugf("Fetch OpenID configuration from %s", uri) + data, err = downloadJSON(uri) + if err != nil { + return nil, errors.Wrap(err, "failed to fetch OpenID configuration") + } + v.cache.Set(cacheKey, data) + } + + // XXX: since cache stores bytes, we parse it again at every usage :( ? + config := &OpenIDConfiguration{} + err = json.Unmarshal(data, config) + if err != nil { + return nil, errors.Wrap(err, "failed to parse OpenID configuration") + } + if config.JWKSUri == "" { + return nil, fmt.Errorf("no jwks_uri attribute in OpenID configuration") + } + return config, nil +} + +func (v *jwtGenericValidator) jwks() (*JWKS, error) { + cacheKey := "jwks:" + v.Issuer + data, err := v.cache.Get(cacheKey) + + // Cache is empty or expired: fetch again. + if err != nil { + config, err := v.config() + if err != nil { + return nil, err + } + uri := config.JWKSUri + log.Debugf("Fetch public keys from %s", uri) + data, err = downloadJSON(uri) + if err != nil { + return nil, errors.Wrap(err, "failed to fetch JWKS") + } + v.cache.Set(cacheKey, data) + } + + // XXX: since cache stores bytes, we parse it again at every usage :( ? + var jwks = &JWKS{} + err = json.Unmarshal(data, jwks) + if err != nil { + return nil, errors.Wrap(err, "failed to parse JWKS") + } + + if len(jwks.Keys) < 1 { + return nil, fmt.Errorf("no JWKS found") + } + return jwks, nil +} + +func (v *jwtGenericValidator) ValidateRequest(r *http.Request) (*Claims, error) { + // 1. Extract JWT from request headers + token, err := fromHeader(r) + if err != nil { + return nil, err + } + + // 2. Read JWT headers + if len(token.Headers) < 1 { + return nil, fmt.Errorf("no headers in the token") + } + header := token.Headers[0] + if header.Algorithm != string(v.SignatureAlgorithm) { + return nil, fmt.Errorf("invalid algorithm") + } + + // 3. Get public key with specified ID + keys, err := v.jwks() + if err != nil { + return nil, err + } + var key *jose.JSONWebKey + for _, k := range keys.Keys { + if k.KeyID == header.KeyID { + key = &k + break + } + } + if key == nil { + return nil, fmt.Errorf("no JWT key with id %q", header.KeyID) + } + + // 4. Parse and verify signature. + jwtClaims := jwt.Claims{} + err = token.Claims(key, &jwtClaims) + if err != nil { + return nil, errors.Wrap(err, "failed to read JWT payload") + } + + // 5. Validate issuer, claims and expiration. + // Will check audience only when request comes in, leave empty for now. + audience := []string{} + expected := jwt.Expected{Issuer: v.Issuer, Audience: audience} + expected = expected.WithTime(time.Now()) + err = jwtClaims.Validate(expected) + if err != nil { + return nil, errors.Wrap(err, "invalid JWT claims") + } + + // 6. Extract relevant claims for Doorman. + claims, err := v.ClaimExtractor.Extract(token, key) + if err != nil { + return nil, errors.Wrap(err, "failed to extract JWT claims") + } + return claims, nil +} + +// fromHeader reads the authorization header value and parses it as JSON Web Token. +func fromHeader(r *http.Request) (*jwt.JSONWebToken, error) { + if authorizationHeader := r.Header.Get("Authorization"); len(authorizationHeader) > 7 && strings.EqualFold(authorizationHeader[0:7], "BEARER ") { + raw := []byte(authorizationHeader[7:]) + return jwt.ParseSigned(string(raw)) + } + return nil, fmt.Errorf("token not found") +} + +func downloadJSON(uri string) ([]byte, error) { + response, err := http.Get(uri) + if err != nil { + return nil, err + } + if contentHeader := response.Header.Get("Content-Type"); !strings.HasPrefix(contentHeader, "application/json") { + return nil, fmt.Errorf("%s has not a JSON content-type", uri) + } + defer response.Body.Close() + data, err := ioutil.ReadAll(response.Body) + if err != nil { + return nil, errors.Wrap(err, "could not download JSON") + } + return data, nil +} diff --git a/doorman/jwt_generic_test.go b/doorman/jwt_generic_test.go new file mode 100644 index 0000000..12732b5 --- /dev/null +++ b/doorman/jwt_generic_test.go @@ -0,0 +1,141 @@ +package doorman + +import ( + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestFetchOpenIDConfiguration(t *testing.T) { + // Not available + validator := newJWTGenericValidator("https://missing.com") + _, err := validator.config() + require.NotNil(t, err) + assert.Contains(t, err.Error(), "connection refused") + // Bad content-type + validator = newJWTGenericValidator("https://mozilla.org") + _, err = validator.config() + require.NotNil(t, err) + assert.Contains(t, err.Error(), "has not a JSON content-type") + // Bad JSON + validator = newJWTGenericValidator("https://mozilla.org") + validator.cache.Set("config:https://mozilla.org", []byte("")) + _, err = validator.config() + require.NotNil(t, err) + assert.Contains(t, err.Error(), "invalid character '<'") + // Good one + validator = newJWTGenericValidator("https://auth.mozilla.auth0.com/") + config, err := validator.config() + require.Nil(t, err) + assert.Contains(t, config.JWKSUri, ".well-known/jwks.json") +} + +func TestDownloadKeys(t *testing.T) { + validator := newJWTGenericValidator("https://fake.com") + validator.cache.Set("config:https://fake.com", + []byte("{\"jwks_uri\":\"http://z\"}")) + // Bad URL + _, err := validator.jwks() + require.NotNil(t, err) + assert.Contains(t, err.Error(), "no such host") + // Bad content-type + validator.cache.Set("config:https://fake.com", + []byte("{\"jwks_uri\":\"http://mozilla.org\"}")) + _, err = validator.jwks() + require.NotNil(t, err) + assert.Contains(t, err.Error(), "has not a JSON content-type") + + // Bad JSON + validator.cache.Set("jwks:https://fake.com", []byte("")) + _, err = validator.jwks() + require.NotNil(t, err) + assert.Contains(t, err.Error(), "invalid character '<'") + + // Missing Keys attribute + validator.cache.Set("jwks:https://fake.com", []byte("{}")) + _, err = validator.jwks() + require.NotNil(t, err) + assert.Contains(t, err.Error(), "no JWKS found") + + // Good one + validator = newJWTGenericValidator("https://auth.mozilla.auth0.com") + keys, err := validator.jwks() + require.Nil(t, err) + assert.Equal(t, 1, len(keys.Keys)) +} + +func TestFromHeader(t *testing.T) { + r, _ := http.NewRequest("GET", "/", nil) + + _, err := fromHeader(r) + require.NotNil(t, err) + assert.Equal(t, "token not found", err.Error()) + + r.Header.Set("Authorization", "Basic abc") + _, err = fromHeader(r) + require.NotNil(t, err) + assert.Equal(t, "token not found", err.Error()) + + r.Header.Set("Authorization", "Bearer abc zy") + _, err = fromHeader(r) + require.NotNil(t, err) + assert.Equal(t, "square/go-jose: compact JWS format must have three parts", err.Error()) +} + +func TestValidateRequest(t *testing.T) { + goodJWT := "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ik1rWkRORGN5UmtOR1JURkROamxCTmpaRk9FSkJOMFpCTnpKQlFUTkVNRGhDTUVFd05rRkdPQSJ9.eyJuYW1lIjoiTWF0aGlldSBMZXBsYXRyZSIsImdpdmVuX25hbWUiOiJNYXRoaWV1IiwiZmFtaWx5X25hbWUiOiJMZXBsYXRyZSIsIm5pY2tuYW1lIjoiTWF0aGlldSBMZXBsYXRyZSIsInBpY3R1cmUiOiJodHRwczovL3MuZ3JhdmF0YXIuY29tL2F2YXRhci85NzE5N2YwMTFhM2Q5ZDQ5NGFlODEzNTY2ZjI0Njc5YT9zPTQ4MCZyPXBnJmQ9aHR0cHMlM0ElMkYlMkZjZG4uYXV0aDAuY29tJTJGYXZhdGFycyUyRm1sLnBuZyIsInVwZGF0ZWRfYXQiOiIyMDE3LTEyLTA0VDE1OjUyOjMzLjc2MVoiLCJpc3MiOiJodHRwczovL2F1dGgubW96aWxsYS5hdXRoMC5jb20vIiwic3ViIjoiYWR8TW96aWxsYS1MREFQfG1sZXBsYXRyZSIsImF1ZCI6IlNMb2NmN1NhMWliZDVHTkpNTXFPNTM5ZzdjS3ZXQk9JIiwiZXhwIjoxNTEzMDA3NTcwLCJpYXQiOjE1MTI0MDI3NzAsImFtciI6WyJtZmEiXSwiYWNyIjoiaHR0cDovL3NjaGVtYXMub3BlbmlkLm5ldC9wYXBlL3BvbGljaWVzLzIwMDcvMDYvbXVsdGktZmFjdG9yIiwibm9uY2UiOiJQRkxyLmxtYWhCQWRYaEVSWm0zYVFxc2ZuWjhwcWt0VSIsImF0X2hhc2giOiJTN0Rha1BrZVA0Tnk4SWpTOGxnMHJBIiwiaHR0cHM6Ly9zc28ubW96aWxsYS5jb20vY2xhaW0vZ3JvdXBzIjpbIkludHJhbmV0V2lraSIsIlN0YXRzRGFzaGJvYXJkIiwicGhvbmVib29rX2FjY2VzcyIsImNvcnAtdnBuIiwidnBuX2NvcnAiLCJ2cG5fZGVmYXVsdCIsIkNsb3Vkc2VydmljZXNXaWtpIiwidGVhbV9tb2NvIiwiaXJjY2xvdWQiLCJva3RhX21mYSIsImNsb3Vkc2VydmljZXNfZGV2IiwidnBuX2tpbnRvMV9zdGFnZSIsInZwbl9raW50bzFfcHJvZCIsImVnZW5jaWFfZGUiLCJhY3RpdmVfc2NtX2xldmVsXzEiLCJhbGxfc2NtX2xldmVsXzEiLCJzZXJ2aWNlX3NhZmFyaWJvb2tzIl0sImh0dHBzOi8vc3NvLm1vemlsbGEuY29tL2NsYWltL2VtYWlscyI6WyJtbGVwbGF0cmVAbW96aWxsYS5jb20iLCJtYXRoaWV1QG1vemlsbGEuY29tIiwibWF0aGlldS5sZXBsYXRyZUBtb3ppbGxhLmNvbSJdLCJodHRwczovL3Nzby5tb3ppbGxhLmNvbS9jbGFpbS9kbiI6Im1haWw9bWxlcGxhdHJlQG1vemlsbGEuY29tLG89Y29tLGRjPW1vemlsbGEiLCJodHRwczovL3Nzby5tb3ppbGxhLmNvbS9jbGFpbS9vcmdhbml6YXRpb25Vbml0cyI6Im1haWw9bWxlcGxhdHJlQG1vemlsbGEuY29tLG89Y29tLGRjPW1vemlsbGEiLCJodHRwczovL3Nzby5tb3ppbGxhLmNvbS9jbGFpbS9lbWFpbF9hbGlhc2VzIjpbIm1hdGhpZXVAbW96aWxsYS5jb20iLCJtYXRoaWV1LmxlcGxhdHJlQG1vemlsbGEuY29tIl0sImh0dHBzOi8vc3NvLm1vemlsbGEuY29tL2NsYWltL19IUkRhdGEiOnsicGxhY2Vob2xkZXIiOiJlbXB0eSJ9fQ.MK3Z1Nj15MfbM2TcO4FWVTTYPqAbUhL26pYOFa92mPnEUR2W_oJhwoZ8Vwq7dJcvTZfPq-aZKBnqHoPHHYlQbtaqfflhHmY9iRH0aPlxLQed_WVem4YqMn9xw0az4xHnf0UlzLU58kI97bqUFvvzs0fg_OTdDdO3owVUcaZrG8-xalCqQGQqwTfiH514gxeZ_Ki6610HSVDvpPvmODWPz87IDdgS6WkyM-SyAc3aYukP38aqRo-PUjEdpGbOtV_T_W2x8A3yQDxu0Bcq0WJz-FUEu2BHq1Vn6rmLm7BVYjDD6rYseusp8M0bvTfvXA-9OhJWGAAh6KrN9fnw7r30LQ" + r, _ := http.NewRequest("GET", "/", nil) + r.Header.Set("Authorization", "Bearer "+goodJWT) + + // Fail to fetch JWKS + validator := newJWTGenericValidator("https://perlinpimpin.com") + + _, err := validator.ValidateRequest(r) + require.NotNil(t, err) + assert.Contains(t, err.Error(), "no such host") + + validator = newJWTGenericValidator("https://auth.mozilla.auth0.com/") + + // Cannot extract JWT + r.Header.Set("Authorization", "Bearer abc") + _, err = validator.ValidateRequest(r) + require.NotNil(t, err) + assert.Contains(t, err.Error(), "compact JWS format must have three parts") + + // Unknown public key + r.Header.Set("Authorization", "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6ImFiYyJ9.abc.123") + _, err = validator.ValidateRequest(r) + require.NotNil(t, err) + assert.Contains(t, err.Error(), "no JWT key with id \"abc\"") + + // // Invalid algorithm + r.Header.Set("Authorization", "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ") + _, err = validator.ValidateRequest(r) + require.NotNil(t, err) + assert.Contains(t, err.Error(), "invalid algorithm") + + // Bad signature + r.Header.Set("Authorization", "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ik1rWkRORGN5UmtOR1JURkROamxCTmpaRk9FSkJOMFpCTnpKQlFUTkVNRGhDTUVFd05rRkdPQSJ9.eyJuYW1lIjoiTWF0aGlldSBMZXBsYXRyZSIsImdpdmVuX25hbWUiOiJNYXRoaWV1IiwiZmFtaWx5X25hbWUiOiJMZXBsYXRyZSIsIm5pY2tuYW1lIjoiTWF0aGlldSBMZXBsYXRyZSIsInBpY3R1cmUiOiJodHRwczovL3MuZ3JhdmF0YXIuY29tL2F2YXRhci85NzE5N2YwMTFhM2Q5ZDQ5NGFlODEzNTY2ZjI0Njc5YT9zPTQ4MCZyPXBnJmQ9aHR0cHMlM0ElMkYlMkZjZG4uYXV0aDAuY29tJTJGYXZhdGFycyUyRm1sLnBuZyIsInVwZGF0ZWRfYXQiOiIyMDE3LTEyLTA0VDE1OjUyOjMzLjc2MVoiLCJpc3MiOiJodHRwczovL2F1dGgubW96aWxsYS5hdXRoMC5jb20vIiwic3ViIjoiYWR8TW96aWxsYS1MREFQfG1sZXBsYXRyZSIsImF1ZCI6IlNMb2NmN1NhMWliZDVHTkpNTXFPNTM5ZzdjS3ZXQk9JIiwiZXhwIjoxNTEzMDA3NTcwLCJpYXQiOjE1MTI0MDI3NzAsImFtciI6WyJtZmEiXSwiYWNyIjoiaHR0cDovL3NjaGVtYXMub3BlbmlkLm5ldC9wYXBlL3BvbGljaWVzLzIwMDcvMDYvbXVsdGktZmFjdG9yIiwibm9uY2UiOiJQRkxyLmxtYWhCQWRYaEVSWm0zYVFxc2ZuWjhwcWt0VSIsImF0X2hhc2giOiJTN0Rha1BrZVA0Tnk4SWpTOGxnMHJBIiwiaHR0cHM6Ly9zc28ubW96aWxsYS5jb20vY2xhaW0vZ3JvdXBzIjpbIkludHJhbmV0V2lraSIsIlN0YXRzRGFzaGJvYXJkIiwicGhvbmVib29rX2FjY2VzcyIsImNvcnAtdnBuIiwidnBuX2NvcnAiLCJ2cG5fZGVmYXVsdCIsIkNsb3Vkc2VydmljZXNXaWtpIiwidGVhbV9tb2NvIiwiaXJjY2xvdWQiLCJva3RhX21mYSIsImNsb3Vkc2VydmljZXNfZGV2IiwidnBuX2tpbnRvMV9zdGFnZSIsInZwbl9raW50bzFfcHJvZCIsImVnZW5jaWFfZGUiLCJhY3RpdmVfc2NtX2xldmVsXzEiLCJhbGxfc2NtX2xldmVsXzEiLCJzZXJ2aWNlX3NhZmFyaWJvb2tzIl0sImh0dHBzOi8vc3NvLm1vemlsbGEuY29tL2NsYWltL2VtYWlscyI6WyJtbGVwbGF0cmVAbW96aWxsYS5jb20iLCJtYXRoaWV1QG1vemlsbGEuY29tIiwibWF0aGlldS5sZXBsYXRyZUBtb3ppbGxhLmNvbSJdLCJodHRwczovL3Nzby5tb3ppbGxhLmNvbS9jbGFpbS9kbiI6Im1haWw9bWxlcGxhdHJlQG1vemlsbGEuY29tLG89Y29tLGRjPW1vemlsbGEiLCJodHRwczovL3Nzby5tb3ppbGxhLmNvbS9jbGFpbS9vcmdhbml6YXRpb25Vbml0cyI6Im1haWw9bWxlcGxhdHJlQG1vemlsbGEuY29tLG89Y29tLGRjPW1vemlsbGEiLCJodHRwczovL3Nzby5tb3ppbGxhLmNvbS9jbGFpbS9lbWFpbF9hbGlhc2VzIjpbIm1hdGhpZXVAbW96aWxsYS5jb20iLCJtYXRoaWV1LmxlcGxhdHJlQG1vemlsbGEuY29tIl0sImh0dHBzOi8vc3NvLm1vemlsbGEuY29tL2NsYWltL19IUkRhdGEiOnsicGxhY2Vob2xkZXIiOiJlbXB0eSJ9fQ.123") + _, err = validator.ValidateRequest(r) + require.NotNil(t, err) + assert.Contains(t, err.Error(), "error in cryptographic primitive") + + // Invalid claims + r.Header.Set("Authorization", "Bearer "+goodJWT) + _, err = validator.ValidateRequest(r) + require.NotNil(t, err) + assert.Contains(t, err.Error(), "validation failed, token is expired") +} + +func BenchmarkParseKeys(b *testing.B) { + // Warm cache. + validator := newJWTGenericValidator("https://auth.mozilla.auth0.com") + validator.jwks() + b.ResetTimer() + // Bench parsing of cache bytes into keys objects. + for i := 0; i < b.N; i++ { + validator.jwks() + } +} diff --git a/doorman/middleware.go b/doorman/middleware.go index 96d7aa6..bc3ff7f 100644 --- a/doorman/middleware.go +++ b/doorman/middleware.go @@ -46,13 +46,13 @@ func VerifyJWTMiddleware(doorman Doorman) gin.HandlerFunc { } // No JWT validator configured for this service. if validator == nil { - // Do nothing. + // Do nothing. The principals list will be empty. c.Next() return } // Verify the JWT - claims, err := validator.ExtractClaims(c.Request) + claims, err := validator.ValidateRequest(c.Request) if err != nil { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{ "message": err.Error(), diff --git a/doorman/middleware_test.go b/doorman/middleware_test.go index 30a24e9..5f131af 100644 --- a/doorman/middleware_test.go +++ b/doorman/middleware_test.go @@ -23,7 +23,7 @@ func (v *TestValidator) Initialize() error { args := v.Called() return args.Error(0) } -func (v *TestValidator) ExtractClaims(request *http.Request) (*Claims, error) { +func (v *TestValidator) ValidateRequest(request *http.Request) (*Claims, error) { args := v.Called(request) return args.Get(0).(*Claims), args.Error(1) } @@ -45,14 +45,14 @@ func TestJWTMiddleware(t *testing.T) { Email: "user@corp.com", Groups: []string{"Employee", "Admins"}, } - v.On("ExtractClaims", mock.Anything).Return(claims, nil) + v.On("ValidateRequest", mock.Anything).Return(claims, nil) c, _ := gin.CreateTestContext(httptest.NewRecorder()) c.Request, _ = http.NewRequest("GET", "/get", nil) c.Request.Header.Set("Origin", audience) handler(c) - v.AssertCalled(t, "ExtractClaims", c.Request) + v.AssertCalled(t, "ValidateRequest", c.Request) // Principals are set in context. principals, ok := c.Get(PrincipalsContextKey) @@ -94,7 +94,7 @@ func TestJWTMiddleware(t *testing.T) { Audience: []string{audience}, } v = &TestValidator{} - v.On("ExtractClaims", mock.Anything).Return(claims, nil) + v.On("ValidateRequest", mock.Anything).Return(claims, nil) doorman.jwtValidators[audience] = v c, _ = gin.CreateTestContext(httptest.NewRecorder()) c.Request, _ = http.NewRequest("GET", "/get", nil) @@ -109,7 +109,7 @@ func TestJWTMiddleware(t *testing.T) { Audience: []string{"http://some.other.api"}, } v = &TestValidator{} - v.On("ExtractClaims", mock.Anything).Return(claims, nil) + v.On("ValidateRequest", mock.Anything).Return(claims, nil) doorman.jwtValidators[audience] = v c, _ = gin.CreateTestContext(httptest.NewRecorder()) c.Request, _ = http.NewRequest("GET", "/get", nil)