Require typed issuer refusals in live registration verification
All checks were successful
Build and Publish Container Image / build-and-push (push) Successful in 40s

Assistant: codex
Assistant-Model: gpt-5.6-luna
Assistant-Session: 01a07ff8-19d0-7820-b4d0-1353833cb7fc
This commit is contained in:
tegwick 2026-09-08 16:46:02 +02:00
parent 471465df22
commit dcebd46fa6
6 changed files with 168 additions and 13 deletions

View file

@ -3,6 +3,10 @@ package authclient
import (
"bytes"
"context"
"errors"
"io"
"net/http"
"net/url"
"strings"
"testing"
@ -160,3 +164,62 @@ func TestHasAllRolesNamesWhatIsMissing(t *testing.T) {
}
var _ = domain.Client{}
type verifyTransport func(*http.Request) (*http.Response, error)
func (f verifyTransport) RoundTrip(r *http.Request) (*http.Response, error) { return f(r) }
func TestVerifyClientDoesNotConfuseFailuresWithRefusals(t *testing.T) {
for _, target := range []string{"scope", "predecessor"} {
for _, failure := range []string{"transport", "server", "invalid_token", "unrelated_refusal", "malformed_refusal"} {
t.Run(target+"/"+failure, func(t *testing.T) {
c, _, _ := provider(t)
original := c.HTTP.Transport
o := verifyOpts()
if target == "predecessor" {
o.PreviousNamed, o.Previous = true, "old-secret"
}
c.HTTP.Transport = verifyTransport(func(r *http.Request) (*http.Response, error) {
if r.Method == http.MethodPost && r.URL.Path == "/token" {
body, _ := io.ReadAll(r.Body)
r.Body = io.NopCloser(bytes.NewReader(body))
form, _ := url.ParseQuery(string(body))
_, password, _ := r.BasicAuth()
password, _ = url.QueryUnescape(password)
negative := target == "scope" && form.Get("scope") != o.Scope || target == "predecessor" && password == o.Previous
if negative {
status, payload := 503, `{"error":"unavailable"}`
switch failure {
case "transport":
return nil, errors.New("transport failed")
case "invalid_token":
status, payload = 200, `{"token_type":"Bearer","access_token":"invalid","expires_in":900}`
case "unrelated_refusal":
status, payload = 400, `{"error":"invalid_profile_usage","feature":"client_id","description":"must not disclose this"}`
case "malformed_refusal":
status, payload = 400, `not-json-sensitive-body`
}
return &http.Response{StatusCode: status, Header: http.Header{}, Body: io.NopCloser(strings.NewReader(payload)), Request: r}, nil
}
}
return original.RoundTrip(r)
})
var out bytes.Buffer
if err := runVerify(context.Background(), c, o, &out); err == nil {
t.Fatal("a failed negative check was accepted as issuer refusal")
}
if !strings.Contains(out.String(), "PASS exchange and JWKS signature for granted scopes") {
t.Fatalf("test did not reach the negative checks: %s", out.String())
}
if !strings.Contains(out.String(), "expected token-endpoint refusal was not proved") {
t.Fatalf("wrong failure: %s", out.String())
}
for _, value := range []string{o.Secret, o.Previous, "must not disclose this", "not-json-sensitive-body", "eyJ"} {
if value != "" && strings.Contains(out.String(), value) {
t.Fatal("verification disclosed input or provider payload")
}
}
})
}
}
}