Skip to content

Commit 9fd1709

Browse files
committed
feat: add scopes validator for logical evalulation
1 parent c064f20 commit 9fd1709

File tree

8 files changed

+213
-82
lines changed

8 files changed

+213
-82
lines changed

.schema/config.schema.json

+16
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,16 @@
201201
"default": "none",
202202
"description": "Sets the strategy validation algorithm."
203203
},
204+
"scopesValidator": {
205+
"title": "Scope Validator",
206+
"type": "string",
207+
"enum": [
208+
"default",
209+
"any"
210+
],
211+
"default": "default",
212+
"description": "Sets the strategy verifier algorithm. Default is logical AND and any serves as OR"
213+
},
204214
"configErrorsRedirect": {
205215
"type": "object",
206216
"title": "HTTP Redirect Error Handler",
@@ -604,6 +614,9 @@
604614
"scope_strategy": {
605615
"$ref": "#/definitions/scopeStrategy"
606616
},
617+
"scopes_validator": {
618+
"$ref": "#/definitions/scopesValidator"
619+
},
607620
"token_from": {
608621
"title": "Token From",
609622
"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.",
@@ -712,6 +725,9 @@
712725
"scope_strategy": {
713726
"$ref": "#/definitions/scopeStrategy"
714727
},
728+
"scopes_validator": {
729+
"$ref": "#/definitions/scopesValidator"
730+
},
715731
"pre_authorization": {
716732
"title": "Pre-Authorization",
717733
"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.",
+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package credentials
2+
3+
import (
4+
"github.com/ory/herodot"
5+
"github.com/pkg/errors"
6+
)
7+
8+
type ScopesValidator func(scopeResult map[string]bool) error
9+
10+
func DefaultValidation(scopeResult map[string]bool) error {
11+
for sc, result := range scopeResult {
12+
if !result {
13+
return errors.WithStack(herodot.ErrInternalServerError.WithReasonf(`JSON Web Token is missing required scope "%s".`, sc))
14+
}
15+
}
16+
17+
return nil
18+
}
19+
20+
func AnyValidation(scopeResult map[string]bool) error {
21+
for _, result := range scopeResult {
22+
if result {
23+
return nil
24+
}
25+
}
26+
27+
return errors.WithStack(herodot.ErrInternalServerError.WithReasonf(`JSON Web Token is missing required scope`))
28+
}

credentials/verifier.go

+7-6
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,11 @@ type VerifierRegistry interface {
2525
}
2626

2727
type ValidationContext struct {
28-
Algorithms []string
29-
Issuers []string
30-
Audiences []string
31-
ScopeStrategy fosite.ScopeStrategy
32-
Scope []string
33-
KeyURLs []url.URL
28+
Algorithms []string
29+
Issuers []string
30+
Audiences []string
31+
ScopeStrategy fosite.ScopeStrategy
32+
ScopesValidator ScopesValidator
33+
Scope []string
34+
KeyURLs []url.URL
3435
}

credentials/verifier_default.go

+8-3
Original file line numberDiff line numberDiff line change
@@ -120,11 +120,16 @@ func (v *VerifierDefault) Verify(
120120
claims["scp"] = s
121121

122122
if r.ScopeStrategy != nil {
123+
scopeResult := make(map[string]bool, len(r.Scope))
124+
123125
for _, sc := range r.Scope {
124-
if !r.ScopeStrategy(s, sc) {
125-
return nil, herodot.ErrUnauthorized.WithReasonf(`JSON Web Token is missing required scope "%s".`, sc)
126-
}
126+
scopeResult[sc] = r.ScopeStrategy(s, sc)
127+
}
128+
129+
if err := r.ScopesValidator(scopeResult); err != nil {
130+
return nil, err
127131
}
132+
128133
} else {
129134
if len(r.Scope) > 0 {
130135
return nil, errors.WithStack(helper.ErrRuleFeatureDisabled.WithReason("Scope validation was requested but scope strategy is set to \"none\"."))

credentials/verifier_default_test.go

+130-66
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,13 @@ func TestVerifierDefault(t *testing.T) {
4646
{
4747
d: "should pass because JWT is valid",
4848
c: &ValidationContext{
49-
Algorithms: []string{"HS256"},
50-
Audiences: []string{"aud-1", "aud-2"},
51-
Issuers: []string{"iss-1", "iss-2"},
52-
Scope: []string{"scope-1", "scope-2"},
53-
KeyURLs: []url.URL{*x.ParseURLOrPanic("file://../test/stub/jwks-hs.json")},
54-
ScopeStrategy: fosite.ExactScopeStrategy,
49+
Algorithms: []string{"HS256"},
50+
Audiences: []string{"aud-1", "aud-2"},
51+
Issuers: []string{"iss-1", "iss-2"},
52+
Scope: []string{"scope-1", "scope-2"},
53+
KeyURLs: []url.URL{*x.ParseURLOrPanic("file://../test/stub/jwks-hs.json")},
54+
ScopeStrategy: fosite.ExactScopeStrategy,
55+
ScopesValidator: DefaultValidation,
5556
},
5657
token: sign(jwt.MapClaims{
5758
"sub": "sub",
@@ -68,15 +69,69 @@ func TestVerifierDefault(t *testing.T) {
6869
"scp": []string{"scope-3", "scope-2", "scope-1"},
6970
},
7071
},
72+
{
73+
d: "should pass because one of scopes is valid",
74+
c: &ValidationContext{
75+
Algorithms: []string{"HS256"},
76+
Audiences: []string{"aud-1", "aud-2"},
77+
Issuers: []string{"iss-1", "iss-2"},
78+
Scope: []string{"scope-1", "not-scope-2"},
79+
KeyURLs: []url.URL{*x.ParseURLOrPanic("file://../test/stub/jwks-hs.json")},
80+
ScopeStrategy: fosite.ExactScopeStrategy,
81+
ScopesValidator: AnyValidation,
82+
},
83+
token: sign(jwt.MapClaims{
84+
"sub": "sub",
85+
"exp": now.Add(time.Hour).Unix(),
86+
"aud": []string{"aud-1", "aud-2"},
87+
"iss": "iss-2",
88+
"scope": []string{"scope-3", "scope-2", "scope-1"},
89+
}, "file://../test/stub/jwks-hs.json"),
90+
expectClaims: jwt.MapClaims{
91+
"sub": "sub",
92+
"exp": float64(now.Add(time.Hour).Unix()),
93+
"aud": []interface{}{"aud-1", "aud-2"},
94+
"iss": "iss-2",
95+
"scp": []string{"scope-3", "scope-2", "scope-1"},
96+
},
97+
},
98+
{
99+
d: "should fail because one of scopes is invalid and validation is strict",
100+
c: &ValidationContext{
101+
Algorithms: []string{"HS256"},
102+
Audiences: []string{"aud-1", "aud-2"},
103+
Issuers: []string{"iss-1", "iss-2"},
104+
Scope: []string{"scope-1", "not-scope-2"},
105+
KeyURLs: []url.URL{*x.ParseURLOrPanic("file://../test/stub/jwks-hs.json")},
106+
ScopeStrategy: fosite.ExactScopeStrategy,
107+
ScopesValidator: DefaultValidation,
108+
},
109+
token: sign(jwt.MapClaims{
110+
"sub": "sub",
111+
"exp": now.Add(time.Hour).Unix(),
112+
"aud": []string{"aud-1", "aud-2"},
113+
"iss": "iss-2",
114+
"scope": []string{"scope-3", "scope-2", "scope-1"},
115+
}, "file://../test/stub/jwks-hs.json"),
116+
expectClaims: jwt.MapClaims{
117+
"sub": "sub",
118+
"exp": float64(now.Add(time.Hour).Unix()),
119+
"aud": []interface{}{"aud-1", "aud-2"},
120+
"iss": "iss-2",
121+
"scp": []string{"scope-3", "scope-2", "scope-1"},
122+
},
123+
expectErr: true,
124+
},
71125
{
72126
d: "should pass even when scope is a string",
73127
c: &ValidationContext{
74-
Algorithms: []string{"HS256"},
75-
Audiences: []string{"aud-1", "aud-2"},
76-
Issuers: []string{"iss-1", "iss-2"},
77-
Scope: []string{"scope-1", "scope-2"},
78-
KeyURLs: []url.URL{*x.ParseURLOrPanic("file://../test/stub/jwks-hs.json")},
79-
ScopeStrategy: fosite.ExactScopeStrategy,
128+
Algorithms: []string{"HS256"},
129+
Audiences: []string{"aud-1", "aud-2"},
130+
Issuers: []string{"iss-1", "iss-2"},
131+
Scope: []string{"scope-1", "scope-2"},
132+
KeyURLs: []url.URL{*x.ParseURLOrPanic("file://../test/stub/jwks-hs.json")},
133+
ScopeStrategy: fosite.ExactScopeStrategy,
134+
ScopesValidator: DefaultValidation,
80135
},
81136
token: sign(jwt.MapClaims{
82137
"sub": "sub",
@@ -96,12 +151,13 @@ func TestVerifierDefault(t *testing.T) {
96151
{
97152
d: "should pass when scope is keyed as scp",
98153
c: &ValidationContext{
99-
Algorithms: []string{"HS256"},
100-
Audiences: []string{"aud-1", "aud-2"},
101-
Issuers: []string{"iss-1", "iss-2"},
102-
Scope: []string{"scope-1", "scope-2"},
103-
KeyURLs: []url.URL{*x.ParseURLOrPanic("file://../test/stub/jwks-hs.json")},
104-
ScopeStrategy: fosite.ExactScopeStrategy,
154+
Algorithms: []string{"HS256"},
155+
Audiences: []string{"aud-1", "aud-2"},
156+
Issuers: []string{"iss-1", "iss-2"},
157+
Scope: []string{"scope-1", "scope-2"},
158+
KeyURLs: []url.URL{*x.ParseURLOrPanic("file://../test/stub/jwks-hs.json")},
159+
ScopeStrategy: fosite.ExactScopeStrategy,
160+
ScopesValidator: DefaultValidation,
105161
},
106162
token: sign(jwt.MapClaims{
107163
"sub": "sub",
@@ -121,12 +177,13 @@ func TestVerifierDefault(t *testing.T) {
121177
{
122178
d: "should pass when scope is keyed as scopes",
123179
c: &ValidationContext{
124-
Algorithms: []string{"HS256"},
125-
Audiences: []string{"aud-1", "aud-2"},
126-
Issuers: []string{"iss-1", "iss-2"},
127-
Scope: []string{"scope-1", "scope-2"},
128-
KeyURLs: []url.URL{*x.ParseURLOrPanic("file://../test/stub/jwks-hs.json")},
129-
ScopeStrategy: fosite.ExactScopeStrategy,
180+
Algorithms: []string{"HS256"},
181+
Audiences: []string{"aud-1", "aud-2"},
182+
Issuers: []string{"iss-1", "iss-2"},
183+
Scope: []string{"scope-1", "scope-2"},
184+
KeyURLs: []url.URL{*x.ParseURLOrPanic("file://../test/stub/jwks-hs.json")},
185+
ScopeStrategy: fosite.ExactScopeStrategy,
186+
ScopesValidator: DefaultValidation,
130187
},
131188
token: sign(jwt.MapClaims{
132189
"sub": "sub",
@@ -164,12 +221,13 @@ func TestVerifierDefault(t *testing.T) {
164221
{
165222
d: "should fail when algorithm does not match",
166223
c: &ValidationContext{
167-
Algorithms: []string{"HS256"},
168-
Audiences: []string{"aud-1", "aud-2"},
169-
Issuers: []string{"iss-1", "iss-2"},
170-
Scope: []string{"scope-1", "scope-2"},
171-
KeyURLs: []url.URL{*x.ParseURLOrPanic("file://../test/stub/jwks-rsa-single.json")},
172-
ScopeStrategy: fosite.ExactScopeStrategy,
224+
Algorithms: []string{"HS256"},
225+
Audiences: []string{"aud-1", "aud-2"},
226+
Issuers: []string{"iss-1", "iss-2"},
227+
Scope: []string{"scope-1", "scope-2"},
228+
KeyURLs: []url.URL{*x.ParseURLOrPanic("file://../test/stub/jwks-rsa-single.json")},
229+
ScopeStrategy: fosite.ExactScopeStrategy,
230+
ScopesValidator: DefaultValidation,
173231
},
174232
token: sign(jwt.MapClaims{
175233
"sub": "sub",
@@ -183,12 +241,13 @@ func TestVerifierDefault(t *testing.T) {
183241
{
184242
d: "should fail when audience mismatches",
185243
c: &ValidationContext{
186-
Algorithms: []string{"HS256"},
187-
Audiences: []string{"aud-1", "aud-2"},
188-
Issuers: []string{"iss-1", "iss-2"},
189-
Scope: []string{"scope-1", "scope-2"},
190-
KeyURLs: []url.URL{*x.ParseURLOrPanic("file://../test/stub/jwks-hs.json")},
191-
ScopeStrategy: fosite.ExactScopeStrategy,
244+
Algorithms: []string{"HS256"},
245+
Audiences: []string{"aud-1", "aud-2"},
246+
Issuers: []string{"iss-1", "iss-2"},
247+
Scope: []string{"scope-1", "scope-2"},
248+
KeyURLs: []url.URL{*x.ParseURLOrPanic("file://../test/stub/jwks-hs.json")},
249+
ScopeStrategy: fosite.ExactScopeStrategy,
250+
ScopesValidator: DefaultValidation,
192251
},
193252
token: sign(jwt.MapClaims{
194253
"sub": "sub",
@@ -202,12 +261,13 @@ func TestVerifierDefault(t *testing.T) {
202261
{
203262
d: "should fail when issuer mismatches",
204263
c: &ValidationContext{
205-
Algorithms: []string{"HS256"},
206-
Audiences: []string{"aud-1", "aud-2"},
207-
Issuers: []string{"iss-1", "iss-2"},
208-
Scope: []string{"scope-1", "scope-2"},
209-
KeyURLs: []url.URL{*x.ParseURLOrPanic("file://../test/stub/jwks-hs.json")},
210-
ScopeStrategy: fosite.ExactScopeStrategy,
264+
Algorithms: []string{"HS256"},
265+
Audiences: []string{"aud-1", "aud-2"},
266+
Issuers: []string{"iss-1", "iss-2"},
267+
Scope: []string{"scope-1", "scope-2"},
268+
KeyURLs: []url.URL{*x.ParseURLOrPanic("file://../test/stub/jwks-hs.json")},
269+
ScopeStrategy: fosite.ExactScopeStrategy,
270+
ScopesValidator: DefaultValidation,
211271
},
212272
token: sign(jwt.MapClaims{
213273
"sub": "sub",
@@ -221,12 +281,13 @@ func TestVerifierDefault(t *testing.T) {
221281
{
222282
d: "should fail when issuer mismatches",
223283
c: &ValidationContext{
224-
Algorithms: []string{"HS256"},
225-
Audiences: []string{"aud-1", "aud-2"},
226-
Issuers: []string{"iss-1", "iss-2"},
227-
Scope: []string{"scope-1", "scope-2"},
228-
KeyURLs: []url.URL{*x.ParseURLOrPanic("file://../test/stub/jwks-hs.json")},
229-
ScopeStrategy: fosite.ExactScopeStrategy,
284+
Algorithms: []string{"HS256"},
285+
Audiences: []string{"aud-1", "aud-2"},
286+
Issuers: []string{"iss-1", "iss-2"},
287+
Scope: []string{"scope-1", "scope-2"},
288+
KeyURLs: []url.URL{*x.ParseURLOrPanic("file://../test/stub/jwks-hs.json")},
289+
ScopeStrategy: fosite.ExactScopeStrategy,
290+
ScopesValidator: DefaultValidation,
230291
},
231292
token: sign(jwt.MapClaims{
232293
"sub": "sub",
@@ -240,12 +301,13 @@ func TestVerifierDefault(t *testing.T) {
240301
{
241302
d: "should fail when expired",
242303
c: &ValidationContext{
243-
Algorithms: []string{"HS256"},
244-
Audiences: []string{"aud-1", "aud-2"},
245-
Issuers: []string{"iss-1", "iss-2"},
246-
Scope: []string{"scope-1", "scope-2"},
247-
KeyURLs: []url.URL{*x.ParseURLOrPanic("file://../test/stub/jwks-hs.json")},
248-
ScopeStrategy: fosite.ExactScopeStrategy,
304+
Algorithms: []string{"HS256"},
305+
Audiences: []string{"aud-1", "aud-2"},
306+
Issuers: []string{"iss-1", "iss-2"},
307+
Scope: []string{"scope-1", "scope-2"},
308+
KeyURLs: []url.URL{*x.ParseURLOrPanic("file://../test/stub/jwks-hs.json")},
309+
ScopeStrategy: fosite.ExactScopeStrategy,
310+
ScopesValidator: DefaultValidation,
249311
},
250312
token: sign(jwt.MapClaims{
251313
"sub": "sub",
@@ -259,12 +321,13 @@ func TestVerifierDefault(t *testing.T) {
259321
{
260322
d: "should fail when nbf in future",
261323
c: &ValidationContext{
262-
Algorithms: []string{"HS256"},
263-
Audiences: []string{"aud-1", "aud-2"},
264-
Issuers: []string{"iss-1", "iss-2"},
265-
Scope: []string{"scope-1", "scope-2"},
266-
KeyURLs: []url.URL{*x.ParseURLOrPanic("file://../test/stub/jwks-hs.json")},
267-
ScopeStrategy: fosite.ExactScopeStrategy,
324+
Algorithms: []string{"HS256"},
325+
Audiences: []string{"aud-1", "aud-2"},
326+
Issuers: []string{"iss-1", "iss-2"},
327+
Scope: []string{"scope-1", "scope-2"},
328+
KeyURLs: []url.URL{*x.ParseURLOrPanic("file://../test/stub/jwks-hs.json")},
329+
ScopeStrategy: fosite.ExactScopeStrategy,
330+
ScopesValidator: DefaultValidation,
268331
},
269332
token: sign(jwt.MapClaims{
270333
"sub": "sub",
@@ -279,12 +342,13 @@ func TestVerifierDefault(t *testing.T) {
279342
{
280343
d: "should fail when iat in future",
281344
c: &ValidationContext{
282-
Algorithms: []string{"HS256"},
283-
Audiences: []string{"aud-1", "aud-2"},
284-
Issuers: []string{"iss-1", "iss-2"},
285-
Scope: []string{"scope-1", "scope-2"},
286-
KeyURLs: []url.URL{*x.ParseURLOrPanic("file://../test/stub/jwks-hs.json")},
287-
ScopeStrategy: fosite.ExactScopeStrategy,
345+
Algorithms: []string{"HS256"},
346+
Audiences: []string{"aud-1", "aud-2"},
347+
Issuers: []string{"iss-1", "iss-2"},
348+
Scope: []string{"scope-1", "scope-2"},
349+
KeyURLs: []url.URL{*x.ParseURLOrPanic("file://../test/stub/jwks-hs.json")},
350+
ScopeStrategy: fosite.ExactScopeStrategy,
351+
ScopesValidator: DefaultValidation,
288352
},
289353
token: sign(jwt.MapClaims{
290354
"sub": "sub",

driver/configuration/provider.go

+2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package configuration
55

66
import (
77
"encoding/json"
8+
"github.com/ory/oathkeeper/credentials"
89
"net/url"
910
"testing"
1011
"time"
@@ -70,6 +71,7 @@ type Provider interface {
7071
PrometheusHideRequestPaths() bool
7172
PrometheusCollapseRequestPaths() bool
7273

74+
ToScopesValidation(value string, key string) credentials.ScopesValidator
7375
ToScopeStrategy(value string, key string) fosite.ScopeStrategy
7476
ParseURLs(sources []string) ([]url.URL, error)
7577
JSONWebKeyURLs() []string

0 commit comments

Comments
 (0)