fix: restore behavior that unknown scopes get ignored

This commit is contained in:
Elias Schneider
2026-07-06 22:42:38 +02:00
parent 6120bb4dd8
commit 34e9a6d198
4 changed files with 52 additions and 11 deletions

View File

@@ -74,14 +74,16 @@ func (b *ClientPreviewBuilder) BuildClientPreview(ctx context.Context, client mo
}
func (b *ClientPreviewBuilder) validatedScopes(ctx context.Context, client model.OidcClient, scopes []string) (fosite.Arguments, error) {
scopeStrategy := b.strategies.config.GetScopeStrategy(ctx)
clientScopes := Client{OidcClient: client}.GetScopes()
// Mirror the authorize endpoint's scope handling so the preview matches the tokens a real
// authorization would produce, including dropping unknown scopes when configured.
requested := fosite.Arguments(fosite.RemoveEmpty(scopes))
validated, err := fosite.FilterRequestedScopes(b.strategies.config.GetScopeStrategy(ctx), Client{OidcClient: client}, requested, b.strategies.config.GetIgnoreUnknownScopes(ctx))
if err != nil {
return nil, err
}
scopeArgs := make(fosite.Arguments, 0, len(scopes))
for _, scope := range fosite.RemoveEmpty(scopes) {
if !scopeStrategy(clientScopes, scope) {
return nil, fosite.ErrInvalidScope.WithHintf("The OAuth 2.0 Client is not allowed to request scope '%s'.", scope)
}
scopeArgs := make(fosite.Arguments, 0, len(validated))
for _, scope := range validated {
if !scopeArgs.Has(scope) {
scopeArgs = append(scopeArgs, scope)
}

View File

@@ -60,7 +60,7 @@ func TestClientPreviewBuilderUsesFositeTokenStrategies(t *testing.T) {
require.Equal(t, true, preview.UserInfo["email_verified"])
}
func TestClientPreviewBuilderRejectsInvalidScope(t *testing.T) {
func TestClientPreviewBuilderIgnoresUnknownScopes(t *testing.T) {
db := testutils.NewDatabaseForTest(t)
signerKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.NoError(t, err)
@@ -72,13 +72,20 @@ func TestClientPreviewBuilderRejectsInvalidScope(t *testing.T) {
})
require.NoError(t, err)
require.NoError(t, db.Create(&model.User{
Base: model.Base{ID: "test-user"},
Username: "test-user",
}).Error)
// The preview mirrors the authorize endpoint: unknown scopes are dropped
// from the previewed tokens instead of failing the preview.
builder := newClientPreviewBuilder(newClaimsService(db, nil, "https://issuer.example.com", nil), provider.tokenStrategies)
_, err = builder.BuildClientPreview(t.Context(), model.OidcClient{
preview, err := builder.BuildClientPreview(t.Context(), model.OidcClient{
Base: model.Base{ID: "test-client"},
Name: "Test Client",
}, "test-user", []string{"openid", "unknown"}, "")
require.Error(t, err)
require.ErrorContains(t, err, "invalid_scope")
require.NoError(t, err)
require.ElementsMatch(t, []string{"openid"}, stringSliceClaim(t, preview.AccessToken["scp"]))
}
func stringSliceClaim(t *testing.T, value any) []string {

View File

@@ -46,6 +46,7 @@ func newProvider(store *Store, authenticator *federatedClientAuthenticator, sign
AccessTokenIssuer: config.BaseURL,
TokenURL: config.TokenBaseURL + "/api/oidc/token",
ScopeStrategy: fosite.ExactScopeStrategy,
IgnoreUnknownScopes: true,
AudienceMatchingStrategy: fosite.ExactAudienceMatchingStrategy,
RedirectURIMatcher: matchRedirectURI,
EnforcePKCEForPublicClients: true,

View File

@@ -283,3 +283,34 @@ func generateEd25519TestKey(t *testing.T) any {
require.NoError(t, err)
return key
}
func TestProviderIgnoresUnknownScopes(t *testing.T) {
db := testutils.NewDatabaseForTest(t)
signerKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.NoError(t, err)
require.NoError(t, db.Create(&model.OidcClient{
Base: model.Base{ID: "test-client"},
Name: "Test Client",
CallbackURLs: model.UrlList{"https://app.example.com/callback"},
}).Error)
provider, err := newProvider(NewStore(db, nil), nil, testTokenSigner{key: signerKey}, Config{ //nolint:gosec // static test-only provider secret
BaseURL: "https://issuer.example.com",
TokenBaseURL: "https://issuer.example.com",
Secret: []byte("test-secret"),
})
require.NoError(t, err)
// Clients such as MCP clients blindly request scopes Pocket ID does not support, like
// "phone". These are dropped instead of failing the request with invalid_scope.
req := httptest.NewRequestWithContext(
t.Context(),
http.MethodGet,
"/api/oidc/authorize?client_id=test-client&response_type=code&scope=openid+email+phone&state=state-with-enough-entropy&redirect_uri=https://app.example.com/callback",
nil,
)
ar, err := provider.NewAuthorizeRequest(req.Context(), req)
require.NoError(t, err)
require.Equal(t, fosite.Arguments{"openid", "email"}, ar.GetRequestedScopes())
}