Files
base/apis/credential_test.go
zeekayandHanzo Dev 2d55a31aa3 a publishable key is refused wherever it sits, not where it was read
The refusal asked which credential a door would resolve — one channel,
the first — while the proxies forward every channel. So a decoy in the
read channel hid the key beside it: `Authorization: nonsense` is a bare
token by any reading, so it was judged, found harmless and admitted,
while `X-API-Key: pk-`, a repeat Authorization value, or a second `key`
parameter went on to the issuer at 200. Holding only the publishable key
is enough to write any of those.

apis.Credentials is what a request PRESENTS: every value of every
credential header and every `key` parameter. apis.Credential stays what a
door RESOLVES, the first of them. Two questions about one request, and a
refusal has to ask the second.

Nothing is dropped and no other credential is disturbed — a request
presenting a publishable key is held to what a publishable key reaches,
so an upstream's own credential in any of those channels travels to it
untouched.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-24 07:23:11 -07:00

149 lines
6.7 KiB
Go

package apis
import (
"net/http"
"net/http/httptest"
"slices"
"testing"
)
// TestCredentialReadsEverySpelling pins that one token is one credential
// however it was written.
//
// A reader that accepted only the canonical spelling did not answer "no
// credential" for the rest — it answered "no credential" to the RULE while the
// door beside it, and the upstream past that, read the token fine.
func TestCredentialReadsEverySpelling(t *testing.T) {
for _, c := range []struct {
how string
headers map[string]string
query string
want string
}{
{"a Bearer token", map[string]string{"Authorization": "Bearer tok"}, "", "tok"},
{"a lowercase scheme", map[string]string{"Authorization": "bearer tok"}, "", "tok"},
{"an uppercase scheme", map[string]string{"Authorization": "BEARER tok"}, "", "tok"},
{"two spaces", map[string]string{"Authorization": "Bearer tok"}, "", "tok"},
{"a tab", map[string]string{"Authorization": "Bearer\ttok"}, "", "tok"},
{"trailing space", map[string]string{"Authorization": " Bearer tok "}, "", "tok"},
{"a bare token", map[string]string{"Authorization": "tok"}, "", "tok"},
{"the alias header", map[string]string{"X-Authorization": "Bearer tok"}, "", "tok"},
{"the alias header bare", map[string]string{"X-Authorization": "tok"}, "", "tok"},
{"the legacy header", map[string]string{"X-Auth-Token": "tok"}, "", "tok"},
{"the machine header", map[string]string{"X-API-Key": "tok"}, "", "tok"},
{"the query", nil, "?key=tok", "tok"},
{"the query after a semicolon", nil, "?a=1;key=tok", "tok"},
{"the query escaped", nil, "?key=t%6fk", "tok"},
{"the query name escaped", nil, "?k%65y=tok", "tok"},
// A value naming another scheme is not a token. Handing back the blob
// whole is what makes `?key=` match on a password.
{"another scheme", map[string]string{"Authorization": "Basic dXNlcjpwYXNz"}, "", ""},
{"another scheme over a query", map[string]string{"Authorization": "Basic dXNlcjpwYXNz"}, "?key=tok", "tok"},
{"another scheme over a header", map[string]string{
"Authorization": "Basic dXNlcjpwYXNz", "X-API-Key": "tok"}, "", "tok"},
// The order: the canonical header wins where a request carries several.
{"both headers", map[string]string{"Authorization": "Bearer tok", "X-API-Key": "other"}, "", "tok"},
{"a header over a query", map[string]string{"Authorization": "Bearer tok"}, "?key=other", "tok"},
{"nothing at all", nil, "", ""},
{"an empty header", map[string]string{"Authorization": ""}, "?key=tok", "tok"},
} {
r := httptest.NewRequest(http.MethodGet, "/anywhere"+c.query, nil)
for k, v := range c.headers {
r.Header.Set(k, v)
}
if got := Credential(r); got != c.want {
t.Errorf("%s: Credential = %q, want %q", c.how, got, c.want)
}
}
}
// TestCredentialReadsPastAnUnreadableValue pins that a repeated header does not
// hide the credential. Both values are forwarded whole, so a value this side
// cannot read must not stop it reading the next one.
func TestCredentialReadsPastAnUnreadableValue(t *testing.T) {
r := httptest.NewRequest(http.MethodGet, "/anywhere", nil)
r.Header.Add("Authorization", "Basic dXNlcjpwYXNz")
r.Header.Add("Authorization", "Bearer tok")
if got := Credential(r); got != "tok" {
t.Errorf("Credential = %q, want %q", got, "tok")
}
}
// TestQueryForwardsTheCredentialItWasJudgedOn pins that the query a proxy may
// forward presents the same credential this side read, and only that one.
//
// A query presenting `key` twice is judged here by the first and read by a
// last-occurrence server by the second, so `?key=nonsense&key=pk-` passed as
// carrying no publishable key and handed one on.
func TestQueryForwardsTheCredentialItWasJudgedOn(t *testing.T) {
for _, c := range []struct{ how, query, want, judged string }{
{"one key", "?key=pk-x", "key=pk-x", "pk-x"},
{"a decoy in front", "?key=nonsense&key=pk-x", "key=nonsense", "nonsense"},
{"a decoy after a semicolon", "?key=nonsense;key=pk-x", "key=nonsense", "nonsense"},
{"a decoy under an escaped name", "?key=nonsense&k%65y=pk-x", "key=nonsense", "nonsense"},
{"an escaped name first", "?k%65y=pk-x&key=nonsense", "k%65y=pk-x", "pk-x"},
{"a stronger key in front", "?key=sk-y&key=pk-x", "key=sk-y", "sk-y"},
{"three of them", "?key=a&key=b&key=c", "key=a", "a"},
{"the rest of the query kept", "?a=1&key=nonsense&b=2&key=pk-x", "a=1&key=nonsense&b=2", "nonsense"},
{"no key at all", "?a=1&b=2", "a=1&b=2", ""},
{"semicolons left alone", "?a=1;b=2", "a=1;b=2", ""},
{"a leading separator kept", "?&key=pk-x", "&key=pk-x", "pk-x"},
{"a trailing separator kept", "?key=pk-x&", "key=pk-x&", "pk-x"},
{"nothing at all", "", "", ""},
} {
r := httptest.NewRequest(http.MethodGet, "/anywhere"+c.query, nil)
if got := Query(r); got != c.want {
t.Errorf("%s: Query = %q, want %q", c.how, got, c.want)
}
if got := Credential(r); got != c.judged {
t.Errorf("%s: Credential = %q, want %q", c.how, got, c.judged)
}
// The forwarded query presents the credential that was judged. Read it
// back the way the far side would: one reader, both ends.
forwarded := httptest.NewRequest(http.MethodGet, "/anywhere?"+Query(r), nil)
if got := Credential(forwarded); got != c.judged {
t.Errorf("%s: the forwarded query presents %q, judged on %q", c.how, got, c.judged)
}
}
}
// TestCredentialsIsEveryChannel pins the difference between the two questions.
// Credential is which credential a door resolves; Credentials is what the
// request presents — and a proxy forwards all of it, so a refusal reads this one.
func TestCredentialsIsEveryChannel(t *testing.T) {
r := httptest.NewRequest(http.MethodGet, "/anywhere?key=first&b=2;key=second", nil)
r.Header.Add("Authorization", "nonsense")
r.Header.Add("Authorization", "Bearer second-value")
r.Header.Set("X-Authorization", "Basic dXNlcjpwYXNz") // no token: a scheme we do not read
r.Header.Set("X-Auth-Token", "legacy")
r.Header.Set("X-API-Key", "machine")
want := []string{"nonsense", "second-value", "legacy", "machine", "first", "second"}
got := Credentials(r)
if !slices.Equal(got, want) {
t.Errorf("Credentials = %q, want %q", got, want)
}
// The door resolves the first, which is exactly the decoy that hid the rest.
if c := Credential(r); c != "nonsense" {
t.Errorf("Credential = %q, want %q", c, "nonsense")
}
}
// TestCredentialsIsEmptyWhenNothingIsPresented pins the other end: a request
// carrying no credential presents none, so nothing is held to anything.
func TestCredentialsIsEmptyWhenNothingIsPresented(t *testing.T) {
r := httptest.NewRequest(http.MethodGet, "/anywhere?a=1&key=", nil)
r.Header.Set("Authorization", "Basic dXNlcjpwYXNz")
if got := Credentials(r); len(got) != 0 {
t.Errorf("Credentials = %q, want none", got)
}
}