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
87 changes: 42 additions & 45 deletions client/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -227,58 +227,55 @@ type MTLSEndpoints struct {
Revocation fapi.URL
}

// ApplyForSenderConstrain overrides endpoints' Token,
// BackchannelAuthentication and Revocation fields with m's own
// advertised aliases,
// wherever m advertises one (RFC 8705 §5) — for a
// Config.SenderConstrain == SenderConstrainMTLS client, whose
// certificate-bound access tokens may need to be requested, and CIBA
// polls sent, on a separate mTLS-only origin than the server's plain
// endpoints. m may be nil (the server never advertised
// mtls_endpoint_aliases at all); ApplyForSenderConstrain then leaves
// endpoints untouched and reports false. That's not an error: the
// aliases are optional (RFC 8705 §5), and a server that advertises none
// accepts mutual TLS at its ordinary endpoints, which is where a
// SenderConstrainMTLS client then sends these calls.
// ApplyForSenderConstrain overrides endpoints with every alias m
// advertises, for a Config.SenderConstrain == SenderConstrainMTLS client.
// RFC 8705 §5: a client "intending to do mutual TLS (for OAuth client
// authentication and/or to acquire or use certificate-bound tokens) when
// making a request directly to the authorization server MUST use the
// alias URL of the endpoint", for whichever of its endpoints m advertises
// one: Token, PushedAuthorizationRequest, BackchannelAuthentication and
// Revocation. Authorization is never aliased: the user agent, not the
// client, makes that request.
//
// m may be nil (the server never advertised mtls_endpoint_aliases at
// all); ApplyForSenderConstrain then leaves endpoints untouched and
// reports false. That's not an error: the aliases are optional, and a
// server that advertises none accepts mutual TLS at its ordinary
// endpoints. ApplyForClientAuth does exactly the same: §5 doesn't
// distinguish the two reasons for doing mutual TLS.
func (m *MTLSEndpoints) ApplyForSenderConstrain(endpoints *Endpoints) bool {
if m == nil {
return false
}
if !m.Token.IsZero() {
endpoints.Token = m.Token
}
if !m.BackchannelAuthentication.IsZero() {
endpoints.BackchannelAuthentication = m.BackchannelAuthentication
}
if !m.Revocation.IsZero() {
endpoints.Revocation = m.Revocation
}
return true
return m.apply(endpoints)
}

// ApplyForClientAuth is ApplyForSenderConstrain's own counterpart for
// RFC 8705 §2 client authentication
// (Config.ClientAuthMethod == ClientAuthMethodSelfSignedTLSClientAuth
// or ClientAuthMethodTLSClientAuth): overrides endpoints' Token,
// PushedAuthorizationRequest and Revocation fields, since a
// certificate-authenticated client must present its certificate at PAR
// and token revocation (RevokeToken) too, not just at the token
// endpoint — the same asymmetry a server's own client authentication
// enforces. m may be nil, in which case
// ApplyForClientAuth leaves endpoints untouched and reports false, for
// the same reason ApplyForSenderConstrain does.
// ApplyForClientAuth is ApplyForSenderConstrain, for a client that
// authenticates with its certificate (Config.ClientAuthMethod ==
// ClientAuthMethodSelfSignedTLSClientAuth or ClientAuthMethodTLSClientAuth,
// RFC 8705 §2): it overrides endpoints with every alias m advertises, and
// reports false for a nil m. The two are the same because RFC 8705 §5
// applies to a client doing mutual TLS for either reason; a client doing
// both needs only one call.
func (m *MTLSEndpoints) ApplyForClientAuth(endpoints *Endpoints) bool {
return m.apply(endpoints)
}

// apply overrides each endpoint m advertises an alias for — see
// ApplyForSenderConstrain.
func (m *MTLSEndpoints) apply(endpoints *Endpoints) bool {
if m == nil {
return false
}
if !m.Token.IsZero() {
endpoints.Token = m.Token
}
if !m.PushedAuthorizationRequest.IsZero() {
endpoints.PushedAuthorizationRequest = m.PushedAuthorizationRequest
}
if !m.Revocation.IsZero() {
endpoints.Revocation = m.Revocation
for _, f := range []struct {
alias fapi.URL
target *fapi.URL
}{
{m.Token, &endpoints.Token},
{m.PushedAuthorizationRequest, &endpoints.PushedAuthorizationRequest},
{m.BackchannelAuthentication, &endpoints.BackchannelAuthentication},
{m.Revocation, &endpoints.Revocation},
} {
if !f.alias.IsZero() {
*f.target = f.alias
}
}
return true
}
Expand Down
104 changes: 39 additions & 65 deletions client/mtls_endpoints_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,71 +16,45 @@ func mustEndpointURL(t *testing.T, raw string) fapi.URL {
return u
}

// TestMTLSEndpointsApplyForSenderConstrainOverridesTokenAndBackchannel
// covers ApplyForSenderConstrain's own field selection: Token and
// BackchannelAuthentication move to their mTLS alias; every other
// endpoint (Authorization, PushedAuthorizationRequest) is untouched.
func TestMTLSEndpointsApplyForSenderConstrainOverridesTokenAndBackchannel(t *testing.T) {
aliases := &client.MTLSEndpoints{
Token: mustEndpointURL(t, "https://mtls.example.com/token"),
BackchannelAuthentication: mustEndpointURL(t, "https://mtls.example.com/backchannel"),
}
endpoints := client.Endpoints{
Authorization: mustEndpointURL(t, "https://as.example.com/authorize"),
Token: mustEndpointURL(t, "https://as.example.com/token"),
PushedAuthorizationRequest: mustEndpointURL(t, "https://as.example.com/par"),
BackchannelAuthentication: mustEndpointURL(t, "https://as.example.com/backchannel"),
}

if !aliases.ApplyForSenderConstrain(&endpoints) {
t.Fatal("ApplyForSenderConstrain() = false, want true")
}
if got, want := endpoints.Token.String(), aliases.Token.String(); got != want {
t.Errorf("Token = %s, want alias %s", got, want)
}
if got, want := endpoints.BackchannelAuthentication.String(), aliases.BackchannelAuthentication.String(); got != want {
t.Errorf("BackchannelAuthentication = %s, want alias %s", got, want)
}
if got, want := endpoints.Authorization.String(), "https://as.example.com/authorize"; got != want {
t.Errorf("Authorization = %s, want untouched %s", got, want)
}
if got, want := endpoints.PushedAuthorizationRequest.String(), "https://as.example.com/par"; got != want {
t.Errorf("PushedAuthorizationRequest = %s, want untouched %s", got, want)
}
}

// TestMTLSEndpointsApplyForClientAuthOverridesTokenAndPAR covers
// ApplyForClientAuth's own field selection: Token,
// PushedAuthorizationRequest and Revocation move to their mTLS alias;
// BackchannelAuthentication is untouched (only ApplyForSenderConstrain
// ever moves it).
func TestMTLSEndpointsApplyForClientAuthOverridesTokenAndPAR(t *testing.T) {
aliases := &client.MTLSEndpoints{
Token: mustEndpointURL(t, "https://mtls.example.com/token"),
PushedAuthorizationRequest: mustEndpointURL(t, "https://mtls.example.com/par"),
Revocation: mustEndpointURL(t, "https://mtls.example.com/revoke"),
}
endpoints := client.Endpoints{
Token: mustEndpointURL(t, "https://as.example.com/token"),
PushedAuthorizationRequest: mustEndpointURL(t, "https://as.example.com/par"),
BackchannelAuthentication: mustEndpointURL(t, "https://as.example.com/backchannel"),
Revocation: mustEndpointURL(t, "https://as.example.com/revoke"),
}

if !aliases.ApplyForClientAuth(&endpoints) {
t.Fatal("ApplyForClientAuth() = false, want true")
}
if got, want := endpoints.Token.String(), aliases.Token.String(); got != want {
t.Errorf("Token = %s, want alias %s", got, want)
}
if got, want := endpoints.PushedAuthorizationRequest.String(), aliases.PushedAuthorizationRequest.String(); got != want {
t.Errorf("PushedAuthorizationRequest = %s, want alias %s", got, want)
}
if got, want := endpoints.Revocation.String(), aliases.Revocation.String(); got != want {
t.Errorf("Revocation = %s, want alias %s", got, want)
}
if got, want := endpoints.BackchannelAuthentication.String(), "https://as.example.com/backchannel"; got != want {
t.Errorf("BackchannelAuthentication = %s, want untouched %s", got, want)
// TestMTLSEndpointsApplyEveryAdvertisedAlias covers RFC 8705 §5: a client
// doing mutual TLS, for client authentication or for certificate-bound
// tokens alike, uses every alias the server advertises for an endpoint it
// calls directly. Authorization, which the user agent calls, is never
// aliased.
func TestMTLSEndpointsApplyEveryAdvertisedAlias(t *testing.T) {
for name, apply := range map[string]func(*client.MTLSEndpoints, *client.Endpoints) bool{
"ApplyForSenderConstrain": (*client.MTLSEndpoints).ApplyForSenderConstrain,
"ApplyForClientAuth": (*client.MTLSEndpoints).ApplyForClientAuth,
} {
t.Run(name, func(t *testing.T) {
aliases := &client.MTLSEndpoints{
Token: mustEndpointURL(t, "https://mtls.example.com/token"),
PushedAuthorizationRequest: mustEndpointURL(t, "https://mtls.example.com/par"),
BackchannelAuthentication: mustEndpointURL(t, "https://mtls.example.com/backchannel"),
Revocation: mustEndpointURL(t, "https://mtls.example.com/revoke"),
}
endpoints := client.Endpoints{
Authorization: mustEndpointURL(t, "https://as.example.com/authorize"),
Token: mustEndpointURL(t, "https://as.example.com/token"),
PushedAuthorizationRequest: mustEndpointURL(t, "https://as.example.com/par"),
BackchannelAuthentication: mustEndpointURL(t, "https://as.example.com/backchannel"),
Revocation: mustEndpointURL(t, "https://as.example.com/revoke"),
}
if !apply(aliases, &endpoints) {
t.Fatalf("%s() = false, want true", name)
}
for field, got := range map[string][2]string{
"Token": {endpoints.Token.String(), aliases.Token.String()},
"PushedAuthorizationRequest": {endpoints.PushedAuthorizationRequest.String(), aliases.PushedAuthorizationRequest.String()},
"BackchannelAuthentication": {endpoints.BackchannelAuthentication.String(), aliases.BackchannelAuthentication.String()},
"Revocation": {endpoints.Revocation.String(), aliases.Revocation.String()},
"Authorization": {endpoints.Authorization.String(), "https://as.example.com/authorize"},
} {
if got[0] != got[1] {
t.Errorf("%s = %s, want %s", field, got[0], got[1])
}
}
})
}
}

Expand Down
7 changes: 3 additions & 4 deletions cmd/conformance-client/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -578,10 +578,9 @@ func buildModuleClient(ctx context.Context, d moduleDriver, module suiteModule)
}
if d.ClientAuthMTLS {
cfg.ClientAuthMethod = storage.ClientAuthMethodSelfSignedTLSClientAuth
// Covers Token+PAR — a superset of what sender-constrain-only
// needs (Token only; RFC 8705 §3 has no PAR-time
// pre-commitment concept), so the MTLS+MTLS combo is correctly
// handled by this branch alone.
// Applies every advertised alias (RFC 8705 §5), as
// ApplyForSenderConstrain does, so the MTLS+MTLS combination
// needs only this call.
// Without mtls_endpoint_aliases (optional, RFC 8705 §5) the
// ordinary endpoints take mutual TLS — the suite's
// *-happy-path-no-mtls-endpoint-aliases modules check exactly
Expand Down
7 changes: 4 additions & 3 deletions docs/guides/mtls.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,10 @@ the `*http.Client` you pass as `Dependencies.HTTP`:

```go
endpoints := discovered.Endpoints
// RFC 8705 §5: send certificate-carrying requests to the mTLS aliases.
discovered.MTLSEndpointAliases.ApplyForClientAuth(&endpoints) // PAR and token
discovered.MTLSEndpointAliases.ApplyForSenderConstrain(&endpoints) // token, CIBA, revocation
// RFC 8705 §5: a client doing mutual TLS sends every request it makes
// directly (PAR, token, CIBA, revocation) to the mTLS aliases, when the
// server advertises them. ApplyForSenderConstrain does the same.
discovered.MTLSEndpointAliases.ApplyForClientAuth(&endpoints)

c, err := client.NewFromDiscovery(discovered, client.Config{
// ClientID, Profile, Limits ...
Expand Down
Loading