package oidc import ( "encoding/json" "errors" "fmt" "log" "net/http" "net/url" "strings" "sync" "time" "github.com/coreos/pkg/timeutil" "github.com/jonboulle/clockwork" phttp "github.com/coreos/go-oidc/http" "github.com/coreos/go-oidc/oauth2" ) 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 EndSessionEndpoint *url.URL CheckSessionIFrame *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"` EndSessionEndpoint string `json:"end_session_endpoint,omitempty"` CheckSessionIFrame string `json:"check_session_iframe,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), EndSessionEndpoint: uriToString(cfg.EndSessionEndpoint), CheckSessionIFrame: uriToString(cfg.CheckSessionIFrame), 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"), EndSessionEndpoint: p.parseURI(e.EndSessionEndpoint, "end_session_endpoint"), CheckSessionIFrame: p.parseURI(e.CheckSessionIFrame, "check_session_iframe"), 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'") } 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.EndSessionEndpoint, "end_session_endpoint", false}, {p.CheckSessionIFrame, "check_session_iframe", 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 } 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} } else { next = &pcsStepRetry{aft: time.Second} log.Printf("go-oidc: 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} } else { next = &pcsStepRetry{aft: timeutil.ExpBackoff(r.aft, time.Minute)} log.Printf("go-oidc: provider config sync failed, 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 }