mirror of
https://github.com/Luzifer/nginx-sso.git
synced 2024-12-23 22:31:17 +00:00
133 lines
3.4 KiB
Go
133 lines
3.4 KiB
Go
package duo
|
|
|
|
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"
|
|
|
|
"github.com/Luzifer/nginx-sso/plugins"
|
|
)
|
|
|
|
const (
|
|
mfaDuoResponseAllow = "allow"
|
|
mfaDuoRequestTimeout = 10 * time.Second
|
|
)
|
|
|
|
var mfaDuoTrustedIPHeaders = []string{"X-Forwarded-For", "X-Real-IP"}
|
|
|
|
type MFADuo struct {
|
|
IKey string `yaml:"ikey"`
|
|
SKey string `yaml:"skey"`
|
|
Host string `yaml:"host"`
|
|
UserAgent string `yaml:"user_agent"`
|
|
}
|
|
|
|
func New() *MFADuo {
|
|
return &MFADuo{}
|
|
}
|
|
|
|
// 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 plugins.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 plugins.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 []plugins.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, plugins.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 plugins.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
|
|
}
|