1
0
Fork 0
mirror of https://github.com/Luzifer/nginx-sso.git synced 2024-10-18 15:44:21 +00:00
nginx-sso/mfa_duo.go
Knut Ahlers 9e0907f494
Add support for Duo MFA (#28)
Squashed commit of the following:

commit f748cc99802dc2a1c2f1b805a280c56fc6cf4123
Author: Knut Ahlers <knut@ahlers.me>
Date:   Fri Dec 28 23:39:07 2018 +0100

    Implement fetching remote IP from mutliple sources

    Signed-off-by: Knut Ahlers <knut@ahlers.me>

commit 416af9eed8
Author: Ben Edmunds <bensammy2@yahoo.co.uk>
Date:   Fri Dec 28 19:35:26 2018 +0000

    Duo MFA, clean up & documentation

commit 0e511023f0
Author: Ben Edmunds <bensammy2@yahoo.co.uk>
Date:   Fri Dec 28 16:34:55 2018 +0000

    Add support for Duo MFA

closes #28
closes #24

Signed-off-by: Knut Ahlers <knut@ahlers.me>
2018-12-28 23:54:11 +01:00

131 lines
3.3 KiB
Go

package main
import (
"net"
"net/http"
"strings"
"time"
duoapi "github.com/duosecurity/duo_api_golang"
"github.com/duosecurity/duo_api_golang/authapi"
"github.com/pkg/errors"
yaml "gopkg.in/yaml.v2"
)
const (
mfaDuoResponseAllow = "allow"
mfaDuoRequestTimeout = 10 * time.Second
)
var mfaDuoTrustedIPHeaders = []string{"X-Forwarded-For", "X-Real-IP"}
func init() {
registerMFAProvider(&mfaDuo{})
}
type mfaDuo struct {
IKey string `yaml:"ikey"`
SKey string `yaml:"skey"`
Host string `yaml:"host"`
UserAgent string `yaml:"user_agent"`
}
// ProviderID needs to return an unique string to identify
// this special MFA provider
func (m mfaDuo) ProviderID() (id string) { return "duo" }
// 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
func (m *mfaDuo) Configure(yamlSource []byte) (err error) {
envelope := struct {
MFA struct {
Duo *mfaDuo `yaml:"duo"`
} `yaml:"mfa"`
}{}
if err := yaml.Unmarshal(yamlSource, &envelope); err != nil {
return err
}
if envelope.MFA.Duo == nil {
return errProviderUnconfigured
}
m.IKey = envelope.MFA.Duo.IKey
m.SKey = envelope.MFA.Duo.SKey
m.Host = envelope.MFA.Duo.Host
m.UserAgent = envelope.MFA.Duo.UserAgent
return nil
}
// 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 {
var keyInput string
// Look for mfaConfigs with own provider name
for _, c := range mfaCfgs {
if c.Provider != m.ProviderID() {
continue
}
remoteIP, err := m.findIP(r)
if err != nil {
return errors.Wrap(err, "Unable to determine remote IP")
}
duo := authapi.NewAuthApi(*duoapi.NewDuoApi(m.IKey, m.SKey, m.Host, m.UserAgent, duoapi.SetTimeout(mfaDuoRequestTimeout)))
for key, values := range r.Form {
if strings.HasSuffix(key, mfaLoginFieldName) && len(values[0]) > 0 {
keyInput = values[0]
}
}
// Check if MFA token provided and fallover to push if not supplied
var auth *authapi.AuthResult
if keyInput != "" {
if auth, err = duo.Auth("passcode", authapi.AuthUsername(user), authapi.AuthPasscode(keyInput), authapi.AuthIpAddr(remoteIP)); err != nil {
return errors.Wrap(err, "Unable to authenticate with Duo using 'passcode' method")
}
} else {
if auth, err = duo.Auth("auto", authapi.AuthUsername(user), authapi.AuthDevice("auto"), authapi.AuthIpAddr(remoteIP)); err != nil {
return errors.Wrap(err, "Unable to authenticate with Duo using 'auto' method")
}
}
if auth.Response.Result == mfaDuoResponseAllow {
return nil
}
}
// Report this provider was not able to verify the MFA request
return errNoValidUserFound
}
func (m mfaDuo) findIP(r *http.Request) (string, error) {
for _, hdr := range mfaDuoTrustedIPHeaders {
if value := r.Header.Get(hdr); value != "" {
return m.parseIP(value)
}
}
return m.parseIP(r.RemoteAddr)
}
func (m mfaDuo) parseIP(s string) (string, error) {
ip, _, err := net.SplitHostPort(s)
if err == nil {
return ip, nil
}
ip2 := net.ParseIP(s)
if ip2 == nil {
return "", errors.New("invalid IP")
}
return ip2.String(), nil
}