Skip to content
Snippets Groups Projects
Commit 88e216bb authored by Stefan Sakalik's avatar Stefan Sakalik Committed by Rohith Jayawardene
Browse files

Add support for Strings claim (#364)

* Add support for claim groups in keycloak-proxy.

* Fixes + simplifications.

* address comment

* fix ed
parent 2d850be0
No related branches found
No related tags found
No related merge requests found
...@@ -450,6 +450,23 @@ match-claims: ...@@ -450,6 +450,23 @@ match-claims:
email: ^.*@example.com$ 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** #### **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. 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.
......
...@@ -28,6 +28,7 @@ import ( ...@@ -28,6 +28,7 @@ import (
"github.com/go-chi/chi/middleware" "github.com/go-chi/chi/middleware"
"github.com/unrolled/secure" "github.com/unrolled/secure"
"go.uber.org/zap" "go.uber.org/zap"
"go.uber.org/zap/zapcore"
) )
const ( const (
...@@ -211,6 +212,65 @@ func (r *oauthProxy) authenticationMiddleware(resource *Resource) func(http.Hand ...@@ -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),
}
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 // admissionMiddleware is responsible checking the access token against the protected resource
func (r *oauthProxy) admissionMiddleware(resource *Resource) func(http.Handler) http.Handler { func (r *oauthProxy) admissionMiddleware(resource *Resource) func(http.Handler) http.Handler {
claimMatches := make(map[string]*regexp.Regexp) claimMatches := make(map[string]*regexp.Regexp)
...@@ -254,39 +314,7 @@ func (r *oauthProxy) admissionMiddleware(resource *Resource) func(http.Handler) ...@@ -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 // step: if we have any claim matching, lets validate the tokens has the claims
for claimName, match := range claimMatches { for claimName, match := range claimMatches {
value, found, err := user.claims.StringClaim(claimName) if !r.checkClaim(user, claimName, match, resource.URL) {
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))
next.ServeHTTP(w, req.WithContext(r.accessForbidden(w, req))) next.ServeHTTP(w, req.WithContext(r.accessForbidden(w, req)))
return return
} }
......
...@@ -1192,6 +1192,7 @@ func TestRolesAdmissionHandlerClaims(t *testing.T) { ...@@ -1192,6 +1192,7 @@ func TestRolesAdmissionHandlerClaims(t *testing.T) {
Matches map[string]string Matches map[string]string
Request fakeRequest Request fakeRequest
}{ }{
// jose.StringClaim test
{ {
Matches: map[string]string{"cal": "test"}, Matches: map[string]string{"cal": "test"},
Request: fakeRequest{ Request: fakeRequest{
...@@ -1269,6 +1270,54 @@ func TestRolesAdmissionHandlerClaims(t *testing.T) { ...@@ -1269,6 +1270,54 @@ func TestRolesAdmissionHandlerClaims(t *testing.T) {
ExpectedCode: http.StatusOK, 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 { for _, c := range requests {
cfg := newFakeKeycloakConfig() cfg := newFakeKeycloakConfig()
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment