Skip to content

Commit

Permalink
feat: add scopes validator for logical evalulation
Browse files Browse the repository at this point in the history
  • Loading branch information
JarekKa committed Dec 11, 2023
1 parent c064f20 commit 9fd1709
Show file tree
Hide file tree
Showing 8 changed files with 213 additions and 82 deletions.
16 changes: 16 additions & 0 deletions .schema/config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,16 @@
"default": "none",
"description": "Sets the strategy validation algorithm."
},
"scopesValidator": {
"title": "Scope Validator",
"type": "string",
"enum": [
"default",
"any"
],
"default": "default",
"description": "Sets the strategy verifier algorithm. Default is logical AND and any serves as OR"
},
"configErrorsRedirect": {
"type": "object",
"title": "HTTP Redirect Error Handler",
Expand Down Expand Up @@ -604,6 +614,9 @@
"scope_strategy": {
"$ref": "#/definitions/scopeStrategy"
},
"scopes_validator": {
"$ref": "#/definitions/scopesValidator"
},
"token_from": {
"title": "Token From",
"description": "The location of the token.\n If not configured, the token will be received from a default location - 'Authorization' header.\n One and only one location (header or query) must be specified.",
Expand Down Expand Up @@ -712,6 +725,9 @@
"scope_strategy": {
"$ref": "#/definitions/scopeStrategy"
},
"scopes_validator": {
"$ref": "#/definitions/scopesValidator"
},
"pre_authorization": {
"title": "Pre-Authorization",
"description": "Enable pre-authorization in cases where the OAuth 2.0 Token Introspection endpoint is protected by OAuth 2.0 Bearer Tokens that can be retrieved using the OAuth 2.0 Client Credentials grant.",
Expand Down
28 changes: 28 additions & 0 deletions credentials/scopes_logical_validator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package credentials

import (
"github.com/ory/herodot"
"github.com/pkg/errors"
)

type ScopesValidator func(scopeResult map[string]bool) error

func DefaultValidation(scopeResult map[string]bool) error {
for sc, result := range scopeResult {
if !result {
return errors.WithStack(herodot.ErrInternalServerError.WithReasonf(`JSON Web Token is missing required scope "%s".`, sc))
}
}

return nil
}

func AnyValidation(scopeResult map[string]bool) error {
for _, result := range scopeResult {
if result {
return nil
}
}

return errors.WithStack(herodot.ErrInternalServerError.WithReasonf(`JSON Web Token is missing required scope`))
}
13 changes: 7 additions & 6 deletions credentials/verifier.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,11 @@ type VerifierRegistry interface {
}

type ValidationContext struct {
Algorithms []string
Issuers []string
Audiences []string
ScopeStrategy fosite.ScopeStrategy
Scope []string
KeyURLs []url.URL
Algorithms []string
Issuers []string
Audiences []string
ScopeStrategy fosite.ScopeStrategy
ScopesValidator ScopesValidator
Scope []string
KeyURLs []url.URL
}
11 changes: 8 additions & 3 deletions credentials/verifier_default.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,11 +120,16 @@ func (v *VerifierDefault) Verify(
claims["scp"] = s

if r.ScopeStrategy != nil {
scopeResult := make(map[string]bool, len(r.Scope))

for _, sc := range r.Scope {
if !r.ScopeStrategy(s, sc) {
return nil, herodot.ErrUnauthorized.WithReasonf(`JSON Web Token is missing required scope "%s".`, sc)
}
scopeResult[sc] = r.ScopeStrategy(s, sc)
}

if err := r.ScopesValidator(scopeResult); err != nil {
return nil, err
}

} else {
if len(r.Scope) > 0 {
return nil, errors.WithStack(helper.ErrRuleFeatureDisabled.WithReason("Scope validation was requested but scope strategy is set to \"none\"."))
Expand Down
196 changes: 130 additions & 66 deletions credentials/verifier_default_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,13 @@ func TestVerifierDefault(t *testing.T) {
{
d: "should pass because JWT is valid",
c: &ValidationContext{
Algorithms: []string{"HS256"},
Audiences: []string{"aud-1", "aud-2"},
Issuers: []string{"iss-1", "iss-2"},
Scope: []string{"scope-1", "scope-2"},
KeyURLs: []url.URL{*x.ParseURLOrPanic("file://../test/stub/jwks-hs.json")},
ScopeStrategy: fosite.ExactScopeStrategy,
Algorithms: []string{"HS256"},
Audiences: []string{"aud-1", "aud-2"},
Issuers: []string{"iss-1", "iss-2"},
Scope: []string{"scope-1", "scope-2"},
KeyURLs: []url.URL{*x.ParseURLOrPanic("file://../test/stub/jwks-hs.json")},
ScopeStrategy: fosite.ExactScopeStrategy,
ScopesValidator: DefaultValidation,
},
token: sign(jwt.MapClaims{
"sub": "sub",
Expand All @@ -68,15 +69,69 @@ func TestVerifierDefault(t *testing.T) {
"scp": []string{"scope-3", "scope-2", "scope-1"},
},
},
{
d: "should pass because one of scopes is valid",
c: &ValidationContext{
Algorithms: []string{"HS256"},
Audiences: []string{"aud-1", "aud-2"},
Issuers: []string{"iss-1", "iss-2"},
Scope: []string{"scope-1", "not-scope-2"},
KeyURLs: []url.URL{*x.ParseURLOrPanic("file://../test/stub/jwks-hs.json")},
ScopeStrategy: fosite.ExactScopeStrategy,
ScopesValidator: AnyValidation,
},
token: sign(jwt.MapClaims{
"sub": "sub",
"exp": now.Add(time.Hour).Unix(),
"aud": []string{"aud-1", "aud-2"},
"iss": "iss-2",
"scope": []string{"scope-3", "scope-2", "scope-1"},
}, "file://../test/stub/jwks-hs.json"),
expectClaims: jwt.MapClaims{
"sub": "sub",
"exp": float64(now.Add(time.Hour).Unix()),
"aud": []interface{}{"aud-1", "aud-2"},
"iss": "iss-2",
"scp": []string{"scope-3", "scope-2", "scope-1"},
},
},
{
d: "should fail because one of scopes is invalid and validation is strict",
c: &ValidationContext{
Algorithms: []string{"HS256"},
Audiences: []string{"aud-1", "aud-2"},
Issuers: []string{"iss-1", "iss-2"},
Scope: []string{"scope-1", "not-scope-2"},
KeyURLs: []url.URL{*x.ParseURLOrPanic("file://../test/stub/jwks-hs.json")},
ScopeStrategy: fosite.ExactScopeStrategy,
ScopesValidator: DefaultValidation,
},
token: sign(jwt.MapClaims{
"sub": "sub",
"exp": now.Add(time.Hour).Unix(),
"aud": []string{"aud-1", "aud-2"},
"iss": "iss-2",
"scope": []string{"scope-3", "scope-2", "scope-1"},
}, "file://../test/stub/jwks-hs.json"),
expectClaims: jwt.MapClaims{
"sub": "sub",
"exp": float64(now.Add(time.Hour).Unix()),
"aud": []interface{}{"aud-1", "aud-2"},
"iss": "iss-2",
"scp": []string{"scope-3", "scope-2", "scope-1"},
},
expectErr: true,
},
{
d: "should pass even when scope is a string",
c: &ValidationContext{
Algorithms: []string{"HS256"},
Audiences: []string{"aud-1", "aud-2"},
Issuers: []string{"iss-1", "iss-2"},
Scope: []string{"scope-1", "scope-2"},
KeyURLs: []url.URL{*x.ParseURLOrPanic("file://../test/stub/jwks-hs.json")},
ScopeStrategy: fosite.ExactScopeStrategy,
Algorithms: []string{"HS256"},
Audiences: []string{"aud-1", "aud-2"},
Issuers: []string{"iss-1", "iss-2"},
Scope: []string{"scope-1", "scope-2"},
KeyURLs: []url.URL{*x.ParseURLOrPanic("file://../test/stub/jwks-hs.json")},
ScopeStrategy: fosite.ExactScopeStrategy,
ScopesValidator: DefaultValidation,
},
token: sign(jwt.MapClaims{
"sub": "sub",
Expand All @@ -96,12 +151,13 @@ func TestVerifierDefault(t *testing.T) {
{
d: "should pass when scope is keyed as scp",
c: &ValidationContext{
Algorithms: []string{"HS256"},
Audiences: []string{"aud-1", "aud-2"},
Issuers: []string{"iss-1", "iss-2"},
Scope: []string{"scope-1", "scope-2"},
KeyURLs: []url.URL{*x.ParseURLOrPanic("file://../test/stub/jwks-hs.json")},
ScopeStrategy: fosite.ExactScopeStrategy,
Algorithms: []string{"HS256"},
Audiences: []string{"aud-1", "aud-2"},
Issuers: []string{"iss-1", "iss-2"},
Scope: []string{"scope-1", "scope-2"},
KeyURLs: []url.URL{*x.ParseURLOrPanic("file://../test/stub/jwks-hs.json")},
ScopeStrategy: fosite.ExactScopeStrategy,
ScopesValidator: DefaultValidation,
},
token: sign(jwt.MapClaims{
"sub": "sub",
Expand All @@ -121,12 +177,13 @@ func TestVerifierDefault(t *testing.T) {
{
d: "should pass when scope is keyed as scopes",
c: &ValidationContext{
Algorithms: []string{"HS256"},
Audiences: []string{"aud-1", "aud-2"},
Issuers: []string{"iss-1", "iss-2"},
Scope: []string{"scope-1", "scope-2"},
KeyURLs: []url.URL{*x.ParseURLOrPanic("file://../test/stub/jwks-hs.json")},
ScopeStrategy: fosite.ExactScopeStrategy,
Algorithms: []string{"HS256"},
Audiences: []string{"aud-1", "aud-2"},
Issuers: []string{"iss-1", "iss-2"},
Scope: []string{"scope-1", "scope-2"},
KeyURLs: []url.URL{*x.ParseURLOrPanic("file://../test/stub/jwks-hs.json")},
ScopeStrategy: fosite.ExactScopeStrategy,
ScopesValidator: DefaultValidation,
},
token: sign(jwt.MapClaims{
"sub": "sub",
Expand Down Expand Up @@ -164,12 +221,13 @@ func TestVerifierDefault(t *testing.T) {
{
d: "should fail when algorithm does not match",
c: &ValidationContext{
Algorithms: []string{"HS256"},
Audiences: []string{"aud-1", "aud-2"},
Issuers: []string{"iss-1", "iss-2"},
Scope: []string{"scope-1", "scope-2"},
KeyURLs: []url.URL{*x.ParseURLOrPanic("file://../test/stub/jwks-rsa-single.json")},
ScopeStrategy: fosite.ExactScopeStrategy,
Algorithms: []string{"HS256"},
Audiences: []string{"aud-1", "aud-2"},
Issuers: []string{"iss-1", "iss-2"},
Scope: []string{"scope-1", "scope-2"},
KeyURLs: []url.URL{*x.ParseURLOrPanic("file://../test/stub/jwks-rsa-single.json")},
ScopeStrategy: fosite.ExactScopeStrategy,
ScopesValidator: DefaultValidation,
},
token: sign(jwt.MapClaims{
"sub": "sub",
Expand All @@ -183,12 +241,13 @@ func TestVerifierDefault(t *testing.T) {
{
d: "should fail when audience mismatches",
c: &ValidationContext{
Algorithms: []string{"HS256"},
Audiences: []string{"aud-1", "aud-2"},
Issuers: []string{"iss-1", "iss-2"},
Scope: []string{"scope-1", "scope-2"},
KeyURLs: []url.URL{*x.ParseURLOrPanic("file://../test/stub/jwks-hs.json")},
ScopeStrategy: fosite.ExactScopeStrategy,
Algorithms: []string{"HS256"},
Audiences: []string{"aud-1", "aud-2"},
Issuers: []string{"iss-1", "iss-2"},
Scope: []string{"scope-1", "scope-2"},
KeyURLs: []url.URL{*x.ParseURLOrPanic("file://../test/stub/jwks-hs.json")},
ScopeStrategy: fosite.ExactScopeStrategy,
ScopesValidator: DefaultValidation,
},
token: sign(jwt.MapClaims{
"sub": "sub",
Expand All @@ -202,12 +261,13 @@ func TestVerifierDefault(t *testing.T) {
{
d: "should fail when issuer mismatches",
c: &ValidationContext{
Algorithms: []string{"HS256"},
Audiences: []string{"aud-1", "aud-2"},
Issuers: []string{"iss-1", "iss-2"},
Scope: []string{"scope-1", "scope-2"},
KeyURLs: []url.URL{*x.ParseURLOrPanic("file://../test/stub/jwks-hs.json")},
ScopeStrategy: fosite.ExactScopeStrategy,
Algorithms: []string{"HS256"},
Audiences: []string{"aud-1", "aud-2"},
Issuers: []string{"iss-1", "iss-2"},
Scope: []string{"scope-1", "scope-2"},
KeyURLs: []url.URL{*x.ParseURLOrPanic("file://../test/stub/jwks-hs.json")},
ScopeStrategy: fosite.ExactScopeStrategy,
ScopesValidator: DefaultValidation,
},
token: sign(jwt.MapClaims{
"sub": "sub",
Expand All @@ -221,12 +281,13 @@ func TestVerifierDefault(t *testing.T) {
{
d: "should fail when issuer mismatches",
c: &ValidationContext{
Algorithms: []string{"HS256"},
Audiences: []string{"aud-1", "aud-2"},
Issuers: []string{"iss-1", "iss-2"},
Scope: []string{"scope-1", "scope-2"},
KeyURLs: []url.URL{*x.ParseURLOrPanic("file://../test/stub/jwks-hs.json")},
ScopeStrategy: fosite.ExactScopeStrategy,
Algorithms: []string{"HS256"},
Audiences: []string{"aud-1", "aud-2"},
Issuers: []string{"iss-1", "iss-2"},
Scope: []string{"scope-1", "scope-2"},
KeyURLs: []url.URL{*x.ParseURLOrPanic("file://../test/stub/jwks-hs.json")},
ScopeStrategy: fosite.ExactScopeStrategy,
ScopesValidator: DefaultValidation,
},
token: sign(jwt.MapClaims{
"sub": "sub",
Expand All @@ -240,12 +301,13 @@ func TestVerifierDefault(t *testing.T) {
{
d: "should fail when expired",
c: &ValidationContext{
Algorithms: []string{"HS256"},
Audiences: []string{"aud-1", "aud-2"},
Issuers: []string{"iss-1", "iss-2"},
Scope: []string{"scope-1", "scope-2"},
KeyURLs: []url.URL{*x.ParseURLOrPanic("file://../test/stub/jwks-hs.json")},
ScopeStrategy: fosite.ExactScopeStrategy,
Algorithms: []string{"HS256"},
Audiences: []string{"aud-1", "aud-2"},
Issuers: []string{"iss-1", "iss-2"},
Scope: []string{"scope-1", "scope-2"},
KeyURLs: []url.URL{*x.ParseURLOrPanic("file://../test/stub/jwks-hs.json")},
ScopeStrategy: fosite.ExactScopeStrategy,
ScopesValidator: DefaultValidation,
},
token: sign(jwt.MapClaims{
"sub": "sub",
Expand All @@ -259,12 +321,13 @@ func TestVerifierDefault(t *testing.T) {
{
d: "should fail when nbf in future",
c: &ValidationContext{
Algorithms: []string{"HS256"},
Audiences: []string{"aud-1", "aud-2"},
Issuers: []string{"iss-1", "iss-2"},
Scope: []string{"scope-1", "scope-2"},
KeyURLs: []url.URL{*x.ParseURLOrPanic("file://../test/stub/jwks-hs.json")},
ScopeStrategy: fosite.ExactScopeStrategy,
Algorithms: []string{"HS256"},
Audiences: []string{"aud-1", "aud-2"},
Issuers: []string{"iss-1", "iss-2"},
Scope: []string{"scope-1", "scope-2"},
KeyURLs: []url.URL{*x.ParseURLOrPanic("file://../test/stub/jwks-hs.json")},
ScopeStrategy: fosite.ExactScopeStrategy,
ScopesValidator: DefaultValidation,
},
token: sign(jwt.MapClaims{
"sub": "sub",
Expand All @@ -279,12 +342,13 @@ func TestVerifierDefault(t *testing.T) {
{
d: "should fail when iat in future",
c: &ValidationContext{
Algorithms: []string{"HS256"},
Audiences: []string{"aud-1", "aud-2"},
Issuers: []string{"iss-1", "iss-2"},
Scope: []string{"scope-1", "scope-2"},
KeyURLs: []url.URL{*x.ParseURLOrPanic("file://../test/stub/jwks-hs.json")},
ScopeStrategy: fosite.ExactScopeStrategy,
Algorithms: []string{"HS256"},
Audiences: []string{"aud-1", "aud-2"},
Issuers: []string{"iss-1", "iss-2"},
Scope: []string{"scope-1", "scope-2"},
KeyURLs: []url.URL{*x.ParseURLOrPanic("file://../test/stub/jwks-hs.json")},
ScopeStrategy: fosite.ExactScopeStrategy,
ScopesValidator: DefaultValidation,
},
token: sign(jwt.MapClaims{
"sub": "sub",
Expand Down
2 changes: 2 additions & 0 deletions driver/configuration/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package configuration

import (
"encoding/json"
"github.com/ory/oathkeeper/credentials"

Check failure on line 8 in driver/configuration/provider.go

View workflow job for this annotation

GitHub Actions / Run tests and lints

File is not `goimports`-ed (goimports)
"net/url"
"testing"
"time"
Expand Down Expand Up @@ -70,6 +71,7 @@ type Provider interface {
PrometheusHideRequestPaths() bool
PrometheusCollapseRequestPaths() bool

ToScopesValidation(value string, key string) credentials.ScopesValidator
ToScopeStrategy(value string, key string) fosite.ScopeStrategy
ParseURLs(sources []string) ([]url.URL, error)
JSONWebKeyURLs() []string
Expand Down
Loading

0 comments on commit 9fd1709

Please sign in to comment.