3
0

Added SAML service

This commit is contained in:
Peter Grlica 2021-04-28 08:48:25 +02:00
parent a1ca8afe49
commit bb1043181c
20 changed files with 710 additions and 29 deletions

View File

@ -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)
}

View File

@ -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
View 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{}
}

View File

@ -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)
}

View 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)
}
}

View File

@ -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

View File

@ -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",
}
}

View File

@ -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)
}

View File

@ -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)

View File

@ -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
View 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
View 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
View File

@ -0,0 +1,9 @@
package saml
type (
IdpIdentityPayload struct {
Name string
Handle string
Identifier string
}
)

54
auth/saml/saml.go Normal file
View 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
View 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)
}

View File

@ -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

View File

@ -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,

View File

@ -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
}

View File

@ -1,7 +1,13 @@
package types
import "github.com/markbates/goth"
type (
AuthProvider struct {
Provider string
}
ExternalAuthUser struct {
goth.User
}
)

View File

@ -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
}