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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 16 additions & 0 deletions oidc/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
32 changes: 32 additions & 0 deletions oidc/provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
47 changes: 26 additions & 21 deletions oidc/testing_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ type TestProvider struct {
invalidJWKs bool
nowFunc func() time.Time
pkceVerifier CodeVerifier
additionalConfig map[string]interface{}

clientAssertionJWT string

Expand Down Expand Up @@ -929,6 +930,14 @@ func (p *TestProvider) UserInfoReply() map[string]interface{} {
return p.replyUserinfo
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you add a godoc here?

// 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.
Expand Down Expand Up @@ -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)
Expand Down
Loading