Skip to content

Commit

Permalink
Add plugin support (#38)
Browse files Browse the repository at this point in the history
* Extract Authenticator and MFAProvider interfaces
* Implement plugin loading
* Add config example

Signed-off-by: Knut Ahlers <knut@ahlers.me>
  • Loading branch information
Luzifer committed Feb 21, 2019
1 parent 3988fa4 commit 97b2840
Show file tree
Hide file tree
Showing 16 changed files with 225 additions and 127 deletions.
8 changes: 5 additions & 3 deletions auth_crowd.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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"}, "-"))

Expand Down Expand Up @@ -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",
Expand Down
8 changes: 5 additions & 3 deletions auth_ldap.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import (

ldap "gopkg.in/ldap.v2"
yaml "gopkg.in/yaml.v2"

"github.com/Luzifer/nginx-sso/plugins"
)

const (
Expand Down Expand Up @@ -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"}, "-"))

Expand All @@ -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",
Expand Down
15 changes: 8 additions & 7 deletions auth_simple.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,18 @@ import (
yaml "gopkg.in/yaml.v2"

"github.com/Luzifer/go_helpers/str"
"github.com/Luzifer/nginx-sso/plugins"
)

func init() {
registerAuthenticator(&authSimple{})
}

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
Expand Down Expand Up @@ -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"}, "-"))

Expand All @@ -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",
Expand Down
5 changes: 3 additions & 2 deletions auth_token.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"strings"

"github.com/Luzifer/go_helpers/str"
"github.com/Luzifer/nginx-sso/plugins"

yaml "gopkg.in/yaml.v2"
)
Expand Down Expand Up @@ -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
Expand Down
7 changes: 4 additions & 3 deletions auth_yubikey.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
yaml "gopkg.in/yaml.v2"

"github.com/Luzifer/go_helpers/str"
"github.com/Luzifer/nginx-sso/plugins"
)

func init() {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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",
Expand Down
3 changes: 3 additions & 0 deletions config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand All @@ -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 {
Expand Down Expand Up @@ -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)
}
Expand Down
53 changes: 7 additions & 46 deletions mfa.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion mfa_duo.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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
Expand Down
6 changes: 4 additions & 2 deletions mfa_totp.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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
Expand All @@ -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
Expand Down
4 changes: 3 additions & 1 deletion mfa_yubikey.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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)
Expand Down
58 changes: 58 additions & 0 deletions plugins.go
Original file line number Diff line number Diff line change
@@ -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")
}
Loading

0 comments on commit 97b2840

Please sign in to comment.