3
0
corteza/server/auth/external/register.go
2022-12-13 17:25:04 +01:00

254 lines
5.9 KiB
Go

package external
import (
"context"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"strings"
"github.com/cortezaproject/corteza/server/pkg/options"
"github.com/cortezaproject/corteza/server/store"
"github.com/cortezaproject/corteza/server/system/types"
"github.com/crusttech/go-oidc"
"go.uber.org/zap"
)
func AddProvider(ctx context.Context, log *zap.Logger, s store.SettingValues, eap *types.ExternalAuthProvider, force bool) error {
var (
prefix = "auth.external.providers." + eap.Key + "."
)
log = log.With(
zap.Bool("force", force),
zap.String("handle", eap.Handle),
zap.String("key", eap.Key),
)
if eap.IssuerUrl != "" {
log = log.With(zap.String("issuer-url", eap.IssuerUrl))
}
log.Info("adding external authentication provider")
ss, _, err := store.SearchSettingValues(ctx, s, types.SettingsFilter{
Prefix: prefix,
})
if err != nil {
return err
}
ex := ss.KV().CutPrefix(prefix)
if !force {
// check if exists before storing it
if len(ex) > 0 && ex.String("key") == eap.Key && ex.String("secret") == eap.Secret {
return nil
}
}
if vv, err := eap.EncodeKV(); err != nil {
return fmt.Errorf("could not encode auth provider values: %w", err)
} else if err = store.UpsertSettingValue(ctx, s, vv...); err != nil {
return fmt.Errorf("could not store auth provider values: %w", err)
}
log.Info("external authentication provider added")
return nil
}
// @todo remove dependency on github.com/crusttech/go-oidc (and github.com/coreos/go-oidc)
//
// and move client registration to corteza codebase
func DiscoverOidcProvider(ctx context.Context, log *zap.Logger, opt options.AuthOpt, name, url string) (eap *types.ExternalAuthProvider, err error) {
var (
provider *oidc.Provider
client *oidc.Client
redirectUrl = strings.Replace(opt.ExternalRedirectURL, "{provider}", OIDC_PROVIDER_PREFIX+name, 1)
)
log = log.With(
zap.String("name", name),
zap.String("url", url),
)
if provider, err = oidc.NewProvider(ctx, url); err != nil {
return
}
client, err = provider.RegisterClient(ctx, &oidc.ClientRegistration{
Name: "Corteza",
RedirectURIs: []string{redirectUrl},
ResponseTypes: []string{"token id_token", "code"},
})
if err != nil {
log.Error("could not register oidc provider", zap.Error(err))
return
}
eap = &types.ExternalAuthProvider{
Handle: OIDC_PROVIDER_PREFIX + name,
RedirectUrl: redirectUrl,
Key: client.ID,
Secret: client.Secret,
IssuerUrl: url,
}
log.Info("oidc provider registered", zap.String("key", client.ID))
return
}
func RegisterOidcProvider(ctx context.Context, log *zap.Logger, s store.SettingValues, opt options.AuthOpt, name, providerUrl string, force, validate, enable bool) (eap *types.ExternalAuthProvider, err error) {
if !force {
prefix := "auth.external.providers." + eap.Key + "."
vv, _, err := store.SearchSettingValues(ctx, s, types.SettingsFilter{
Prefix: prefix,
})
if err != nil || len(vv) > 0 {
return nil, err
}
}
if validate {
// Do basic validation of external auth settings
// will fail if secret or url are not set
if err = staticValidateExternal(opt); err != nil {
return
}
// Do full rediredct-URL check
if err = validateExternalRedirectURL(opt); err != nil {
return
}
}
if opt.ExternalRedirectURL == "" {
return nil, fmt.Errorf("refusing to register OIDC provider without redirect url")
}
p, err := parseExternalProviderUrl(providerUrl)
if err != nil {
return
}
eap, err = DiscoverOidcProvider(ctx, log, opt, name, p.String())
if err != nil {
return
}
vv, err := eap.EncodeKV()
if err != nil {
return
}
if enable {
err = vv.Walk(func(value *types.SettingValue) error {
if strings.HasSuffix(value.Name, ".enabled") {
return value.SetValue(true)
}
return nil
})
if err != nil {
return
}
v := &types.SettingValue{Name: "auth.external.enabled"}
err = v.SetValue(true)
if err != nil {
return
}
vv = append(vv, v)
}
err = store.UpsertSettingValue(ctx, s, vv...)
if err != nil {
return
}
return
}
func parseExternalProviderUrl(in string) (p *url.URL, err error) {
if i := strings.Index(in, "://"); i == -1 {
// Add schema if missing
in = "https://" + in
}
if p, err = url.Parse(in); err != nil {
// Try to parse it
return
} else if i := strings.Index(p.Path, WellKnown); i > -1 {
// Cut off well-known-path
p.Path = p.Path[:i]
}
return
}
// StaticValidateExternal
//
// Simple checks of external auth settings
func staticValidateExternal(opt options.AuthOpt) error {
if opt.ExternalRedirectURL == "" {
return fmt.Errorf("redirect URL is empty")
}
const (
tpt = "test-provider-test"
)
p, err := url.Parse(strings.Replace(opt.ExternalRedirectURL, "{provider}", tpt, 1))
if err != nil {
return fmt.Errorf("invalid redirect URL: %w", err)
}
if !strings.Contains(p.Path, tpt+"/callback") {
return fmt.Errorf("could find injected provider in the URL, make sure you use '%%s' as a placeholder: %w", err)
}
if opt.ExternalCookieSecret == "" {
return fmt.Errorf("AUTH_EXTERNAL_COOKIE_SECRET is empty")
}
if opt.SessionCookieSecure && p.Scheme != "https" {
return fmt.Errorf("session store is secure, redirect URL should have HTTPS")
}
return nil
}
// ValidateExternalRedirectURL
//
// Validates external redirect URL
func validateExternalRedirectURL(opt options.AuthOpt) error {
const tpt = "test-provider-test"
const cb = "/callback"
// Replace placeholders & remove /callback
var url = strings.Replace(opt.ExternalRedirectURL, "{provider}", tpt, 1)
url = url[0 : len(url)-len(cb)]
rsp, err := http.DefaultClient.Get(url)
if err != nil {
return fmt.Errorf("could not get response from redirect URL: %w", err)
}
defer rsp.Body.Close()
body, err := ioutil.ReadAll(rsp.Body)
if err != nil {
return fmt.Errorf("could not read response body: %w", err)
}
if strings.Contains(string(body), tpt) {
return nil
}
return fmt.Errorf("could not validate external auth redirection URL")
}