package oidc import ( "encoding/json" "errors" "fmt" "net/http" "net/mail" "net/url" "sync" "time" phttp "github.com/coreos/go-oidc/http" "github.com/coreos/go-oidc/jose" "github.com/coreos/go-oidc/key" "github.com/coreos/go-oidc/oauth2" ) const ( // amount of time that must pass after the last key sync // completes before another attempt may begin keySyncWindow = 5 * time.Second ) var ( DefaultScope = []string{"openid", "email", "profile"} supportedAuthMethods = map[string]struct{}{ oauth2.AuthMethodClientSecretBasic: struct{}{}, oauth2.AuthMethodClientSecretPost: struct{}{}, } ) type ClientCredentials oauth2.ClientCredentials type ClientIdentity struct { Credentials ClientCredentials Metadata ClientMetadata } type JWAOptions struct { // SigningAlg specifies an JWA alg for signing JWTs. // // Specifying this field implies different actions depending on the context. It may // require objects be serialized and signed as a JWT instead of plain JSON, or // require an existing JWT object use the specified alg. // // See: http://openid.net/specs/openid-connect-registration-1_0.html#ClientMetadata SigningAlg string // EncryptionAlg, if provided, specifies that the returned or sent object be stored // (or nested) within a JWT object and encrypted with the provided JWA alg. EncryptionAlg string // EncryptionEnc specifies the JWA enc algorithm to use with EncryptionAlg. If // EncryptionAlg is provided and EncryptionEnc is omitted, this field defaults // to A128CBC-HS256. // // If EncryptionEnc is provided EncryptionAlg must also be specified. EncryptionEnc string } func (opt JWAOptions) valid() error { if opt.EncryptionEnc != "" && opt.EncryptionAlg == "" { return errors.New("encryption encoding provided with no encryption algorithm") } return nil } func (opt JWAOptions) defaults() JWAOptions { if opt.EncryptionAlg != "" && opt.EncryptionEnc == "" { opt.EncryptionEnc = jose.EncA128CBCHS256 } return opt } var ( // Ensure ClientMetadata satisfies these interfaces. _ json.Marshaler = &ClientMetadata{} _ json.Unmarshaler = &ClientMetadata{} ) // ClientMetadata holds metadata that the authorization server associates // with a client identifier. The fields range from human-facing display // strings such as client name, to items that impact the security of the // protocol, such as the list of valid redirect URIs. // // See http://openid.net/specs/openid-connect-registration-1_0.html#ClientMetadata // // TODO: support language specific claim representations // http://openid.net/specs/openid-connect-registration-1_0.html#LanguagesAndScripts type ClientMetadata struct { RedirectURIs []url.URL // Required // A list of OAuth 2.0 "response_type" values that the client wishes to restrict // itself to. Either "code", "token", or another registered extension. // // If omitted, only "code" will be used. ResponseTypes []string // A list of OAuth 2.0 grant types the client wishes to restrict itself to. // The grant type values used by OIDC are "authorization_code", "implicit", // and "refresh_token". // // If ommitted, only "authorization_code" will be used. GrantTypes []string // "native" or "web". If omitted, "web". ApplicationType string // List of email addresses. Contacts []mail.Address // Name of client to be presented to the end-user. ClientName string // URL that references a logo for the Client application. LogoURI *url.URL // URL of the home page of the Client. ClientURI *url.URL // Profile data policies and terms of use to be provided to the end user. PolicyURI *url.URL TermsOfServiceURI *url.URL // URL to or the value of the client's JSON Web Key Set document. JWKSURI *url.URL JWKS *jose.JWKSet // URL referencing a flie with a single JSON array of redirect URIs. SectorIdentifierURI *url.URL SubjectType string // Options to restrict the JWS alg and enc values used for server responses and requests. IDTokenResponseOptions JWAOptions UserInfoResponseOptions JWAOptions RequestObjectOptions JWAOptions // Client requested authorization method and signing options for the token endpoint. // // Defaults to "client_secret_basic" TokenEndpointAuthMethod string TokenEndpointAuthSigningAlg string // DefaultMaxAge specifies the maximum amount of time in seconds before an authorized // user must reauthroize. // // If 0, no limitation is placed on the maximum. DefaultMaxAge int64 // RequireAuthTime specifies if the auth_time claim in the ID token is required. RequireAuthTime bool // Default Authentication Context Class Reference values for authentication requests. DefaultACRValues []string // URI that a third party can use to initiate a login by the relaying party. // // See: http://openid.net/specs/openid-connect-core-1_0.html#ThirdPartyInitiatedLogin InitiateLoginURI *url.URL // Pre-registered request_uri values that may be cached by the server. RequestURIs []url.URL } // Defaults returns a shallow copy of ClientMetadata with default // values replacing omitted fields. func (m ClientMetadata) Defaults() ClientMetadata { if len(m.ResponseTypes) == 0 { m.ResponseTypes = []string{oauth2.ResponseTypeCode} } if len(m.GrantTypes) == 0 { m.GrantTypes = []string{oauth2.GrantTypeAuthCode} } if m.ApplicationType == "" { m.ApplicationType = "web" } if m.TokenEndpointAuthMethod == "" { m.TokenEndpointAuthMethod = oauth2.AuthMethodClientSecretBasic } m.IDTokenResponseOptions = m.IDTokenResponseOptions.defaults() m.UserInfoResponseOptions = m.UserInfoResponseOptions.defaults() m.RequestObjectOptions = m.RequestObjectOptions.defaults() return m } func (m *ClientMetadata) MarshalJSON() ([]byte, error) { e := m.toEncodableStruct() return json.Marshal(&e) } func (m *ClientMetadata) UnmarshalJSON(data []byte) error { var e encodableClientMetadata if err := json.Unmarshal(data, &e); err != nil { return err } meta, err := e.toStruct() if err != nil { return err } if err := meta.Valid(); err != nil { return err } *m = meta return nil } type encodableClientMetadata struct { RedirectURIs []string `json:"redirect_uris"` // Required ResponseTypes []string `json:"response_types,omitempty"` GrantTypes []string `json:"grant_types,omitempty"` ApplicationType string `json:"application_type,omitempty"` Contacts []string `json:"contacts,omitempty"` ClientName string `json:"client_name,omitempty"` LogoURI string `json:"logo_uri,omitempty"` ClientURI string `json:"client_uri,omitempty"` PolicyURI string `json:"policy_uri,omitempty"` TermsOfServiceURI string `json:"tos_uri,omitempty"` JWKSURI string `json:"jwks_uri,omitempty"` JWKS *jose.JWKSet `json:"jwks,omitempty"` SectorIdentifierURI string `json:"sector_identifier_uri,omitempty"` SubjectType string `json:"subject_type,omitempty"` IDTokenSignedResponseAlg string `json:"id_token_signed_response_alg,omitempty"` IDTokenEncryptedResponseAlg string `json:"id_token_encrypted_response_alg,omitempty"` IDTokenEncryptedResponseEnc string `json:"id_token_encrypted_response_enc,omitempty"` UserInfoSignedResponseAlg string `json:"userinfo_signed_response_alg,omitempty"` UserInfoEncryptedResponseAlg string `json:"userinfo_encrypted_response_alg,omitempty"` UserInfoEncryptedResponseEnc string `json:"userinfo_encrypted_response_enc,omitempty"` RequestObjectSigningAlg string `json:"request_object_signing_alg,omitempty"` RequestObjectEncryptionAlg string `json:"request_object_encryption_alg,omitempty"` RequestObjectEncryptionEnc string `json:"request_object_encryption_enc,omitempty"` TokenEndpointAuthMethod string `json:"token_endpoint_auth_method,omitempty"` TokenEndpointAuthSigningAlg string `json:"token_endpoint_auth_signing_alg,omitempty"` DefaultMaxAge int64 `json:"default_max_age,omitempty"` RequireAuthTime bool `json:"require_auth_time,omitempty"` DefaultACRValues []string `json:"default_acr_values,omitempty"` InitiateLoginURI string `json:"initiate_login_uri,omitempty"` RequestURIs []string `json:"request_uris,omitempty"` } func (c *encodableClientMetadata) toStruct() (ClientMetadata, error) { p := stickyErrParser{} m := ClientMetadata{ RedirectURIs: p.parseURIs(c.RedirectURIs, "redirect_uris"), ResponseTypes: c.ResponseTypes, GrantTypes: c.GrantTypes, ApplicationType: c.ApplicationType, Contacts: p.parseEmails(c.Contacts, "contacts"), ClientName: c.ClientName, LogoURI: p.parseURI(c.LogoURI, "logo_uri"), ClientURI: p.parseURI(c.ClientURI, "client_uri"), PolicyURI: p.parseURI(c.PolicyURI, "policy_uri"), TermsOfServiceURI: p.parseURI(c.TermsOfServiceURI, "tos_uri"), JWKSURI: p.parseURI(c.JWKSURI, "jwks_uri"), JWKS: c.JWKS, SectorIdentifierURI: p.parseURI(c.SectorIdentifierURI, "sector_identifier_uri"), SubjectType: c.SubjectType, TokenEndpointAuthMethod: c.TokenEndpointAuthMethod, TokenEndpointAuthSigningAlg: c.TokenEndpointAuthSigningAlg, DefaultMaxAge: c.DefaultMaxAge, RequireAuthTime: c.RequireAuthTime, DefaultACRValues: c.DefaultACRValues, InitiateLoginURI: p.parseURI(c.InitiateLoginURI, "initiate_login_uri"), RequestURIs: p.parseURIs(c.RequestURIs, "request_uris"), IDTokenResponseOptions: JWAOptions{ c.IDTokenSignedResponseAlg, c.IDTokenEncryptedResponseAlg, c.IDTokenEncryptedResponseEnc, }, UserInfoResponseOptions: JWAOptions{ c.UserInfoSignedResponseAlg, c.UserInfoEncryptedResponseAlg, c.UserInfoEncryptedResponseEnc, }, RequestObjectOptions: JWAOptions{ c.RequestObjectSigningAlg, c.RequestObjectEncryptionAlg, c.RequestObjectEncryptionEnc, }, } if p.firstErr != nil { return ClientMetadata{}, p.firstErr } return m, nil } // stickyErrParser parses URIs and email addresses. Once it encounters // a parse error, subsequent calls become no-op. type stickyErrParser struct { firstErr error } func (p *stickyErrParser) parseURI(s, field string) *url.URL { if p.firstErr != nil || s == "" { return nil } u, err := url.Parse(s) if err == nil { if u.Host == "" { err = errors.New("no host in URI") } else if u.Scheme != "http" && u.Scheme != "https" { err = errors.New("invalid URI scheme") } } if err != nil { p.firstErr = fmt.Errorf("failed to parse %s: %v", field, err) return nil } return u } func (p *stickyErrParser) parseURIs(s []string, field string) []url.URL { if p.firstErr != nil || len(s) == 0 { return nil } uris := make([]url.URL, len(s)) for i, val := range s { if val == "" { p.firstErr = fmt.Errorf("invalid URI in field %s", field) return nil } if u := p.parseURI(val, field); u != nil { uris[i] = *u } } return uris } func (p *stickyErrParser) parseEmails(s []string, field string) []mail.Address { if p.firstErr != nil || len(s) == 0 { return nil } addrs := make([]mail.Address, len(s)) for i, addr := range s { if addr == "" { p.firstErr = fmt.Errorf("invalid email in field %s", field) return nil } a, err := mail.ParseAddress(addr) if err != nil { p.firstErr = fmt.Errorf("invalid email in field %s: %v", field, err) return nil } addrs[i] = *a } return addrs } func (m *ClientMetadata) toEncodableStruct() encodableClientMetadata { return encodableClientMetadata{ RedirectURIs: urisToStrings(m.RedirectURIs), ResponseTypes: m.ResponseTypes, GrantTypes: m.GrantTypes, ApplicationType: m.ApplicationType, Contacts: emailsToStrings(m.Contacts), ClientName: m.ClientName, LogoURI: uriToString(m.LogoURI), ClientURI: uriToString(m.ClientURI), PolicyURI: uriToString(m.PolicyURI), TermsOfServiceURI: uriToString(m.TermsOfServiceURI), JWKSURI: uriToString(m.JWKSURI), JWKS: m.JWKS, SectorIdentifierURI: uriToString(m.SectorIdentifierURI), SubjectType: m.SubjectType, IDTokenSignedResponseAlg: m.IDTokenResponseOptions.SigningAlg, IDTokenEncryptedResponseAlg: m.IDTokenResponseOptions.EncryptionAlg, IDTokenEncryptedResponseEnc: m.IDTokenResponseOptions.EncryptionEnc, UserInfoSignedResponseAlg: m.UserInfoResponseOptions.SigningAlg, UserInfoEncryptedResponseAlg: m.UserInfoResponseOptions.EncryptionAlg, UserInfoEncryptedResponseEnc: m.UserInfoResponseOptions.EncryptionEnc, RequestObjectSigningAlg: m.RequestObjectOptions.SigningAlg, RequestObjectEncryptionAlg: m.RequestObjectOptions.EncryptionAlg, RequestObjectEncryptionEnc: m.RequestObjectOptions.EncryptionEnc, TokenEndpointAuthMethod: m.TokenEndpointAuthMethod, TokenEndpointAuthSigningAlg: m.TokenEndpointAuthSigningAlg, DefaultMaxAge: m.DefaultMaxAge, RequireAuthTime: m.RequireAuthTime, DefaultACRValues: m.DefaultACRValues, InitiateLoginURI: uriToString(m.InitiateLoginURI), RequestURIs: urisToStrings(m.RequestURIs), } } func uriToString(u *url.URL) string { if u == nil { return "" } return u.String() } func urisToStrings(urls []url.URL) []string { if len(urls) == 0 { return nil } sli := make([]string, len(urls)) for i, u := range urls { sli[i] = u.String() } return sli } func emailsToStrings(addrs []mail.Address) []string { if len(addrs) == 0 { return nil } sli := make([]string, len(addrs)) for i, addr := range addrs { sli[i] = addr.String() } return sli } // Valid determines if a ClientMetadata conforms with the OIDC specification. // // 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 (m *ClientMetadata) Valid() error { if len(m.RedirectURIs) == 0 { return errors.New("zero redirect URLs") } validURI := func(u *url.URL, fieldName string) error { if u.Host == "" { return fmt.Errorf("no host for uri field %s", fieldName) } if u.Scheme != "http" && u.Scheme != "https" { return fmt.Errorf("uri field %s scheme is not http or https", fieldName) } return nil } uris := []struct { val *url.URL name string }{ {m.LogoURI, "logo_uri"}, {m.ClientURI, "client_uri"}, {m.PolicyURI, "policy_uri"}, {m.TermsOfServiceURI, "tos_uri"}, {m.JWKSURI, "jwks_uri"}, {m.SectorIdentifierURI, "sector_identifier_uri"}, {m.InitiateLoginURI, "initiate_login_uri"}, } for _, uri := range uris { if uri.val == nil { continue } if err := validURI(uri.val, uri.name); err != nil { return err } } uriLists := []struct { vals []url.URL name string }{ {m.RedirectURIs, "redirect_uris"}, {m.RequestURIs, "request_uris"}, } for _, list := range uriLists { for _, uri := range list.vals { if err := validURI(&uri, list.name); err != nil { return err } } } options := []struct { option JWAOptions name string }{ {m.IDTokenResponseOptions, "id_token response"}, {m.UserInfoResponseOptions, "userinfo response"}, {m.RequestObjectOptions, "request_object"}, } for _, option := range options { if err := option.option.valid(); err != nil { return fmt.Errorf("invalid JWA values for %s: %v", option.name, err) } } return nil } type ClientRegistrationResponse struct { ClientID string // Required ClientSecret string RegistrationAccessToken string RegistrationClientURI string // If IsZero is true, unspecified. ClientIDIssuedAt time.Time // Time at which the client_secret will expire. // If IsZero is true, it will not expire. ClientSecretExpiresAt time.Time ClientMetadata } type encodableClientRegistrationResponse struct { ClientID string `json:"client_id"` // Required ClientSecret string `json:"client_secret,omitempty"` RegistrationAccessToken string `json:"registration_access_token,omitempty"` RegistrationClientURI string `json:"registration_client_uri,omitempty"` ClientIDIssuedAt int64 `json:"client_id_issued_at,omitempty"` // Time at which the client_secret will expire, in seconds since the epoch. // If 0 it will not expire. ClientSecretExpiresAt int64 `json:"client_secret_expires_at"` // Required encodableClientMetadata } func unixToSec(t time.Time) int64 { if t.IsZero() { return 0 } return t.Unix() } func (c *ClientRegistrationResponse) MarshalJSON() ([]byte, error) { e := encodableClientRegistrationResponse{ ClientID: c.ClientID, ClientSecret: c.ClientSecret, RegistrationAccessToken: c.RegistrationAccessToken, RegistrationClientURI: c.RegistrationClientURI, ClientIDIssuedAt: unixToSec(c.ClientIDIssuedAt), ClientSecretExpiresAt: unixToSec(c.ClientSecretExpiresAt), encodableClientMetadata: c.ClientMetadata.toEncodableStruct(), } return json.Marshal(&e) } func secToUnix(sec int64) time.Time { if sec == 0 { return time.Time{} } return time.Unix(sec, 0) } func (c *ClientRegistrationResponse) UnmarshalJSON(data []byte) error { var e encodableClientRegistrationResponse if err := json.Unmarshal(data, &e); err != nil { return err } if e.ClientID == "" { return errors.New("no client_id in client registration response") } metadata, err := e.encodableClientMetadata.toStruct() if err != nil { return err } *c = ClientRegistrationResponse{ ClientID: e.ClientID, ClientSecret: e.ClientSecret, RegistrationAccessToken: e.RegistrationAccessToken, RegistrationClientURI: e.RegistrationClientURI, ClientIDIssuedAt: secToUnix(e.ClientIDIssuedAt), ClientSecretExpiresAt: secToUnix(e.ClientSecretExpiresAt), ClientMetadata: metadata, } return nil } type ClientConfig struct { HTTPClient phttp.Client Credentials ClientCredentials Scope []string RedirectURL string ProviderConfig ProviderConfig KeySet key.PublicKeySet } func NewClient(cfg ClientConfig) (*Client, error) { // Allow empty redirect URL in the case where the client // only needs to verify a given token. ru, err := url.Parse(cfg.RedirectURL) if err != nil { return nil, fmt.Errorf("invalid redirect URL: %v", err) } c := Client{ credentials: cfg.Credentials, httpClient: cfg.HTTPClient, scope: cfg.Scope, redirectURL: ru.String(), providerConfig: newProviderConfigRepo(cfg.ProviderConfig), keySet: cfg.KeySet, } if c.httpClient == nil { c.httpClient = http.DefaultClient } if c.scope == nil { c.scope = make([]string, len(DefaultScope)) copy(c.scope, DefaultScope) } return &c, nil } type Client struct { httpClient phttp.Client providerConfig *providerConfigRepo credentials ClientCredentials redirectURL string scope []string keySet key.PublicKeySet providerSyncer *ProviderConfigSyncer keySetSyncMutex sync.RWMutex lastKeySetSync time.Time } func (c *Client) Healthy() error { now := time.Now().UTC() cfg := c.providerConfig.Get() if cfg.Empty() { return errors.New("oidc client provider config empty") } if !cfg.ExpiresAt.IsZero() && cfg.ExpiresAt.Before(now) { return errors.New("oidc client provider config expired") } return nil } func (c *Client) OAuthClient() (*oauth2.Client, error) { cfg := c.providerConfig.Get() authMethod, err := chooseAuthMethod(cfg) if err != nil { return nil, err } ocfg := oauth2.Config{ Credentials: oauth2.ClientCredentials(c.credentials), RedirectURL: c.redirectURL, AuthURL: cfg.AuthEndpoint.String(), TokenURL: cfg.TokenEndpoint.String(), Scope: c.scope, AuthMethod: authMethod, } return oauth2.NewClient(c.httpClient, ocfg) } func chooseAuthMethod(cfg ProviderConfig) (string, error) { if len(cfg.TokenEndpointAuthMethodsSupported) == 0 { return oauth2.AuthMethodClientSecretBasic, nil } for _, authMethod := range cfg.TokenEndpointAuthMethodsSupported { if _, ok := supportedAuthMethods[authMethod]; ok { return authMethod, nil } } return "", errors.New("no supported auth methods") } // SyncProviderConfig starts the provider config syncer func (c *Client) SyncProviderConfig(discoveryURL string) chan struct{} { r := NewHTTPProviderConfigGetter(c.httpClient, discoveryURL) s := NewProviderConfigSyncer(r, c.providerConfig) stop := s.Run() s.WaitUntilInitialSync() return stop } func (c *Client) maybeSyncKeys() error { tooSoon := func() bool { return time.Now().UTC().Before(c.lastKeySetSync.Add(keySyncWindow)) } // ignore request to sync keys if a sync operation has been // attempted too recently if tooSoon() { return nil } c.keySetSyncMutex.Lock() defer c.keySetSyncMutex.Unlock() // check again, as another goroutine may have been holding // the lock while updating the keys if tooSoon() { return nil } cfg := c.providerConfig.Get() r := NewRemotePublicKeyRepo(c.httpClient, cfg.KeysEndpoint.String()) w := &clientKeyRepo{client: c} _, err := key.Sync(r, w) c.lastKeySetSync = time.Now().UTC() return err } type clientKeyRepo struct { client *Client } func (r *clientKeyRepo) Set(ks key.KeySet) error { pks, ok := ks.(*key.PublicKeySet) if !ok { return errors.New("unable to cast to PublicKey") } r.client.keySet = *pks return nil } func (c *Client) ClientCredsToken(scope []string) (jose.JWT, error) { cfg := c.providerConfig.Get() if !cfg.SupportsGrantType(oauth2.GrantTypeClientCreds) { return jose.JWT{}, fmt.Errorf("%v grant type is not supported", oauth2.GrantTypeClientCreds) } oac, err := c.OAuthClient() if err != nil { return jose.JWT{}, err } t, err := oac.ClientCredsToken(scope) if err != nil { return jose.JWT{}, err } jwt, err := jose.ParseJWT(t.IDToken) if err != nil { return jose.JWT{}, err } return jwt, c.VerifyJWT(jwt) } // ExchangeAuthCode exchanges an OAuth2 auth code for an OIDC JWT ID token. func (c *Client) ExchangeAuthCode(code string) (jose.JWT, error) { oac, err := c.OAuthClient() if err != nil { return jose.JWT{}, err } t, err := oac.RequestToken(oauth2.GrantTypeAuthCode, code) if err != nil { return jose.JWT{}, err } jwt, err := jose.ParseJWT(t.IDToken) if err != nil { return jose.JWT{}, err } return jwt, c.VerifyJWT(jwt) } // RefreshToken uses a refresh token to exchange for a new OIDC JWT ID Token. func (c *Client) RefreshToken(refreshToken string) (jose.JWT, error) { oac, err := c.OAuthClient() if err != nil { return jose.JWT{}, err } t, err := oac.RequestToken(oauth2.GrantTypeRefreshToken, refreshToken) if err != nil { return jose.JWT{}, err } jwt, err := jose.ParseJWT(t.IDToken) if err != nil { return jose.JWT{}, err } return jwt, c.VerifyJWT(jwt) } func (c *Client) VerifyJWT(jwt jose.JWT) error { var keysFunc func() []key.PublicKey if kID, ok := jwt.KeyID(); ok { keysFunc = c.keysFuncWithID(kID) } else { keysFunc = c.keysFuncAll() } v := NewJWTVerifier( c.providerConfig.Get().Issuer.String(), c.credentials.ID, c.maybeSyncKeys, keysFunc) return v.Verify(jwt) } // keysFuncWithID returns a function that retrieves at most unexpired // public key from the Client that matches the provided ID func (c *Client) keysFuncWithID(kID string) func() []key.PublicKey { return func() []key.PublicKey { c.keySetSyncMutex.RLock() defer c.keySetSyncMutex.RUnlock() if c.keySet.ExpiresAt().Before(time.Now()) { return []key.PublicKey{} } k := c.keySet.Key(kID) if k == nil { return []key.PublicKey{} } return []key.PublicKey{*k} } } // keysFuncAll returns a function that retrieves all unexpired public // keys from the Client func (c *Client) keysFuncAll() func() []key.PublicKey { return func() []key.PublicKey { c.keySetSyncMutex.RLock() defer c.keySetSyncMutex.RUnlock() if c.keySet.ExpiresAt().Before(time.Now()) { return []key.PublicKey{} } return c.keySet.Keys() } } type providerConfigRepo struct { mu sync.RWMutex config ProviderConfig // do not access directly, use Get() } func newProviderConfigRepo(pc ProviderConfig) *providerConfigRepo { return &providerConfigRepo{sync.RWMutex{}, pc} } // returns an error to implement ProviderConfigSetter func (r *providerConfigRepo) Set(cfg ProviderConfig) error { r.mu.Lock() defer r.mu.Unlock() r.config = cfg return nil } func (r *providerConfigRepo) Get() ProviderConfig { r.mu.RLock() defer r.mu.RUnlock() return r.config }