зеркало из https://github.com/mozilla/doorman.git
Родитель
e056e5bff6
Коммит
b969c196d5
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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")
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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{}
|
|
@ -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{}
|
|
@ -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")
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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("<html>"))
|
||||
_, 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("<html>"))
|
||||
_, 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()
|
||||
}
|
||||
}
|
|
@ -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(),
|
||||
|
|
|
@ -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)
|
||||
|
|
Загрузка…
Ссылка в новой задаче