Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
17 changes: 15 additions & 2 deletions resource/verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,14 @@ import (
// sender-constrained one way or the other; a bearer token presented
// with neither a DPoP proof nor a client certificate is rejected.
type VerifyRequest struct {
Method string
URL *url.URL
Method string
URL *url.URL

// Authorization is the request's one Authorization header. A request
// with more than one is malformed (RFC 9110 §5.3: the field isn't a
// list): VerifyRequestFromHTTP marks it so for Verify to refuse, and
// an adapter filling this field itself refuses it first, rather than
// passing one of them on. A proxy in front might act on another.
Authorization string

// DPoPProofs is every "DPoP" header value the request carried, in
Expand All @@ -47,6 +53,10 @@ type VerifyRequest struct {
// one). An HTTP adapter reads this straight from the connection's
// own TLS state; this package never terminates TLS itself.
PeerCertificate *x509.Certificate

// repeatedAuthorization is VerifyRequestFromHTTP's mark of a request
// with more than one Authorization header.
repeatedAuthorization bool
}

// AuthorizationContext is what Verify returns for a successfully
Expand Down Expand Up @@ -191,6 +201,9 @@ func parseAuthorization(req VerifyRequest) (dpopProof, raw string, usedDPoP bool
return "", "", true, newError(ErrorInvalidDPoPProof, 401, "multiple DPoP proofs are not permitted", nil)
}

if req.repeatedAuthorization {
return "", "", false, newError(ErrorInvalidRequest, 400, "multiple Authorization headers are not permitted", nil)
}
if strings.TrimSpace(req.Authorization) == "" {
return "", "", false, noCredentials("no Authorization header")
}
Expand Down
6 changes: 5 additions & 1 deletion resource/verify_request_http.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import (
// VerifyRequestFromHTTP builds the VerifyRequest for r, an incoming
// request to the protected resource whose external URL is target: its
// method, Authorization header, every DPoP header (DPoPProofsFromHTTP)
// and TLS client certificate (PeerCertificateFromHTTP). Filling the
// and TLS client certificate (PeerCertificateFromHTTP). A request with
// more than one Authorization header is marked for Verify to refuse as
// invalid_request, as it refuses more than one DPoP header. Filling the
// struct field by field compiles just as well with a field left out, and
// then fails only for the clients that need it — a missing
// PeerCertificate refuses every mTLS-bound token, a missing DPoPProofs
Expand All @@ -34,5 +36,7 @@ func VerifyRequestFromHTTP(r *http.Request, target *url.URL) VerifyRequest {
Authorization: r.Header.Get("Authorization"),
DPoPProofs: DPoPProofsFromHTTP(r),
PeerCertificate: PeerCertificateFromHTTP(r),

repeatedAuthorization: len(r.Header.Values("Authorization")) > 1,
}
}
28 changes: 28 additions & 0 deletions resource/verify_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import (
"crypto/elliptic"
"crypto/rand"
"crypto/sha256"
"errors"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
Expand Down Expand Up @@ -572,3 +575,28 @@ func TestErrorAccessorsWithoutCause(t *testing.T) {
t.Fatalf("Error() = %q, want %q", rerr.Error(), "resource: invalid_request: method is required")
}
}

// TestVerifyRefusesRepeatedAuthorization covers a request with two
// Authorization headers, through VerifyRequestFromHTTP: refused as
// invalid_request rather than verified on the first, while a proxy in
// front might have acted on the other.
func TestVerifyRefusesRepeatedAuthorization(t *testing.T) {
f := newFixture(t)
request := func(authorizations ...string) *http.Request {
r := httptest.NewRequest("GET", f.target.String(), nil)
for _, a := range authorizations {
r.Header.Add("Authorization", a)
}
r.Header.Set("DPoP", f.dpopProof)
return r
}

if _, err := f.verifier.Verify(context.Background(), resource.VerifyRequestFromHTTP(request("DPoP "+f.accessToken), f.target)); err != nil {
t.Fatalf("Verify(one Authorization header) = %v, want nil", err)
}
_, err := f.verifier.Verify(context.Background(), resource.VerifyRequestFromHTTP(request("DPoP "+f.accessToken, "Bearer other"), f.target))
var rerr *resource.Error
if !errors.As(err, &rerr) || rerr.Code() != resource.ErrorInvalidRequest || rerr.HTTPStatus() != http.StatusBadRequest {
t.Errorf("Verify(two Authorization headers) = %v, want a 400 invalid_request", err)
}
}
Loading