aboutsummaryrefslogtreecommitdiffstats
path: root/src/kube2msb/vendor/github.com/coreos/go-oidc/oidc/provider.go
diff options
context:
space:
mode:
Diffstat (limited to 'src/kube2msb/vendor/github.com/coreos/go-oidc/oidc/provider.go')
-rw-r--r--src/kube2msb/vendor/github.com/coreos/go-oidc/oidc/provider.go688
1 files changed, 688 insertions, 0 deletions
diff --git a/src/kube2msb/vendor/github.com/coreos/go-oidc/oidc/provider.go b/src/kube2msb/vendor/github.com/coreos/go-oidc/oidc/provider.go
new file mode 100644
index 0000000..1235890
--- /dev/null
+++ b/src/kube2msb/vendor/github.com/coreos/go-oidc/oidc/provider.go
@@ -0,0 +1,688 @@
+package oidc
+
+import (
+ "encoding/json"
+ "errors"
+ "fmt"
+ "net/http"
+ "net/url"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/coreos/pkg/capnslog"
+ "github.com/coreos/pkg/timeutil"
+ "github.com/jonboulle/clockwork"
+
+ phttp "github.com/coreos/go-oidc/http"
+ "github.com/coreos/go-oidc/oauth2"
+)
+
+var (
+ log = capnslog.NewPackageLogger("github.com/coreos/go-oidc", "http")
+)
+
+const (
+ // Subject Identifier types defined by the OIDC spec. Specifies if the provider
+ // should provide the same sub claim value to all clients (public) or a unique
+ // value for each client (pairwise).
+ //
+ // See: http://openid.net/specs/openid-connect-core-1_0.html#SubjectIDTypes
+ SubjectTypePublic = "public"
+ SubjectTypePairwise = "pairwise"
+)
+
+var (
+ // Default values for omitted provider config fields.
+ //
+ // Use ProviderConfig's Defaults method to fill a provider config with these values.
+ DefaultGrantTypesSupported = []string{oauth2.GrantTypeAuthCode, oauth2.GrantTypeImplicit}
+ DefaultResponseModesSupported = []string{"query", "fragment"}
+ DefaultTokenEndpointAuthMethodsSupported = []string{oauth2.AuthMethodClientSecretBasic}
+ DefaultClaimTypesSupported = []string{"normal"}
+)
+
+const (
+ MaximumProviderConfigSyncInterval = 24 * time.Hour
+ MinimumProviderConfigSyncInterval = time.Minute
+
+ discoveryConfigPath = "/.well-known/openid-configuration"
+)
+
+// internally configurable for tests
+var minimumProviderConfigSyncInterval = MinimumProviderConfigSyncInterval
+
+var (
+ // Ensure ProviderConfig satisfies these interfaces.
+ _ json.Marshaler = &ProviderConfig{}
+ _ json.Unmarshaler = &ProviderConfig{}
+)
+
+// ProviderConfig represents the OpenID Provider Metadata specifying what
+// configurations a provider supports.
+//
+// See: http://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata
+type ProviderConfig struct {
+ Issuer *url.URL // Required
+ AuthEndpoint *url.URL // Required
+ TokenEndpoint *url.URL // Required if grant types other than "implicit" are supported
+ UserInfoEndpoint *url.URL
+ KeysEndpoint *url.URL // Required
+ RegistrationEndpoint *url.URL
+
+ // Servers MAY choose not to advertise some supported scope values even when this
+ // parameter is used, although those defined in OpenID Core SHOULD be listed, if supported.
+ ScopesSupported []string
+ // OAuth2.0 response types supported.
+ ResponseTypesSupported []string // Required
+ // OAuth2.0 response modes supported.
+ //
+ // If omitted, defaults to DefaultResponseModesSupported.
+ ResponseModesSupported []string
+ // OAuth2.0 grant types supported.
+ //
+ // If omitted, defaults to DefaultGrantTypesSupported.
+ GrantTypesSupported []string
+ ACRValuesSupported []string
+ // SubjectTypesSupported specifies strategies for providing values for the sub claim.
+ SubjectTypesSupported []string // Required
+
+ // JWA signing and encryption algorith values supported for ID tokens.
+ IDTokenSigningAlgValues []string // Required
+ IDTokenEncryptionAlgValues []string
+ IDTokenEncryptionEncValues []string
+
+ // JWA signing and encryption algorith values supported for user info responses.
+ UserInfoSigningAlgValues []string
+ UserInfoEncryptionAlgValues []string
+ UserInfoEncryptionEncValues []string
+
+ // JWA signing and encryption algorith values supported for request objects.
+ ReqObjSigningAlgValues []string
+ ReqObjEncryptionAlgValues []string
+ ReqObjEncryptionEncValues []string
+
+ TokenEndpointAuthMethodsSupported []string
+ TokenEndpointAuthSigningAlgValuesSupported []string
+ DisplayValuesSupported []string
+ ClaimTypesSupported []string
+ ClaimsSupported []string
+ ServiceDocs *url.URL
+ ClaimsLocalsSupported []string
+ UILocalsSupported []string
+ ClaimsParameterSupported bool
+ RequestParameterSupported bool
+ RequestURIParamaterSupported bool
+ RequireRequestURIRegistration bool
+
+ Policy *url.URL
+ TermsOfService *url.URL
+
+ // Not part of the OpenID Provider Metadata
+ ExpiresAt time.Time
+}
+
+// Defaults returns a shallow copy of ProviderConfig with default
+// values replacing omitted fields.
+//
+// var cfg oidc.ProviderConfig
+// // Fill provider config with default values for omitted fields.
+// cfg = cfg.Defaults()
+//
+func (p ProviderConfig) Defaults() ProviderConfig {
+ setDefault := func(val *[]string, defaultVal []string) {
+ if len(*val) == 0 {
+ *val = defaultVal
+ }
+ }
+ setDefault(&p.GrantTypesSupported, DefaultGrantTypesSupported)
+ setDefault(&p.ResponseModesSupported, DefaultResponseModesSupported)
+ setDefault(&p.TokenEndpointAuthMethodsSupported, DefaultTokenEndpointAuthMethodsSupported)
+ setDefault(&p.ClaimTypesSupported, DefaultClaimTypesSupported)
+ return p
+}
+
+func (p *ProviderConfig) MarshalJSON() ([]byte, error) {
+ e := p.toEncodableStruct()
+ return json.Marshal(&e)
+}
+
+func (p *ProviderConfig) UnmarshalJSON(data []byte) error {
+ var e encodableProviderConfig
+ if err := json.Unmarshal(data, &e); err != nil {
+ return err
+ }
+ conf, err := e.toStruct()
+ if err != nil {
+ return err
+ }
+ if err := conf.Valid(); err != nil {
+ return err
+ }
+ *p = conf
+ return nil
+}
+
+type encodableProviderConfig struct {
+ Issuer string `json:"issuer"`
+ AuthEndpoint string `json:"authorization_endpoint"`
+ TokenEndpoint string `json:"token_endpoint"`
+ UserInfoEndpoint string `json:"userinfo_endpoint,omitempty"`
+ KeysEndpoint string `json:"jwks_uri"`
+ RegistrationEndpoint string `json:"registration_endpoint,omitempty"`
+
+ // Use 'omitempty' for all slices as per OIDC spec:
+ // "Claims that return multiple values are represented as JSON arrays.
+ // Claims with zero elements MUST be omitted from the response."
+ // http://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse
+
+ ScopesSupported []string `json:"scopes_supported,omitempty"`
+ ResponseTypesSupported []string `json:"response_types_supported,omitempty"`
+ ResponseModesSupported []string `json:"response_modes_supported,omitempty"`
+ GrantTypesSupported []string `json:"grant_types_supported,omitempty"`
+ ACRValuesSupported []string `json:"acr_values_supported,omitempty"`
+ SubjectTypesSupported []string `json:"subject_types_supported,omitempty"`
+
+ IDTokenSigningAlgValues []string `json:"id_token_signing_alg_values_supported,omitempty"`
+ IDTokenEncryptionAlgValues []string `json:"id_token_encryption_alg_values_supported,omitempty"`
+ IDTokenEncryptionEncValues []string `json:"id_token_encryption_enc_values_supported,omitempty"`
+ UserInfoSigningAlgValues []string `json:"userinfo_signing_alg_values_supported,omitempty"`
+ UserInfoEncryptionAlgValues []string `json:"userinfo_encryption_alg_values_supported,omitempty"`
+ UserInfoEncryptionEncValues []string `json:"userinfo_encryption_enc_values_supported,omitempty"`
+ ReqObjSigningAlgValues []string `json:"request_object_signing_alg_values_supported,omitempty"`
+ ReqObjEncryptionAlgValues []string `json:"request_object_encryption_alg_values_supported,omitempty"`
+ ReqObjEncryptionEncValues []string `json:"request_object_encryption_enc_values_supported,omitempty"`
+
+ TokenEndpointAuthMethodsSupported []string `json:"token_endpoint_auth_methods_supported,omitempty"`
+ TokenEndpointAuthSigningAlgValuesSupported []string `json:"token_endpoint_auth_signing_alg_values_supported,omitempty"`
+
+ DisplayValuesSupported []string `json:"display_values_supported,omitempty"`
+ ClaimTypesSupported []string `json:"claim_types_supported,omitempty"`
+ ClaimsSupported []string `json:"claims_supported,omitempty"`
+ ServiceDocs string `json:"service_documentation,omitempty"`
+ ClaimsLocalsSupported []string `json:"claims_locales_supported,omitempty"`
+ UILocalsSupported []string `json:"ui_locales_supported,omitempty"`
+ ClaimsParameterSupported bool `json:"claims_parameter_supported,omitempty"`
+ RequestParameterSupported bool `json:"request_parameter_supported,omitempty"`
+ RequestURIParamaterSupported bool `json:"request_uri_parameter_supported,omitempty"`
+ RequireRequestURIRegistration bool `json:"require_request_uri_registration,omitempty"`
+
+ Policy string `json:"op_policy_uri,omitempty"`
+ TermsOfService string `json:"op_tos_uri,omitempty"`
+}
+
+func (cfg ProviderConfig) toEncodableStruct() encodableProviderConfig {
+ return encodableProviderConfig{
+ Issuer: uriToString(cfg.Issuer),
+ AuthEndpoint: uriToString(cfg.AuthEndpoint),
+ TokenEndpoint: uriToString(cfg.TokenEndpoint),
+ UserInfoEndpoint: uriToString(cfg.UserInfoEndpoint),
+ KeysEndpoint: uriToString(cfg.KeysEndpoint),
+ RegistrationEndpoint: uriToString(cfg.RegistrationEndpoint),
+ ScopesSupported: cfg.ScopesSupported,
+ ResponseTypesSupported: cfg.ResponseTypesSupported,
+ ResponseModesSupported: cfg.ResponseModesSupported,
+ GrantTypesSupported: cfg.GrantTypesSupported,
+ ACRValuesSupported: cfg.ACRValuesSupported,
+ SubjectTypesSupported: cfg.SubjectTypesSupported,
+ IDTokenSigningAlgValues: cfg.IDTokenSigningAlgValues,
+ IDTokenEncryptionAlgValues: cfg.IDTokenEncryptionAlgValues,
+ IDTokenEncryptionEncValues: cfg.IDTokenEncryptionEncValues,
+ UserInfoSigningAlgValues: cfg.UserInfoSigningAlgValues,
+ UserInfoEncryptionAlgValues: cfg.UserInfoEncryptionAlgValues,
+ UserInfoEncryptionEncValues: cfg.UserInfoEncryptionEncValues,
+ ReqObjSigningAlgValues: cfg.ReqObjSigningAlgValues,
+ ReqObjEncryptionAlgValues: cfg.ReqObjEncryptionAlgValues,
+ ReqObjEncryptionEncValues: cfg.ReqObjEncryptionEncValues,
+ TokenEndpointAuthMethodsSupported: cfg.TokenEndpointAuthMethodsSupported,
+ TokenEndpointAuthSigningAlgValuesSupported: cfg.TokenEndpointAuthSigningAlgValuesSupported,
+ DisplayValuesSupported: cfg.DisplayValuesSupported,
+ ClaimTypesSupported: cfg.ClaimTypesSupported,
+ ClaimsSupported: cfg.ClaimsSupported,
+ ServiceDocs: uriToString(cfg.ServiceDocs),
+ ClaimsLocalsSupported: cfg.ClaimsLocalsSupported,
+ UILocalsSupported: cfg.UILocalsSupported,
+ ClaimsParameterSupported: cfg.ClaimsParameterSupported,
+ RequestParameterSupported: cfg.RequestParameterSupported,
+ RequestURIParamaterSupported: cfg.RequestURIParamaterSupported,
+ RequireRequestURIRegistration: cfg.RequireRequestURIRegistration,
+ Policy: uriToString(cfg.Policy),
+ TermsOfService: uriToString(cfg.TermsOfService),
+ }
+}
+
+func (e encodableProviderConfig) toStruct() (ProviderConfig, error) {
+ p := stickyErrParser{}
+ conf := ProviderConfig{
+ Issuer: p.parseURI(e.Issuer, "issuer"),
+ AuthEndpoint: p.parseURI(e.AuthEndpoint, "authorization_endpoint"),
+ TokenEndpoint: p.parseURI(e.TokenEndpoint, "token_endpoint"),
+ UserInfoEndpoint: p.parseURI(e.UserInfoEndpoint, "userinfo_endpoint"),
+ KeysEndpoint: p.parseURI(e.KeysEndpoint, "jwks_uri"),
+ RegistrationEndpoint: p.parseURI(e.RegistrationEndpoint, "registration_endpoint"),
+ ScopesSupported: e.ScopesSupported,
+ ResponseTypesSupported: e.ResponseTypesSupported,
+ ResponseModesSupported: e.ResponseModesSupported,
+ GrantTypesSupported: e.GrantTypesSupported,
+ ACRValuesSupported: e.ACRValuesSupported,
+ SubjectTypesSupported: e.SubjectTypesSupported,
+ IDTokenSigningAlgValues: e.IDTokenSigningAlgValues,
+ IDTokenEncryptionAlgValues: e.IDTokenEncryptionAlgValues,
+ IDTokenEncryptionEncValues: e.IDTokenEncryptionEncValues,
+ UserInfoSigningAlgValues: e.UserInfoSigningAlgValues,
+ UserInfoEncryptionAlgValues: e.UserInfoEncryptionAlgValues,
+ UserInfoEncryptionEncValues: e.UserInfoEncryptionEncValues,
+ ReqObjSigningAlgValues: e.ReqObjSigningAlgValues,
+ ReqObjEncryptionAlgValues: e.ReqObjEncryptionAlgValues,
+ ReqObjEncryptionEncValues: e.ReqObjEncryptionEncValues,
+ TokenEndpointAuthMethodsSupported: e.TokenEndpointAuthMethodsSupported,
+ TokenEndpointAuthSigningAlgValuesSupported: e.TokenEndpointAuthSigningAlgValuesSupported,
+ DisplayValuesSupported: e.DisplayValuesSupported,
+ ClaimTypesSupported: e.ClaimTypesSupported,
+ ClaimsSupported: e.ClaimsSupported,
+ ServiceDocs: p.parseURI(e.ServiceDocs, "service_documentation"),
+ ClaimsLocalsSupported: e.ClaimsLocalsSupported,
+ UILocalsSupported: e.UILocalsSupported,
+ ClaimsParameterSupported: e.ClaimsParameterSupported,
+ RequestParameterSupported: e.RequestParameterSupported,
+ RequestURIParamaterSupported: e.RequestURIParamaterSupported,
+ RequireRequestURIRegistration: e.RequireRequestURIRegistration,
+ Policy: p.parseURI(e.Policy, "op_policy-uri"),
+ TermsOfService: p.parseURI(e.TermsOfService, "op_tos_uri"),
+ }
+ if p.firstErr != nil {
+ return ProviderConfig{}, p.firstErr
+ }
+ return conf, nil
+}
+
+// Empty returns if a ProviderConfig holds no information.
+//
+// This case generally indicates a ProviderConfigGetter has experienced an error
+// and has nothing to report.
+func (p ProviderConfig) Empty() bool {
+ return p.Issuer == nil
+}
+
+func contains(sli []string, ele string) bool {
+ for _, s := range sli {
+ if s == ele {
+ return true
+ }
+ }
+ return false
+}
+
+// Valid determines if a ProviderConfig conforms with the OIDC specification.
+// If Valid returns successfully it guarantees required field are non-nil and
+// URLs are well formed.
+//
+// Valid is called by UnmarshalJSON.
+//
+// NOTE(ericchiang): For development purposes Valid does not mandate 'https' for
+// URLs fields where the OIDC spec requires it. This may change in future releases
+// of this package. See: https://github.com/coreos/go-oidc/issues/34
+func (p ProviderConfig) Valid() error {
+ grantTypes := p.GrantTypesSupported
+ if len(grantTypes) == 0 {
+ grantTypes = DefaultGrantTypesSupported
+ }
+ implicitOnly := true
+ for _, grantType := range grantTypes {
+ if grantType != oauth2.GrantTypeImplicit {
+ implicitOnly = false
+ break
+ }
+ }
+
+ if len(p.SubjectTypesSupported) == 0 {
+ return errors.New("missing required field subject_types_supported")
+ }
+ if len(p.IDTokenSigningAlgValues) == 0 {
+ return errors.New("missing required field id_token_signing_alg_values_supported")
+ }
+
+ if len(p.ScopesSupported) != 0 && !contains(p.ScopesSupported, "openid") {
+ return errors.New("scoped_supported must be unspecified or include 'openid'")
+ }
+
+ if !contains(p.IDTokenSigningAlgValues, "RS256") {
+ return errors.New("id_token_signing_alg_values_supported must include 'RS256'")
+ }
+ if contains(p.TokenEndpointAuthMethodsSupported, "none") {
+ return errors.New("token_endpoint_auth_signing_alg_values_supported cannot include 'none'")
+ }
+
+ uris := []struct {
+ val *url.URL
+ name string
+ required bool
+ }{
+ {p.Issuer, "issuer", true},
+ {p.AuthEndpoint, "authorization_endpoint", true},
+ {p.TokenEndpoint, "token_endpoint", !implicitOnly},
+ {p.UserInfoEndpoint, "userinfo_endpoint", false},
+ {p.KeysEndpoint, "jwks_uri", true},
+ {p.RegistrationEndpoint, "registration_endpoint", false},
+ {p.ServiceDocs, "service_documentation", false},
+ {p.Policy, "op_policy_uri", false},
+ {p.TermsOfService, "op_tos_uri", false},
+ }
+
+ for _, uri := range uris {
+ if uri.val == nil {
+ if !uri.required {
+ continue
+ }
+ return fmt.Errorf("empty value for required uri field %s", uri.name)
+ }
+ if uri.val.Host == "" {
+ return fmt.Errorf("no host for uri field %s", uri.name)
+ }
+ if uri.val.Scheme != "http" && uri.val.Scheme != "https" {
+ return fmt.Errorf("uri field %s schemeis not http or https", uri.name)
+ }
+ }
+ return nil
+}
+
+// Supports determines if provider supports a client given their respective metadata.
+func (p ProviderConfig) Supports(c ClientMetadata) error {
+ if err := p.Valid(); err != nil {
+ return fmt.Errorf("invalid provider config: %v", err)
+ }
+ if err := c.Valid(); err != nil {
+ return fmt.Errorf("invalid client config: %v", err)
+ }
+
+ // Fill default values for omitted fields
+ c = c.Defaults()
+ p = p.Defaults()
+
+ // Do the supported values list the requested one?
+ supports := []struct {
+ supported []string
+ requested string
+ name string
+ }{
+ {p.IDTokenSigningAlgValues, c.IDTokenResponseOptions.SigningAlg, "id_token_signed_response_alg"},
+ {p.IDTokenEncryptionAlgValues, c.IDTokenResponseOptions.EncryptionAlg, "id_token_encryption_response_alg"},
+ {p.IDTokenEncryptionEncValues, c.IDTokenResponseOptions.EncryptionEnc, "id_token_encryption_response_enc"},
+ {p.UserInfoSigningAlgValues, c.UserInfoResponseOptions.SigningAlg, "userinfo_signed_response_alg"},
+ {p.UserInfoEncryptionAlgValues, c.UserInfoResponseOptions.EncryptionAlg, "userinfo_encryption_response_alg"},
+ {p.UserInfoEncryptionEncValues, c.UserInfoResponseOptions.EncryptionEnc, "userinfo_encryption_response_enc"},
+ {p.ReqObjSigningAlgValues, c.RequestObjectOptions.SigningAlg, "request_object_signing_alg"},
+ {p.ReqObjEncryptionAlgValues, c.RequestObjectOptions.EncryptionAlg, "request_object_encryption_alg"},
+ {p.ReqObjEncryptionEncValues, c.RequestObjectOptions.EncryptionEnc, "request_object_encryption_enc"},
+ }
+ for _, field := range supports {
+ if field.requested == "" {
+ continue
+ }
+ if !contains(field.supported, field.requested) {
+ return fmt.Errorf("provider does not support requested value for field %s", field.name)
+ }
+ }
+
+ stringsEqual := func(s1, s2 string) bool { return s1 == s2 }
+
+ // For lists, are the list of requested values a subset of the supported ones?
+ supportsAll := []struct {
+ supported []string
+ requested []string
+ name string
+ // OAuth2.0 response_type can be space separated lists where order doesn't matter.
+ // For example "id_token token" is the same as "token id_token"
+ // Support a custom compare method.
+ comp func(s1, s2 string) bool
+ }{
+ {p.GrantTypesSupported, c.GrantTypes, "grant_types", stringsEqual},
+ {p.ResponseTypesSupported, c.ResponseTypes, "response_type", oauth2.ResponseTypesEqual},
+ }
+ for _, field := range supportsAll {
+ requestLoop:
+ for _, req := range field.requested {
+ for _, sup := range field.supported {
+ if field.comp(req, sup) {
+ continue requestLoop
+ }
+ }
+ return fmt.Errorf("provider does not support requested value for field %s", field.name)
+ }
+ }
+
+ // TODO(ericchiang): Are there more checks we feel comfortable with begin strict about?
+
+ return nil
+}
+
+func (p ProviderConfig) SupportsGrantType(grantType string) bool {
+ var supported []string
+ if len(p.GrantTypesSupported) == 0 {
+ supported = DefaultGrantTypesSupported
+ } else {
+ supported = p.GrantTypesSupported
+ }
+
+ for _, t := range supported {
+ if t == grantType {
+ return true
+ }
+ }
+ return false
+}
+
+type ProviderConfigGetter interface {
+ Get() (ProviderConfig, error)
+}
+
+type ProviderConfigSetter interface {
+ Set(ProviderConfig) error
+}
+
+type ProviderConfigSyncer struct {
+ from ProviderConfigGetter
+ to ProviderConfigSetter
+ clock clockwork.Clock
+
+ initialSyncDone bool
+ initialSyncWait sync.WaitGroup
+}
+
+func NewProviderConfigSyncer(from ProviderConfigGetter, to ProviderConfigSetter) *ProviderConfigSyncer {
+ return &ProviderConfigSyncer{
+ from: from,
+ to: to,
+ clock: clockwork.NewRealClock(),
+ }
+}
+
+func (s *ProviderConfigSyncer) Run() chan struct{} {
+ stop := make(chan struct{})
+
+ var next pcsStepper
+ next = &pcsStepNext{aft: time.Duration(0)}
+
+ s.initialSyncWait.Add(1)
+ go func() {
+ for {
+ select {
+ case <-s.clock.After(next.after()):
+ next = next.step(s.sync)
+ case <-stop:
+ return
+ }
+ }
+ }()
+
+ return stop
+}
+
+func (s *ProviderConfigSyncer) WaitUntilInitialSync() {
+ s.initialSyncWait.Wait()
+}
+
+func (s *ProviderConfigSyncer) sync() (time.Duration, error) {
+ cfg, err := s.from.Get()
+ if err != nil {
+ return 0, err
+ }
+
+ if err = s.to.Set(cfg); err != nil {
+ return 0, fmt.Errorf("error setting provider config: %v", err)
+ }
+
+ if !s.initialSyncDone {
+ s.initialSyncWait.Done()
+ s.initialSyncDone = true
+ }
+
+ log.Infof("Updating provider config: config=%#v", cfg)
+
+ return nextSyncAfter(cfg.ExpiresAt, s.clock), nil
+}
+
+type pcsStepFunc func() (time.Duration, error)
+
+type pcsStepper interface {
+ after() time.Duration
+ step(pcsStepFunc) pcsStepper
+}
+
+type pcsStepNext struct {
+ aft time.Duration
+}
+
+func (n *pcsStepNext) after() time.Duration {
+ return n.aft
+}
+
+func (n *pcsStepNext) step(fn pcsStepFunc) (next pcsStepper) {
+ ttl, err := fn()
+ if err == nil {
+ next = &pcsStepNext{aft: ttl}
+ log.Debugf("Synced provider config, next attempt in %v", next.after())
+ } else {
+ next = &pcsStepRetry{aft: time.Second}
+ log.Errorf("Provider config sync failed, retrying in %v: %v", next.after(), err)
+ }
+ return
+}
+
+type pcsStepRetry struct {
+ aft time.Duration
+}
+
+func (r *pcsStepRetry) after() time.Duration {
+ return r.aft
+}
+
+func (r *pcsStepRetry) step(fn pcsStepFunc) (next pcsStepper) {
+ ttl, err := fn()
+ if err == nil {
+ next = &pcsStepNext{aft: ttl}
+ log.Infof("Provider config sync no longer failing")
+ } else {
+ next = &pcsStepRetry{aft: timeutil.ExpBackoff(r.aft, time.Minute)}
+ log.Errorf("Provider config sync still failing, retrying in %v: %v", next.after(), err)
+ }
+ return
+}
+
+func nextSyncAfter(exp time.Time, clock clockwork.Clock) time.Duration {
+ if exp.IsZero() {
+ return MaximumProviderConfigSyncInterval
+ }
+
+ t := exp.Sub(clock.Now()) / 2
+ if t > MaximumProviderConfigSyncInterval {
+ t = MaximumProviderConfigSyncInterval
+ } else if t < minimumProviderConfigSyncInterval {
+ t = minimumProviderConfigSyncInterval
+ }
+
+ return t
+}
+
+type httpProviderConfigGetter struct {
+ hc phttp.Client
+ issuerURL string
+ clock clockwork.Clock
+}
+
+func NewHTTPProviderConfigGetter(hc phttp.Client, issuerURL string) *httpProviderConfigGetter {
+ return &httpProviderConfigGetter{
+ hc: hc,
+ issuerURL: issuerURL,
+ clock: clockwork.NewRealClock(),
+ }
+}
+
+func (r *httpProviderConfigGetter) Get() (cfg ProviderConfig, err error) {
+ // If the Issuer value contains a path component, any terminating / MUST be removed before
+ // appending /.well-known/openid-configuration.
+ // https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationRequest
+ discoveryURL := strings.TrimSuffix(r.issuerURL, "/") + discoveryConfigPath
+ req, err := http.NewRequest("GET", discoveryURL, nil)
+ if err != nil {
+ return
+ }
+
+ resp, err := r.hc.Do(req)
+ if err != nil {
+ return
+ }
+ defer resp.Body.Close()
+
+ if err = json.NewDecoder(resp.Body).Decode(&cfg); err != nil {
+ return
+ }
+
+ var ttl time.Duration
+ var ok bool
+ ttl, ok, err = phttp.Cacheable(resp.Header)
+ if err != nil {
+ return
+ } else if ok {
+ cfg.ExpiresAt = r.clock.Now().UTC().Add(ttl)
+ }
+
+ // The issuer value returned MUST be identical to the Issuer URL that was directly used to retrieve the configuration information.
+ // http://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationValidation
+ if !urlEqual(cfg.Issuer.String(), r.issuerURL) {
+ err = fmt.Errorf(`"issuer" in config (%v) does not match provided issuer URL (%v)`, cfg.Issuer, r.issuerURL)
+ return
+ }
+
+ return
+}
+
+func FetchProviderConfig(hc phttp.Client, issuerURL string) (ProviderConfig, error) {
+ if hc == nil {
+ hc = http.DefaultClient
+ }
+
+ g := NewHTTPProviderConfigGetter(hc, issuerURL)
+ return g.Get()
+}
+
+func WaitForProviderConfig(hc phttp.Client, issuerURL string) (pcfg ProviderConfig) {
+ return waitForProviderConfig(hc, issuerURL, clockwork.NewRealClock())
+}
+
+func waitForProviderConfig(hc phttp.Client, issuerURL string, clock clockwork.Clock) (pcfg ProviderConfig) {
+ var sleep time.Duration
+ var err error
+ for {
+ pcfg, err = FetchProviderConfig(hc, issuerURL)
+ if err == nil {
+ break
+ }
+
+ sleep = timeutil.ExpBackoff(sleep, time.Minute)
+ fmt.Printf("Failed fetching provider config, trying again in %v: %v\n", sleep, err)
+ time.Sleep(sleep)
+ }
+
+ return
+}