Skip to content
This repository has been archived by the owner on Dec 7, 2020. It is now read-only.

Add support for Strings claim #364

Merged
merged 6 commits into from
May 25, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,23 @@ match-claims:
email: ^.*@example.com$
```

The proxy supports matching on multivalue Strings claims. The match will succeed if one of the values matches, for example:

```YAML
match-claims:
perms: perm1
```

will successfully match

```JSON
{
"iss": "https://sso.example.com",
"sub": "",
"perms": ["perm1", "perm2"]
}
```

#### **Groups Claims**

You can match on the group claims within a token via the `groups` parameter available within the resource. Note while roles are implicitly required i.e. `roles=admin,user` the user MUST have roles 'admin' AND 'user', groups are applied with an OR operation, so `groups=users,testers` requires the user MUST be within 'users' OR 'testers'. At present the claim name is hardcoded to `groups` i.e a JWT token would look like the below.
Expand Down
94 changes: 61 additions & 33 deletions middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
"github.com/go-chi/chi/middleware"
"github.com/unrolled/secure"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)

const (
Expand Down Expand Up @@ -211,6 +212,65 @@ func (r *oauthProxy) authenticationMiddleware(resource *Resource) func(http.Hand
}
}

// checkClaim checks whether claim in userContext matches claimName, match. It can be String or Strings claim.
func (r *oauthProxy) checkClaim(user *userContext, claimName string, match *regexp.Regexp, resourceURL string) bool {
errFields := []zapcore.Field{
zap.String("claim", claimName),
zap.String("access", "denied"),
zap.String("email", user.email),
zap.String("resource", resourceURL),
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

given Claims is type Claims map[string]interface{} .. we could just and fail quick .. What do you think?

if _, found := user.claims[claimName]; !found {
    r.log.Warn("the token does not have the claim", errFields...)
    return false
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, I've added the statement here.

if _, found := user.claims[claimName]; !found {
r.log.Warn("the token does not have the claim", errFields...)
return false
}

// Check string claim.
valueStr, foundStr, errStr := user.claims.StringClaim(claimName)
// We have found string claim, so let's check whether it matches.
if foundStr {
if match.MatchString(valueStr) {
return true
}
r.log.Warn("claim requirement does not match claim in token", append(errFields,
zap.String("issued", valueStr),
zap.String("required", match.String()),
)...)

return false
}

// Check strings claim.
valueStrs, foundStrs, errStrs := user.claims.StringsClaim(claimName)
// We have found strings claim, so let's check whether it matches.
if foundStrs {
for _, value := range valueStrs {
if match.MatchString(value) {
return true
}
}
r.log.Warn("claim requirement does not match any element claim group in token", append(errFields,
zap.String("issued", fmt.Sprintf("%v", valueStrs)),
zap.String("required", match.String()),
)...)

return false
}

// If this fails, the claim is probably float or int.
if errStr != nil && errStrs != nil {
r.log.Error("unable to extract the claim from token (tried string and strings)", append(errFields,
zap.Error(errStr),
zap.Error(errStrs),
)...)
return false
}

r.log.Warn("unexpected error", errFields...)
return false
}

// admissionMiddleware is responsible checking the access token against the protected resource
func (r *oauthProxy) admissionMiddleware(resource *Resource) func(http.Handler) http.Handler {
claimMatches := make(map[string]*regexp.Regexp)
Expand Down Expand Up @@ -254,39 +314,7 @@ func (r *oauthProxy) admissionMiddleware(resource *Resource) func(http.Handler)

// step: if we have any claim matching, lets validate the tokens has the claims
for claimName, match := range claimMatches {
value, found, err := user.claims.StringClaim(claimName)
if err != nil {
r.log.Error("unable to extract the claim from token",
zap.String("access", "denied"),
zap.String("email", user.email),
zap.String("resource", resource.URL),
zap.Error(err))

next.ServeHTTP(w, req.WithContext(r.accessForbidden(w, req)))
return
}

if !found {
r.log.Warn("the token does not have the claim",
zap.String("access", "denied"),
zap.String("claim", claimName),
zap.String("email", user.email),
zap.String("resource", resource.URL))

next.ServeHTTP(w, req.WithContext(r.accessForbidden(w, req)))
return
}

// step: check the claim is the same
if !match.MatchString(value) {
r.log.Warn("the token claims does not match claim requirement",
zap.String("access", "denied"),
zap.String("claim", claimName),
zap.String("email", user.email),
zap.String("issued", value),
zap.String("required", match.String()),
zap.String("resource", resource.URL))

if !r.checkClaim(user, claimName, match, resource.URL) {
next.ServeHTTP(w, req.WithContext(r.accessForbidden(w, req)))
return
}
Expand Down
49 changes: 49 additions & 0 deletions middleware_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1192,6 +1192,7 @@ func TestRolesAdmissionHandlerClaims(t *testing.T) {
Matches map[string]string
Request fakeRequest
}{
// jose.StringClaim test
{
Matches: map[string]string{"cal": "test"},
Request: fakeRequest{
Expand Down Expand Up @@ -1269,6 +1270,54 @@ func TestRolesAdmissionHandlerClaims(t *testing.T) {
ExpectedCode: http.StatusOK,
},
},
// jose.StringsClaim test
{
Matches: map[string]string{"item": "^t.*t"},
Request: fakeRequest{
URI: uri,
HasToken: true,
TokenClaims: jose.Claims{"item": []string{"nonMatchingClaim", "test", "anotherNonMatching"}},
ExpectedProxy: true,
ExpectedCode: http.StatusOK,
},
},
{
Matches: map[string]string{"item": "^t.*t"},
Request: fakeRequest{
URI: uri,
HasToken: true,
TokenClaims: jose.Claims{"item": []string{"1test", "2test", "3test"}},
ExpectedProxy: false,
ExpectedCode: http.StatusForbidden,
},
},
{
Matches: map[string]string{"item": "^t.*t"},
Request: fakeRequest{
URI: uri,
HasToken: true,
TokenClaims: jose.Claims{"item": []string{}},
ExpectedProxy: false,
ExpectedCode: http.StatusForbidden,
},
},
{
Matches: map[string]string{
"item1": "^t.*t",
"item2": "^another",
},
Request: fakeRequest{
URI: uri,
HasToken: true,
TokenClaims: jose.Claims{
"item1": []string{"randomItem", "test"},
"item2": []string{"randomItem", "anotherItem"},
"item3": []string{"randomItem2", "anotherItem3"},
},
ExpectedProxy: true,
ExpectedCode: http.StatusOK,
},
},
}
for _, c := range requests {
cfg := newFakeKeycloakConfig()
Expand Down