From e1fd3f7852995689a6f44d66920815534b6a51d9 Mon Sep 17 00:00:00 2001 From: Oscar Sanderson Date: Sun, 27 Sep 2026 23:08:57 +0800 Subject: [PATCH] fix(server): return invalid_scope for a scope the client isn't allowed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The pushed authorization request and backchannel authentication endpoints answered a scope the client isn't registered for (or openid under OAuthOnly) with invalid_request, while client_credentials already used invalid_scope. RFC 6749 §4.1.2.1 and §5.2, which RFC 9126 §2.3 defers to, and CIBA Core §13 all define invalid_scope for a requested scope that is "invalid, unknown, or malformed". A missing or non-string scope is a malformed request and stays invalid_request. Co-Authored-By: Claude Opus 5.5 --- fapitest/server_error_test.go | 2 +- server/backchannel_authentication.go | 2 +- server/backchannel_test.go | 29 +++++++++++++++------------- server/par.go | 2 +- server/par_test.go | 8 ++++---- 5 files changed, 23 insertions(+), 20 deletions(-) diff --git a/fapitest/server_error_test.go b/fapitest/server_error_test.go index ca6706c8..b6c84e4e 100644 --- a/fapitest/server_error_test.go +++ b/fapitest/server_error_test.go @@ -34,7 +34,7 @@ func TestServerResponseFromPushedAuthorizationRequest(t *testing.T) { if err == nil { t.Fatal("BeginAuthorization(unregistered scope) = nil error, want error") } - requireServerResponse(t, err, "invalid_request", http.StatusBadRequest) + requireServerResponse(t, err, "invalid_scope", http.StatusBadRequest) } func TestServerResponseFromTokenEndpoint(t *testing.T) { diff --git a/server/backchannel_authentication.go b/server/backchannel_authentication.go index 60096fa2..f6ced720 100644 --- a/server/backchannel_authentication.go +++ b/server/backchannel_authentication.go @@ -366,7 +366,7 @@ func (s *Server) validateBackchannelAuthenticationParameters(verified verifiedBa return verifiedBackchannelRequest{}, newError(ErrorInvalidRequest, 400, "scope must be a string", err) } if err := s.validateScope(scope, client); err != nil { - return verifiedBackchannelRequest{}, newError(ErrorInvalidRequest, 400, "scope is not valid for this client", err) + return verifiedBackchannelRequest{}, newError(ErrorInvalidScope, 400, "scope is not valid for this client", err) } hints := 0 diff --git a/server/backchannel_test.go b/server/backchannel_test.go index 96615e73..5406fdb9 100644 --- a/server/backchannel_test.go +++ b/server/backchannel_test.go @@ -852,21 +852,24 @@ func TestBeginBackchannelAuthenticationRejectsNoMatchingClientKey(t *testing.T) } func TestBeginBackchannelAuthenticationRejectsInvalidScope(t *testing.T) { - cases := map[string]map[string]json.RawMessage{ - "missing scope": { + cases := map[string]struct { + params map[string]json.RawMessage + want server.ErrorCode + }{ + "missing scope": {map[string]json.RawMessage{ "login_hint": jsonRaw(t, "user-1"), - }, - "scope is not a string": { + }, server.ErrorInvalidRequest}, + "scope is not a string": {map[string]json.RawMessage{ "scope": jsonRaw(t, 123), "login_hint": jsonRaw(t, "user-1"), - }, - "scope not allowed for this client": { + }, server.ErrorInvalidRequest}, + "scope not allowed for this client": {map[string]json.RawMessage{ "scope": jsonRaw(t, "openid unregistered_scope"), "login_hint": jsonRaw(t, "user-1"), - }, + }, server.ErrorInvalidScope}, } - for name, params := range cases { + for name, tc := range cases { t.Run(name, func(t *testing.T) { h, _ := newHarnessWithBackchannel(t) - requestObj := h.backchannelRequestObject(t, params) + requestObj := h.backchannelRequestObject(t, tc.params) action, err := h.server.BeginBackchannelAuthentication(context.Background(), server.BeginBackchannelAuthenticationRequest{ HTTP: server.FormRequest{Parameters: backchannelFormParams(h.clientAssertion(t), requestObj)}, @@ -878,8 +881,8 @@ func TestBeginBackchannelAuthenticationRejectsInvalidScope(t *testing.T) { if !ok { t.Fatalf("action = %T, want server.BackchannelAuthenticationLocalError", action) } - if localErr.Error.Code() != server.ErrorInvalidRequest { - t.Fatalf("Code = %q, want %q", localErr.Error.Code(), server.ErrorInvalidRequest) + if localErr.Error.Code() != tc.want { + t.Fatalf("Code = %q, want %q", localErr.Error.Code(), tc.want) } }) } @@ -905,8 +908,8 @@ func TestBeginBackchannelAuthenticationOAuthOnlyRejectsOpenIDScope(t *testing.T) if !ok { t.Fatalf("action = %T, want server.BackchannelAuthenticationLocalError", action) } - if localErr.Error.Code() != server.ErrorInvalidRequest { - t.Fatalf("Code = %q, want %q", localErr.Error.Code(), server.ErrorInvalidRequest) + if localErr.Error.Code() != server.ErrorInvalidScope { + t.Fatalf("Code = %q, want %q", localErr.Error.Code(), server.ErrorInvalidScope) } } diff --git a/server/par.go b/server/par.go index 71c1119b..18dca7d0 100644 --- a/server/par.go +++ b/server/par.go @@ -672,7 +672,7 @@ func (s *Server) validateAuthorizationParameters(params map[string]json.RawMessa return nil, newError(ErrorInvalidRequest, 400, "scope must be a string", err) } if err := s.validateScope(scope, client); err != nil { - return nil, newError(ErrorInvalidRequest, 400, "scope is not valid for this client", err) + return nil, newError(ErrorInvalidScope, 400, "scope is not valid for this client", err) } } diff --git a/server/par_test.go b/server/par_test.go index fdb29143..99706806 100644 --- a/server/par_test.go +++ b/server/par_test.go @@ -1292,8 +1292,8 @@ func TestPushAuthorizationRequestScopeNotAllowed(t *testing.T) { _, err := h.server.PushAuthorizationRequest(context.Background(), server.PushAuthorizationRequest{ HTTP: server.FormRequest{Parameters: params}, }) - if code := serverErrorCode(t, err); code != server.ErrorInvalidRequest { - t.Fatalf("error code = %q, want %q", code, server.ErrorInvalidRequest) + if code := serverErrorCode(t, err); code != server.ErrorInvalidScope { + t.Fatalf("error code = %q, want %q", code, server.ErrorInvalidScope) } } @@ -1314,8 +1314,8 @@ func TestPushAuthorizationRequestOAuthOnlyRejectsOpenIDScope(t *testing.T) { _, err := h.server.PushAuthorizationRequest(context.Background(), server.PushAuthorizationRequest{ HTTP: server.FormRequest{Parameters: params}, }) - if code := serverErrorCode(t, err); code != server.ErrorInvalidRequest { - t.Fatalf("error code = %q, want %q", code, server.ErrorInvalidRequest) + if code := serverErrorCode(t, err); code != server.ErrorInvalidScope { + t.Fatalf("error code = %q, want %q", code, server.ErrorInvalidScope) } }