Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add scopes validator for logical evalulation #1143

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
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
13 changes: 13 additions & 0 deletions .schema/config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,13 @@
"default": "none",
"description": "Sets the strategy validation algorithm."
},
"scopeValidation": {
"title": "Scope Validation",
"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 +611,9 @@
"scope_strategy": {
"$ref": "#/definitions/scopeStrategy"
},
"scope_validation": {
"$ref": "#/definitions/ScopeValidation"
},
"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 +722,9 @@
"scope_strategy": {
"$ref": "#/definitions/scopeStrategy"
},
"scope_validation": {
"$ref": "#/definitions/scopeValidation"
},
"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
32 changes: 32 additions & 0 deletions credentials/scopes_logical_validator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0

package credentials

import (
"github.com/pkg/errors"

"github.com/ory/herodot"
)

type ScopeValidation 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
ScopeValidation ScopeValidation
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.ScopeValidation(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,
ScopeValidation: 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,
ScopeValidation: 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,
ScopeValidation: 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,
ScopeValidation: 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,
ScopeValidation: 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,
ScopeValidation: 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,
ScopeValidation: 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,
ScopeValidation: 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,
ScopeValidation: 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,
ScopeValidation: 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,
ScopeValidation: 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,
ScopeValidation: 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,
ScopeValidation: DefaultValidation,
},
token: sign(jwt.MapClaims{
"sub": "sub",
Expand Down
3 changes: 3 additions & 0 deletions driver/configuration/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import (
"testing"
"time"

"github.com/ory/oathkeeper/credentials"

"github.com/rs/cors"

"github.com/ory/fosite"
Expand Down Expand Up @@ -70,6 +72,7 @@ type Provider interface {
PrometheusHideRequestPaths() bool
PrometheusCollapseRequestPaths() bool

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