3
0

Refactor & merge external auth settings

This commit is contained in:
Denis Arh
2019-07-15 14:57:25 +02:00
parent de26f15c8e
commit 14f1fe4b58
15 changed files with 413 additions and 480 deletions
+17 -10
View File
@@ -22,7 +22,8 @@ import (
// Will perform OpenID connect auto-configuration
func Auth(ctx context.Context, c *cli.Config) *cobra.Command {
var (
enableDiscoveredProvider bool
enableDiscoveredProvider bool
skipValidationOnAutoDiscoveredProvider bool
)
cmd := &cobra.Command{
@@ -39,22 +40,22 @@ func Auth(ctx context.Context, c *cli.Config) *cobra.Command {
var (
name, providerUrl = args[0], args[1]
es = service.DefaultAuthSettings
)
eas, err := external.ExternalAuthSettings(service.DefaultIntSettings)
cli.HandleError(err)
if !skipValidationOnAutoDiscoveredProvider {
// Do basic validation of external auth settings
// will fail if secret or url are not set
cli.HandleError(es.StaticValidateExternal())
// Do basic validation of external auth settings
// will fail if secret or url are not set
cli.HandleError(eas.ValidateStatic())
// Do full rediredct-URL check
cli.HandleError(eas.ValidateRedirectURL())
// Do full rediredct-URL check
cli.HandleError(es.ValidateExternalRedirectURL())
}
p, err := parseExternalProviderUrl(providerUrl)
cli.HandleError(err)
eap, err := external.RegisterNewOpenIdClient(ctx, eas, name, p.String())
eap, err := external.RegisterNewOpenIdClient(ctx, es, name, p.String())
cli.HandleError(err)
vv, err := eap.MakeValueSet("openid-connect." + name)
@@ -90,6 +91,12 @@ func Auth(ctx context.Context, c *cli.Config) *cobra.Command {
false,
"Enable this provider and external auth")
autoDiscoverCmd.Flags().BoolVar(
&skipValidationOnAutoDiscoveredProvider,
"skip-validation",
false,
"Skip validation")
jwtCmd := &cobra.Command{
Use: "jwt [email-or-id]",
Short: "Generates new JWT for a user",
+3 -7
View File
@@ -3,16 +3,12 @@ package external
import (
"go.uber.org/zap"
"github.com/cortezaproject/corteza-server/internal/settings"
"github.com/cortezaproject/corteza-server/pkg/logger"
"github.com/cortezaproject/corteza-server/system/internal/service"
)
func Init(settingsService settings.Service) {
if eas, err := ExternalAuthSettings(settingsService); err != nil {
log().Error("failed load external authentication settings", zap.Error(err))
} else {
setupGoth(eas)
}
func Init() {
setupGoth(service.DefaultAuthSettings)
}
func log() *zap.Logger {
+19 -17
View File
@@ -12,6 +12,8 @@ import (
"github.com/markbates/goth/providers/linkedin"
"github.com/markbates/goth/providers/openidConnect"
"go.uber.org/zap"
"github.com/cortezaproject/corteza-server/system/internal/service"
)
// We're expecting that our users will be able to complete
@@ -22,16 +24,16 @@ const (
WellKnown = "/.well-known/openid-configuration"
)
func setupGoth(eas *externalAuthSettings) {
if eas == nil || !eas.enabled {
func setupGoth(as service.AuthSettings) {
if !as.ExternalEnabled {
log().Info("external authentication disabled")
return
}
store := sessions.NewCookieStore([]byte(eas.sessionStoreSecret))
store := sessions.NewCookieStore([]byte(as.ExternalSessionStoreSecret))
store.MaxAge(gothMaxSessionStoreAge)
store.Options.HttpOnly = true
store.Options.Secure = eas.sessionStoreSecure
store.Options.Secure = as.ExternalSessionStoreSecure
gothic.Store = store
log().Debug("registering cookie session store")
@@ -41,11 +43,11 @@ func setupGoth(eas *externalAuthSettings) {
}
setupGothProviders(eas)
setupGothProviders(as)
}
func setupGothProviders(eas *externalAuthSettings) {
func setupGothProviders(as service.AuthSettings) {
var (
err error
scopes = []string{"email"}
@@ -58,32 +60,32 @@ func setupGothProviders(eas *externalAuthSettings) {
}
var enabled = 0
for _, pc := range eas.providers {
if pc.enabled {
for _, pc := range as.ExternalProviders {
if pc.Enabled {
enabled++
}
}
log().Debug("initializing enabled external authentication providers", zap.Int("count", enabled))
for name, pc := range eas.providers {
for name, pc := range as.ExternalProviders {
var provider goth.Provider
log := log().With(zap.String("provider", name))
if !pc.enabled {
if !pc.Enabled {
continue
}
if strings.Index(name, "openid-connect.") == 0 {
if pc.issuerUrl == "" {
if pc.IssuerUrl == "" {
log.Error("failed to discover OIDC provider, URL empty")
continue
}
wellKnown := strings.TrimSuffix(pc.issuerUrl, "/") + WellKnown
wellKnown := strings.TrimSuffix(pc.IssuerUrl, "/") + WellKnown
if provider, err = openidConnect.New(pc.key, pc.secret, pc.redirectUrl, wellKnown, scopes...); err != nil {
if provider, err = openidConnect.New(pc.Key, pc.Secret, pc.RedirectUrl, wellKnown, scopes...); err != nil {
log.Error("failed to discover OIDC provider", zap.Error(err), zap.String("well-known", wellKnown))
continue
} else {
@@ -92,13 +94,13 @@ func setupGothProviders(eas *externalAuthSettings) {
} else {
switch name {
case "github":
provider = github.New(pc.key, pc.secret, pc.redirectUrl, scopes...)
provider = github.New(pc.Key, pc.Secret, pc.RedirectUrl, scopes...)
case "facebook":
provider = facebook.New(pc.key, pc.secret, pc.redirectUrl, scopes...)
provider = facebook.New(pc.Key, pc.Secret, pc.RedirectUrl, scopes...)
case "gplus":
provider = gplus.New(pc.key, pc.secret, pc.redirectUrl, scopes...)
provider = gplus.New(pc.Key, pc.Secret, pc.RedirectUrl, scopes...)
case "linkedin":
provider = linkedin.New(pc.key, pc.secret, pc.redirectUrl, scopes...)
provider = linkedin.New(pc.Key, pc.Secret, pc.RedirectUrl, scopes...)
}
}
+9 -7
View File
@@ -5,15 +5,17 @@ import (
"fmt"
"github.com/crusttech/go-oidc"
"github.com/cortezaproject/corteza-server/system/internal/service"
)
// @todo remove dependency on github.com/crusttech/go-oidc (and github.com/coreos/go-oidc)
// and move client registration to corteza codebase
func RegisterNewOpenIdClient(ctx context.Context, eas *externalAuthSettings, name, url string) (eap *externalAuthProvider, err error) {
func RegisterNewOpenIdClient(ctx context.Context, eas service.AuthSettings, name, url string) (eap *service.AuthSettingsExternalAuthProvider, err error) {
var (
provider *oidc.Provider
client *oidc.Client
redirectUrl = fmt.Sprintf(eas.redirectUrl, "openid-connect."+name)
redirectUrl = fmt.Sprintf(eas.ExternalRedirectUrl, "openid-connect."+name)
)
if provider, err = oidc.NewProvider(ctx, url); err != nil {
@@ -30,11 +32,11 @@ func RegisterNewOpenIdClient(ctx context.Context, eas *externalAuthSettings, nam
return
}
eap = &externalAuthProvider{
redirectUrl: redirectUrl,
key: client.ID,
secret: client.Secret,
issuerUrl: url,
eap = &service.AuthSettingsExternalAuthProvider{
RedirectUrl: redirectUrl,
Key: client.ID,
Secret: client.Secret,
IssuerUrl: url,
}
return
-225
View File
@@ -1,225 +0,0 @@
package external
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"strings"
"github.com/pkg/errors"
intset "github.com/cortezaproject/corteza-server/internal/settings"
)
const (
settingsKeyBase = "auth.external."
settingsKeyProviders = settingsKeyBase + "providers."
settingsKeyRedirectUrl = settingsKeyBase + "redirect-url"
settingsKeySessionStoreSecret = settingsKeyBase + "session-store-secret"
settingsKeySessionStoreSecure = settingsKeyBase + "session-store-secure"
)
type (
externalAuthSettings struct {
enabled bool
redirectUrl string
sessionStoreSecret string
sessionStoreSecure bool
providers map[string]externalAuthProvider
}
externalAuthProvider struct {
enabled bool
key string
secret string
redirectUrl string
issuerUrl string
}
)
func ExternalAuthProvider(kv intset.KV) (eap externalAuthProvider, err error) {
for k, v := range kv {
ld := strings.LastIndex(k, ".")
switch k[ld+1:] {
case "enabled":
err = v.Unmarshal(&eap.enabled)
case "key":
err = v.Unmarshal(&eap.key)
case "secret":
err = v.Unmarshal(&eap.secret)
case "issuer":
err = v.Unmarshal(&eap.issuerUrl)
}
if err != nil {
return
}
}
return
}
func (eas externalAuthSettings) Enabled() bool {
return eas.enabled
}
func (eas externalAuthSettings) ValidateStatic() error {
if eas.redirectUrl == "" {
return errors.New("redirect URL is empty")
}
const (
tpt = "test-provider-test"
)
p, err := url.Parse(fmt.Sprintf(eas.redirectUrl, tpt))
if err != nil {
return errors.Wrap(err, "invalid redirect URL")
}
if !strings.Contains(p.Path, tpt+"/callback") {
return errors.Wrap(err, "could find injected provider in the URL, make sure you use '%s' as a placeholder")
}
if eas.sessionStoreSecret == "" {
return errors.New("session store secret is empty")
}
if eas.sessionStoreSecure && p.Scheme != "https" {
return errors.New("session store is secure, redirect URL should have HTTPS")
}
return nil
}
func (eas externalAuthSettings) ValidateRedirectURL() error {
const tpt = "test-provider-test"
const cb = "/callback"
// Replace placeholders & remove /callback
var url = fmt.Sprintf(eas.redirectUrl, tpt)
url = url[0 : len(url)-len(cb)]
rsp, err := http.DefaultClient.Get(url)
if err != nil {
return errors.Wrap(err, "could not get response from redirect URL")
}
defer rsp.Body.Close()
body, err := ioutil.ReadAll(rsp.Body)
if strings.Contains(string(body), tpt) {
return nil
}
return errors.New("could not validate external auth redirection URL")
}
func (eap externalAuthProvider) MakeValueSet(name string) (vv intset.ValueSet, err error) {
set := func(name string, value interface{}) error {
v := &intset.Value{Name: name}
if v.Value, err = json.Marshal(value); err != nil {
return err
}
vv = append(vv, v)
return nil
}
prefix := settingsKeyProviders + name
if err = set(prefix+".enabled", eap.enabled); err != nil {
return nil, err
}
if err = set(prefix+".key", eap.key); err != nil {
return nil, err
}
if err = set(prefix+".secret", eap.secret); err != nil {
return nil, err
}
if err = set(prefix+".issuer", eap.issuerUrl); err != nil {
return nil, err
}
return vv, err
}
// ExternalAuthSettings maps from plain values to externalAuthSettings struct
//
// see settings.Initialize() func
func ExternalAuthSettings(s intset.Service) (eas *externalAuthSettings, err error) {
// Read all settings and populate struct
settings, err := s.FindByPrefix(settingsKeyBase)
if err != nil {
return nil, errors.Wrap(err, "could not load settings for external auth provider")
}
kv := settings.KV()
eas = &externalAuthSettings{
enabled: kv.Bool(settingsKeyBase + "enabled"),
redirectUrl: kv.String(settingsKeyRedirectUrl),
sessionStoreSecret: kv.String(settingsKeySessionStoreSecret),
}
if !kv.Has(settingsKeySessionStoreSecure) {
// If auth.external.session-store-secure is not explicitly set;
// check if redirectUrl uses HTTPS schema and assume we want secure session store
eas.sessionStoreSecure = strings.Index(eas.redirectUrl, "https://") == 0
} else {
eas.sessionStoreSecure = kv.Bool(settingsKeySessionStoreSecure)
}
if eas.providers, err = extractProviders(eas.redirectUrl, kv); err != nil {
return nil, err
}
return
}
func extractProviders(redirectUrl string, kv intset.KV) (providers map[string]externalAuthProvider, err error) {
// Standard providers:
providers = map[string]externalAuthProvider{
"github": {},
"facebook": {},
"gplus": {},
"linkedin": {},
}
oidcKeyBase := settingsKeyProviders + "openid-connect."
for k := range kv.Filter(oidcKeyBase) {
if len(k) < len(oidcKeyBase)+2 {
// skip invalid keys
continue
}
// find next dot:
name := k[len(oidcKeyBase):]
dotPos := strings.Index(name, ".")
if dotPos > 0 {
name = name[:dotPos]
}
providers["openid-connect."+name] = externalAuthProvider{}
}
for provider := range providers {
if eap, err := ExternalAuthProvider(kv.Filter(settingsKeyProviders + provider)); err != nil {
return nil, err
} else {
if eap.enabled {
eap.redirectUrl = fmt.Sprintf(redirectUrl, provider)
}
providers[provider] = eap
}
}
return
}
-142
View File
@@ -1,142 +0,0 @@
package external
import (
"reflect"
"testing"
intset "github.com/cortezaproject/corteza-server/internal/settings"
"github.com/jmoiron/sqlx/types"
)
func Test_extractProviders(t *testing.T) {
type args struct {
redirectUrl string
kv intset.KV
}
tests := []struct {
name string
args args
wantProviders map[string]externalAuthProvider
wantErr bool
}{
{
name: "Empty KV",
args: args{},
wantProviders: map[string]externalAuthProvider{
"github": externalAuthProvider{},
"linkedin": externalAuthProvider{},
"gplus": externalAuthProvider{},
"facebook": externalAuthProvider{},
},
},
{
name: "Random config",
args: args{
redirectUrl: "http://%s",
kv: intset.KV{
"auth.external.providers.openid-connect.foo.enabled": types.JSONText("true"),
"auth.external.providers.openid-connect.foo.issuer": types.JSONText(`"url"`),
"auth.external.providers.openid-connect.foo.key": types.JSONText(`"key"`),
"auth.external.providers.openid-connect.foo.secret": types.JSONText(`"secret"`),
"auth.external.providers.openid-connect.bar.enabled": types.JSONText("true"),
"auth.external.providers.openid-connect.bar.issuer": types.JSONText(`"url"`),
"auth.external.providers.openid-connect.bar.key": types.JSONText(`"key"`),
"auth.external.providers.openid-connect.bar.secret": types.JSONText(`"secret"`),
"auth.external.providers.openid-connect.baz.enabled": types.JSONText("false"),
"auth.external.providers.github.enabled": types.JSONText(`false`),
"auth.external.providers.facebook.enabled": types.JSONText(`true`),
"auth.external.providers.facebook.secret": types.JSONText(`"fb-secret"`),
},
},
wantProviders: map[string]externalAuthProvider{
"openid-connect.foo": externalAuthProvider{
enabled: true,
key: "key",
secret: "secret",
redirectUrl: "http://openid-connect.foo",
issuerUrl: "url",
},
"openid-connect.bar": externalAuthProvider{
enabled: true,
key: "key",
secret: "secret",
redirectUrl: "http://openid-connect.bar",
issuerUrl: "url",
},
"openid-connect.baz": externalAuthProvider{
enabled: false,
},
"github": externalAuthProvider{
enabled: false,
},
"linkedin": externalAuthProvider{
enabled: false,
},
"gplus": externalAuthProvider{
enabled: false,
},
"facebook": externalAuthProvider{
enabled: true,
secret: "fb-secret",
redirectUrl: "http://facebook",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotProviders, err := extractProviders(tt.args.redirectUrl, tt.args.kv)
if (err != nil) != tt.wantErr {
t.Errorf("extractProviders() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(gotProviders, tt.wantProviders) {
t.Errorf("extractProviders() = %v, want %v", gotProviders, tt.wantProviders)
}
})
}
}
func TestExternalAuthProvider(t *testing.T) {
type args struct {
kv intset.KV
}
tests := []struct {
name string
args args
wantEap externalAuthProvider
wantErr bool
}{
{
args: args{
kv: intset.KV{
"foo.enabled": types.JSONText("true"),
"foo.issuer": types.JSONText(`"example.tld"`),
"foo.key": types.JSONText(`"key"`),
"foo.secret": types.JSONText(`"secret"`),
},
},
wantEap: externalAuthProvider{
enabled: true,
key: "key",
secret: "secret",
issuerUrl: "example.tld",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotEap, err := ExternalAuthProvider(tt.args.kv)
if (err != nil) != tt.wantErr {
t.Errorf("ExternalAuthProvider() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(gotEap, tt.wantEap) {
t.Errorf("ExternalAuthProvider() = %v, want %v", gotEap, tt.wantEap)
}
})
}
}
+17 -17
View File
@@ -29,7 +29,7 @@ type (
credentials repository.CredentialsRepository
users repository.UserRepository
roles repository.RoleRepository
settings authSettings
settings AuthSettings
notifications AuthNotificationService
providerValidator func(string) error
@@ -130,7 +130,7 @@ func (svc auth) log(fields ...zapcore.Field) *zap.Logger {
// 2.3. create credentials for that social login
//
func (svc auth) External(profile goth.User) (u *types.User, err error) {
if !svc.settings.externalEnabled {
if !svc.settings.ExternalEnabled {
return nil, errors.New("external authentication disabled")
}
@@ -250,7 +250,7 @@ func (svc auth) External(profile goth.User) (u *types.User, err error) {
// FrontendRedirectURL - a proxy to frontend redirect url setting
func (svc auth) FrontendRedirectURL() string {
return svc.settings.frontendUrlRedirect
return svc.settings.FrontendUrlRedirect
}
// InternalSignUp protocol
@@ -259,11 +259,11 @@ func (svc auth) FrontendRedirectURL() string {
//
// We're accepting the whole user object here and copy all we need to the new user
func (svc auth) InternalSignUp(input *types.User, password string) (u *types.User, err error) {
if !svc.settings.internalEnabled {
if !svc.settings.InternalEnabled {
return nil, errors.New("internal authentication disabled")
}
if !svc.settings.internalSignUpEnabled {
if !svc.settings.InternalSignUpEnabled {
return nil, errors.New("internal signup disabled")
}
@@ -324,7 +324,7 @@ func (svc auth) InternalSignUp(input *types.User, password string) (u *types.Use
Handle: input.Handle,
// Do we need confirmed email?
EmailConfirmed: !svc.settings.internalSignUpEmailConfirmationRequired,
EmailConfirmed: !svc.settings.InternalSignUpEmailConfirmationRequired,
})
if err != nil {
@@ -367,7 +367,7 @@ func (svc auth) validateInternalSignUp(email string) (err error) {
// Expects plain text password as an input
func (svc auth) InternalLogin(email string, password string) (u *types.User, err error) {
if !svc.settings.internalEnabled {
if !svc.settings.InternalEnabled {
return nil, errors.New("internal authentication disabled")
}
@@ -468,7 +468,7 @@ func (svc auth) checkPassword(password string, cc types.CredentialsSet) (err err
func (svc auth) SetPassword(userID uint64, newPassword string) (err error) {
log := svc.log(zap.Uint64("userID", userID))
if !svc.settings.internalEnabled {
if !svc.settings.InternalEnabled {
return errors.New("internal authentication disabled")
}
@@ -490,7 +490,7 @@ func (svc auth) SetPassword(userID uint64, newPassword string) (err error) {
func (svc auth) ChangePassword(userID uint64, oldPassword, newPassword string) (err error) {
log := svc.log(zap.Uint64("userID", userID))
if !svc.settings.internalEnabled {
if !svc.settings.InternalEnabled {
return errors.New("internal authentication disabled")
}
@@ -577,7 +577,7 @@ func (svc auth) ValidateAuthRequestToken(token string) (user *types.User, err er
}
func (svc auth) ValidateEmailConfirmationToken(token string) (user *types.User, err error) {
if !svc.settings.internalEnabled {
if !svc.settings.InternalEnabled {
return nil, errors.New("internal authentication disabled")
}
@@ -595,11 +595,11 @@ func (svc auth) ValidateEmailConfirmationToken(token string) (user *types.User,
}
func (svc auth) ValidatePasswordResetToken(token string) (user *types.User, err error) {
if !svc.settings.internalEnabled {
if !svc.settings.InternalEnabled {
return nil, errors.New("internal authentication disabled")
}
if !svc.settings.internalPasswordResetEnabled {
if !svc.settings.InternalPasswordResetEnabled {
return nil, errors.New("password reset disabled")
}
@@ -619,12 +619,12 @@ func (svc auth) ValidatePasswordResetToken(token string) (user *types.User, err
// ExchangePasswordResetToken exchanges reset password token for a new one and returns it with user info
func (svc auth) ExchangePasswordResetToken(token string) (user *types.User, exchangedToken string, err error) {
if !svc.settings.internalEnabled {
if !svc.settings.InternalEnabled {
err = errors.New("internal authentication disabled")
return
}
if !svc.settings.internalPasswordResetEnabled {
if !svc.settings.InternalPasswordResetEnabled {
err = errors.New("password reset disabled")
return
}
@@ -646,7 +646,7 @@ func (svc auth) ExchangePasswordResetToken(token string) (user *types.User, exch
}
func (svc auth) SendEmailAddressConfirmationToken(email string) error {
if !svc.settings.internalEnabled {
if !svc.settings.InternalEnabled {
return errors.New("internal authentication disabled")
}
@@ -683,11 +683,11 @@ func (svc auth) sendEmailAddressConfirmationToken(u *types.User) (err error) {
func (svc auth) SendPasswordResetToken(email string) error {
if !svc.settings.internalEnabled {
if !svc.settings.InternalEnabled {
return errors.New("internal authentication disabled")
}
if !svc.settings.internalPasswordResetEnabled {
if !svc.settings.InternalPasswordResetEnabled {
return errors.New("password reset disabled")
}
+7 -7
View File
@@ -18,7 +18,7 @@ type (
ctx context.Context
logger *zap.Logger
settings authSettings
settings AuthSettings
}
AuthNotificationService interface {
@@ -127,20 +127,20 @@ func (svc authNotification) log(fields ...zapcore.Field) *zap.Logger {
func (svc authNotification) EmailConfirmation(lang string, emailAddress string, token string) error {
return svc.send("email-confirmation", lang, authNotificationPayload{
EmailAddress: emailAddress,
URL: svc.settings.frontendUrlEmailConfirmation + token,
URL: svc.settings.FrontendUrlEmailConfirmation + token,
})
}
func (svc authNotification) PasswordReset(lang string, emailAddress string, token string) error {
return svc.send("password-reset", lang, authNotificationPayload{
EmailAddress: emailAddress,
URL: svc.settings.frontendUrlPasswordReset + token,
URL: svc.settings.FrontendUrlPasswordReset + token,
})
}
func (svc authNotification) newMail() *gomail.Message {
m := gomail.NewMessage()
m.SetAddressHeader("From", svc.settings.mailFromAddress, svc.settings.mailFromName)
m.SetAddressHeader("From", svc.settings.MailFromAddress, svc.settings.MailFromName)
return m
}
@@ -148,9 +148,9 @@ func (svc authNotification) send(name, lang string, payload authNotificationPayl
ntf := svc.newMail()
payload.Logo = template.URL(defaultLogo)
payload.BaseURL = svc.settings.frontendUrlBase
payload.SignatureName = svc.settings.mailFromName
payload.SignatureEmail = svc.settings.mailFromAddress
payload.BaseURL = svc.settings.FrontendUrlBase
payload.SignatureName = svc.settings.MailFromName
payload.SignatureEmail = svc.settings.MailFromAddress
ntf.SetAddressHeader("To", payload.EmailAddress, "")
ntf.SetHeader("Subject", svc.render(emailTemplates[name+"."+lang+".subject"], payload))
+234 -37
View File
@@ -1,74 +1,182 @@
package service
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"strings"
"github.com/markbates/goth"
"github.com/pkg/errors"
intset "github.com/cortezaproject/corteza-server/internal/settings"
)
type (
authSettings struct {
AuthSettings struct {
// Password reset path (<frontend password reset url> "?token=" + <token>)
frontendUrlPasswordReset string
FrontendUrlPasswordReset string
// EmailAddress confirmation path (<frontend email confirmation url> "?token=" + <token>)
frontendUrlEmailConfirmation string
FrontendUrlEmailConfirmation string
// Where to redirect user after external auth flow
frontendUrlRedirect string
FrontendUrlRedirect string
// Webapp Base URL
frontendUrlBase string
FrontendUrlBase string
mailFromAddress string
mailFromName string
// Is external authentication
externalEnabled bool
MailFromAddress string
MailFromName string
// Is internal authentication (username + password) enabled
internalEnabled bool
InternalEnabled bool
// Can users register
internalSignUpEnabled bool
InternalSignUpEnabled bool
// Users should confirm their emails when signing-up
internalSignUpEmailConfirmationRequired bool
InternalSignUpEmailConfirmationRequired bool
// Can users reset their passwords
internalPasswordResetEnabled bool
InternalPasswordResetEnabled bool
// Is external authentication
ExternalEnabled bool
// Where to redirect (url used for registration)
ExternalRedirectUrl string
// session secret to use
ExternalSessionStoreSecret string
// session store should be secure
ExternalSessionStoreSecure bool
// all external providers we know
ExternalProviders map[string]AuthSettingsExternalAuthProvider
}
authSettingsStore interface {
Bool(string) bool
String(string) string
AuthSettingsExternalAuthProvider struct {
Enabled bool
Key string
Secret string
RedirectUrl string
IssuerUrl string
}
)
// AuthSettings maps from plain values to authSettings struct
// ParseAuthSettings maps from plain values to AuthSettings struct
//
// see settings.Initialize() func
func AuthSettings(kv authSettingsStore) authSettings {
return authSettings{
frontendUrlPasswordReset: kv.String("auth.frontend.url.password-reset"),
frontendUrlEmailConfirmation: kv.String("auth.frontend.url.email-confirmation"),
frontendUrlRedirect: kv.String("auth.frontend.url.redirect"),
frontendUrlBase: kv.String("auth.frontend.url.base"),
func ParseAuthSettings(kv intset.KV) (as AuthSettings, err error) {
as = AuthSettings{
FrontendUrlPasswordReset: kv.String("auth.frontend.url.password-reset"),
FrontendUrlEmailConfirmation: kv.String("auth.frontend.url.email-confirmation"),
FrontendUrlRedirect: kv.String("auth.frontend.url.redirect"),
FrontendUrlBase: kv.String("auth.frontend.url.base"),
mailFromAddress: kv.String("auth.mail.from-address"),
mailFromName: kv.String("auth.mail.from-name"),
MailFromAddress: kv.String("auth.mail.from-address"),
MailFromName: kv.String("auth.mail.from-name"),
externalEnabled: kv.Bool("auth.external.enabled"),
internalEnabled: kv.Bool("auth.internal.enabled"),
InternalEnabled: kv.Bool("auth.internal.enabled"),
internalSignUpEnabled: kv.Bool("auth.internal.signup.enabled"),
internalSignUpEmailConfirmationRequired: kv.Bool("auth.internal.signup-email-confirmation-required"),
InternalSignUpEnabled: kv.Bool("auth.internal.signup.enabled"),
InternalSignUpEmailConfirmationRequired: kv.Bool("auth.internal.signup-email-confirmation-required"),
internalPasswordResetEnabled: kv.Bool("auth.internal.password-reset.enabled"),
InternalPasswordResetEnabled: kv.Bool("auth.internal.password-reset.enabled"),
ExternalEnabled: kv.Bool("auth.external.enabled"),
ExternalRedirectUrl: kv.String("auth.external.redirect-url"),
ExternalSessionStoreSecret: kv.String("auth.external.session-store-secret"),
ExternalSessionStoreSecure: kv.Bool("auth.external.session-store-secure"),
}
as.ExternalProviders, err = as.parseExternalProviders(kv)
return
}
func (s authSettings) Format() map[string]interface{} {
func (as *AuthSettings) parseExternalProviders(kv intset.KV) (map[string]AuthSettingsExternalAuthProvider, error) {
// Standard providers:
var (
ep = map[string]AuthSettingsExternalAuthProvider{
"github": {},
"facebook": {},
"gplus": {},
"linkedin": {},
}
// Add all oidc providers we find
extKeyBase = "auth.external.providers."
oidcKeyBase = extKeyBase + "openid-connect."
)
for k := range kv.Filter(oidcKeyBase) {
if len(k) < len(oidcKeyBase)+2 {
// skip invalid keys
continue
}
// find next dot:
name := k[len(oidcKeyBase):]
dotPos := strings.Index(name, ".")
if dotPos > 0 {
name = name[:dotPos]
}
ep["openid-connect."+name] = AuthSettingsExternalAuthProvider{}
}
for provider := range ep {
if p, err := as.parseExternalProvider(kv.Filter(extKeyBase + provider)); err != nil {
return nil, err
} else {
if as.ExternalRedirectUrl != "" && p.Enabled {
p.RedirectUrl = fmt.Sprintf(as.ExternalRedirectUrl, provider)
}
ep[provider] = *p
}
}
return ep, nil
}
// Parses external provider out of KV set
//
// Function only looks at the end of key string (after last dot)
// so passing multiple providers will result in overriding values
func (as *AuthSettings) parseExternalProvider(kv intset.KV) (p *AuthSettingsExternalAuthProvider, err error) {
p = &AuthSettingsExternalAuthProvider{}
for k, v := range kv {
ld := strings.LastIndex(k, ".")
switch k[ld+1:] {
case "enabled":
err = v.Unmarshal(&p.Enabled)
case "key":
err = v.Unmarshal(&p.Key)
case "secret":
err = v.Unmarshal(&p.Secret)
case "issuer":
err = v.Unmarshal(&p.IssuerUrl)
}
if err != nil {
return
}
}
return
}
func (as AuthSettings) Format() map[string]interface{} {
type (
externalProvider struct {
Label string `json:"label"`
@@ -109,12 +217,101 @@ func (s authSettings) Format() map[string]interface{} {
}
return map[string]interface{}{
"internalEnabled": s.internalEnabled,
"internalPasswordResetEnabled": s.internalPasswordResetEnabled,
"internalSignUpEmailConfirmationRequired": s.internalSignUpEmailConfirmationRequired,
"internalSignUpEnabled": s.internalSignUpEnabled,
"internalEnabled": as.InternalEnabled,
"internalPasswordResetEnabled": as.InternalPasswordResetEnabled,
"internalSignUpEmailConfirmationRequired": as.InternalSignUpEmailConfirmationRequired,
"internalSignUpEnabled": as.InternalSignUpEnabled,
"externalEnabled": s.externalEnabled,
"externalEnabled": as.ExternalEnabled,
"externalProviders": providers,
}
}
// StaticValidateExternal
//
// Simple checks of external auth settings
func (as AuthSettings) StaticValidateExternal() error {
if as.ExternalRedirectUrl == "" {
return errors.New("redirect URL is empty")
}
const (
tpt = "test-provider-test"
)
p, err := url.Parse(fmt.Sprintf(as.ExternalRedirectUrl, tpt))
if err != nil {
return errors.Wrap(err, "invalid redirect URL")
}
if !strings.Contains(p.Path, tpt+"/callback") {
return errors.Wrap(err, "could find injected provider in the URL, make sure you use '%s' as a placeholder")
}
if as.ExternalSessionStoreSecret == "" {
return errors.New("session store secret is empty")
}
if as.ExternalSessionStoreSecure && p.Scheme != "https" {
return errors.New("session store is secure, redirect URL should have HTTPS")
}
return nil
}
// ValidateExternalRedirectURL
//
// Validates external redirect URL
func (as AuthSettings) ValidateExternalRedirectURL() error {
const tpt = "test-provider-test"
const cb = "/callback"
// Replace placeholders & remove /callback
var url = fmt.Sprintf(as.ExternalRedirectUrl, tpt)
url = url[0 : len(url)-len(cb)]
rsp, err := http.DefaultClient.Get(url)
if err != nil {
return errors.Wrap(err, "could not get response from redirect URL")
}
defer rsp.Body.Close()
body, err := ioutil.ReadAll(rsp.Body)
if strings.Contains(string(body), tpt) {
return nil
}
return errors.New("could not validate external auth redirection URL")
}
func (p AuthSettingsExternalAuthProvider) MakeValueSet(name string) (vv intset.ValueSet, err error) {
set := func(name string, value interface{}) error {
v := &intset.Value{Name: name}
if v.Value, err = json.Marshal(value); err != nil {
return err
}
vv = append(vv, v)
return nil
}
prefix := "auth.external.providers." + name
if err = set(prefix+".enabled", p.Enabled); err != nil {
return nil, err
}
if err = set(prefix+".key", p.Key); err != nil {
return nil, err
}
if err = set(prefix+".secret", p.Secret); err != nil {
return nil, err
}
if err = set(prefix+".issuer", p.IssuerUrl); err != nil {
return nil, err
}
return vv, err
}
@@ -0,0 +1,95 @@
package service
import (
"reflect"
"testing"
"github.com/jmoiron/sqlx/types"
intset "github.com/cortezaproject/corteza-server/internal/settings"
)
func Test_extractProviders(t *testing.T) {
type args struct {
redirectUrl string
kv intset.KV
}
tests := []struct {
name string
args args
wantProviders map[string]AuthSettingsExternalAuthProvider
wantErr bool
}{
{
name: "Empty KV",
args: args{},
wantProviders: map[string]AuthSettingsExternalAuthProvider{
"github": AuthSettingsExternalAuthProvider{},
"linkedin": AuthSettingsExternalAuthProvider{},
"gplus": AuthSettingsExternalAuthProvider{},
"facebook": AuthSettingsExternalAuthProvider{},
},
},
{
name: "Random config",
args: args{
kv: intset.KV{
"auth.external.redirect-url": types.JSONText(`"http://%s"`),
"auth.external.providers.openid-connect.foo.enabled": types.JSONText("true"),
"auth.external.providers.openid-connect.foo.issuer": types.JSONText(`"url"`),
"auth.external.providers.openid-connect.foo.key": types.JSONText(`"key"`),
"auth.external.providers.openid-connect.foo.secret": types.JSONText(`"secret"`),
"auth.external.providers.openid-connect.bar.enabled": types.JSONText("true"),
"auth.external.providers.openid-connect.bar.issuer": types.JSONText(`"url"`),
"auth.external.providers.openid-connect.bar.key": types.JSONText(`"key"`),
"auth.external.providers.openid-connect.bar.secret": types.JSONText(`"secret"`),
"auth.external.providers.openid-connect.baz.enabled": types.JSONText("false"),
"auth.external.providers.github.enabled": types.JSONText(`false`),
"auth.external.providers.facebook.enabled": types.JSONText(`true`),
"auth.external.providers.facebook.secret": types.JSONText(`"fb-secret"`),
},
},
wantProviders: map[string]AuthSettingsExternalAuthProvider{
"openid-connect.foo": AuthSettingsExternalAuthProvider{
Enabled: true,
Key: "key",
Secret: "secret",
RedirectUrl: "http://openid-connect.foo",
IssuerUrl: "url",
},
"openid-connect.bar": AuthSettingsExternalAuthProvider{
Enabled: true,
Key: "key",
Secret: "secret",
RedirectUrl: "http://openid-connect.bar",
IssuerUrl: "url",
},
"openid-connect.baz": AuthSettingsExternalAuthProvider{},
"github": AuthSettingsExternalAuthProvider{},
"linkedin": AuthSettingsExternalAuthProvider{},
"gplus": AuthSettingsExternalAuthProvider{},
"facebook": AuthSettingsExternalAuthProvider{
Enabled: true,
Secret: "fb-secret",
RedirectUrl: "http://facebook",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
as, err := ParseAuthSettings(tt.args.kv)
if (err != nil) != tt.wantErr {
t.Errorf("extractProviders() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(as.ExternalProviders, tt.wantProviders) {
t.Errorf("extractProviders()\ngot: %v\nwant: %v\n", as.ExternalProviders, tt.wantProviders)
}
})
}
}
+3 -3
View File
@@ -65,7 +65,7 @@ func TestAuth_External_Existing(t *testing.T) {
svc := makeMockAuthService(usrRpoMock, crdRpoMock)
svc.logger = zap.NewNop()
svc.settings.externalEnabled = true
svc.settings.ExternalEnabled = true
{
auser, err := svc.External(p)
@@ -111,7 +111,7 @@ func TestAuth_External_NonExisting(t *testing.T) {
svc := makeMockAuthService(usrRpoMock, crdRpoMock)
svc.logger = zap.NewNop()
svc.settings.externalEnabled = true
svc.settings.ExternalEnabled = true
{
auser, err := svc.External(p)
@@ -137,7 +137,7 @@ func Test_auth_validateInternalLogin(t *testing.T) {
}
svc := auth{}
svc.logger = zap.NewNop()
svc.settings.internalEnabled = true
svc.settings.InternalEnabled = true
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
+1 -1
View File
@@ -30,7 +30,7 @@ var (
DefaultSettings SettingsService
DefaultAuthNotification AuthNotificationService
DefaultAuthSettings authSettings
DefaultAuthSettings AuthSettings
DefaultAuth AuthService
DefaultUser UserService
+4 -4
View File
@@ -35,7 +35,7 @@ type (
BulkSet(vv internalSettings.ValueSet) (err error)
Get(name string, ownedBy uint64) (out *internalSettings.Value, err error)
LoadAuthSettings() (authSettings, error)
LoadAuthSettings() (AuthSettings, error)
AutoDiscovery() error
}
)
@@ -99,12 +99,12 @@ func (svc settings) Get(name string, ownedBy uint64) (out *internalSettings.Valu
}
// Loads auth.% settings, initializes & fills auth settings struct
func (svc settings) LoadAuthSettings() (authSettings, error) {
func (svc settings) LoadAuthSettings() (AuthSettings, error) {
vv, err := svc.internalSettings.FindByPrefix("auth.")
if err != nil {
return authSettings{}, err
return AuthSettings{}, err
}
return AuthSettings(vv.KV()), nil
return ParseAuthSettings(vv.KV())
}
// AutoDiscovery orchestrates settings auto discovery
+2 -2
View File
@@ -26,7 +26,7 @@ type (
ctx context.Context
logger *zap.Logger
settings authSettings
settings AuthSettings
auth userAuth
@@ -245,7 +245,7 @@ func (svc user) Unsuspend(ID uint64) (err error) {
func (svc user) SetPassword(userID uint64, newPassword string) (err error) {
log := svc.log(zap.Uint64("userID", userID))
if !svc.settings.internalEnabled {
if !svc.settings.InternalEnabled {
return errors.New("internal authentication disabled")
}
+2 -1
View File
@@ -64,7 +64,8 @@ func Configure() *cli.Config {
service.DefaultAuthSettings, _ = service.DefaultSettings.LoadAuthSettings()
}
external.Init(service.DefaultIntSettings)
// Initialize external authentication (from default settings)
external.Init()
go service.Watchers(ctx)
return nil
},