Added SAML service
This commit is contained in:
parent
a1ca8afe49
commit
bb1043181c
@ -4,9 +4,11 @@ import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
authHandlers "github.com/cortezaproject/corteza-server/auth/handlers"
|
||||
"strings"
|
||||
|
||||
authHandlers "github.com/cortezaproject/corteza-server/auth/handlers"
|
||||
"github.com/cortezaproject/corteza-server/auth/saml"
|
||||
|
||||
authService "github.com/cortezaproject/corteza-server/auth"
|
||||
authSettings "github.com/cortezaproject/corteza-server/auth/settings"
|
||||
autService "github.com/cortezaproject/corteza-server/automation/service"
|
||||
@ -482,5 +484,8 @@ func updateAuthSettings(svc authServicer, current *types.AppSettings) {
|
||||
as.MultiFactor.EmailOTP.Enabled = cas.MultiFactor.EmailOTP.Enabled
|
||||
as.MultiFactor.EmailOTP.Enforced = cas.MultiFactor.EmailOTP.Enforced
|
||||
|
||||
// SAML
|
||||
saml.UpdateSettings(current, as)
|
||||
|
||||
svc.UpdateSettings(as)
|
||||
}
|
||||
|
||||
99
auth/auth.go
99
auth/auth.go
@ -4,11 +4,20 @@ import (
|
||||
"context"
|
||||
"embed"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Masterminds/sprig"
|
||||
"github.com/cortezaproject/corteza-server/auth/external"
|
||||
"github.com/cortezaproject/corteza-server/auth/handlers"
|
||||
"github.com/cortezaproject/corteza-server/auth/oauth2"
|
||||
"github.com/cortezaproject/corteza-server/auth/request"
|
||||
"github.com/cortezaproject/corteza-server/auth/saml"
|
||||
"github.com/cortezaproject/corteza-server/auth/settings"
|
||||
"github.com/cortezaproject/corteza-server/pkg/actionlog"
|
||||
"github.com/cortezaproject/corteza-server/pkg/auth"
|
||||
@ -20,12 +29,6 @@ import (
|
||||
"github.com/go-chi/chi"
|
||||
oauth2def "github.com/go-oauth2/oauth2/v4"
|
||||
"go.uber.org/zap"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type (
|
||||
@ -227,6 +230,60 @@ func New(ctx context.Context, log *zap.Logger, s store.Storer, opt options.AuthO
|
||||
return
|
||||
}
|
||||
|
||||
// LoadSamlService takes care of certificate preloading, fetching of the
|
||||
// IDP metadata once the auth settings are loaded and registers the
|
||||
// SAML middleware
|
||||
func (svc *service) LoadSamlService(ctx context.Context, s *settings.Settings) (srvc *saml.SamlSPService, err error) {
|
||||
links := handlers.GetLinks()
|
||||
|
||||
certManager := saml.NewCertManager(&saml.CertStoreLoader{Storer: svc.store})
|
||||
|
||||
cert, err := certManager.Parse([]byte(s.Saml.Cert), []byte(s.Saml.Key))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
idpUrl, err := url.Parse(s.Saml.IDP.URL)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// idp metadata needs to be loaded before
|
||||
// the internal samlsp package
|
||||
md, err := saml.FetchIDPMetadata(ctx, *idpUrl)
|
||||
ru, err := url.Parse(svc.opt.BaseURL)
|
||||
|
||||
rootURL := &url.URL{
|
||||
Scheme: ru.Scheme,
|
||||
User: ru.User,
|
||||
Host: ru.Host,
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
srvc, err = saml.NewSamlSPService(saml.SamlSPArgs{
|
||||
AcsURL: links.SamlCallback,
|
||||
MetaURL: links.SamlMetadata,
|
||||
SloURL: links.SamlLogout,
|
||||
|
||||
IdpURL: *idpUrl,
|
||||
Host: *rootURL,
|
||||
|
||||
Cert: cert,
|
||||
IdpMeta: md,
|
||||
|
||||
IdentityPayload: saml.IdpIdentityPayload{
|
||||
Name: s.Saml.IDP.IdentName,
|
||||
Handle: s.Saml.IDP.IdentHandle,
|
||||
Identifier: s.Saml.IDP.IdentIdentifier,
|
||||
},
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (svc *service) UpdateSettings(s *settings.Settings) {
|
||||
if svc.settings.LocalEnabled != s.LocalEnabled {
|
||||
svc.log.Debug("setting changed", zap.Bool("localEnabled", s.LocalEnabled))
|
||||
@ -252,6 +309,36 @@ func (svc *service) UpdateSettings(s *settings.Settings) {
|
||||
svc.log.Debug("setting changed", zap.Any("mfa", s.MultiFactor))
|
||||
}
|
||||
|
||||
if svc.settings.Saml != s.Saml {
|
||||
var (
|
||||
log = svc.log.Named("saml")
|
||||
ss *saml.SamlSPService
|
||||
err error
|
||||
)
|
||||
|
||||
switch true {
|
||||
case s.Saml.Cert == "", s.Saml.Key == "":
|
||||
log.Warn("certificate private/public keys empty (see 'auth.external.saml' settings)")
|
||||
break
|
||||
|
||||
case s.Saml.IDP.URL == "":
|
||||
log.Warn("could not get IDP url (see 'auth.external.saml.idp')")
|
||||
break
|
||||
|
||||
default:
|
||||
ss, err = svc.LoadSamlService(context.Background(), s)
|
||||
|
||||
if err != nil {
|
||||
log.Warn("could not reload service", zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
if ss != nil {
|
||||
log.Debug("settings changed, reloading")
|
||||
svc.handlers.SamlSPService = *ss
|
||||
}
|
||||
}
|
||||
|
||||
if len(svc.settings.Providers) != len(s.Providers) {
|
||||
svc.log.Debug("setting changed", zap.Int("providers", len(s.Providers)))
|
||||
external.SetupGothProviders(svc.opt.ExternalRedirectURL, s.Providers...)
|
||||
|
||||
76
auth/external/auth_handler.go
vendored
Normal file
76
auth/external/auth_handler.go
vendored
Normal file
@ -0,0 +1,76 @@
|
||||
package external
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/cortezaproject/corteza-server/auth/saml"
|
||||
"github.com/cortezaproject/corteza-server/system/types"
|
||||
"github.com/crewjam/saml/samlsp"
|
||||
"github.com/markbates/goth/gothic"
|
||||
)
|
||||
|
||||
type (
|
||||
ExternalAuthHandler interface {
|
||||
BeginUserAuth(http.ResponseWriter, *http.Request)
|
||||
CompleteUserAuth(http.ResponseWriter, *http.Request) (u *types.ExternalAuthUser, err error)
|
||||
}
|
||||
|
||||
externalSamlAuthHandler struct {
|
||||
service saml.SamlSPService
|
||||
}
|
||||
|
||||
externalDefaultAuthHandler struct{}
|
||||
)
|
||||
|
||||
func (eh *externalDefaultAuthHandler) BeginUserAuth(w http.ResponseWriter, r *http.Request) {
|
||||
gothic.BeginAuthHandler(w, r)
|
||||
}
|
||||
|
||||
func (eh *externalDefaultAuthHandler) CompleteUserAuth(w http.ResponseWriter, r *http.Request) (u *types.ExternalAuthUser, err error) {
|
||||
gu, err := gothic.CompleteUserAuth(w, r)
|
||||
u = &types.ExternalAuthUser{gu}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (eh *externalSamlAuthHandler) BeginUserAuth(w http.ResponseWriter, r *http.Request) {
|
||||
_, err := eh.service.Handler().Session.GetSession(r)
|
||||
|
||||
if err == samlsp.ErrNoSession {
|
||||
eh.service.Handler().HandleStartAuthFlow(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func (eh *externalSamlAuthHandler) CompleteUserAuth(w http.ResponseWriter, r *http.Request) (u *types.ExternalAuthUser, err error) {
|
||||
var (
|
||||
session samlsp.Session
|
||||
)
|
||||
|
||||
if session, err = eh.service.Handler().Session.GetSession(r); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if session != nil {
|
||||
s := (session.(samlsp.JWTSessionClaims)).GetAttributes()
|
||||
|
||||
u = &types.ExternalAuthUser{}
|
||||
u.Provider = "saml"
|
||||
|
||||
// get identifier for use with Corteza (email)
|
||||
u.Email = eh.service.GuessIdentifier(s)
|
||||
u.Name = s.Get(eh.service.IDPUserMeta.Name)
|
||||
u.NickName = s.Get(eh.service.IDPUserMeta.Handle)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func NewSamlExternalHandler(s saml.SamlSPService) *externalSamlAuthHandler {
|
||||
return &externalSamlAuthHandler{
|
||||
service: s,
|
||||
}
|
||||
}
|
||||
|
||||
func NewDefaultExternalHandler() *externalDefaultAuthHandler {
|
||||
return &externalDefaultAuthHandler{}
|
||||
}
|
||||
@ -3,15 +3,15 @@ package handlers
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/cortezaproject/corteza-server/auth/external"
|
||||
"github.com/cortezaproject/corteza-server/auth/request"
|
||||
"github.com/cortezaproject/corteza-server/pkg/api"
|
||||
"github.com/cortezaproject/corteza-server/system/types"
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/markbates/goth"
|
||||
"github.com/markbates/goth/gothic"
|
||||
"go.uber.org/zap"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func copyProviderToContext(r *http.Request) *http.Request {
|
||||
@ -22,25 +22,25 @@ func (h AuthHandlers) externalInit(w http.ResponseWriter, r *http.Request) {
|
||||
r = copyProviderToContext(r)
|
||||
h.Log.Info("starting external authentication flow")
|
||||
|
||||
gothic.BeginAuthHandler(w, r)
|
||||
beginUserAuth(w, r, external.NewDefaultExternalHandler())
|
||||
}
|
||||
|
||||
func (h AuthHandlers) externalCallback(w http.ResponseWriter, r *http.Request) {
|
||||
r = copyProviderToContext(r)
|
||||
h.Log.Info("external authentication callback")
|
||||
|
||||
if user, err := gothic.CompleteUserAuth(w, r); err != nil {
|
||||
if user, err := completeUserAuth(w, r, external.NewDefaultExternalHandler()); err != nil {
|
||||
h.Log.Error("failed to complete user auth", zap.Error(err))
|
||||
h.handleFailedExternalAuth(w, r, err)
|
||||
} else {
|
||||
h.handleSuccessfulExternalAuth(w, r, user)
|
||||
h.handleSuccessfulExternalAuth(w, r, *user)
|
||||
}
|
||||
}
|
||||
|
||||
// Handles authentication via external auth providers of
|
||||
// unknown an user + appending authentication on external providers
|
||||
// to a current user
|
||||
func (h AuthHandlers) handleSuccessfulExternalAuth(w http.ResponseWriter, r *http.Request, cred goth.User) {
|
||||
func (h AuthHandlers) handleSuccessfulExternalAuth(w http.ResponseWriter, r *http.Request, cred types.ExternalAuthUser) {
|
||||
var (
|
||||
user *types.User
|
||||
err error
|
||||
@ -91,3 +91,11 @@ func (h AuthHandlers) handleFailedExternalAuth(w http.ResponseWriter, r *http.Re
|
||||
fmt.Fprintf(w, "SSO Error: %v", err.Error())
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func beginUserAuth(w http.ResponseWriter, r *http.Request, eh external.ExternalAuthHandler) {
|
||||
eh.BeginUserAuth(w, r)
|
||||
}
|
||||
|
||||
func completeUserAuth(w http.ResponseWriter, r *http.Request, eh external.ExternalAuthHandler) (u *types.ExternalAuthUser, err error) {
|
||||
return eh.CompleteUserAuth(w, r)
|
||||
}
|
||||
|
||||
23
auth/handlers/handle_saml.go
Normal file
23
auth/handlers/handle_saml.go
Normal file
@ -0,0 +1,23 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/cortezaproject/corteza-server/auth/external"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func (h AuthHandlers) samlInit(w http.ResponseWriter, r *http.Request) {
|
||||
r = copyProviderToContext(r)
|
||||
h.Log.Info("starting saml authentication flow")
|
||||
|
||||
ex := external.NewSamlExternalHandler(h.SamlSPService)
|
||||
beginUserAuth(w, r, ex)
|
||||
|
||||
if user, err := completeUserAuth(w, r, ex); err != nil {
|
||||
h.Log.Error("failed to complete user auth", zap.Error(err))
|
||||
h.handleFailedExternalAuth(w, r, err)
|
||||
} else {
|
||||
h.handleSuccessfulExternalAuth(w, r, *user)
|
||||
}
|
||||
}
|
||||
@ -11,6 +11,7 @@ import (
|
||||
|
||||
"github.com/cortezaproject/corteza-server/auth/external"
|
||||
"github.com/cortezaproject/corteza-server/auth/request"
|
||||
"github.com/cortezaproject/corteza-server/auth/saml"
|
||||
"github.com/cortezaproject/corteza-server/auth/settings"
|
||||
"github.com/cortezaproject/corteza-server/pkg/auth"
|
||||
"github.com/cortezaproject/corteza-server/pkg/options"
|
||||
@ -19,13 +20,12 @@ import (
|
||||
"github.com/go-oauth2/oauth2/v4/server"
|
||||
"github.com/gorilla/csrf"
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/markbates/goth"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type (
|
||||
authService interface {
|
||||
External(ctx context.Context, profile goth.User) (u *types.User, err error)
|
||||
External(ctx context.Context, profile types.ExternalAuthUser) (u *types.User, err error)
|
||||
InternalSignUp(ctx context.Context, input *types.User, password string) (u *types.User, err error)
|
||||
InternalLogin(ctx context.Context, email string, password string) (u *types.User, err error)
|
||||
SetPassword(ctx context.Context, userID uint64, password string) (err error)
|
||||
@ -97,6 +97,7 @@ type (
|
||||
DefaultClient *types.AuthClient
|
||||
Opt options.AuthOpt
|
||||
Settings *settings.Settings
|
||||
SamlSPService saml.SamlSPService
|
||||
}
|
||||
|
||||
handlerFn func(req *request.AuthReq) error
|
||||
@ -276,6 +277,11 @@ func (h *AuthHandlers) enrichTmplData(req *request.AuthReq) interface{} {
|
||||
sort.Sort(providers)
|
||||
|
||||
var pp = make([]provider, 0, len(providers))
|
||||
|
||||
if h.Settings.Saml.Enabled {
|
||||
pp = append(pp, provider(saml.TemplateProvider(h.Settings.Saml.IDP.URL, h.Settings.Saml.IDP.Name)))
|
||||
}
|
||||
|
||||
for i := range providers {
|
||||
if !providers[i].Enabled {
|
||||
continue
|
||||
|
||||
@ -34,6 +34,11 @@ type (
|
||||
|
||||
External,
|
||||
|
||||
SamlInit,
|
||||
SamlCallback,
|
||||
SamlMetadata,
|
||||
SamlLogout,
|
||||
|
||||
Assets string
|
||||
}
|
||||
)
|
||||
@ -71,6 +76,11 @@ func GetLinks() Links {
|
||||
|
||||
External: b + "auth/external",
|
||||
|
||||
SamlInit: b + "auth/external/saml/init",
|
||||
SamlCallback: b + "auth/external/saml/callback",
|
||||
SamlMetadata: b + "auth/external/saml/metadata",
|
||||
SamlLogout: b + "auth/external/saml/slo",
|
||||
|
||||
Assets: b + "auth/assets/public",
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,7 +15,6 @@ import (
|
||||
"github.com/go-oauth2/oauth2/v4"
|
||||
"github.com/go-oauth2/oauth2/v4/server"
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/markbates/goth"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
@ -81,7 +80,7 @@ type (
|
||||
}
|
||||
|
||||
authServiceMocked struct {
|
||||
external func(context.Context, goth.User) (u *types.User, err error)
|
||||
external func(context.Context, types.ExternalAuthUser) (u *types.User, err error)
|
||||
internalSignUp func(context.Context, *types.User, string) (u *types.User, err error)
|
||||
internalLogin func(context.Context, string, string) (u *types.User, err error)
|
||||
setPassword func(context.Context, uint64, string) (err error)
|
||||
@ -110,7 +109,7 @@ func (u userServiceMocked) Update(ctx context.Context, user *types.User) (*types
|
||||
//
|
||||
// Mocking authService
|
||||
//
|
||||
func (s authServiceMocked) External(ctx context.Context, profile goth.User) (u *types.User, err error) {
|
||||
func (s authServiceMocked) External(ctx context.Context, profile types.ExternalAuthUser) (u *types.User, err error) {
|
||||
return s.external(ctx, profile)
|
||||
}
|
||||
|
||||
|
||||
@ -1,12 +1,13 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/cortezaproject/corteza-server/auth/request"
|
||||
"github.com/cortezaproject/corteza-server/pkg/actionlog"
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/go-chi/httprate"
|
||||
"github.com/gorilla/csrf"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func (h *AuthHandlers) MountHttpRoutes(r chi.Router) {
|
||||
@ -94,6 +95,12 @@ func (h *AuthHandlers) MountHttpRoutes(r chi.Router) {
|
||||
r.Post(tbp(l.OAuth2DefaultClient), h.handle(h.oauth2authorizeDefaultClientProc))
|
||||
})
|
||||
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Handle(tbp(l.SamlMetadata), h.SamlSPService)
|
||||
r.Handle(tbp(l.SamlCallback), h.SamlSPService)
|
||||
r.HandleFunc(tbp(l.SamlInit), h.samlInit)
|
||||
})
|
||||
|
||||
r.Route(tbp(l.External)+"/{provider}", func(r chi.Router) {
|
||||
// External provider
|
||||
r.Get("/", h.externalInit)
|
||||
|
||||
@ -57,6 +57,7 @@ func NewAuthUser(s *settings.Settings, u *types.User, perm bool, permLifetime ti
|
||||
User: u,
|
||||
PermSession: perm,
|
||||
PermLifetime: permLifetime,
|
||||
MFAStatus: make(map[authType]authStatus),
|
||||
}
|
||||
|
||||
au.set(s, u)
|
||||
|
||||
122
auth/saml/cert.go
Normal file
122
auth/saml/cert.go
Normal file
@ -0,0 +1,122 @@
|
||||
package saml
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/cortezaproject/corteza-server/system/types"
|
||||
)
|
||||
|
||||
const settingsPrefix = "auth.external.providers.saml"
|
||||
|
||||
type (
|
||||
Cert struct {
|
||||
Cert []byte
|
||||
Key []byte
|
||||
}
|
||||
|
||||
certManager struct {
|
||||
loader CertLoader
|
||||
}
|
||||
|
||||
CertLoader interface {
|
||||
Load(ctx context.Context) (*Cert, error)
|
||||
Key(ctx context.Context) ([]byte, error)
|
||||
Cert(ctx context.Context) ([]byte, error)
|
||||
}
|
||||
|
||||
CertStoreLoader struct {
|
||||
Storer storer
|
||||
}
|
||||
|
||||
CertFsLoader struct {
|
||||
KeyFile string
|
||||
CertFile string
|
||||
}
|
||||
|
||||
storer interface {
|
||||
SearchSettings(ctx context.Context, f types.SettingsFilter) (types.SettingValueSet, types.SettingsFilter, error)
|
||||
}
|
||||
)
|
||||
|
||||
func (cm *certManager) Load(ctx context.Context) (cc *Cert, err error) {
|
||||
return cm.loader.Load(ctx)
|
||||
}
|
||||
|
||||
func (cm *certManager) Parse(cert []byte, key []byte) (tls.Certificate, error) {
|
||||
return tls.X509KeyPair(cert, key)
|
||||
}
|
||||
|
||||
func (l *CertStoreLoader) Cert(ctx context.Context) (c []byte, err error) {
|
||||
return l.get(ctx, "cert")
|
||||
}
|
||||
|
||||
func (l *CertStoreLoader) Key(ctx context.Context) ([]byte, error) {
|
||||
return l.get(ctx, "key")
|
||||
}
|
||||
|
||||
func (l *CertStoreLoader) get(ctx context.Context, key string) (c []byte, err error) {
|
||||
var s types.SettingValueSet
|
||||
|
||||
if s, _, err = l.Storer.SearchSettings(ctx, types.SettingsFilter{Prefix: settingsPrefix}); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if cc := s.First(fmt.Sprintf("%s.%s", settingsPrefix, key)); cc != nil {
|
||||
return []byte(cc.String()), nil
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (l *CertStoreLoader) Load(ctx context.Context) (cc *Cert, err error) {
|
||||
var (
|
||||
c, k []byte
|
||||
)
|
||||
|
||||
if c, err = l.Cert(ctx); err != nil && c != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if k, err = l.Key(ctx); err != nil && k != nil {
|
||||
return
|
||||
}
|
||||
|
||||
cc = &Cert{Cert: c, Key: k}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (l *CertFsLoader) Cert(ctx context.Context) (c []byte, err error) {
|
||||
return ioutil.ReadFile(l.CertFile)
|
||||
}
|
||||
|
||||
func (l *CertFsLoader) Key(ctx context.Context) ([]byte, error) {
|
||||
return ioutil.ReadFile(l.KeyFile)
|
||||
}
|
||||
|
||||
func (l *CertFsLoader) Load(ctx context.Context) (cc *Cert, err error) {
|
||||
var (
|
||||
c, k []byte
|
||||
)
|
||||
|
||||
if c, err = l.Cert(ctx); err != nil && c != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if k, err = l.Key(ctx); err != nil && k != nil {
|
||||
return
|
||||
}
|
||||
|
||||
cc = &Cert{Cert: c, Key: k}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func NewCertManager(l CertLoader) *certManager {
|
||||
return &certManager{
|
||||
loader: l,
|
||||
}
|
||||
}
|
||||
90
auth/saml/cert_test.go
Normal file
90
auth/saml/cert_test.go
Normal file
@ -0,0 +1,90 @@
|
||||
package saml
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/cortezaproject/corteza-server/system/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type (
|
||||
ssFn func(ctx context.Context, f types.SettingsFilter) (types.SettingValueSet, types.SettingsFilter, error)
|
||||
certMockLoader struct {
|
||||
}
|
||||
|
||||
mockStorer struct {
|
||||
ss func(ctx context.Context, f types.SettingsFilter) (types.SettingValueSet, types.SettingsFilter, error)
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
c = &types.SettingValue{Name: fmt.Sprintf("%s.cert", settingsPrefix)}
|
||||
k = &types.SettingValue{Name: fmt.Sprintf("%s.key", settingsPrefix)}
|
||||
)
|
||||
|
||||
func Test_loadCertFromSettings(t *testing.T) {
|
||||
|
||||
tcc := []struct {
|
||||
name string
|
||||
err error
|
||||
expect *Cert
|
||||
ss ssFn
|
||||
}{
|
||||
{
|
||||
name: "cert, key load success",
|
||||
expect: &Cert{Cert: []byte("CERT"), Key: []byte("KEY")},
|
||||
ss: func(ctx context.Context, f types.SettingsFilter) (types.SettingValueSet, types.SettingsFilter, error) {
|
||||
c.SetValue("CERT")
|
||||
k.SetValue("KEY")
|
||||
|
||||
return types.SettingValueSet{c, k}, f, nil
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no permissions to fetch settings",
|
||||
err: errors.New("no permissions"),
|
||||
expect: &Cert{},
|
||||
ss: func(ctx context.Context, f types.SettingsFilter) (types.SettingValueSet, types.SettingsFilter, error) {
|
||||
return nil, types.SettingsFilter{}, errors.New("no permissions")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no cert, key in store",
|
||||
expect: &Cert{},
|
||||
ss: func(ctx context.Context, f types.SettingsFilter) (types.SettingValueSet, types.SettingsFilter, error) {
|
||||
return nil, types.SettingsFilter{}, nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tcc {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var (
|
||||
req = require.New(t)
|
||||
loader = CertStoreLoader{
|
||||
Storer: mockStorer{
|
||||
ss: tc.ss,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
cc, err := loader.Load(context.Background())
|
||||
|
||||
if tc.err != nil {
|
||||
req.Error(err)
|
||||
} else {
|
||||
req.NoError(err)
|
||||
}
|
||||
|
||||
req.Equal(tc.expect, cc)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (cm mockStorer) SearchSettings(ctx context.Context, f types.SettingsFilter) (types.SettingValueSet, types.SettingsFilter, error) {
|
||||
return cm.ss(ctx, f)
|
||||
}
|
||||
9
auth/saml/idp.go
Normal file
9
auth/saml/idp.go
Normal file
@ -0,0 +1,9 @@
|
||||
package saml
|
||||
|
||||
type (
|
||||
IdpIdentityPayload struct {
|
||||
Name string
|
||||
Handle string
|
||||
Identifier string
|
||||
}
|
||||
)
|
||||
54
auth/saml/saml.go
Normal file
54
auth/saml/saml.go
Normal file
@ -0,0 +1,54 @@
|
||||
package saml
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/cortezaproject/corteza-server/auth/settings"
|
||||
"github.com/cortezaproject/corteza-server/system/types"
|
||||
"github.com/crewjam/saml"
|
||||
"github.com/crewjam/saml/samlsp"
|
||||
)
|
||||
|
||||
type (
|
||||
templateProvider struct {
|
||||
Label, Handle, Icon string
|
||||
}
|
||||
)
|
||||
|
||||
// FetchIDPMetadata loads the idp metadata, usually the url
|
||||
// is configured in settings
|
||||
func FetchIDPMetadata(ctx context.Context, u url.URL) (*saml.EntityDescriptor, error) {
|
||||
return samlsp.FetchMetadata(ctx, http.DefaultClient, u)
|
||||
}
|
||||
|
||||
// TemplateProvider adds a wrapper to the button
|
||||
// data that is displayed on the login form
|
||||
func TemplateProvider(url, name string) templateProvider {
|
||||
if name == "" {
|
||||
name = url
|
||||
}
|
||||
|
||||
return templateProvider{
|
||||
Label: name,
|
||||
Handle: "saml/init",
|
||||
Icon: "key",
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateSettings applies the app settings to the
|
||||
// auth specific settings
|
||||
func UpdateSettings(source *types.AppSettings, dest *settings.Settings) {
|
||||
cas := source.Auth
|
||||
|
||||
dest.Saml.Enabled = cas.External.Saml.Enabled
|
||||
dest.Saml.Cert = cas.External.Saml.Cert
|
||||
dest.Saml.Key = cas.External.Saml.Key
|
||||
|
||||
dest.Saml.IDP.URL = cas.External.Saml.IDP.URL
|
||||
dest.Saml.IDP.Name = cas.External.Saml.IDP.Name
|
||||
dest.Saml.IDP.IdentName = cas.External.Saml.IDP.IdentName
|
||||
dest.Saml.IDP.IdentHandle = cas.External.Saml.IDP.IdentHandle
|
||||
dest.Saml.IDP.IdentIdentifier = cas.External.Saml.IDP.IdentIdentifier
|
||||
}
|
||||
134
auth/saml/sp.go
Normal file
134
auth/saml/sp.go
Normal file
@ -0,0 +1,134 @@
|
||||
package saml
|
||||
|
||||
import (
|
||||
"crypto/rsa"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/crewjam/saml"
|
||||
"github.com/crewjam/saml/samlsp"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const defaultNameIdentifier = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
|
||||
|
||||
type (
|
||||
SamlSPService struct {
|
||||
IdpURL url.URL
|
||||
Host url.URL
|
||||
IDPUserMeta *IdpIdentityPayload
|
||||
IDPMeta *saml.EntityDescriptor
|
||||
|
||||
sp saml.ServiceProvider
|
||||
handler *samlsp.Middleware
|
||||
}
|
||||
|
||||
SamlSPArgs struct {
|
||||
AcsURL string
|
||||
MetaURL string
|
||||
SloURL string
|
||||
|
||||
// user meta from idp
|
||||
IdentityPayload IdpIdentityPayload
|
||||
|
||||
IdpURL url.URL
|
||||
Host url.URL
|
||||
Cert tls.Certificate
|
||||
IdpMeta *saml.EntityDescriptor
|
||||
}
|
||||
)
|
||||
|
||||
// NewSamlSPService loads the certificates and registers the
|
||||
// already fetched IDP metadata into the SAML middleware
|
||||
func NewSamlSPService(args SamlSPArgs) (s *SamlSPService, err error) {
|
||||
var (
|
||||
keyPair = args.Cert
|
||||
)
|
||||
|
||||
keyPair.Leaf, err = x509.ParseCertificate(keyPair.Certificate[0])
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
metadataURL, _ := url.Parse(args.MetaURL)
|
||||
acsURL, _ := url.Parse(args.AcsURL)
|
||||
logoutURL, _ := url.Parse(args.SloURL)
|
||||
|
||||
sp := saml.ServiceProvider{
|
||||
Key: keyPair.PrivateKey.(*rsa.PrivateKey),
|
||||
Certificate: keyPair.Leaf,
|
||||
IDPMetadata: args.IdpMeta,
|
||||
|
||||
MetadataURL: *args.Host.ResolveReference(metadataURL),
|
||||
AcsURL: *args.Host.ResolveReference(acsURL),
|
||||
SloURL: *args.Host.ResolveReference(logoutURL),
|
||||
}
|
||||
|
||||
opts := samlsp.Options{
|
||||
URL: args.Host,
|
||||
Key: sp.Key,
|
||||
Certificate: sp.Certificate,
|
||||
IDPMetadata: args.IdpMeta,
|
||||
}
|
||||
|
||||
// internal samlsp service
|
||||
handler, err := samlsp.New(opts)
|
||||
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "could not init SAML SP handler")
|
||||
return
|
||||
}
|
||||
|
||||
handler.RequestTracker = samlsp.DefaultRequestTracker(opts, &handler.ServiceProvider)
|
||||
handler.ServiceProvider = sp
|
||||
|
||||
s = &SamlSPService{
|
||||
sp: sp,
|
||||
handler: handler,
|
||||
|
||||
IdpURL: args.IdpURL,
|
||||
Host: args.Host,
|
||||
IDPUserMeta: &args.IdentityPayload,
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (ssp *SamlSPService) NameIdentifier() string {
|
||||
return strings.TrimPrefix(defaultNameIdentifier, "urn:oasis:names:tc:SAML:1.1:nameid-format:")
|
||||
}
|
||||
|
||||
// GuessIdentifier tries to guess the necessary (email) key
|
||||
// for external authentication
|
||||
func (ssp *SamlSPService) GuessIdentifier(payload map[string][]string) string {
|
||||
tryValues := []string{
|
||||
ssp.IDPUserMeta.Identifier,
|
||||
ssp.NameIdentifier(),
|
||||
defaultNameIdentifier,
|
||||
"urn:oasis:names:tc:SAML:attribute:subject-id",
|
||||
"email",
|
||||
"mail",
|
||||
}
|
||||
|
||||
for _, v := range tryValues {
|
||||
if _, ok := payload[v]; ok {
|
||||
return payload[v][0]
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func (ssp *SamlSPService) Handler() *samlsp.Middleware {
|
||||
return ssp.handler
|
||||
}
|
||||
|
||||
// ServeHTTP enables us to use the service directly
|
||||
// in the router
|
||||
func (ssp SamlSPService) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
ssp.handler.ServeHTTP(w, r)
|
||||
}
|
||||
@ -9,6 +9,27 @@ type (
|
||||
ExternalEnabled bool
|
||||
Providers []Provider
|
||||
|
||||
Saml struct {
|
||||
Enabled bool
|
||||
|
||||
// SAML certificate
|
||||
Cert string
|
||||
|
||||
// SAML certificate private key
|
||||
Key string
|
||||
|
||||
// Identity provider hostname
|
||||
IDP struct {
|
||||
URL string
|
||||
Name string
|
||||
|
||||
// identifier payload from idp
|
||||
IdentName string
|
||||
IdentHandle string
|
||||
IdentIdentifier string
|
||||
}
|
||||
}
|
||||
|
||||
MultiFactor struct {
|
||||
EmailOTP struct {
|
||||
// Can users use email for MFA
|
||||
|
||||
@ -60,8 +60,13 @@ var (
|
||||
)
|
||||
|
||||
func defaultProviderValidator(provider string) error {
|
||||
_, err := goth.GetProvider(provider)
|
||||
return err
|
||||
switch provider {
|
||||
case "saml":
|
||||
return nil
|
||||
default:
|
||||
_, err := goth.GetProvider(provider)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
func Auth() *auth {
|
||||
@ -95,7 +100,7 @@ func Auth() *auth {
|
||||
//
|
||||
// External login/signup does not:
|
||||
// - validate provider on profile, only uses it for matching credentials
|
||||
func (svc auth) External(ctx context.Context, profile goth.User) (u *types.User, err error) {
|
||||
func (svc auth) External(ctx context.Context, profile types.ExternalAuthUser) (u *types.User, err error) {
|
||||
var (
|
||||
authProvider = &types.AuthProvider{Provider: profile.Provider}
|
||||
|
||||
@ -123,6 +128,7 @@ func (svc auth) External(ctx context.Context, profile goth.User) (u *types.User,
|
||||
cc types.CredentialsSet
|
||||
f = types.CredentialsFilter{Kind: profile.Provider, Credentials: profile.UserID}
|
||||
)
|
||||
|
||||
if cc, _, err = store.SearchCredentials(ctx, svc.store, f); err == nil {
|
||||
// Credentials found, load user
|
||||
for _, c := range cc {
|
||||
@ -190,7 +196,6 @@ func (svc auth) External(ctx context.Context, profile goth.User) (u *types.User,
|
||||
// Find user via his email
|
||||
if u, err = store.LookupUserByEmail(ctx, svc.store, profile.Email); errors.IsNotFound(err) {
|
||||
// @todo check if it is ok to auto-create a user here
|
||||
|
||||
// In case we do not have this email, create a new user
|
||||
u = &types.User{
|
||||
Email: profile.Email,
|
||||
|
||||
@ -51,6 +51,28 @@ type (
|
||||
// Is external authentication
|
||||
Enabled bool
|
||||
|
||||
// Saml
|
||||
Saml struct {
|
||||
Enabled bool
|
||||
|
||||
// SAML certificate
|
||||
Cert string `kv:"cert"`
|
||||
|
||||
// SAML certificate private key
|
||||
Key string `kv:"key"`
|
||||
|
||||
// Identity provider settings
|
||||
IDP struct {
|
||||
URL string `kv:"url"`
|
||||
Name string
|
||||
|
||||
// identifier payload from idp
|
||||
IdentName string `kv:"ident-name"`
|
||||
IdentHandle string `kv:"ident-handle"`
|
||||
IdentIdentifier string `kv:"ident-identifier"`
|
||||
} `kv:"idp"`
|
||||
}
|
||||
|
||||
// all external providers we know
|
||||
Providers ExternalAuthProviderSet
|
||||
}
|
||||
|
||||
@ -1,7 +1,13 @@
|
||||
package types
|
||||
|
||||
import "github.com/markbates/goth"
|
||||
|
||||
type (
|
||||
AuthProvider struct {
|
||||
Provider string
|
||||
}
|
||||
|
||||
ExternalAuthUser struct {
|
||||
goth.User
|
||||
}
|
||||
)
|
||||
|
||||
4
vendor/github.com/crewjam/saml/samlsp/middleware.go
generated
vendored
4
vendor/github.com/crewjam/saml/samlsp/middleware.go
generated
vendored
@ -5,7 +5,6 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/crewjam/saml"
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
)
|
||||
|
||||
// Middleware implements middleware than allows a web application
|
||||
@ -51,15 +50,12 @@ type Middleware struct {
|
||||
// on the URIs specified by m.ServiceProvider.MetadataURL and
|
||||
// m.ServiceProvider.AcsURL.
|
||||
func (m *Middleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
spew.Dump("SERVE", r.URL.Path, m.ServiceProvider.MetadataURL)
|
||||
if r.URL.Path == m.ServiceProvider.MetadataURL.Path {
|
||||
spew.Dump("1")
|
||||
m.serveMetadata(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if r.URL.Path == m.ServiceProvider.AcsURL.Path {
|
||||
spew.Dump("2")
|
||||
m.serveACS(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user