1
0
Fork 0
mirror of https://github.com/Luzifer/nginx-sso.git synced 2024-12-20 04:41: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:
Knut Ahlers 2019-02-22 00:10:43 +01:00 committed by GitHub
parent 3988fa4f38
commit 97b284034f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 225 additions and 127 deletions

View file

@ -7,6 +7,8 @@ import (
crowd "github.com/jda/go-crowd"
log "github.com/sirupsen/logrus"
yaml "gopkg.in/yaml.v2"
"github.com/Luzifer/nginx-sso/plugins"
)
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.
// If the user did not login correctly the errNoValidUserFound
// 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"}, "-"))
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
// method. If no login using this method is possible the function
// needs to return nil.
func (a authCrowd) LoginFields() (fields []loginField) {
return []loginField{
func (a authCrowd) LoginFields() (fields []plugins.LoginField) {
return []plugins.LoginField{
{
Label: "Username",
Name: "username",

View file

@ -9,6 +9,8 @@ import (
ldap "gopkg.in/ldap.v2"
yaml "gopkg.in/yaml.v2"
"github.com/Luzifer/nginx-sso/plugins"
)
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.
// If the user did not login correctly the errNoValidUserFound
// 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"}, "-"))
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
// method. If no login using this method is possible the function
// needs to return nil.
func (a authLDAP) LoginFields() (fields []loginField) {
return []loginField{
func (a authLDAP) LoginFields() (fields []plugins.LoginField) {
return []plugins.LoginField{
{
Label: "Username",
Name: "username",

View file

@ -8,6 +8,7 @@ import (
yaml "gopkg.in/yaml.v2"
"github.com/Luzifer/go_helpers/str"
"github.com/Luzifer/nginx-sso/plugins"
)
func init() {
@ -15,10 +16,10 @@ func init() {
}
type authSimple struct {
EnableBasicAuth bool `yaml:"enable_basic_auth"`
Users map[string]string `yaml:"users"`
Groups map[string][]string `yaml:"groups"`
MFA map[string][]mfaConfig `yaml:"mfa"`
EnableBasicAuth bool `yaml:"enable_basic_auth"`
Users map[string]string `yaml:"users"`
Groups map[string][]string `yaml:"groups"`
MFA map[string][]plugins.MFAConfig `yaml:"mfa"`
}
// 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.
// If the user did not login correctly the errNoValidUserFound
// 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"}, "-"))
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
// method. If no login using this method is possible the function
// needs to return nil.
func (a authSimple) LoginFields() (fields []loginField) {
return []loginField{
func (a authSimple) LoginFields() (fields []plugins.LoginField) {
return []plugins.LoginField{
{
Label: "Username",
Name: "username",

View file

@ -5,6 +5,7 @@ import (
"strings"
"github.com/Luzifer/go_helpers/str"
"github.com/Luzifer/nginx-sso/plugins"
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.
// If the user did not login correctly the errNoValidUserFound
// 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
}
// 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.
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
// needs to destroy any persistent stored cookies

View file

@ -8,6 +8,7 @@ import (
yaml "gopkg.in/yaml.v2"
"github.com/Luzifer/go_helpers/str"
"github.com/Luzifer/nginx-sso/plugins"
)
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.
// If the user did not login correctly the errNoValidUserFound
// 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"}, "-"))
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
// method. If no login using this method is possible the function
// needs to return nil.
func (a authYubikey) LoginFields() (fields []loginField) {
return []loginField{
func (a authYubikey) LoginFields() (fields []plugins.LoginField) {
return []plugins.LoginField{
{
Label: "Yubikey One-Time-Password",
Name: "key-input",

View file

@ -50,6 +50,9 @@ mfa:
host: "HOST"
user_agent: "nginx-sso"
plugins:
directory: ./plugins/
providers:
# Authentication against an Atlassian Crowd directory server
# Supports: Users, Groups

10
main.go
View file

@ -13,6 +13,7 @@ import (
"github.com/flosch/pongo2"
"github.com/gorilla/context"
"github.com/gorilla/sessions"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
yaml "gopkg.in/yaml.v2"
@ -39,6 +40,9 @@ type mainConfig struct {
HideMFAField bool `yaml:"hide_mfa_field"`
Names map[string]string `yaml:"names"`
} `yaml:"login"`
Plugins struct {
Directory string `yaml:"directory"`
} `yaml:"plugins"`
}
func (m *mainConfig) GetSessionOpts() *sessions.Options {
@ -100,6 +104,12 @@ func loadConfiguration() error {
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 {
return fmt.Errorf("Unable to configure authentication: %s", err)
}

53
mfa.go
View file

@ -6,66 +6,27 @@ import (
"sync"
log "github.com/sirupsen/logrus"
"github.com/Luzifer/nginx-sso/plugins"
)
const mfaLoginFieldName = "mfa-token"
var mfaLoginField = loginField{
var mfaLoginField = plugins.LoginField{
Label: "MFA Token",
Name: mfaLoginFieldName,
Placeholder: "(optional)",
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 (
mfaRegistry = []mfaProvider{}
mfaRegistry = []plugins.MFAProvider{}
mfaRegistryMutex sync.RWMutex
activeMFAProviders = []mfaProvider{}
activeMFAProviders = []plugins.MFAProvider{}
)
func registerMFAProvider(m mfaProvider) {
func registerMFAProvider(m plugins.MFAProvider) {
mfaRegistryMutex.Lock()
defer mfaRegistryMutex.Unlock()
@ -94,7 +55,7 @@ func initializeMFAProviders(yamlSource []byte) error {
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 {
// User has no configured MFA devices, their MFA is automatically valid
return nil

View file

@ -10,6 +10,8 @@ import (
"github.com/duosecurity/duo_api_golang/authapi"
"github.com/pkg/errors"
yaml "gopkg.in/yaml.v2"
"github.com/Luzifer/nginx-sso/plugins"
)
const (
@ -62,7 +64,7 @@ func (m *mfaDuo) 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
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
// Look for mfaConfigs with own provider name

View file

@ -8,6 +8,8 @@ import (
"github.com/pkg/errors"
"github.com/pquerna/otp"
"github.com/pquerna/otp/totp"
"github.com/Luzifer/nginx-sso/plugins"
)
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
// 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
for _, c := range mfaCfgs {
// 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
}
func (m mfaTOTP) exec(c mfaConfig) (string, error) {
func (m mfaTOTP) exec(c plugins.MFAConfig) (string, error) {
secret := c.AttributeString("secret")
// By default use Google Authenticator compatible settings

View file

@ -7,6 +7,8 @@ import (
"github.com/GeertJohan/yubigo"
"github.com/pkg/errors"
yaml "gopkg.in/yaml.v2"
"github.com/Luzifer/nginx-sso/plugins"
)
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
// 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
yubiAuth, err := yubigo.NewYubiAuth(m.ClientID, m.SecretKey)

58
plugins.go Normal file
View 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
View 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
View 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
View file

@ -0,0 +1,4 @@
package plugins
type RegisterAuthenticatorFunc func(Authenticator)
type RegisterMFAProviderFunc func(MFAProvider)

View file

@ -7,71 +7,21 @@ import (
"sync"
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 (
errProviderUnconfigured = errors.New("No valid configuration found for this provider")
errNoValidUserFound = errors.New("No valid users found")
authenticatorRegistry = []authenticator{}
authenticatorRegistry = []plugins.Authenticator{}
authenticatorRegistryMutex sync.RWMutex
activeAuthenticators = []authenticator{}
activeAuthenticators = []plugins.Authenticator{}
)
func registerAuthenticator(a authenticator) {
func registerAuthenticator(a plugins.Authenticator) {
authenticatorRegistryMutex.Lock()
defer authenticatorRegistryMutex.Unlock()
@ -82,7 +32,7 @@ func initializeAuthenticators(yamlSource []byte) error {
authenticatorRegistryMutex.Lock()
defer authenticatorRegistryMutex.Unlock()
tmp := []authenticator{}
tmp := []plugins.Authenticator{}
for _, a := range authenticatorRegistry {
err := a.Configure(yamlSource)
@ -126,7 +76,7 @@ func detectUser(res http.ResponseWriter, r *http.Request) (string, []string, err
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()
defer authenticatorRegistryMutex.RUnlock()
@ -158,11 +108,11 @@ func logoutUser(res http.ResponseWriter, r *http.Request) error {
return nil
}
func getFrontendAuthenticators() map[string][]loginField {
func getFrontendAuthenticators() map[string][]plugins.LoginField {
authenticatorRegistryMutex.RLock()
defer authenticatorRegistryMutex.RUnlock()
output := map[string][]loginField{}
output := map[string][]plugins.LoginField{}
for _, a := range activeAuthenticators {
if len(a.LoginFields()) == 0 {
continue