Refactor SSO configuration.

It makes more sense to base provider defaults on brand. Type is not
1:1 to brand.

Splits apart OIDC and OAuth2 to match actual specs.
This commit is contained in:
Tommie Gannert 2022-05-27 22:15:52 +02:00
parent d351a48379
commit 09f0dca6aa
5 changed files with 85 additions and 54 deletions

View file

@ -48,7 +48,7 @@ type oauth2IdentityProvider struct {
func (p *oauth2IdentityProvider) AuthorizationURL(ctx context.Context, callbackURL, nonce string) (string, error) {
u, err := resolveURL(p.authorizationURL, url.Values{
"client_id": []string{p.cfg.OIDC.ClientID},
"client_id": []string{p.cfg.OAuth2.ClientID},
"response_type": []string{"code"},
"redirect_uri": []string{callbackURL},
"scope": []string{strings.Join(p.scopes, " ")},
@ -121,8 +121,8 @@ func (p *oauth2IdentityProvider) getAccessToken(ctx context.Context, callbackURL
"grant_type": []string{"authorization_code"},
"code": []string{code},
"redirect_uri": []string{callbackURL},
"client_id": []string{p.cfg.OIDC.ClientID},
"client_secret": []string{p.cfg.OIDC.ClientSecret},
"client_id": []string{p.cfg.OAuth2.ClientID},
"client_secret": []string{p.cfg.OAuth2.ClientSecret},
}
hreq, err := http.NewRequestWithContext(ctx, http.MethodPost, p.accessTokenURL, strings.NewReader(body.Encode()))
if err != nil {

View file

@ -36,7 +36,7 @@ type oidcIdentityProvider struct {
}
func newOIDCIdentityProvider(ctx context.Context, cfg *config.IdentityProvider, hc *http.Client) (*oidcIdentityProvider, error) {
p := &oidcIdentityProvider{
return &oidcIdentityProvider{
oauth2IdentityProvider: &oauth2IdentityProvider{
cfg: cfg,
hc: hc,
@ -48,18 +48,7 @@ func newOIDCIdentityProvider(ctx context.Context, cfg *config.IdentityProvider,
displayNamePath: "name",
suggestedUserIDPath: "preferred_username",
},
}
// TODO: Complement starts and waits for the "base image" without
// first starting httpmockserver, which means we cannot always do
// this sanity check.
if false {
if _, _, err := p.get(ctx); err != nil {
return nil, err
}
}
return p, nil
}, nil
}
func (p *oidcIdentityProvider) AuthorizationURL(ctx context.Context, callbackURL, nonce string) (string, error) {

View file

@ -42,12 +42,9 @@ func NewAuthenticator(ctx context.Context, cfg *config.SSO) (*Authenticator, err
providers: make(map[string]identityProvider, len(cfg.Providers)),
}
for _, pcfg := range cfg.Providers {
typ := pcfg.Type
if typ == "" {
typ = config.IdentityProviderType(pcfg.ID)
}
pcfg = pcfg.WithDefaults()
switch typ {
switch pcfg.Type {
case config.SSOTypeOIDC:
p, err := newOIDCIdentityProvider(ctx, &pcfg, hc)
if err != nil {
@ -57,7 +54,7 @@ func NewAuthenticator(ctx context.Context, cfg *config.SSO) (*Authenticator, err
case config.SSOTypeGitHub:
a.providers[pcfg.ID] = newGitHubIdentityProvider(&pcfg, hc)
default:
return nil, fmt.Errorf("unknown SSO provider type: %s", typ)
return nil, fmt.Errorf("unknown SSO provider type: %s", pcfg.Type)
}
}

View file

@ -85,20 +85,19 @@ func main() {
cfg.ClientAPI.Login.SSO.Enabled = true
cfg.ClientAPI.Login.SSO.Providers = []config.IdentityProvider{
{
ID: "github",
Name: "Fake GitHub",
OIDC: config.OIDC{
Brand: "github",
OAuth2: config.OAuth2{
ClientID: "aclientid",
ClientSecret: "aclientsecret",
},
},
{
ID: "google",
Name: "Fake Google",
Type: "oidc",
OIDC: config.OIDC{
Brand: "google",
OAuth2: config.OAuth2{
ClientID: "aclientid",
ClientSecret: "aclientsecret",
},
OIDC: config.OIDC{
DiscoveryURL: "https://accounts.google.com/.well-known/openid-configuration",
},
},

View file

@ -138,7 +138,8 @@ func (sso *SSO) Verify(configErrs *ConfigErrors) {
var foundDefaultProvider bool
seenPIDs := make(map[string]bool, len(sso.Providers))
for _, p := range sso.Providers {
p.Verify(configErrs)
p = p.WithDefaults()
p.verifyNormalized(configErrs)
if p.ID == sso.DefaultProviderID {
foundDefaultProvider = true
}
@ -158,42 +159,71 @@ func (sso *SSO) Verify(configErrs *ConfigErrors) {
}
}
// See https://github.com/matrix-org/matrix-doc/blob/old_master/informal/idp-brands.md.
type IdentityProvider struct {
// ID is the unique identifier of this IdP. We use the brand identifiers as provider
// identifiers for simplicity.
// ID is the unique identifier of this IdP. If empty, the brand will be used.
ID string `yaml:"id"`
// Name is a human-friendly name of the provider.
// Name is a human-friendly name of the provider. If empty, a default based on
// the brand will be used.
Name string `yaml:"name"`
// Brand is a hint on how to display the IdP to the user. If this is empty, a default
// based on the type is used.
// Brand is a hint on how to display the IdP to the user.
//
// See https://github.com/matrix-org/matrix-doc/blob/old_master/informal/idp-brands.md.
Brand SSOBrand `yaml:"brand"`
// Icon is an MXC URI describing how to display the IdP to the user. Prefer using `brand`.
Icon string `yaml:"icon"`
// Type describes how this provider is implemented. It must match "github". If this is
// empty, the ID is used, which means there is a weak expectation that ID is also a
// valid type, unless you have a complicated setup.
// Type describes how this IdP is implemented. If this is empty, a default is chosen
// based on brand.
Type IdentityProviderType `yaml:"type"`
// OIDC contains settings for providers based on OpenID Connect (OAuth 2).
// OAuth2 contains settings for IdPs based on OpenID Connect and OAuth2.
OAuth2 OAuth2 `yaml:"oauth2"`
// OIDC contains settings for IdPs based on OpenID Connect.
OIDC OIDC `yaml:"oidc"`
}
type OIDC struct {
func (idp *IdentityProvider) WithDefaults() IdentityProvider {
p := *idp
if p.ID == "" {
p.ID = string(p.Brand)
}
if p.OIDC.DiscoveryURL == "" {
p.OIDC.DiscoveryURL = oidcDefaultDiscoveryURLs[idp.Brand]
}
if p.Type == "" {
if p.OIDC.DiscoveryURL != "" {
p.Type = SSOTypeOIDC
} else if p.Brand == SSOBrandGitHub {
p.Type = SSOTypeGitHub
}
}
if p.Name == "" {
p.Name = oidcDefaultNames[p.Brand]
}
return p
}
type OAuth2 struct {
ClientID string `yaml:"client_id"`
ClientSecret string `yaml:"client_secret"`
}
type OIDC struct {
DiscoveryURL string `yaml:"discovery_url"`
}
func (idp *IdentityProvider) Verify(configErrs *ConfigErrors) {
checkNotEmpty(configErrs, "client_api.sso.providers.id", idp.ID)
if !checkIdentityProviderBrand(SSOBrand(idp.ID)) {
configErrs.Add(fmt.Sprintf("unrecognised ID config key %q: %s", "client_api.sso.providers", idp.ID))
p := idp.WithDefaults()
p.verifyNormalized(configErrs)
}
func (idp *IdentityProvider) verifyNormalized(configErrs *ConfigErrors) {
checkNotEmpty(configErrs, "client_api.sso.providers.id", idp.ID)
checkNotEmpty(configErrs, "client_api.sso.providers.name", idp.Name)
if idp.Brand != "" && !checkIdentityProviderBrand(idp.Brand) {
configErrs.Add(fmt.Sprintf("unrecognised brand in identity provider %q for config key %q: %s", idp.ID, "client_api.sso.providers", idp.Brand))
@ -201,23 +231,19 @@ func (idp *IdentityProvider) Verify(configErrs *ConfigErrors) {
if idp.Icon != "" {
checkURL(configErrs, "client_api.sso.providers.icon", idp.Icon)
}
typ := idp.Type
if idp.Type == "" {
typ = IdentityProviderType(idp.ID)
}
switch typ {
switch idp.Type {
case SSOTypeOIDC:
checkNotEmpty(configErrs, "client_api.sso.providers.oidc.client_id", idp.OIDC.ClientID)
checkNotEmpty(configErrs, "client_api.sso.providers.oidc.client_secret", idp.OIDC.ClientSecret)
checkNotEmpty(configErrs, "client_api.sso.providers.oidc.client_id", idp.OAuth2.ClientID)
checkNotEmpty(configErrs, "client_api.sso.providers.oidc.client_secret", idp.OAuth2.ClientSecret)
checkNotEmpty(configErrs, "client_api.sso.providers.oidc.discovery_url", idp.OIDC.DiscoveryURL)
case SSOTypeGitHub:
checkNotEmpty(configErrs, "client_api.sso.providers.oidc.client_id", idp.OIDC.ClientID)
checkNotEmpty(configErrs, "client_api.sso.providers.oidc.client_secret", idp.OIDC.ClientSecret)
checkNotEmpty(configErrs, "client_api.sso.providers.oidc.client_id", idp.OAuth2.ClientID)
checkNotEmpty(configErrs, "client_api.sso.providers.oidc.client_secret", idp.OAuth2.ClientSecret)
default:
configErrs.Add(fmt.Sprintf("unrecognised type in identity provider %q for config key %q: %s", idp.ID, "client_api.sso.providers", typ))
configErrs.Add(fmt.Sprintf("unrecognised type in identity provider %q for config key %q: %s", idp.ID, "client_api.sso.providers", idp.Type))
}
}
@ -231,6 +257,7 @@ func checkIdentityProviderBrand(s SSOBrand) bool {
}
}
// SSOBrand corresponds to https://github.com/matrix-org/matrix-spec-proposals/blob/old_master/informal/idp-brands.md
type SSOBrand string
const (
@ -242,6 +269,25 @@ const (
SSOBrandTwitter SSOBrand = "twitter"
)
var (
oidcDefaultDiscoveryURLs = map[SSOBrand]string{
// https://developers.facebook.com/docs/facebook-login/limited-login/token/
SSOBrandFacebook: "https://www.facebook.com/.well-known/openid-configuration/",
// https://docs.gitlab.com/ee/integration/openid_connect_provider.html
SSOBrandGitLab: "https://gitlab.com/.well-known/openid-configuration",
// https://developers.google.com/identity/protocols/oauth2/openid-connect
SSOBrandGoogle: "https://accounts.google.com/.well-known/openid-configuration",
}
oidcDefaultNames = map[SSOBrand]string{
SSOBrandApple: "Apple",
SSOBrandFacebook: "Facebook",
SSOBrandGitHub: "GitHub",
SSOBrandGitLab: "GitLab",
SSOBrandGoogle: "Google",
SSOBrandTwitter: "Twitter",
}
)
type IdentityProviderType string
const (