diff --git a/client/config.go b/client/config.go index 505993d6..6cf0d2df 100644 --- a/client/config.go +++ b/client/config.go @@ -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 } diff --git a/client/mtls_endpoints_test.go b/client/mtls_endpoints_test.go index 5fd2c806..d43f5152 100644 --- a/client/mtls_endpoints_test.go +++ b/client/mtls_endpoints_test.go @@ -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]) + } + } + }) } } diff --git a/cmd/conformance-client/main.go b/cmd/conformance-client/main.go index e4406bb8..2c75c421 100644 --- a/cmd/conformance-client/main.go +++ b/cmd/conformance-client/main.go @@ -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 diff --git a/docs/guides/mtls.md b/docs/guides/mtls.md index dc868a6c..2558694e 100644 --- a/docs/guides/mtls.md +++ b/docs/guides/mtls.md @@ -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 ...