254 lines
5.9 KiB
Go
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")
|
|
}
|