diff --git a/CHANGELOG.md b/CHANGELOG.md index 42ab53a..38779c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ Canonical reference for changes, improvements, and bugfixes for cap. ## Next +* feat (oidc): add Claims for exposing provider server metadata ([PR #172](https://github.com/hashicorp/cap/pull/172)) + ## 0.11.0 * fix (ldap): fix slice append to be concurrent safe when searching for ldap diff --git a/oidc/provider.go b/oidc/provider.go index 7116a46..30a3195 100644 --- a/oidc/provider.go +++ b/oidc/provider.go @@ -116,6 +116,22 @@ func NewProvider(c *Config) (*Provider, error) { return p, nil } +// Claims unmarshals raw fields returned by the provider during discovery. +// +// For a list of fields defined by the OpenID Connect spec see: +// https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata +// see also: https://datatracker.ietf.org/doc/html/rfc8414 +// +// This list of fields may include 'authorization_response_iss_parameter_supported' +// which can be used to prevent mix-up attacks. +// https://datatracker.ietf.org/doc/html/rfc9207#section-3 +func (p *Provider) Claims(v any) error { + if p == nil { + return fmt.Errorf("provider is nil") + } + return p.provider.Claims(v) +} + // Done with the provider's background resources and must be called for every // Provider created func (p *Provider) Done() { diff --git a/oidc/provider_test.go b/oidc/provider_test.go index 546f2a2..4806974 100644 --- a/oidc/provider_test.go +++ b/oidc/provider_test.go @@ -1756,6 +1756,38 @@ func TestProvider_DiscoveryInfo(t *testing.T) { } } +func TestProvider_Claims(t *testing.T) { + t.Parallel() + assert, require := assert.New(t), require.New(t) + tp := StartTestProvider(t) + config := testNewConfig( + t, + "test-client-id", + "test-client-secret", + "https://test-redirect", + tp, + ) + tp.SetAdditionalConfiguration(map[string]interface{}{ + "authorization_required": true, + }) + provider, err := NewProvider(config) + require.NoError(err) + + providerMetadata := struct { + ScopesSupported []string `json:"scopes_supported"` + SubjectTypesSupported []string `json:"subject_types_supported"` + ResponseTypesSupported []string `json:"response_types_supported"` + AuthRequired bool `json:"authorization_required"` + }{} + + err = provider.Claims(&providerMetadata) + require.NoError(err) + assert.ElementsMatch(providerMetadata.ScopesSupported, []string{"openid"}) + assert.ElementsMatch(providerMetadata.SubjectTypesSupported, []string{"public"}) + assert.ElementsMatch(providerMetadata.ResponseTypesSupported, []string{"code", "id_token", "token id_token"}) + assert.True(providerMetadata.AuthRequired) +} + var _ JWTSerializer = &mockSerializer{} type mockSerializer struct { diff --git a/oidc/testing_provider.go b/oidc/testing_provider.go index 3125eb8..97056bc 100644 --- a/oidc/testing_provider.go +++ b/oidc/testing_provider.go @@ -176,6 +176,7 @@ type TestProvider struct { invalidJWKs bool nowFunc func() time.Time pkceVerifier CodeVerifier + additionalConfig map[string]interface{} clientAssertionJWT string @@ -929,6 +930,14 @@ func (p *TestProvider) UserInfoReply() map[string]interface{} { return p.replyUserinfo } +// SetAdditionalConfiguration sets additional provider metadata to be returned +// during provider discovery. +func (p *TestProvider) SetAdditionalConfiguration(config map[string]interface{}) { + p.mu.Lock() + defer p.mu.Unlock() + p.additionalConfig = config +} + // Addr returns the current base URL for the test provider's running webserver, // which can be used as an OIDC issuer for discovery and is also used for the // iss claim when issuing JWTs. @@ -1178,29 +1187,25 @@ func (p *TestProvider) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - reply := struct { - Issuer string `json:"issuer"` - AuthEndpoint string `json:"authorization_endpoint"` - TokenEndpoint string `json:"token_endpoint"` - JWKSURI string `json:"jwks_uri"` - UserinfoEndpoint string `json:"userinfo_endpoint,omitempty"` - SupportedAlgs []string `json:"id_token_signing_alg_values_supported"` - SupportedScopes []string `json:"scopes_supported"` - SubjectTypesSupported []string `json:"subject_types_supported"` - ResponseTypesSupported []string `json:"response_types_supported"` - }{ - Issuer: p.Addr(), - AuthEndpoint: p.Addr() + authorize, - TokenEndpoint: p.Addr() + token, - JWKSURI: p.Addr() + wellKnownJwks, - UserinfoEndpoint: p.Addr() + userInfo, - SupportedAlgs: []string{string(p.alg)}, - SupportedScopes: p.supportedScopes, - SubjectTypesSupported: []string{"public"}, - ResponseTypesSupported: []string{"code", "id_token", "token id_token"}, + reply := map[string]interface{}{ + "issuer": p.Addr(), + "authorization_endpoint": p.Addr() + authorize, + "token_endpoint": p.Addr() + token, + "jwks_uri": p.Addr() + wellKnownJwks, + "userinfo_endpoint": p.Addr() + userInfo, + "id_token_signing_alg_values_supported": []string{string(p.alg)}, + "scopes_supported": p.supportedScopes, + "subject_types_supported": []string{"public"}, + "response_types_supported": []string{"code", "id_token", "token id_token"}, } if p.disableUserInfo { - reply.UserinfoEndpoint = "" + reply["userinfo_endpoint"] = "" + } + + if p.additionalConfig != nil { + for k, v := range p.additionalConfig { + reply[k] = v + } } err := p.writeJSON(w, &reply)