Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
cb2c67b
feat: allow custom scope claim name
alepane21 Apr 1, 2026
74635d0
feat: add documentation about new config
alepane21 Apr 1, 2026
e1aaf99
fix: force scope_claim min length
alepane21 Apr 1, 2026
136505b
Merge branch 'main' into ale/eng-9325-router-allow-different-name-for…
alepane21 Apr 1, 2026
0e6074b
Merge branch 'main' into ale/eng-9325-router-allow-different-name-for…
alepane21 Apr 2, 2026
0143c03
Merge branch 'main' into ale/eng-9325-router-allow-different-name-for…
alepane21 Apr 2, 2026
58e3f00
Merge branch 'main' into ale/eng-9325-router-allow-different-name-for…
alepane21 Apr 7, 2026
79ed1ff
fix: avoid useless complexity
alepane21 Apr 7, 2026
e93fd0b
Merge remote-tracking branch 'origin/ale/eng-9325-router-allow-differ…
alepane21 Apr 7, 2026
d8065f7
fix: set scope_claims only if different from an empty string
alepane21 Apr 7, 2026
6f53a97
fix: do not allow empty scopeClaim
alepane21 Apr 7, 2026
34e1fec
Merge branch 'main' into ale/eng-9325-router-allow-different-name-for…
alepane21 Apr 8, 2026
0b6b798
Merge branch 'main' into ale/eng-9325-router-allow-different-name-for…
alepane21 Apr 8, 2026
9e7f199
Merge branch 'main' into ale/eng-9325-router-allow-different-name-for…
alepane21 Apr 9, 2026
9efcd97
Merge branch 'main' into ale/eng-9325-router-allow-different-name-for…
alepane21 Apr 10, 2026
447d602
fix: update scope_claim description to clarify support for top level …
alepane21 Apr 10, 2026
83024a1
Merge branch 'main' into ale/eng-9325-router-allow-different-name-for…
alepane21 Apr 10, 2026
6c3998c
Merge branch 'main' into ale/eng-9325-router-allow-different-name-for…
alepane21 Apr 21, 2026
ec2490f
refactor: simplify scope claim handling in authentication process
alepane21 Apr 21, 2026
565db60
Merge branch 'main' into ale/eng-9325-router-allow-different-name-for…
alepane21 Apr 21, 2026
fedae41
fix: correct JSON formatting in employees query body
alepane21 Apr 21, 2026
463c127
Merge remote-tracking branch 'origin/ale/eng-9325-router-allow-differ…
alepane21 Apr 21, 2026
ec5a8d2
Merge branch 'main' into ale/eng-9325-router-allow-different-name-for…
alepane21 Apr 21, 2026
17399bd
fix: use default scope claim in authentication initialization
alepane21 Apr 21, 2026
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
4 changes: 3 additions & 1 deletion docs-website/router/authentication-and-authorization.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ In the current router version, the configuration and behavior of authentication
secret: <your_secret>
header_key_id: some-key-id
header_name: Authorization # Optional, Authorization is the default value
scope_claim: scope # Optional, scope is the default value
header_value_prefix: Bearer # Optional, Bearer is the default value
header_sources:
- type: header
Expand All @@ -54,6 +55,8 @@ In the current router version, the configuration and behavior of authentication

The router configuration facilitates the setup of multiple JWKS (JSON Web Key Set) endpoints, each customizable with distinct retrieval settings. It allows specification of supported JWT (JSON Web Token) algorithms per endpoint. It also allows the use of secrets for symmetric algorithms, as it would be a security risk to expose them over a JWKS endpoint. Centralizing header rules application across all keys from every JWKS endpoint simplifies management. This setup grants centralized control while offering flexibility in the retrieval and processing of keys.

Use `scope_claim` when your provider stores authorization scopes in a claim other than `scope`, such as `scp` or `roles`.

For more information on the attributes, visit the auth configuration parameter section page [here](/router/configuration#authentication).

### Disabling Authentication for Introspection Operations
Expand Down Expand Up @@ -176,4 +179,3 @@ If we send 6 simultaneous requests with unknown KIDs:
The 2nd, 3rd and 4th requests are rate-limited because the burst capacity is set to 1. After the first request passes, the number of available burst tokens is 0 and subsequent requests must wait until the `interval` elapses. If `burst` were set to 2, the second request would also pass immediately.

The `max_wait` setting prevents excessive wait times. In this example, since the 5th and 6th requests would require waiting longer than the configured `max_wait` of 110s, they immediately return with a 401 Unauthorized status instead of attempting to refresh.

2 changes: 2 additions & 0 deletions docs-website/router/configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1567,6 +1567,7 @@ In addition to the above JWKS configuration flavours, you can define a list of a

| YAML | Required | Description | Default Value |
| ------------------- | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------- | ------------- |
| scope_claim | <Icon icon="square" /> | The JWT claim used to read scopes for authorization. Use this when your identity provider stores scopes in a claim other than `scope`. | scope |
| header_name | <Icon icon="square" /> | The name of the header. The header is used to extract the token from the request. The default value is 'Authorization'. | Authorization |
| header_value_prefix | <Icon icon="square" /> | The prefix of the header value. The prefix is used to extract the token from the header value. The default value is 'Bearer'. | Bearer |

Expand Down Expand Up @@ -1602,6 +1603,7 @@ authentication:
interval: 30s
burst: 2
header_name: Authorization # This is the default value
scope_claim: scope # This is the default value
header_value_prefix: Bearer # This is the default value
header_sources:
- type: header
Expand Down
51 changes: 51 additions & 0 deletions router-tests/security/authentication_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -606,6 +606,57 @@ func TestAuthentication(t *testing.T) {
require.Equal(t, `{"data":{"employees":[{"id":1,"startDate":"January 2020"},{"id":2,"startDate":"July 2022"},{"id":3,"startDate":"June 2021"},{"id":4,"startDate":"July 2022"},{"id":5,"startDate":"July 2022"},{"id":7,"startDate":"September 2022"},{"id":8,"startDate":"September 2022"},{"id":10,"startDate":"November 2022"},{"id":11,"startDate":"November 2022"},{"id":12,"startDate":"December 2022"}]}}`, string(data))
})
})
t.Run("scopes required valid token from configured custom scope claim", func(t *testing.T) {
Comment thread
alepane21 marked this conversation as resolved.
Outdated
t.Parallel()

authServer, err := jwks.NewServer(t)
require.NoError(t, err)
t.Cleanup(authServer.Close)

tokenDecoder, err := authentication.NewJwksTokenDecoder(
testutils.NewContextWithCancel(t),
zap.NewNop(),
[]authentication.JWKSConfig{toJWKSConfig(authServer.JWKSURL(), time.Second*5)},
)
require.NoError(t, err)

authenticator, err := authentication.NewHttpHeaderAuthenticator(authentication.HttpHeaderAuthenticatorOptions{
Name: testutils.JwksName,
TokenDecoder: tokenDecoder,
})
require.NoError(t, err)

accessController, err := core.NewAccessController(core.AccessControllerOptions{
Authenticators: []authentication.Authenticator{authenticator},
AuthenticationRequired: false,
SkipIntrospectionQueries: false,
IntrospectionSkipSecret: "",
ScopeClaim: "scp",
})
require.NoError(t, err)

testenv.Run(t, &testenv.Config{
RouterOptions: []core.Option{
core.WithAccessController(accessController),
},
}, func(t *testing.T, xEnv *testenv.Environment) {
token, err := authServer.Token(map[string]any{
"scp": "read:employee read:private",
})
require.NoError(t, err)
header := http.Header{
"Authorization": []string{"Bearer " + token},
}
res, err := xEnv.MakeRequest(http.MethodPost, "/graphql", header, strings.NewReader(employeesQueryRequiringClaims))
require.NoError(t, err)
defer res.Body.Close()
require.Equal(t, http.StatusOK, res.StatusCode)
require.Equal(t, testutils.JwksName, res.Header.Get(xAuthenticatedByHeader))
data, err := io.ReadAll(res.Body)
Comment thread
alepane21 marked this conversation as resolved.
Outdated
require.NoError(t, err)
require.Equal(t, `{"data":{"employees":[{"id":1,"startDate":"January 2020"},{"id":2,"startDate":"July 2022"},{"id":3,"startDate":"June 2021"},{"id":4,"startDate":"July 2022"},{"id":5,"startDate":"July 2022"},{"id":7,"startDate":"September 2022"},{"id":8,"startDate":"September 2022"},{"id":10,"startDate":"November 2022"},{"id":11,"startDate":"November 2022"},{"id":12,"startDate":"December 2022"}]}}`, string(data))
})
})
t.Run("scopes required valid token AND scopes present with alias", func(t *testing.T) {
t.Parallel()

Expand Down
4 changes: 4 additions & 0 deletions router/core/access_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type AccessControllerOptions struct {
AuthenticationRequired bool
SkipIntrospectionQueries bool
IntrospectionSkipSecret string
ScopeClaim string
}

// AccessController handles both authentication and authorization for the Router
Expand All @@ -30,6 +31,7 @@ type AccessController struct {
authenticators []authentication.Authenticator
skipIntrospectionQueries bool
introspectionSkipSecret string
scopeClaim string
}

// NewAccessController creates a new AccessController.
Expand All @@ -40,6 +42,7 @@ func NewAccessController(opts AccessControllerOptions) (*AccessController, error
skipIntrospectionQueries: opts.SkipIntrospectionQueries,
authenticators: opts.Authenticators,
introspectionSkipSecret: opts.IntrospectionSkipSecret,
scopeClaim: opts.ScopeClaim,
}, nil
}

Expand All @@ -52,6 +55,7 @@ func (a *AccessController) Access(w http.ResponseWriter, r *http.Request) (*http
return nil, errors.Join(err, ErrUnauthorized)
}
if auth != nil {
auth.SetScopesClaim(a.scopeClaim)
w.Header().Set("X-Authenticated-By", auth.Authenticator())
return r.WithContext(authentication.NewContext(r.Context(), auth)), nil
}
Expand Down
3 changes: 3 additions & 0 deletions router/core/ratelimiter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,9 @@ func (f *FakeAuthenticator) Claims() authentication.Claims {
return f.claims
}

func (f *FakeAuthenticator) SetScopesClaim(scopeClaim string) {
}

func (f *FakeAuthenticator) SetScopes(scopes []string) {
//TODO implement me
panic("implement me")
Expand Down
1 change: 1 addition & 0 deletions router/core/supervisor_instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ func newRouter(ctx context.Context, params RouterResources, additionalOptions ..
AuthenticationRequired: cfg.Authorization.RequireAuthentication,
SkipIntrospectionQueries: cfg.Authentication.IgnoreIntrospection,
IntrospectionSkipSecret: cfg.IntrospectionConfig.Secret,
ScopeClaim: cfg.Authentication.JWT.ScopeClaim,
})
if err != nil {
return nil, fmt.Errorf("could not create access controller: %w", err)
Expand Down
21 changes: 18 additions & 3 deletions router/pkg/authentication/authentication.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import (

type Claims map[string]any

const DefaultScopeClaim = "scope"

// Provider is an interface that represents entities that might provide
// authentication information. If no authentication information is available,
// the AuthenticationHeaders method should return nil.
Expand All @@ -31,6 +33,8 @@ type Authentication interface {
// Claims returns the claims of the authenticated request, as returned by
// the Authenticator.
Claims() Claims
// SetScopesClaim sets the claim key used by Scopes and SetScopes.
SetScopesClaim(scopeClaim string)
Comment thread
alepane21 marked this conversation as resolved.
Outdated
// SetScopes sets the scopes of the authenticated request. It will replace the scopes already parsed from the claims.
// If users desire to append the scopes, they can first run `Scopes` to get the current scopes, and then append the new scopes
SetScopes(scopes []string)
Expand All @@ -42,6 +46,7 @@ type Authentication interface {
type authentication struct {
authenticator string
claims Claims
scopeClaim string
Comment thread
alepane21 marked this conversation as resolved.
}

func (a *authentication) Authenticator() string {
Expand All @@ -55,6 +60,13 @@ func (a *authentication) Claims() Claims {
return a.claims
}

func (a *authentication) SetScopesClaim(scopeClaim string) {
if a == nil {
return
}
a.scopeClaim = scopeClaim
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

func (a *authentication) SetScopes(scopes []string) {
if a == nil {
return
Expand All @@ -63,14 +75,14 @@ func (a *authentication) SetScopes(scopes []string) {
a.claims = make(Claims)
}
// per https://datatracker.ietf.org/doc/html/rfc8693#section-2.1-4.8, scopes should be space separated
a.claims["scope"] = strings.Join(scopes, " ")
a.claims[a.scopeClaim] = strings.Join(scopes, " ")
}

func (a *authentication) Scopes() []string {
if a == nil {
return nil
}
scopes, ok := a.claims["scope"].(string)
scopes, ok := a.claims[a.scopeClaim].(string)
if !ok {
return nil
}
Expand Down Expand Up @@ -105,6 +117,7 @@ func Authenticate(ctx context.Context, authenticators []Authenticator, p Provide
return &authentication{
authenticator: auth.Name(),
claims: claims,
scopeClaim: DefaultScopeClaim,
}, nil
}
// If no authentication failed error will be nil here,
Expand All @@ -113,5 +126,7 @@ func Authenticate(ctx context.Context, authenticators []Authenticator, p Provide
}

func NewEmptyAuthentication() Authentication {
return &authentication{}
return &authentication{
scopeClaim: DefaultScopeClaim,
}
}
Comment thread
alepane21 marked this conversation as resolved.
Outdated
1 change: 1 addition & 0 deletions router/pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -594,6 +594,7 @@ type HeaderSource struct {

type JWTAuthenticationConfiguration struct {
JWKS []JWKSConfiguration `yaml:"jwks"`
ScopeClaim string `yaml:"scope_claim" envDefault:"scope"`
HeaderName string `yaml:"header_name" envDefault:"Authorization"`
HeaderValuePrefix string `yaml:"header_value_prefix" envDefault:"Bearer"`
HeaderSources []HeaderSource `yaml:"header_sources"`
Expand Down
6 changes: 6 additions & 0 deletions router/pkg/config/config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -2108,6 +2108,12 @@
]
}
},
"scope_claim": {
"type": "string",
"description": "The JWT claim to use when reading scopes for authorization. The default value is 'scope'.",
"default": "scope",
"minLength": 1
},
Comment thread
coderabbitai[bot] marked this conversation as resolved.
"header_name": {
"type": "string",
"description": "The name of the header. The header is used to extract the token from the request. The default value is 'Authorization'.",
Expand Down
35 changes: 35 additions & 0 deletions router/pkg/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1411,6 +1411,41 @@ authentication:
require.NoError(t, err)
})

t.Run("verify scope claim can be configured", func(t *testing.T) {
Comment thread
alepane21 marked this conversation as resolved.
Outdated
t.Parallel()

f := createTempFileFromFixture(t, `
version: "1"

authentication:
jwt:
scope_claim: "scp"
jwks:
- url: "http://url/valid.json"

`)
cfg, err := LoadConfig([]string{f})
require.NoError(t, err)
require.Equal(t, "scp", cfg.Config.Authentication.JWT.ScopeClaim)
})

t.Run("verify scope claim defaults to scope", func(t *testing.T) {
t.Parallel()

f := createTempFileFromFixture(t, `
version: "1"

authentication:
jwt:
jwks:
- url: "http://url/valid.json"

`)
cfg, err := LoadConfig([]string{f})
require.NoError(t, err)
require.Equal(t, "scope", cfg.Config.Authentication.JWT.ScopeClaim)
})

t.Run("verify both secret and url are not allowed together", func(t *testing.T) {
t.Parallel()

Expand Down
1 change: 1 addition & 0 deletions router/pkg/config/fixtures/full.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,7 @@ authentication:
- url: 'https://example.com/.well-known/jwks3.json'
header_name: Authorization
header_value_prefix: Bearer
scope_claim: customscp
header_sources:
- type: header
name: X-Authorization
Expand Down
1 change: 1 addition & 0 deletions router/pkg/config/testdata/config_defaults.json
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,7 @@
"Authentication": {
"JWT": {
"JWKS": null,
"ScopeClaim": "scope",
"HeaderName": "Authorization",
"HeaderValuePrefix": "Bearer",
"HeaderSources": null
Expand Down
1 change: 1 addition & 0 deletions router/pkg/config/testdata/config_full.json
Original file line number Diff line number Diff line change
Expand Up @@ -602,6 +602,7 @@
"Audiences": null
}
],
"ScopeClaim": "customscp",
"HeaderName": "Authorization",
"HeaderValuePrefix": "Bearer",
"HeaderSources": [
Expand Down
Loading