mirror of
https://github.com/Luzifer/nginx-sso.git
synced 2024-12-20 12:51:17 +00:00
Add plugin support (#38)
* Extract Authenticator and MFAProvider interfaces * Implement plugin loading * Add config example Signed-off-by: Knut Ahlers <knut@ahlers.me>
This commit is contained in:
parent
3988fa4f38
commit
97b284034f
16 changed files with 225 additions and 127 deletions
|
@ -7,6 +7,8 @@ import (
|
||||||
crowd "github.com/jda/go-crowd"
|
crowd "github.com/jda/go-crowd"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
yaml "gopkg.in/yaml.v2"
|
yaml "gopkg.in/yaml.v2"
|
||||||
|
|
||||||
|
"github.com/Luzifer/nginx-sso/plugins"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
@ -106,7 +108,7 @@ func (a authCrowd) DetectUser(res http.ResponseWriter, r *http.Request) (string,
|
||||||
// in order to use DetectUser for the next login.
|
// in order to use DetectUser for the next login.
|
||||||
// If the user did not login correctly the errNoValidUserFound
|
// If the user did not login correctly the errNoValidUserFound
|
||||||
// needs to be returned
|
// needs to be returned
|
||||||
func (a authCrowd) Login(res http.ResponseWriter, r *http.Request) (string, []mfaConfig, error) {
|
func (a authCrowd) Login(res http.ResponseWriter, r *http.Request) (string, []plugins.MFAConfig, error) {
|
||||||
username := r.FormValue(strings.Join([]string{a.AuthenticatorID(), "username"}, "-"))
|
username := r.FormValue(strings.Join([]string{a.AuthenticatorID(), "username"}, "-"))
|
||||||
password := r.FormValue(strings.Join([]string{a.AuthenticatorID(), "password"}, "-"))
|
password := r.FormValue(strings.Join([]string{a.AuthenticatorID(), "password"}, "-"))
|
||||||
|
|
||||||
|
@ -139,8 +141,8 @@ func (a authCrowd) Login(res http.ResponseWriter, r *http.Request) (string, []mf
|
||||||
// LoginFields needs to return the fields required for this login
|
// LoginFields needs to return the fields required for this login
|
||||||
// method. If no login using this method is possible the function
|
// method. If no login using this method is possible the function
|
||||||
// needs to return nil.
|
// needs to return nil.
|
||||||
func (a authCrowd) LoginFields() (fields []loginField) {
|
func (a authCrowd) LoginFields() (fields []plugins.LoginField) {
|
||||||
return []loginField{
|
return []plugins.LoginField{
|
||||||
{
|
{
|
||||||
Label: "Username",
|
Label: "Username",
|
||||||
Name: "username",
|
Name: "username",
|
||||||
|
|
|
@ -9,6 +9,8 @@ import (
|
||||||
|
|
||||||
ldap "gopkg.in/ldap.v2"
|
ldap "gopkg.in/ldap.v2"
|
||||||
yaml "gopkg.in/yaml.v2"
|
yaml "gopkg.in/yaml.v2"
|
||||||
|
|
||||||
|
"github.com/Luzifer/nginx-sso/plugins"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -147,7 +149,7 @@ func (a authLDAP) DetectUser(res http.ResponseWriter, r *http.Request) (string,
|
||||||
// in order to use DetectUser for the next login.
|
// in order to use DetectUser for the next login.
|
||||||
// If the user did not login correctly the errNoValidUserFound
|
// If the user did not login correctly the errNoValidUserFound
|
||||||
// needs to be returned
|
// needs to be returned
|
||||||
func (a authLDAP) Login(res http.ResponseWriter, r *http.Request) (string, []mfaConfig, error) {
|
func (a authLDAP) Login(res http.ResponseWriter, r *http.Request) (string, []plugins.MFAConfig, error) {
|
||||||
username := r.FormValue(strings.Join([]string{a.AuthenticatorID(), "username"}, "-"))
|
username := r.FormValue(strings.Join([]string{a.AuthenticatorID(), "username"}, "-"))
|
||||||
password := r.FormValue(strings.Join([]string{a.AuthenticatorID(), "password"}, "-"))
|
password := r.FormValue(strings.Join([]string{a.AuthenticatorID(), "password"}, "-"))
|
||||||
|
|
||||||
|
@ -171,8 +173,8 @@ func (a authLDAP) Login(res http.ResponseWriter, r *http.Request) (string, []mfa
|
||||||
// LoginFields needs to return the fields required for this login
|
// LoginFields needs to return the fields required for this login
|
||||||
// method. If no login using this method is possible the function
|
// method. If no login using this method is possible the function
|
||||||
// needs to return nil.
|
// needs to return nil.
|
||||||
func (a authLDAP) LoginFields() (fields []loginField) {
|
func (a authLDAP) LoginFields() (fields []plugins.LoginField) {
|
||||||
return []loginField{
|
return []plugins.LoginField{
|
||||||
{
|
{
|
||||||
Label: "Username",
|
Label: "Username",
|
||||||
Name: "username",
|
Name: "username",
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
yaml "gopkg.in/yaml.v2"
|
yaml "gopkg.in/yaml.v2"
|
||||||
|
|
||||||
"github.com/Luzifer/go_helpers/str"
|
"github.com/Luzifer/go_helpers/str"
|
||||||
|
"github.com/Luzifer/nginx-sso/plugins"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
@ -18,7 +19,7 @@ type authSimple struct {
|
||||||
EnableBasicAuth bool `yaml:"enable_basic_auth"`
|
EnableBasicAuth bool `yaml:"enable_basic_auth"`
|
||||||
Users map[string]string `yaml:"users"`
|
Users map[string]string `yaml:"users"`
|
||||||
Groups map[string][]string `yaml:"groups"`
|
Groups map[string][]string `yaml:"groups"`
|
||||||
MFA map[string][]mfaConfig `yaml:"mfa"`
|
MFA map[string][]plugins.MFAConfig `yaml:"mfa"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// AuthenticatorID needs to return an unique string to identify
|
// AuthenticatorID needs to return an unique string to identify
|
||||||
|
@ -109,7 +110,7 @@ func (a authSimple) DetectUser(res http.ResponseWriter, r *http.Request) (string
|
||||||
// in order to use DetectUser for the next login.
|
// in order to use DetectUser for the next login.
|
||||||
// If the user did not login correctly the errNoValidUserFound
|
// If the user did not login correctly the errNoValidUserFound
|
||||||
// needs to be returned
|
// needs to be returned
|
||||||
func (a authSimple) Login(res http.ResponseWriter, r *http.Request) (string, []mfaConfig, error) {
|
func (a authSimple) Login(res http.ResponseWriter, r *http.Request) (string, []plugins.MFAConfig, error) {
|
||||||
username := r.FormValue(strings.Join([]string{a.AuthenticatorID(), "username"}, "-"))
|
username := r.FormValue(strings.Join([]string{a.AuthenticatorID(), "username"}, "-"))
|
||||||
password := r.FormValue(strings.Join([]string{a.AuthenticatorID(), "password"}, "-"))
|
password := r.FormValue(strings.Join([]string{a.AuthenticatorID(), "password"}, "-"))
|
||||||
|
|
||||||
|
@ -133,8 +134,8 @@ func (a authSimple) Login(res http.ResponseWriter, r *http.Request) (string, []m
|
||||||
// LoginFields needs to return the fields required for this login
|
// LoginFields needs to return the fields required for this login
|
||||||
// method. If no login using this method is possible the function
|
// method. If no login using this method is possible the function
|
||||||
// needs to return nil.
|
// needs to return nil.
|
||||||
func (a authSimple) LoginFields() (fields []loginField) {
|
func (a authSimple) LoginFields() (fields []plugins.LoginField) {
|
||||||
return []loginField{
|
return []plugins.LoginField{
|
||||||
{
|
{
|
||||||
Label: "Username",
|
Label: "Username",
|
||||||
Name: "username",
|
Name: "username",
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/Luzifer/go_helpers/str"
|
"github.com/Luzifer/go_helpers/str"
|
||||||
|
"github.com/Luzifer/nginx-sso/plugins"
|
||||||
|
|
||||||
yaml "gopkg.in/yaml.v2"
|
yaml "gopkg.in/yaml.v2"
|
||||||
)
|
)
|
||||||
|
@ -92,14 +93,14 @@ func (a authToken) DetectUser(res http.ResponseWriter, r *http.Request) (string,
|
||||||
// in order to use DetectUser for the next login.
|
// in order to use DetectUser for the next login.
|
||||||
// If the user did not login correctly the errNoValidUserFound
|
// If the user did not login correctly the errNoValidUserFound
|
||||||
// needs to be returned
|
// needs to be returned
|
||||||
func (a authToken) Login(res http.ResponseWriter, r *http.Request) (string, []mfaConfig, error) {
|
func (a authToken) Login(res http.ResponseWriter, r *http.Request) (string, []plugins.MFAConfig, error) {
|
||||||
return "", nil, errNoValidUserFound
|
return "", nil, errNoValidUserFound
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoginFields needs to return the fields required for this login
|
// LoginFields needs to return the fields required for this login
|
||||||
// method. If no login using this method is possible the function
|
// method. If no login using this method is possible the function
|
||||||
// needs to return nil.
|
// needs to return nil.
|
||||||
func (a authToken) LoginFields() []loginField { return nil }
|
func (a authToken) LoginFields() []plugins.LoginField { return nil }
|
||||||
|
|
||||||
// Logout is called when the user visits the logout endpoint and
|
// Logout is called when the user visits the logout endpoint and
|
||||||
// needs to destroy any persistent stored cookies
|
// needs to destroy any persistent stored cookies
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
yaml "gopkg.in/yaml.v2"
|
yaml "gopkg.in/yaml.v2"
|
||||||
|
|
||||||
"github.com/Luzifer/go_helpers/str"
|
"github.com/Luzifer/go_helpers/str"
|
||||||
|
"github.com/Luzifer/nginx-sso/plugins"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
@ -89,7 +90,7 @@ func (a authYubikey) DetectUser(res http.ResponseWriter, r *http.Request) (strin
|
||||||
// in order to use DetectUser for the next login.
|
// in order to use DetectUser for the next login.
|
||||||
// If the user did not login correctly the errNoValidUserFound
|
// If the user did not login correctly the errNoValidUserFound
|
||||||
// needs to be returned
|
// needs to be returned
|
||||||
func (a authYubikey) Login(res http.ResponseWriter, r *http.Request) (string, []mfaConfig, error) {
|
func (a authYubikey) Login(res http.ResponseWriter, r *http.Request) (string, []plugins.MFAConfig, error) {
|
||||||
keyInput := r.FormValue(strings.Join([]string{a.AuthenticatorID(), "key-input"}, "-"))
|
keyInput := r.FormValue(strings.Join([]string{a.AuthenticatorID(), "key-input"}, "-"))
|
||||||
|
|
||||||
yubiAuth, err := yubigo.NewYubiAuth(a.ClientID, a.SecretKey)
|
yubiAuth, err := yubigo.NewYubiAuth(a.ClientID, a.SecretKey)
|
||||||
|
@ -122,8 +123,8 @@ func (a authYubikey) Login(res http.ResponseWriter, r *http.Request) (string, []
|
||||||
// LoginFields needs to return the fields required for this login
|
// LoginFields needs to return the fields required for this login
|
||||||
// method. If no login using this method is possible the function
|
// method. If no login using this method is possible the function
|
||||||
// needs to return nil.
|
// needs to return nil.
|
||||||
func (a authYubikey) LoginFields() (fields []loginField) {
|
func (a authYubikey) LoginFields() (fields []plugins.LoginField) {
|
||||||
return []loginField{
|
return []plugins.LoginField{
|
||||||
{
|
{
|
||||||
Label: "Yubikey One-Time-Password",
|
Label: "Yubikey One-Time-Password",
|
||||||
Name: "key-input",
|
Name: "key-input",
|
||||||
|
|
|
@ -50,6 +50,9 @@ mfa:
|
||||||
host: "HOST"
|
host: "HOST"
|
||||||
user_agent: "nginx-sso"
|
user_agent: "nginx-sso"
|
||||||
|
|
||||||
|
plugins:
|
||||||
|
directory: ./plugins/
|
||||||
|
|
||||||
providers:
|
providers:
|
||||||
# Authentication against an Atlassian Crowd directory server
|
# Authentication against an Atlassian Crowd directory server
|
||||||
# Supports: Users, Groups
|
# Supports: Users, Groups
|
||||||
|
|
10
main.go
10
main.go
|
@ -13,6 +13,7 @@ import (
|
||||||
"github.com/flosch/pongo2"
|
"github.com/flosch/pongo2"
|
||||||
"github.com/gorilla/context"
|
"github.com/gorilla/context"
|
||||||
"github.com/gorilla/sessions"
|
"github.com/gorilla/sessions"
|
||||||
|
"github.com/pkg/errors"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
yaml "gopkg.in/yaml.v2"
|
yaml "gopkg.in/yaml.v2"
|
||||||
|
|
||||||
|
@ -39,6 +40,9 @@ type mainConfig struct {
|
||||||
HideMFAField bool `yaml:"hide_mfa_field"`
|
HideMFAField bool `yaml:"hide_mfa_field"`
|
||||||
Names map[string]string `yaml:"names"`
|
Names map[string]string `yaml:"names"`
|
||||||
} `yaml:"login"`
|
} `yaml:"login"`
|
||||||
|
Plugins struct {
|
||||||
|
Directory string `yaml:"directory"`
|
||||||
|
} `yaml:"plugins"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mainConfig) GetSessionOpts() *sessions.Options {
|
func (m *mainConfig) GetSessionOpts() *sessions.Options {
|
||||||
|
@ -100,6 +104,12 @@ func loadConfiguration() error {
|
||||||
return fmt.Errorf("Unable to load configuration file: %s", err)
|
return fmt.Errorf("Unable to load configuration file: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if mainCfg.Plugins.Directory != "" {
|
||||||
|
if err = loadPlugins(mainCfg.Plugins.Directory); err != nil {
|
||||||
|
return errors.Wrap(err, "Unable to load plugins")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if err = initializeAuthenticators(yamlSource); err != nil {
|
if err = initializeAuthenticators(yamlSource); err != nil {
|
||||||
return fmt.Errorf("Unable to configure authentication: %s", err)
|
return fmt.Errorf("Unable to configure authentication: %s", err)
|
||||||
}
|
}
|
||||||
|
|
53
mfa.go
53
mfa.go
|
@ -6,66 +6,27 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"github.com/Luzifer/nginx-sso/plugins"
|
||||||
)
|
)
|
||||||
|
|
||||||
const mfaLoginFieldName = "mfa-token"
|
const mfaLoginFieldName = "mfa-token"
|
||||||
|
|
||||||
var mfaLoginField = loginField{
|
var mfaLoginField = plugins.LoginField{
|
||||||
Label: "MFA Token",
|
Label: "MFA Token",
|
||||||
Name: mfaLoginFieldName,
|
Name: mfaLoginFieldName,
|
||||||
Placeholder: "(optional)",
|
Placeholder: "(optional)",
|
||||||
Type: "text",
|
Type: "text",
|
||||||
}
|
}
|
||||||
|
|
||||||
type mfaConfig struct {
|
|
||||||
Provider string `yaml:"provider"`
|
|
||||||
Attributes map[string]interface{} `yaml:"attributes"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m mfaConfig) AttributeInt(key string) int {
|
|
||||||
if v, ok := m.Attributes[key]; ok && v != "" {
|
|
||||||
if sv, ok := v.(int); ok {
|
|
||||||
return sv
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m mfaConfig) AttributeString(key string) string {
|
|
||||||
if v, ok := m.Attributes[key]; ok {
|
|
||||||
if sv, ok := v.(string); ok {
|
|
||||||
return sv
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
type mfaProvider interface {
|
|
||||||
// ProviderID needs to return an unique string to identify
|
|
||||||
// this special MFA provider
|
|
||||||
ProviderID() (id string)
|
|
||||||
|
|
||||||
// Configure loads the configuration for the Authenticator from the
|
|
||||||
// global config.yaml file which is passed as a byte-slice.
|
|
||||||
// If no configuration for the Authenticator is supplied the function
|
|
||||||
// needs to return the errProviderUnconfigured
|
|
||||||
Configure(yamlSource []byte) (err error)
|
|
||||||
|
|
||||||
// ValidateMFA takes the user from the login cookie and performs a
|
|
||||||
// validation against the provided MFA configuration for this user
|
|
||||||
ValidateMFA(res http.ResponseWriter, r *http.Request, user string, mfaCfgs []mfaConfig) error
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
mfaRegistry = []mfaProvider{}
|
mfaRegistry = []plugins.MFAProvider{}
|
||||||
mfaRegistryMutex sync.RWMutex
|
mfaRegistryMutex sync.RWMutex
|
||||||
|
|
||||||
activeMFAProviders = []mfaProvider{}
|
activeMFAProviders = []plugins.MFAProvider{}
|
||||||
)
|
)
|
||||||
|
|
||||||
func registerMFAProvider(m mfaProvider) {
|
func registerMFAProvider(m plugins.MFAProvider) {
|
||||||
mfaRegistryMutex.Lock()
|
mfaRegistryMutex.Lock()
|
||||||
defer mfaRegistryMutex.Unlock()
|
defer mfaRegistryMutex.Unlock()
|
||||||
|
|
||||||
|
@ -94,7 +55,7 @@ func initializeMFAProviders(yamlSource []byte) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateMFA(res http.ResponseWriter, r *http.Request, user string, mfaCfgs []mfaConfig) error {
|
func validateMFA(res http.ResponseWriter, r *http.Request, user string, mfaCfgs []plugins.MFAConfig) error {
|
||||||
if len(mfaCfgs) == 0 {
|
if len(mfaCfgs) == 0 {
|
||||||
// User has no configured MFA devices, their MFA is automatically valid
|
// User has no configured MFA devices, their MFA is automatically valid
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -10,6 +10,8 @@ import (
|
||||||
"github.com/duosecurity/duo_api_golang/authapi"
|
"github.com/duosecurity/duo_api_golang/authapi"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
yaml "gopkg.in/yaml.v2"
|
yaml "gopkg.in/yaml.v2"
|
||||||
|
|
||||||
|
"github.com/Luzifer/nginx-sso/plugins"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -62,7 +64,7 @@ func (m *mfaDuo) Configure(yamlSource []byte) (err error) {
|
||||||
|
|
||||||
// ValidateMFA takes the user from the login cookie and performs a
|
// ValidateMFA takes the user from the login cookie and performs a
|
||||||
// validation against the provided MFA configuration for this user
|
// validation against the provided MFA configuration for this user
|
||||||
func (m mfaDuo) ValidateMFA(res http.ResponseWriter, r *http.Request, user string, mfaCfgs []mfaConfig) error {
|
func (m mfaDuo) ValidateMFA(res http.ResponseWriter, r *http.Request, user string, mfaCfgs []plugins.MFAConfig) error {
|
||||||
var keyInput string
|
var keyInput string
|
||||||
|
|
||||||
// Look for mfaConfigs with own provider name
|
// Look for mfaConfigs with own provider name
|
||||||
|
|
|
@ -8,6 +8,8 @@ import (
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/pquerna/otp"
|
"github.com/pquerna/otp"
|
||||||
"github.com/pquerna/otp/totp"
|
"github.com/pquerna/otp/totp"
|
||||||
|
|
||||||
|
"github.com/Luzifer/nginx-sso/plugins"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
@ -30,7 +32,7 @@ func (m mfaTOTP) Configure(yamlSource []byte) (err error) { return nil }
|
||||||
|
|
||||||
// ValidateMFA takes the user from the login cookie and performs a
|
// ValidateMFA takes the user from the login cookie and performs a
|
||||||
// validation against the provided MFA configuration for this user
|
// validation against the provided MFA configuration for this user
|
||||||
func (m mfaTOTP) ValidateMFA(res http.ResponseWriter, r *http.Request, user string, mfaCfgs []mfaConfig) error {
|
func (m mfaTOTP) ValidateMFA(res http.ResponseWriter, r *http.Request, user string, mfaCfgs []plugins.MFAConfig) error {
|
||||||
// Look for mfaConfigs with own provider name
|
// Look for mfaConfigs with own provider name
|
||||||
for _, c := range mfaCfgs {
|
for _, c := range mfaCfgs {
|
||||||
// Provider has been renamed, keep "google" for backwards compatibility
|
// Provider has been renamed, keep "google" for backwards compatibility
|
||||||
|
@ -54,7 +56,7 @@ func (m mfaTOTP) ValidateMFA(res http.ResponseWriter, r *http.Request, user stri
|
||||||
return errNoValidUserFound
|
return errNoValidUserFound
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m mfaTOTP) exec(c mfaConfig) (string, error) {
|
func (m mfaTOTP) exec(c plugins.MFAConfig) (string, error) {
|
||||||
secret := c.AttributeString("secret")
|
secret := c.AttributeString("secret")
|
||||||
|
|
||||||
// By default use Google Authenticator compatible settings
|
// By default use Google Authenticator compatible settings
|
||||||
|
|
|
@ -7,6 +7,8 @@ import (
|
||||||
"github.com/GeertJohan/yubigo"
|
"github.com/GeertJohan/yubigo"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
yaml "gopkg.in/yaml.v2"
|
yaml "gopkg.in/yaml.v2"
|
||||||
|
|
||||||
|
"github.com/Luzifer/nginx-sso/plugins"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
@ -49,7 +51,7 @@ func (m *mfaYubikey) Configure(yamlSource []byte) (err error) {
|
||||||
|
|
||||||
// ValidateMFA takes the user from the login cookie and performs a
|
// ValidateMFA takes the user from the login cookie and performs a
|
||||||
// validation against the provided MFA configuration for this user
|
// validation against the provided MFA configuration for this user
|
||||||
func (m mfaYubikey) ValidateMFA(res http.ResponseWriter, r *http.Request, user string, mfaCfgs []mfaConfig) error {
|
func (m mfaYubikey) ValidateMFA(res http.ResponseWriter, r *http.Request, user string, mfaCfgs []plugins.MFAConfig) error {
|
||||||
var keyInput string
|
var keyInput string
|
||||||
|
|
||||||
yubiAuth, err := yubigo.NewYubiAuth(m.ClientID, m.SecretKey)
|
yubiAuth, err := yubigo.NewYubiAuth(m.ClientID, m.SecretKey)
|
||||||
|
|
58
plugins.go
Normal file
58
plugins.go
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"plugin"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"github.com/Luzifer/nginx-sso/plugins"
|
||||||
|
)
|
||||||
|
|
||||||
|
func loadPlugins(pluginDir string) error {
|
||||||
|
logger := log.WithField("plugin_dir", pluginDir)
|
||||||
|
|
||||||
|
d, err := os.Stat(pluginDir)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
logger.Warn("Plugin directory not found, skipping")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return errors.Wrap(err, "Could not stat plugin dir")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !d.IsDir() {
|
||||||
|
return errors.New("Plugin directory is not a directory")
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.Wrap(filepath.Walk(pluginDir, func(path string, info os.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.HasSuffix(path, ".so") {
|
||||||
|
// Ignore that file, is not a plugin
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
p, err := plugin.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "Unable to load plugin %q", path)
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := p.Lookup("Register")
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "Unable to find register function in %q", path)
|
||||||
|
}
|
||||||
|
|
||||||
|
f.(func(plugins.RegisterAuthenticatorFunc, plugins.RegisterMFAProviderFunc))(
|
||||||
|
registerAuthenticator,
|
||||||
|
registerMFAProvider,
|
||||||
|
)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}), "Unable to load plugins")
|
||||||
|
}
|
55
plugins/auth.go
Normal file
55
plugins/auth.go
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
package plugins
|
||||||
|
|
||||||
|
import "net/http"
|
||||||
|
|
||||||
|
type Authenticator interface {
|
||||||
|
// AuthenticatorID needs to return an unique string to identify
|
||||||
|
// this special authenticator
|
||||||
|
AuthenticatorID() (id string)
|
||||||
|
|
||||||
|
// Configure loads the configuration for the Authenticator from the
|
||||||
|
// global config.yaml file which is passed as a byte-slice.
|
||||||
|
// If no configuration for the Authenticator is supplied the function
|
||||||
|
// needs to return the errProviderUnconfigured
|
||||||
|
Configure(yamlSource []byte) (err error)
|
||||||
|
|
||||||
|
// DetectUser is used to detect a user without a login form from
|
||||||
|
// a cookie, header or other methods
|
||||||
|
// If no user was detected the errNoValidUserFound needs to be
|
||||||
|
// returned
|
||||||
|
DetectUser(res http.ResponseWriter, r *http.Request) (user string, groups []string, err error)
|
||||||
|
|
||||||
|
// Login is called when the user submits the login form and needs
|
||||||
|
// to authenticate the user or throw an error. If the user has
|
||||||
|
// successfully logged in the persistent cookie should be written
|
||||||
|
// in order to use DetectUser for the next login.
|
||||||
|
// With the login result an array of mfaConfig must be returned. In
|
||||||
|
// case there is no MFA config or the provider does not support MFA
|
||||||
|
// return nil.
|
||||||
|
// If the user did not login correctly the errNoValidUserFound
|
||||||
|
// needs to be returned
|
||||||
|
Login(res http.ResponseWriter, r *http.Request) (user string, mfaConfigs []MFAConfig, err error)
|
||||||
|
|
||||||
|
// LoginFields needs to return the fields required for this login
|
||||||
|
// method. If no login using this method is possible the function
|
||||||
|
// needs to return nil.
|
||||||
|
LoginFields() (fields []LoginField)
|
||||||
|
|
||||||
|
// Logout is called when the user visits the logout endpoint and
|
||||||
|
// needs to destroy any persistent stored cookies
|
||||||
|
Logout(res http.ResponseWriter, r *http.Request) (err error)
|
||||||
|
|
||||||
|
// SupportsMFA returns the MFA detection capabilities of the login
|
||||||
|
// provider. If the provider can provide mfaConfig objects from its
|
||||||
|
// configuration return true. If this is true the login interface
|
||||||
|
// will display an additional field for this provider for the user
|
||||||
|
// to fill in their MFA token.
|
||||||
|
SupportsMFA() bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type LoginField struct {
|
||||||
|
Label string
|
||||||
|
Name string
|
||||||
|
Placeholder string
|
||||||
|
Type string
|
||||||
|
}
|
44
plugins/mfa.go
Normal file
44
plugins/mfa.go
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
package plugins
|
||||||
|
|
||||||
|
import "net/http"
|
||||||
|
|
||||||
|
type MFAProvider interface {
|
||||||
|
// ProviderID needs to return an unique string to identify
|
||||||
|
// this special MFA provider
|
||||||
|
ProviderID() (id string)
|
||||||
|
|
||||||
|
// Configure loads the configuration for the Authenticator from the
|
||||||
|
// global config.yaml file which is passed as a byte-slice.
|
||||||
|
// If no configuration for the Authenticator is supplied the function
|
||||||
|
// needs to return the errProviderUnconfigured
|
||||||
|
Configure(yamlSource []byte) (err error)
|
||||||
|
|
||||||
|
// ValidateMFA takes the user from the login cookie and performs a
|
||||||
|
// validation against the provided MFA configuration for this user
|
||||||
|
ValidateMFA(res http.ResponseWriter, r *http.Request, user string, mfaCfgs []MFAConfig) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type MFAConfig struct {
|
||||||
|
Provider string `yaml:"provider"`
|
||||||
|
Attributes map[string]interface{} `yaml:"attributes"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m MFAConfig) AttributeInt(key string) int {
|
||||||
|
if v, ok := m.Attributes[key]; ok && v != "" {
|
||||||
|
if sv, ok := v.(int); ok {
|
||||||
|
return sv
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m MFAConfig) AttributeString(key string) string {
|
||||||
|
if v, ok := m.Attributes[key]; ok {
|
||||||
|
if sv, ok := v.(string); ok {
|
||||||
|
return sv
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
4
plugins/register.go
Normal file
4
plugins/register.go
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
package plugins
|
||||||
|
|
||||||
|
type RegisterAuthenticatorFunc func(Authenticator)
|
||||||
|
type RegisterMFAProviderFunc func(MFAProvider)
|
68
registry.go
68
registry.go
|
@ -7,71 +7,21 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"github.com/Luzifer/nginx-sso/plugins"
|
||||||
)
|
)
|
||||||
|
|
||||||
type authenticator interface {
|
|
||||||
// AuthenticatorID needs to return an unique string to identify
|
|
||||||
// this special authenticator
|
|
||||||
AuthenticatorID() (id string)
|
|
||||||
|
|
||||||
// Configure loads the configuration for the Authenticator from the
|
|
||||||
// global config.yaml file which is passed as a byte-slice.
|
|
||||||
// If no configuration for the Authenticator is supplied the function
|
|
||||||
// needs to return the errProviderUnconfigured
|
|
||||||
Configure(yamlSource []byte) (err error)
|
|
||||||
|
|
||||||
// DetectUser is used to detect a user without a login form from
|
|
||||||
// a cookie, header or other methods
|
|
||||||
// If no user was detected the errNoValidUserFound needs to be
|
|
||||||
// returned
|
|
||||||
DetectUser(res http.ResponseWriter, r *http.Request) (user string, groups []string, err error)
|
|
||||||
|
|
||||||
// Login is called when the user submits the login form and needs
|
|
||||||
// to authenticate the user or throw an error. If the user has
|
|
||||||
// successfully logged in the persistent cookie should be written
|
|
||||||
// in order to use DetectUser for the next login.
|
|
||||||
// With the login result an array of mfaConfig must be returned. In
|
|
||||||
// case there is no MFA config or the provider does not support MFA
|
|
||||||
// return nil.
|
|
||||||
// If the user did not login correctly the errNoValidUserFound
|
|
||||||
// needs to be returned
|
|
||||||
Login(res http.ResponseWriter, r *http.Request) (user string, mfaConfigs []mfaConfig, err error)
|
|
||||||
|
|
||||||
// LoginFields needs to return the fields required for this login
|
|
||||||
// method. If no login using this method is possible the function
|
|
||||||
// needs to return nil.
|
|
||||||
LoginFields() (fields []loginField)
|
|
||||||
|
|
||||||
// Logout is called when the user visits the logout endpoint and
|
|
||||||
// needs to destroy any persistent stored cookies
|
|
||||||
Logout(res http.ResponseWriter, r *http.Request) (err error)
|
|
||||||
|
|
||||||
// SupportsMFA returns the MFA detection capabilities of the login
|
|
||||||
// provider. If the provider can provide mfaConfig objects from its
|
|
||||||
// configuration return true. If this is true the login interface
|
|
||||||
// will display an additional field for this provider for the user
|
|
||||||
// to fill in their MFA token.
|
|
||||||
SupportsMFA() bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type loginField struct {
|
|
||||||
Label string
|
|
||||||
Name string
|
|
||||||
Placeholder string
|
|
||||||
Type string
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
errProviderUnconfigured = errors.New("No valid configuration found for this provider")
|
errProviderUnconfigured = errors.New("No valid configuration found for this provider")
|
||||||
errNoValidUserFound = errors.New("No valid users found")
|
errNoValidUserFound = errors.New("No valid users found")
|
||||||
|
|
||||||
authenticatorRegistry = []authenticator{}
|
authenticatorRegistry = []plugins.Authenticator{}
|
||||||
authenticatorRegistryMutex sync.RWMutex
|
authenticatorRegistryMutex sync.RWMutex
|
||||||
|
|
||||||
activeAuthenticators = []authenticator{}
|
activeAuthenticators = []plugins.Authenticator{}
|
||||||
)
|
)
|
||||||
|
|
||||||
func registerAuthenticator(a authenticator) {
|
func registerAuthenticator(a plugins.Authenticator) {
|
||||||
authenticatorRegistryMutex.Lock()
|
authenticatorRegistryMutex.Lock()
|
||||||
defer authenticatorRegistryMutex.Unlock()
|
defer authenticatorRegistryMutex.Unlock()
|
||||||
|
|
||||||
|
@ -82,7 +32,7 @@ func initializeAuthenticators(yamlSource []byte) error {
|
||||||
authenticatorRegistryMutex.Lock()
|
authenticatorRegistryMutex.Lock()
|
||||||
defer authenticatorRegistryMutex.Unlock()
|
defer authenticatorRegistryMutex.Unlock()
|
||||||
|
|
||||||
tmp := []authenticator{}
|
tmp := []plugins.Authenticator{}
|
||||||
for _, a := range authenticatorRegistry {
|
for _, a := range authenticatorRegistry {
|
||||||
err := a.Configure(yamlSource)
|
err := a.Configure(yamlSource)
|
||||||
|
|
||||||
|
@ -126,7 +76,7 @@ func detectUser(res http.ResponseWriter, r *http.Request) (string, []string, err
|
||||||
return "", nil, errNoValidUserFound
|
return "", nil, errNoValidUserFound
|
||||||
}
|
}
|
||||||
|
|
||||||
func loginUser(res http.ResponseWriter, r *http.Request) (string, []mfaConfig, error) {
|
func loginUser(res http.ResponseWriter, r *http.Request) (string, []plugins.MFAConfig, error) {
|
||||||
authenticatorRegistryMutex.RLock()
|
authenticatorRegistryMutex.RLock()
|
||||||
defer authenticatorRegistryMutex.RUnlock()
|
defer authenticatorRegistryMutex.RUnlock()
|
||||||
|
|
||||||
|
@ -158,11 +108,11 @@ func logoutUser(res http.ResponseWriter, r *http.Request) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getFrontendAuthenticators() map[string][]loginField {
|
func getFrontendAuthenticators() map[string][]plugins.LoginField {
|
||||||
authenticatorRegistryMutex.RLock()
|
authenticatorRegistryMutex.RLock()
|
||||||
defer authenticatorRegistryMutex.RUnlock()
|
defer authenticatorRegistryMutex.RUnlock()
|
||||||
|
|
||||||
output := map[string][]loginField{}
|
output := map[string][]plugins.LoginField{}
|
||||||
for _, a := range activeAuthenticators {
|
for _, a := range activeAuthenticators {
|
||||||
if len(a.LoginFields()) == 0 {
|
if len(a.LoginFields()) == 0 {
|
||||||
continue
|
continue
|
||||||
|
|
Loading…
Reference in a new issue