From 20bb1b4745a75b9d72e74c4964d170d7a6c7cf3a Mon Sep 17 00:00:00 2001 From: Knut Ahlers Date: Sat, 29 Dec 2018 00:38:56 +0100 Subject: [PATCH] [#25] Make TOTP provider fully configurable (#29) Signed-off-by: Knut Ahlers --- config.yaml | 11 ++++-- mfa.go | 10 ++++++ mfa_google.go | 63 --------------------------------- mfa_totp.go | 98 +++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 117 insertions(+), 65 deletions(-) delete mode 100644 mfa_google.go create mode 100644 mfa_totp.go diff --git a/config.yaml b/config.yaml index 10f89d8..4c23c43 100644 --- a/config.yaml +++ b/config.yaml @@ -44,6 +44,7 @@ mfa: secret_key: "foobar" duo: + # Get your ikey / skey / host from https://duo.com/docs/duoweb#first-steps ikey: "IKEY" skey: "SKEY" host: "HOST" @@ -103,9 +104,15 @@ providers: mfa: luzifer: - provider: duo - - provider: google + + - provider: totp attributes: - secret: MZXW6YTBOIFA + secret: MZXW6YTBOIFA # required + period: 30 # optional, defaults to 30 (Google Authenticator) + skew: 1 # optional, defaults to 1 (Google Authenticator) + digits: 8 # optional, defaults to 6 (Google Authenticator) + algorithm: sha1 # optional (sha1, sha256, sha512), defaults to sha1 (Google Authenticator) + - provider: yubikey attributes: device: ccccccfcvuul diff --git a/mfa.go b/mfa.go index 10e0ea2..2358cb7 100644 --- a/mfa.go +++ b/mfa.go @@ -26,6 +26,16 @@ func newMFAConfig(provider string, attrs map[string]interface{}) mfaConfig { return mfaConfig{Provider: provider, Attributes: attrs} } +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 { diff --git a/mfa_google.go b/mfa_google.go deleted file mode 100644 index fdbcdf9..0000000 --- a/mfa_google.go +++ /dev/null @@ -1,63 +0,0 @@ -package main - -import ( - "net/http" - "strings" - "time" - - "github.com/pkg/errors" - "github.com/pquerna/otp/totp" -) - -func init() { - registerMFAProvider(&mfaGoogle{}) -} - -type mfaGoogle struct{} - -// ProviderID needs to return an unique string to identify -// this special MFA provider -func (m mfaGoogle) ProviderID() (id string) { - return "google" -} - -// 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 mfaGoogle) 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 mfaGoogle) ValidateMFA(res http.ResponseWriter, r *http.Request, user string, mfaCfgs []mfaConfig) error { - // Look for mfaConfigs with own provider name - for _, c := range mfaCfgs { - if c.Provider != m.ProviderID() { - continue - } - - token, err := m.exec(c) - if err != nil { - return errors.Wrap(err, "Generating the MFA token failed") - } - - for key, values := range r.Form { - if strings.HasSuffix(key, mfaLoginFieldName) && values[0] == token { - return nil - } - } - } - - // Report this provider was not able to verify the MFA request - return errNoValidUserFound -} - -func (m mfaGoogle) exec(c mfaConfig) (string, error) { - secret := c.AttributeString("secret") - - if n := len(secret) % 8; n != 0 { - secret = secret + strings.Repeat("=", 8-n) - } - - return totp.GenerateCode(strings.ToUpper(secret), time.Now()) -} diff --git a/mfa_totp.go b/mfa_totp.go new file mode 100644 index 0000000..0b41edb --- /dev/null +++ b/mfa_totp.go @@ -0,0 +1,98 @@ +package main + +import ( + "net/http" + "strings" + "time" + + "github.com/pkg/errors" + "github.com/pquerna/otp" + "github.com/pquerna/otp/totp" +) + +func init() { + registerMFAProvider(&mfaTOTP{}) +} + +type mfaTOTP struct{} + +// ProviderID needs to return an unique string to identify +// this special MFA provider +func (m mfaTOTP) ProviderID() (id string) { + return "totp" +} + +// 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 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 { + // Look for mfaConfigs with own provider name + for _, c := range mfaCfgs { + // Provider has been renamed, keep "google" for backwards compatibility + if c.Provider != m.ProviderID() && c.Provider != "google" { + continue + } + + token, err := m.exec(c) + if err != nil { + return errors.Wrap(err, "Generating the MFA token failed") + } + + for key, values := range r.Form { + if strings.HasSuffix(key, mfaLoginFieldName) && values[0] == token { + return nil + } + } + } + + // Report this provider was not able to verify the MFA request + return errNoValidUserFound +} + +func (m mfaTOTP) exec(c mfaConfig) (string, error) { + secret := c.AttributeString("secret") + + // By default use Google Authenticator compatible settings + generatorOpts := totp.ValidateOpts{ + Period: 30, + Skew: 1, + Digits: otp.DigitsSix, + Algorithm: otp.AlgorithmSHA1, + } + + if period := c.AttributeInt("period"); period > 0 { + generatorOpts.Period = uint(period) + } + + if skew := c.AttributeInt("skew"); skew > 0 { + generatorOpts.Skew = uint(skew) + } + + if digits := c.AttributeInt("digits"); digits > 0 { + generatorOpts.Digits = otp.Digits(digits) + } + + if algorithm := c.AttributeString("algorithm"); algorithm != "" { + switch algorithm { + case "sha1": + generatorOpts.Algorithm = otp.AlgorithmSHA1 + case "sha256": + generatorOpts.Algorithm = otp.AlgorithmSHA256 + case "sha512": + generatorOpts.Algorithm = otp.AlgorithmSHA512 + default: + return "", errors.Errorf("Unsupported algorithm %q", algorithm) + } + } + + if n := len(secret) % 8; n != 0 { + secret = secret + strings.Repeat("=", 8-n) + } + + return totp.GenerateCodeCustom(strings.ToUpper(secret), time.Now(), generatorOpts) +}