mirror of
https://github.com/volatiletech/authboss.git
synced 2025-02-13 13:58:38 +02:00
Add twofactor setup e-mail validation options
This commit is contained in:
parent
97b72a4816
commit
931ccfba1f
12
CHANGELOG.md
12
CHANGELOG.md
@ -3,6 +3,18 @@
|
||||
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
||||
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- Add e-mail confirmation before 2fa setup feature
|
||||
- Add config value TwoFactorEmailAuthRequired
|
||||
|
||||
### Deprecated
|
||||
|
||||
- Deprecate the config field ConfirmMethod in favor of MailRouteMethod. See
|
||||
documentation for these config fields to understand how to use them now.
|
||||
|
||||
## [2.1.0] - 2018-10-28
|
||||
|
||||
### Added
|
||||
|
@ -18,6 +18,13 @@ const (
|
||||
SessionLastAction = "last_action"
|
||||
// Session2FA is set when a user has been authenticated with a second factor
|
||||
Session2FA = "twofactor"
|
||||
// Session2FAAuthToken is a random token set in the session to be verified
|
||||
// by e-mail.
|
||||
Session2FAAuthToken = "twofactor_auth_token"
|
||||
// Session2FAAuthed is in the session (and set to "true") when the user
|
||||
// has successfully verified the token sent via e-mail in the two factor
|
||||
// e-mail authentication process.
|
||||
Session2FAAuthed = "twofactor_authed"
|
||||
// SessionOAuth2State is the xsrf protection key for oauth.
|
||||
SessionOAuth2State = "oauth2_state"
|
||||
// SessionOAuth2Params is the additional settings for oauth
|
||||
|
33
config.go
33
config.go
@ -40,7 +40,8 @@ type Config struct {
|
||||
// an unsuccessful oauth2 login
|
||||
OAuth2LoginNotOK string
|
||||
|
||||
// RecoverOK is the redirect path after a successful recovery of a password.
|
||||
// RecoverOK is the redirect path after a successful recovery of a
|
||||
// password.
|
||||
RecoverOK string
|
||||
|
||||
// RegisterOK is the redirect path after a successful registration.
|
||||
@ -50,12 +51,20 @@ type Config struct {
|
||||
// (eg https://www.happiness.com:8080) for url generation.
|
||||
// No trailing slash.
|
||||
RootURL string
|
||||
|
||||
// TwoFactorEmailAuthNotOK is where a user is redirected when
|
||||
// the user attempts to add 2fa to their account without verifying
|
||||
// their e-mail OR when they've completed the first step towards
|
||||
// verification and need to check their e-mail to proceed.
|
||||
TwoFactorEmailAuthNotOK string
|
||||
}
|
||||
|
||||
Modules struct {
|
||||
// BCryptCost is the cost of the bcrypt password hashing function.
|
||||
BCryptCost int
|
||||
|
||||
// ConfirmMethod IS DEPRECATED! See MailRouteMethod instead.
|
||||
//
|
||||
// ConfirmMethod controls which http method confirm expects.
|
||||
// This is because typically this is a GET request since it's a link
|
||||
// from an e-mail, but in api-like cases it needs to be able to be a
|
||||
@ -77,6 +86,21 @@ type Config struct {
|
||||
// (default should be DELETE)
|
||||
LogoutMethod string
|
||||
|
||||
// MailRouteMethod is used to set the type of request that's used for
|
||||
// routes that require a token from an e-mail link's query string.
|
||||
// This is things like confirm and two factor e-mail auth.
|
||||
//
|
||||
// You should probably set this to POST if you are building an API
|
||||
// so that the user goes to the frontend with their link & token
|
||||
// and the front-end calls the API with the token in a POST JSON body.
|
||||
//
|
||||
// This configuration setting deprecates ConfirmMethod.
|
||||
// If ConfirmMethod is set to the default value (GET) then
|
||||
// MailRouteMethod is used. If ConfirMethod is not the default value
|
||||
// then it is used until Authboss v3 when only MailRouteMethod will be
|
||||
// used.
|
||||
MailRouteMethod string
|
||||
|
||||
// RegisterPreserveFields are fields used with registration that are
|
||||
// to be rendered when post fails in a normal way
|
||||
// (for example validation errors), they will be passed back in the
|
||||
@ -105,6 +129,11 @@ type Config struct {
|
||||
// OAuthProvider documentation for more details.
|
||||
OAuth2Providers map[string]OAuth2Provider
|
||||
|
||||
// TwoFactorEmailAuthRequired forces users to first confirm they have
|
||||
// access to their e-mail with the current device by clicking a link
|
||||
// and confirming a token stored in the session.
|
||||
TwoFactorEmailAuthRequired bool
|
||||
|
||||
// TOTP2FAIssuer is the issuer that appears in the url when scanning
|
||||
// a qr code for google authenticator.
|
||||
TOTP2FAIssuer string
|
||||
@ -201,6 +230,7 @@ func (c *Config) Defaults() {
|
||||
c.Paths.RecoverOK = "/"
|
||||
c.Paths.RegisterOK = "/"
|
||||
c.Paths.RootURL = "http://localhost:8080"
|
||||
c.Paths.TwoFactorEmailAuthNotOK = "/"
|
||||
|
||||
c.Modules.BCryptCost = bcrypt.DefaultCost
|
||||
c.Modules.ConfirmMethod = http.MethodGet
|
||||
@ -209,6 +239,7 @@ func (c *Config) Defaults() {
|
||||
c.Modules.LockWindow = 5 * time.Minute
|
||||
c.Modules.LockDuration = 12 * time.Hour
|
||||
c.Modules.LogoutMethod = "DELETE"
|
||||
c.Modules.MailRouteMethod = http.MethodGet
|
||||
c.Modules.RecoverLoginAfterRecovery = false
|
||||
c.Modules.RecoverTokenDuration = 24 * time.Hour
|
||||
}
|
||||
|
@ -56,7 +56,11 @@ func (c *Confirm) Init(ab *authboss.Authboss) (err error) {
|
||||
}
|
||||
|
||||
var callbackMethod func(string, http.Handler)
|
||||
switch c.Config.Modules.ConfirmMethod {
|
||||
methodConfig := c.Config.Modules.ConfirmMethod
|
||||
if methodConfig == http.MethodGet {
|
||||
methodConfig = c.Config.Modules.MailRouteMethod
|
||||
}
|
||||
switch methodConfig {
|
||||
case http.MethodGet:
|
||||
callbackMethod = c.Authboss.Config.Core.Router.Get
|
||||
case http.MethodPost:
|
||||
|
@ -190,6 +190,8 @@ func NewHTTPBodyReader(readJSON, useUsernameNotEmail bool) *HTTPBodyReader {
|
||||
"confirm": {Rules{FieldName: FormValueConfirm, Required: true}},
|
||||
"recover_start": {pidRules},
|
||||
"recover_end": {passwordRule},
|
||||
|
||||
"twofactor_verify_end": {Rules{FieldName: FormValueToken, Required: true}},
|
||||
},
|
||||
Confirms: map[string][]string{
|
||||
"register": {FormValuePassword, authboss.ConfirmPrefix + FormValuePassword},
|
||||
@ -268,6 +270,12 @@ func (h HTTPBodyReader) Read(page string, r *http.Request) (authboss.Validator,
|
||||
Token: values[FormValueToken],
|
||||
NewPassword: values[FormValuePassword],
|
||||
}, nil
|
||||
case "twofactor_verify_end":
|
||||
// Reuse ConfirmValues here, it's the same values we need
|
||||
return ConfirmValues{
|
||||
HTTPFormValidator: HTTPFormValidator{Values: values, Ruleset: rules, ConfirmFields: confirms},
|
||||
Token: values[FormValueToken],
|
||||
}, nil
|
||||
case "totp2fa_confirm", "totp2fa_remove", "totp2fa_validate":
|
||||
return TwoFA{
|
||||
HTTPFormValidator: HTTPFormValidator{Values: values, Ruleset: rules, ConfirmFields: confirms},
|
||||
|
@ -97,17 +97,35 @@ func (s *SMS) Setup() error {
|
||||
return errors.New("must have SMS.Sender set")
|
||||
}
|
||||
|
||||
middleware := authboss.MountedMiddleware(s.Authboss, true, s.Authboss.Config.Modules.RoutesRedirectOnUnauthed, false, false)
|
||||
s.Authboss.Core.Router.Get("/2fa/sms/setup", middleware(s.Core.ErrorHandler.Wrap(s.GetSetup)))
|
||||
s.Authboss.Core.Router.Post("/2fa/sms/setup", middleware(s.Core.ErrorHandler.Wrap(s.PostSetup)))
|
||||
abmw := authboss.MountedMiddleware(s.Authboss, true, s.Authboss.Config.Modules.RoutesRedirectOnUnauthed, false, false)
|
||||
|
||||
var middleware, verified func(func(w http.ResponseWriter, r *http.Request) error) http.Handler
|
||||
middleware = func(handler func(http.ResponseWriter, *http.Request) error) http.Handler {
|
||||
return abmw(s.Core.ErrorHandler.Wrap(handler))
|
||||
}
|
||||
|
||||
if s.Authboss.Config.Modules.TwoFactorEmailAuthRequired {
|
||||
emailVerify, err := twofactor.SetupEmailVerify(s.Authboss, "totp", "/2fa/totp/setup")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
verified = func(handler func(http.ResponseWriter, *http.Request) error) http.Handler {
|
||||
return abmw(emailVerify.Wrap(s.Core.ErrorHandler.Wrap(handler)))
|
||||
}
|
||||
} else {
|
||||
verified = middleware
|
||||
}
|
||||
|
||||
s.Authboss.Core.Router.Get("/2fa/sms/setup", verified(s.GetSetup))
|
||||
s.Authboss.Core.Router.Post("/2fa/sms/setup", verified(s.PostSetup))
|
||||
|
||||
confirm := &SMSValidator{SMS: s, Page: PageSMSConfirm}
|
||||
s.Authboss.Core.Router.Get("/2fa/sms/confirm", middleware(s.Core.ErrorHandler.Wrap(confirm.Get)))
|
||||
s.Authboss.Core.Router.Post("/2fa/sms/confirm", middleware(s.Core.ErrorHandler.Wrap(confirm.Post)))
|
||||
s.Authboss.Core.Router.Get("/2fa/sms/confirm", verified(confirm.Get))
|
||||
s.Authboss.Core.Router.Post("/2fa/sms/confirm", verified(confirm.Post))
|
||||
|
||||
remove := &SMSValidator{SMS: s, Page: PageSMSRemove}
|
||||
s.Authboss.Core.Router.Get("/2fa/sms/remove", middleware(s.Core.ErrorHandler.Wrap(remove.Get)))
|
||||
s.Authboss.Core.Router.Post("/2fa/sms/remove", middleware(s.Core.ErrorHandler.Wrap(remove.Post)))
|
||||
s.Authboss.Core.Router.Get("/2fa/sms/remove", middleware(remove.Get))
|
||||
s.Authboss.Core.Router.Post("/2fa/sms/remove", middleware(remove.Post))
|
||||
|
||||
validate := &SMSValidator{SMS: s, Page: PageSMSValidate}
|
||||
s.Authboss.Core.Router.Get("/2fa/sms/validate", s.Core.ErrorHandler.Wrap(validate.Get))
|
||||
|
@ -66,17 +66,35 @@ type TOTP struct {
|
||||
|
||||
// Setup the module
|
||||
func (t *TOTP) Setup() error {
|
||||
middleware := authboss.MountedMiddleware(t.Authboss, true, t.Authboss.Config.Modules.RoutesRedirectOnUnauthed, true, false)
|
||||
t.Authboss.Core.Router.Get("/2fa/totp/setup", middleware(t.Core.ErrorHandler.Wrap(t.GetSetup)))
|
||||
t.Authboss.Core.Router.Post("/2fa/totp/setup", middleware(t.Core.ErrorHandler.Wrap(t.PostSetup)))
|
||||
abmw := authboss.MountedMiddleware(t.Authboss, true, t.Authboss.Config.Modules.RoutesRedirectOnUnauthed, true, false)
|
||||
|
||||
t.Authboss.Core.Router.Get("/2fa/totp/qr", middleware(t.Core.ErrorHandler.Wrap(t.GetQRCode)))
|
||||
var middleware, verified func(func(w http.ResponseWriter, r *http.Request) error) http.Handler
|
||||
middleware = func(handler func(http.ResponseWriter, *http.Request) error) http.Handler {
|
||||
return abmw(t.Core.ErrorHandler.Wrap(handler))
|
||||
}
|
||||
|
||||
t.Authboss.Core.Router.Get("/2fa/totp/confirm", middleware(t.Core.ErrorHandler.Wrap(t.GetConfirm)))
|
||||
t.Authboss.Core.Router.Post("/2fa/totp/confirm", middleware(t.Core.ErrorHandler.Wrap(t.PostConfirm)))
|
||||
if t.Authboss.Config.Modules.TwoFactorEmailAuthRequired {
|
||||
emailVerify, err := twofactor.SetupEmailVerify(t.Authboss, "totp", "/2fa/totp/setup")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
verified = func(handler func(http.ResponseWriter, *http.Request) error) http.Handler {
|
||||
return abmw(emailVerify.Wrap(t.Core.ErrorHandler.Wrap(handler)))
|
||||
}
|
||||
} else {
|
||||
verified = middleware
|
||||
}
|
||||
|
||||
t.Authboss.Core.Router.Get("/2fa/totp/remove", middleware(t.Core.ErrorHandler.Wrap(t.GetRemove)))
|
||||
t.Authboss.Core.Router.Post("/2fa/totp/remove", middleware(t.Core.ErrorHandler.Wrap(t.PostRemove)))
|
||||
t.Authboss.Core.Router.Get("/2fa/totp/setup", verified(t.GetSetup))
|
||||
t.Authboss.Core.Router.Post("/2fa/totp/setup", verified(t.PostSetup))
|
||||
|
||||
t.Authboss.Core.Router.Get("/2fa/totp/qr", verified(t.GetQRCode))
|
||||
|
||||
t.Authboss.Core.Router.Get("/2fa/totp/confirm", verified(t.GetConfirm))
|
||||
t.Authboss.Core.Router.Post("/2fa/totp/confirm", verified(t.PostConfirm))
|
||||
|
||||
t.Authboss.Core.Router.Get("/2fa/totp/remove", middleware(t.GetRemove))
|
||||
t.Authboss.Core.Router.Post("/2fa/totp/remove", middleware(t.PostRemove))
|
||||
|
||||
t.Authboss.Core.Router.Get("/2fa/totp/validate", t.Core.ErrorHandler.Wrap(t.GetValidate))
|
||||
t.Authboss.Core.Router.Post("/2fa/totp/validate", t.Core.ErrorHandler.Wrap(t.PostValidate))
|
||||
|
@ -1,14 +1,37 @@
|
||||
// Package twofactor allows authentication via one time passwords
|
||||
package twofactor
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
import "github.com/volatiletech/authboss"
|
||||
|
||||
"github.com/volatiletech/authboss"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
// Page constants
|
||||
const (
|
||||
PageRecovery2FA = "recovery2fa"
|
||||
PageVerify2FA = "twofactor_verify"
|
||||
PageVerifyEnd2FA = "twofactor_verify_end"
|
||||
)
|
||||
|
||||
// Email constants
|
||||
const (
|
||||
EmailVerifyHTML = "twofactor_verify_email_html"
|
||||
EmailVerifyTxt = "twofactor_verify_email_txt"
|
||||
)
|
||||
|
||||
// Form value constants
|
||||
const (
|
||||
FormValueToken = "token"
|
||||
)
|
||||
|
||||
// Data constants
|
||||
const (
|
||||
DataRecoveryCode = "recovery_code"
|
||||
DataRecoveryCodes = "recovery_codes"
|
||||
DataNumRecoveryCodes = "n_recovery_codes"
|
||||
DataVerifyURL = "url"
|
||||
)
|
||||
|
||||
const (
|
||||
alphabet = "abcdefghijklmnopqrstuvwxyz0123456789"
|
||||
recoveryCodeLength = 10
|
||||
verifyEmailTokenSize = 16
|
||||
)
|
||||
|
||||
// User interface
|
||||
@ -24,165 +47,3 @@ type User interface {
|
||||
// bcrypt'd recovery codes
|
||||
PutRecoveryCodes(codes string)
|
||||
}
|
||||
|
||||
// Page constants
|
||||
const (
|
||||
PageRecovery2FA = "recovery2fa"
|
||||
)
|
||||
|
||||
// Data constants
|
||||
const (
|
||||
DataRecoveryCode = "recovery_code"
|
||||
DataRecoveryCodes = "recovery_codes"
|
||||
DataNumRecoveryCodes = "n_recovery_codes"
|
||||
)
|
||||
|
||||
const (
|
||||
alphabet = "abcdefghijklmnopqrstuvwxyz0123456789"
|
||||
recoveryCodeLength = 10
|
||||
)
|
||||
|
||||
// Recovery for two-factor authentication is handled by this type
|
||||
type Recovery struct {
|
||||
*authboss.Authboss
|
||||
}
|
||||
|
||||
// Setup the module to provide recovery regeneration routes
|
||||
func (rc *Recovery) Setup() error {
|
||||
middleware := authboss.MountedMiddleware(rc.Authboss, true, rc.Authboss.Config.Modules.RoutesRedirectOnUnauthed, true, false)
|
||||
rc.Authboss.Core.Router.Get("/2fa/recovery/regen", middleware(rc.Authboss.Core.ErrorHandler.Wrap(rc.GetRegen)))
|
||||
rc.Authboss.Core.Router.Post("/2fa/recovery/regen", middleware(rc.Authboss.Core.ErrorHandler.Wrap(rc.PostRegen)))
|
||||
|
||||
return rc.Authboss.Core.ViewRenderer.Load(PageRecovery2FA)
|
||||
}
|
||||
|
||||
// GetRegen shows a button that enables a user to regen their codes
|
||||
// as well as how many codes are currently remaining.
|
||||
func (rc *Recovery) GetRegen(w http.ResponseWriter, r *http.Request) error {
|
||||
abUser, err := rc.CurrentUser(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
user := abUser.(User)
|
||||
|
||||
var nCodes int
|
||||
codes := user.GetRecoveryCodes()
|
||||
if len(codes) != 0 {
|
||||
nCodes++
|
||||
}
|
||||
for _, c := range codes {
|
||||
if c == ',' {
|
||||
nCodes++
|
||||
}
|
||||
}
|
||||
|
||||
data := authboss.HTMLData{DataNumRecoveryCodes: nCodes}
|
||||
return rc.Authboss.Core.Responder.Respond(w, r, http.StatusOK, PageRecovery2FA, data)
|
||||
}
|
||||
|
||||
// PostRegen regenerates the codes
|
||||
func (rc *Recovery) PostRegen(w http.ResponseWriter, r *http.Request) error {
|
||||
abUser, err := rc.CurrentUser(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
user := abUser.(User)
|
||||
|
||||
codes, err := GenerateRecoveryCodes()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
hashedCodes, err := BCryptRecoveryCodes(codes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
user.PutRecoveryCodes(EncodeRecoveryCodes(hashedCodes))
|
||||
if err = rc.Authboss.Config.Storage.Server.Save(r.Context(), user); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data := authboss.HTMLData{DataRecoveryCodes: codes}
|
||||
return rc.Authboss.Core.Responder.Respond(w, r, http.StatusOK, PageRecovery2FA, data)
|
||||
}
|
||||
|
||||
// GenerateRecoveryCodes creates 10 recovery codes of the form:
|
||||
// abd34-1b24do (using alphabet, of length recoveryCodeLength).
|
||||
func GenerateRecoveryCodes() ([]string, error) {
|
||||
byt := make([]byte, 10*recoveryCodeLength)
|
||||
if _, err := io.ReadFull(rand.Reader, byt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
codes := make([]string, 10)
|
||||
for i := range codes {
|
||||
builder := new(strings.Builder)
|
||||
for j := 0; j < recoveryCodeLength; j++ {
|
||||
if recoveryCodeLength/2 == j {
|
||||
builder.WriteByte('-')
|
||||
}
|
||||
|
||||
randNumber := byt[i*recoveryCodeLength+j] % byte(len(alphabet))
|
||||
builder.WriteByte(alphabet[randNumber])
|
||||
}
|
||||
codes[i] = builder.String()
|
||||
}
|
||||
|
||||
return codes, nil
|
||||
}
|
||||
|
||||
// BCryptRecoveryCodes hashes each recovery code given and return them in a new
|
||||
// slice.
|
||||
func BCryptRecoveryCodes(codes []string) ([]string, error) {
|
||||
cryptedCodes := make([]string, len(codes))
|
||||
for i, c := range codes {
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(c), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cryptedCodes[i] = string(hash)
|
||||
}
|
||||
|
||||
return cryptedCodes, nil
|
||||
}
|
||||
|
||||
// UseRecoveryCode deletes the code that was used from the string slice and
|
||||
// returns it, the bool is true if a code was used
|
||||
func UseRecoveryCode(codes []string, inputCode string) ([]string, bool) {
|
||||
input := []byte(inputCode)
|
||||
use := -1
|
||||
|
||||
for i, c := range codes {
|
||||
err := bcrypt.CompareHashAndPassword([]byte(c), input)
|
||||
if err == nil {
|
||||
use = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if use < 0 {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
ret := make([]string, len(codes)-1)
|
||||
for j := range codes {
|
||||
if j == use {
|
||||
continue
|
||||
}
|
||||
set := j
|
||||
if j > use {
|
||||
set--
|
||||
}
|
||||
ret[set] = codes[j]
|
||||
}
|
||||
|
||||
return ret, true
|
||||
}
|
||||
|
||||
// EncodeRecoveryCodes is an alias for strings.Join(",")
|
||||
func EncodeRecoveryCodes(codes []string) string { return strings.Join(codes, ",") }
|
||||
|
||||
// DecodeRecoveryCodes is an alias for strings.Split(",")
|
||||
func DecodeRecoveryCodes(codes string) []string { return strings.Split(codes, ",") }
|
||||
|
157
otp/twofactor/twofactor_recover.go
Normal file
157
otp/twofactor/twofactor_recover.go
Normal file
@ -0,0 +1,157 @@
|
||||
// Package twofactor allows authentication via one time passwords
|
||||
package twofactor
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/volatiletech/authboss"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// Recovery for two-factor authentication is handled by this type
|
||||
type Recovery struct {
|
||||
*authboss.Authboss
|
||||
}
|
||||
|
||||
// Setup the module to provide recovery regeneration routes
|
||||
func (rc *Recovery) Setup() error {
|
||||
middleware := authboss.MountedMiddleware(rc.Authboss, true, rc.Authboss.Config.Modules.RoutesRedirectOnUnauthed, true, false)
|
||||
rc.Authboss.Core.Router.Get("/2fa/recovery/regen", middleware(rc.Authboss.Core.ErrorHandler.Wrap(rc.GetRegen)))
|
||||
rc.Authboss.Core.Router.Post("/2fa/recovery/regen", middleware(rc.Authboss.Core.ErrorHandler.Wrap(rc.PostRegen)))
|
||||
|
||||
return rc.Authboss.Core.ViewRenderer.Load(PageRecovery2FA)
|
||||
}
|
||||
|
||||
// GetRegen shows a button that enables a user to regen their codes
|
||||
// as well as how many codes are currently remaining.
|
||||
func (rc *Recovery) GetRegen(w http.ResponseWriter, r *http.Request) error {
|
||||
abUser, err := rc.CurrentUser(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
user := abUser.(User)
|
||||
|
||||
var nCodes int
|
||||
codes := user.GetRecoveryCodes()
|
||||
if len(codes) != 0 {
|
||||
nCodes++
|
||||
}
|
||||
for _, c := range codes {
|
||||
if c == ',' {
|
||||
nCodes++
|
||||
}
|
||||
}
|
||||
|
||||
data := authboss.HTMLData{DataNumRecoveryCodes: nCodes}
|
||||
return rc.Authboss.Core.Responder.Respond(w, r, http.StatusOK, PageRecovery2FA, data)
|
||||
}
|
||||
|
||||
// PostRegen regenerates the codes
|
||||
func (rc *Recovery) PostRegen(w http.ResponseWriter, r *http.Request) error {
|
||||
abUser, err := rc.CurrentUser(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
user := abUser.(User)
|
||||
|
||||
codes, err := GenerateRecoveryCodes()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
hashedCodes, err := BCryptRecoveryCodes(codes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
user.PutRecoveryCodes(EncodeRecoveryCodes(hashedCodes))
|
||||
if err = rc.Authboss.Config.Storage.Server.Save(r.Context(), user); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data := authboss.HTMLData{DataRecoveryCodes: codes}
|
||||
return rc.Authboss.Core.Responder.Respond(w, r, http.StatusOK, PageRecovery2FA, data)
|
||||
}
|
||||
|
||||
// GenerateRecoveryCodes creates 10 recovery codes of the form:
|
||||
// abd34-1b24do (using alphabet, of length recoveryCodeLength).
|
||||
func GenerateRecoveryCodes() ([]string, error) {
|
||||
byt := make([]byte, 10*recoveryCodeLength)
|
||||
if _, err := io.ReadFull(rand.Reader, byt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
codes := make([]string, 10)
|
||||
for i := range codes {
|
||||
builder := new(strings.Builder)
|
||||
for j := 0; j < recoveryCodeLength; j++ {
|
||||
if recoveryCodeLength/2 == j {
|
||||
builder.WriteByte('-')
|
||||
}
|
||||
|
||||
randNumber := byt[i*recoveryCodeLength+j] % byte(len(alphabet))
|
||||
builder.WriteByte(alphabet[randNumber])
|
||||
}
|
||||
codes[i] = builder.String()
|
||||
}
|
||||
|
||||
return codes, nil
|
||||
}
|
||||
|
||||
// BCryptRecoveryCodes hashes each recovery code given and return them in a new
|
||||
// slice.
|
||||
func BCryptRecoveryCodes(codes []string) ([]string, error) {
|
||||
cryptedCodes := make([]string, len(codes))
|
||||
for i, c := range codes {
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(c), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cryptedCodes[i] = string(hash)
|
||||
}
|
||||
|
||||
return cryptedCodes, nil
|
||||
}
|
||||
|
||||
// UseRecoveryCode deletes the code that was used from the string slice and
|
||||
// returns it, the bool is true if a code was used
|
||||
func UseRecoveryCode(codes []string, inputCode string) ([]string, bool) {
|
||||
input := []byte(inputCode)
|
||||
use := -1
|
||||
|
||||
for i, c := range codes {
|
||||
err := bcrypt.CompareHashAndPassword([]byte(c), input)
|
||||
if err == nil {
|
||||
use = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if use < 0 {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
ret := make([]string, len(codes)-1)
|
||||
for j := range codes {
|
||||
if j == use {
|
||||
continue
|
||||
}
|
||||
set := j
|
||||
if j > use {
|
||||
set--
|
||||
}
|
||||
ret[set] = codes[j]
|
||||
}
|
||||
|
||||
return ret, true
|
||||
}
|
||||
|
||||
// EncodeRecoveryCodes is an alias for strings.Join(",")
|
||||
func EncodeRecoveryCodes(codes []string) string { return strings.Join(codes, ",") }
|
||||
|
||||
// DecodeRecoveryCodes is an alias for strings.Split(",")
|
||||
func DecodeRecoveryCodes(codes string) []string { return strings.Split(codes, ",") }
|
235
otp/twofactor/twofactor_verify.go
Normal file
235
otp/twofactor/twofactor_verify.go
Normal file
@ -0,0 +1,235 @@
|
||||
package twofactor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/subtle"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
|
||||
"github.com/volatiletech/authboss"
|
||||
)
|
||||
|
||||
// EmailVerify has a middleware function that prevents access to routes
|
||||
// unless e-mail has been verified.
|
||||
//
|
||||
// It does this by first setting where the user is coming from and generating
|
||||
// an e-mail with a random token. The token is stored in the session.
|
||||
//
|
||||
// When the user clicks the e-mail link with the token, the token is confirmed
|
||||
// by this middleware and the user is forwarded to the e-mail auth redirect.
|
||||
type EmailVerify struct {
|
||||
*authboss.Authboss
|
||||
|
||||
TwofactorKind string
|
||||
TwofactorSetupURL string
|
||||
}
|
||||
|
||||
// SetupEmailVerify registers routes for a particular 2fa method
|
||||
func SetupEmailVerify(ab *authboss.Authboss, twofactorKind, setupURL string) (EmailVerify, error) {
|
||||
e := EmailVerify{
|
||||
Authboss: ab,
|
||||
TwofactorKind: twofactorKind,
|
||||
TwofactorSetupURL: setupURL,
|
||||
}
|
||||
|
||||
middleware := authboss.MountedMiddleware(ab, true, ab.Config.Modules.RoutesRedirectOnUnauthed, true, false)
|
||||
e.Authboss.Core.Router.Get("/2fa/"+twofactorKind+"/email/verify", middleware(ab.Core.ErrorHandler.Wrap(e.GetStart)))
|
||||
e.Authboss.Core.Router.Post("/2fa/"+twofactorKind+"/email/verify", middleware(ab.Core.ErrorHandler.Wrap(e.PostStart)))
|
||||
|
||||
var routerMethod func(string, http.Handler)
|
||||
switch ab.Config.Modules.MailRouteMethod {
|
||||
case http.MethodGet:
|
||||
routerMethod = ab.Core.Router.Get
|
||||
case http.MethodPost:
|
||||
routerMethod = ab.Core.Router.Post
|
||||
default:
|
||||
return e, errors.New("MailRouteMethod must be set to something in the config")
|
||||
}
|
||||
routerMethod("/2fa/"+twofactorKind+"/email/verify/end", middleware(ab.Core.ErrorHandler.Wrap(e.End)))
|
||||
|
||||
if err := e.Authboss.Core.ViewRenderer.Load(PageVerify2FA); err != nil {
|
||||
return e, err
|
||||
}
|
||||
|
||||
return e, e.Authboss.Core.MailRenderer.Load(EmailVerifyHTML, EmailVerifyTxt)
|
||||
}
|
||||
|
||||
// GetStart shows the e-mail address and asks you to confirm that you would
|
||||
// like to proceed.
|
||||
func (e EmailVerify) GetStart(w http.ResponseWriter, r *http.Request) error {
|
||||
cu, err := e.Authboss.CurrentUser(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
user := cu.(User)
|
||||
|
||||
data := authboss.HTMLData{"email": user.GetEmail()}
|
||||
return e.Authboss.Core.Responder.Respond(w, r, http.StatusOK, PageVerify2FA, data)
|
||||
}
|
||||
|
||||
// PostStart sends an e-mail and shoves the user's token into the session
|
||||
func (e EmailVerify) PostStart(w http.ResponseWriter, r *http.Request) error {
|
||||
cu, err := e.Authboss.CurrentUser(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
user := cu.(User)
|
||||
ctx := r.Context()
|
||||
logger := e.Authboss.Logger(ctx)
|
||||
|
||||
token, err := GenerateToken()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
authboss.PutSession(w, authboss.Session2FAAuthToken, token)
|
||||
logger.Infof("generated new 2fa e-mail verify token for user: %s", user.GetPID())
|
||||
goVerifyEmail(e, ctx, user.GetEmail(), token)
|
||||
|
||||
ro := authboss.RedirectOptions{
|
||||
Code: http.StatusTemporaryRedirect,
|
||||
RedirectPath: e.Authboss.Config.Paths.TwoFactorEmailAuthNotOK,
|
||||
Success: "An e-mail has been sent to confirm 2FA activation.",
|
||||
}
|
||||
return e.Authboss.Config.Core.Redirector.Redirect(w, r, ro)
|
||||
}
|
||||
|
||||
// This is here so it can be mocked out by a test
|
||||
var goVerifyEmail = func(e EmailVerify, ctx context.Context, to, token string) {
|
||||
go e.SendVerifyEmail(ctx, to, token)
|
||||
}
|
||||
|
||||
// SendVerifyEmail to the user
|
||||
func (e EmailVerify) SendVerifyEmail(ctx context.Context, to, token string) {
|
||||
logger := e.Authboss.Logger(ctx)
|
||||
|
||||
mailURL := e.mailURL(token)
|
||||
|
||||
email := authboss.Email{
|
||||
To: []string{to},
|
||||
From: e.Config.Mail.From,
|
||||
FromName: e.Config.Mail.FromName,
|
||||
Subject: e.Config.Mail.SubjectPrefix + "Add 2FA to Account",
|
||||
}
|
||||
|
||||
logger.Infof("sending add 2fa verification e-mail to: %s", to)
|
||||
|
||||
ro := authboss.EmailResponseOptions{
|
||||
Data: authboss.NewHTMLData(DataVerifyURL, mailURL),
|
||||
HTMLTemplate: EmailVerifyHTML,
|
||||
TextTemplate: EmailVerifyTxt,
|
||||
}
|
||||
if err := e.Authboss.Email(ctx, email, ro); err != nil {
|
||||
logger.Errorf("failed to send 2fa verification e-mail to %s: %+v", to, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (e EmailVerify) mailURL(token string) string {
|
||||
query := url.Values{FormValueToken: []string{token}}
|
||||
|
||||
if len(e.Config.Mail.RootURL) != 0 {
|
||||
return fmt.Sprintf("%s?%s",
|
||||
e.Config.Mail.RootURL+"/2fa/"+e.TwofactorKind+"/email/verify/end",
|
||||
query.Encode())
|
||||
}
|
||||
|
||||
p := path.Join(e.Config.Paths.Mount, "/2fa/"+e.TwofactorKind+"/email/verify/end")
|
||||
return fmt.Sprintf("%s%s?%s", e.Config.Paths.RootURL, p, query.Encode())
|
||||
}
|
||||
|
||||
// End confirms the token passed in by the user (by the link in the e-mail)
|
||||
func (e EmailVerify) End(w http.ResponseWriter, r *http.Request) error {
|
||||
values, err := e.Authboss.Core.BodyReader.Read(PageVerifyEnd2FA, r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tokenValues := MustHaveEmailVerifyTokenValues(values)
|
||||
wantToken := tokenValues.GetToken()
|
||||
|
||||
givenToken, _ := authboss.GetSession(r, authboss.Session2FAAuthToken)
|
||||
|
||||
if 1 != subtle.ConstantTimeCompare([]byte(wantToken), []byte(givenToken)) {
|
||||
ro := authboss.RedirectOptions{
|
||||
Code: http.StatusTemporaryRedirect,
|
||||
Failure: "invalid 2fa e-mail verification token",
|
||||
RedirectPath: e.Authboss.Config.Paths.TwoFactorEmailAuthNotOK,
|
||||
}
|
||||
return e.Authboss.Core.Redirector.Redirect(w, r, ro)
|
||||
}
|
||||
|
||||
authboss.DelSession(w, authboss.Session2FAAuthToken)
|
||||
authboss.PutSession(w, authboss.Session2FAAuthed, "true")
|
||||
|
||||
ro := authboss.RedirectOptions{
|
||||
Code: http.StatusTemporaryRedirect,
|
||||
RedirectPath: e.TwofactorSetupURL,
|
||||
}
|
||||
return e.Authboss.Core.Redirector.Redirect(w, r, ro)
|
||||
}
|
||||
|
||||
// Wrap a route and stop it from being accessed unless the Session2FAAuthed
|
||||
// session value is "true".
|
||||
func (e EmailVerify) Wrap(handler http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if !e.Authboss.Config.Modules.TwoFactorEmailAuthRequired {
|
||||
handler.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// If this value exists the user's already verified
|
||||
authed, _ := authboss.GetSession(r, authboss.Session2FAAuthed)
|
||||
if authed == "true" {
|
||||
handler.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
redirURL := path.Join(e.Authboss.Config.Paths.Mount, "2fa", e.TwofactorKind, "email/verify")
|
||||
ro := authboss.RedirectOptions{
|
||||
Code: http.StatusTemporaryRedirect,
|
||||
Failure: "You must first authorize adding 2fa by e-mail.",
|
||||
RedirectPath: redirURL,
|
||||
}
|
||||
|
||||
if err := e.Authboss.Core.Redirector.Redirect(w, r, ro); err != nil {
|
||||
logger := e.Authboss.RequestLogger(r)
|
||||
logger.Errorf("failed to redirect client: %+v", err)
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// EmailVerifyTokenValuer returns a token from the body
|
||||
type EmailVerifyTokenValuer interface {
|
||||
authboss.Validator
|
||||
|
||||
GetToken() string
|
||||
}
|
||||
|
||||
// MustHaveEmailVerifyTokenValues upgrades a validatable set of values
|
||||
// to ones specific to a user that needs to be recovered.
|
||||
func MustHaveEmailVerifyTokenValues(v authboss.Validator) EmailVerifyTokenValuer {
|
||||
if u, ok := v.(EmailVerifyTokenValuer); ok {
|
||||
return u
|
||||
}
|
||||
|
||||
panic(fmt.Sprintf("bodyreader returned a type that could not be upgraded to an EmailVerifyTokenValues: %T", v))
|
||||
}
|
||||
|
||||
// GenerateToken used for authenticating e-mails for 2fa setup
|
||||
func GenerateToken() (string, error) {
|
||||
rawToken := make([]byte, verifyEmailTokenSize)
|
||||
if _, err := io.ReadFull(rand.Reader, rawToken); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return base64.URLEncoding.EncodeToString(rawToken), nil
|
||||
}
|
332
otp/twofactor/twofactor_verify_test.go
Normal file
332
otp/twofactor/twofactor_verify_test.go
Normal file
@ -0,0 +1,332 @@
|
||||
package twofactor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/volatiletech/authboss"
|
||||
"github.com/volatiletech/authboss/internal/mocks"
|
||||
)
|
||||
|
||||
func TestSetupEmailVerify(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
router := &mocks.Router{}
|
||||
renderer := &mocks.Renderer{}
|
||||
mailRenderer := &mocks.Renderer{}
|
||||
|
||||
ab := &authboss.Authboss{}
|
||||
ab.Config.Core.Router = router
|
||||
ab.Config.Core.ViewRenderer = renderer
|
||||
ab.Config.Core.MailRenderer = mailRenderer
|
||||
ab.Config.Core.ErrorHandler = &mocks.ErrorHandler{}
|
||||
|
||||
ab.Config.Modules.MailRouteMethod = http.MethodGet
|
||||
|
||||
if _, err := SetupEmailVerify(ab, "totp", "/2fa/totp/setup"); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if err := router.HasGets("/2fa/totp/email/verify", "/2fa/totp/email/verify/end"); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if err := router.HasPosts("/2fa/totp/email/verify"); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if err := renderer.HasLoadedViews(PageVerify2FA); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if err := mailRenderer.HasLoadedViews(EmailVerifyHTML, EmailVerifyTxt); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
type testEmailVerifyHarness struct {
|
||||
emailverify EmailVerify
|
||||
ab *authboss.Authboss
|
||||
|
||||
bodyReader *mocks.BodyReader
|
||||
mailer *mocks.Emailer
|
||||
responder *mocks.Responder
|
||||
renderer *mocks.Renderer
|
||||
redirector *mocks.Redirector
|
||||
session *mocks.ClientStateRW
|
||||
storer *mocks.ServerStorer
|
||||
}
|
||||
|
||||
func testEmailVerifySetup() *testEmailVerifyHarness {
|
||||
harness := &testEmailVerifyHarness{}
|
||||
|
||||
harness.ab = authboss.New()
|
||||
harness.bodyReader = &mocks.BodyReader{}
|
||||
harness.mailer = &mocks.Emailer{}
|
||||
harness.redirector = &mocks.Redirector{}
|
||||
harness.renderer = &mocks.Renderer{}
|
||||
harness.responder = &mocks.Responder{}
|
||||
harness.session = mocks.NewClientRW()
|
||||
harness.storer = mocks.NewServerStorer()
|
||||
|
||||
harness.ab.Config.Core.BodyReader = harness.bodyReader
|
||||
harness.ab.Config.Core.Logger = mocks.Logger{}
|
||||
harness.ab.Config.Core.Responder = harness.responder
|
||||
harness.ab.Config.Core.Redirector = harness.redirector
|
||||
harness.ab.Config.Core.Mailer = harness.mailer
|
||||
harness.ab.Config.Core.MailRenderer = harness.renderer
|
||||
harness.ab.Config.Storage.SessionState = harness.session
|
||||
harness.ab.Config.Storage.Server = harness.storer
|
||||
|
||||
harness.ab.Config.Modules.TwoFactorEmailAuthRequired = true
|
||||
|
||||
harness.emailverify = EmailVerify{
|
||||
Authboss: harness.ab,
|
||||
TwofactorKind: "totp",
|
||||
TwofactorSetupURL: "/2fa/totp/setup",
|
||||
}
|
||||
|
||||
return harness
|
||||
}
|
||||
|
||||
func (h *testEmailVerifyHarness) loadClientState(w http.ResponseWriter, r **http.Request) {
|
||||
req, err := h.ab.LoadClientState(w, *r)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
*r = req
|
||||
}
|
||||
|
||||
func (h *testEmailVerifyHarness) putUserInCtx(u *mocks.User, r **http.Request) {
|
||||
req := (*r).WithContext(context.WithValue((*r).Context(), authboss.CTXKeyUser, u))
|
||||
*r = req
|
||||
}
|
||||
|
||||
func TestEmailVerifyGetStart(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
h := testEmailVerifySetup()
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
r := mocks.Request("GET")
|
||||
w := h.ab.NewResponse(rec)
|
||||
|
||||
u := &mocks.User{Email: "test@test.com"}
|
||||
h.putUserInCtx(u, &r)
|
||||
h.loadClientState(w, &r)
|
||||
|
||||
if err := h.emailverify.GetStart(w, r); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if got := h.responder.Data["email"]; got != "test@test.com" {
|
||||
t.Error("email was wrong:", got)
|
||||
}
|
||||
|
||||
if got := h.responder.Page; got != PageVerify2FA {
|
||||
t.Error("page was wrong:", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmailVerifyPostStart(t *testing.T) {
|
||||
// NOT t.Parallel()
|
||||
h := testEmailVerifySetup()
|
||||
|
||||
save := goVerifyEmail
|
||||
goVerifyEmail = func(e EmailVerify, ctx context.Context, to string, token string) {
|
||||
e.SendVerifyEmail(ctx, to, token)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
goVerifyEmail = save
|
||||
}()
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
r := mocks.Request("POST")
|
||||
w := h.ab.NewResponse(rec)
|
||||
|
||||
u := &mocks.User{Email: "test@test.com"}
|
||||
h.putUserInCtx(u, &r)
|
||||
h.loadClientState(w, &r)
|
||||
|
||||
if err := h.emailverify.PostStart(w, r); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ro := h.redirector.Options
|
||||
if ro.Code != http.StatusTemporaryRedirect {
|
||||
t.Error("code wrong:", ro.Code)
|
||||
}
|
||||
|
||||
if ro.Success != "An e-mail has been sent to confirm 2FA activation." {
|
||||
t.Error("message was wrong:", ro.Success)
|
||||
}
|
||||
|
||||
mail := h.mailer.Email
|
||||
if mail.To[0] != "test@test.com" {
|
||||
t.Error("email was sent to wrong person:", mail.To)
|
||||
}
|
||||
|
||||
if mail.Subject != "Add 2FA to Account" {
|
||||
t.Error("subject wrong:", mail.Subject)
|
||||
}
|
||||
|
||||
urlRgx := regexp.MustCompile(`^http://localhost:8080/auth/2fa/totp/email/verify/end\?token=[_a-zA-Z0-9=%]+$`)
|
||||
|
||||
data := h.renderer.Data
|
||||
if !urlRgx.MatchString(data[DataVerifyURL].(string)) {
|
||||
t.Error("url is wrong:", data[DataVerifyURL])
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmailVerifyEnd(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
h := testEmailVerifySetup()
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
r := mocks.Request("POST")
|
||||
w := h.ab.NewResponse(rec)
|
||||
|
||||
h.bodyReader.Return = mocks.Values{Token: "abc"}
|
||||
|
||||
h.session.ClientValues[authboss.Session2FAAuthToken] = "abc"
|
||||
h.loadClientState(w, &r)
|
||||
|
||||
if err := h.emailverify.End(w, r); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
ro := h.redirector.Options
|
||||
if ro.Code != http.StatusTemporaryRedirect {
|
||||
t.Error("code wrong:", ro.Code)
|
||||
}
|
||||
|
||||
if ro.RedirectPath != "/2fa/totp/setup" {
|
||||
t.Error("redir path wrong:", ro.RedirectPath)
|
||||
}
|
||||
|
||||
// Flush session state
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
||||
if h.session.ClientValues[authboss.Session2FAAuthed] != "true" {
|
||||
t.Error("authed value not set")
|
||||
}
|
||||
|
||||
if h.session.ClientValues[authboss.Session2FAAuthToken] != "" {
|
||||
t.Error("auth token not removed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmailVerifyEndFail(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
h := testEmailVerifySetup()
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
r := mocks.Request("POST")
|
||||
w := h.ab.NewResponse(rec)
|
||||
|
||||
h.bodyReader.Return = mocks.Values{Token: "abc"}
|
||||
|
||||
h.session.ClientValues[authboss.Session2FAAuthToken] = "notabc"
|
||||
h.loadClientState(w, &r)
|
||||
|
||||
if err := h.emailverify.End(w, r); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
ro := h.redirector.Options
|
||||
if ro.Code != http.StatusTemporaryRedirect {
|
||||
t.Error("code wrong:", ro.Code)
|
||||
}
|
||||
|
||||
if ro.RedirectPath != "/" {
|
||||
t.Error("redir path wrong:", ro.RedirectPath)
|
||||
}
|
||||
|
||||
if ro.Failure != "invalid 2fa e-mail verification token" {
|
||||
t.Error("did not get correct failure")
|
||||
}
|
||||
|
||||
if h.session.ClientValues[authboss.Session2FAAuthed] != "" {
|
||||
t.Error("should not be authed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmailVerifyWrap(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("NotRequired", func(t *testing.T) {
|
||||
h := testEmailVerifySetup()
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
r := mocks.Request("POST")
|
||||
w := h.ab.NewResponse(rec)
|
||||
|
||||
h.ab.Config.Modules.TwoFactorEmailAuthRequired = false
|
||||
|
||||
called := false
|
||||
server := h.emailverify.Wrap(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
called = true
|
||||
}))
|
||||
|
||||
server.ServeHTTP(w, r)
|
||||
if !called {
|
||||
t.Error("should have called the handler")
|
||||
}
|
||||
})
|
||||
t.Run("Success", func(t *testing.T) {
|
||||
h := testEmailVerifySetup()
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
r := mocks.Request("POST")
|
||||
w := h.ab.NewResponse(rec)
|
||||
|
||||
h.session.ClientValues[authboss.Session2FAAuthed] = "true"
|
||||
h.loadClientState(w, &r)
|
||||
|
||||
called := false
|
||||
server := h.emailverify.Wrap(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
called = true
|
||||
}))
|
||||
|
||||
server.ServeHTTP(w, r)
|
||||
if !called {
|
||||
t.Error("should have called the handler")
|
||||
}
|
||||
})
|
||||
t.Run("Fail", func(t *testing.T) {
|
||||
h := testEmailVerifySetup()
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
r := mocks.Request("POST")
|
||||
w := h.ab.NewResponse(rec)
|
||||
|
||||
called := false
|
||||
server := h.emailverify.Wrap(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
called = true
|
||||
}))
|
||||
|
||||
server.ServeHTTP(w, r)
|
||||
if called {
|
||||
t.Error("should not have called the handler")
|
||||
}
|
||||
|
||||
ro := h.redirector.Options
|
||||
if ro.Code != http.StatusTemporaryRedirect {
|
||||
t.Error("code wrong:", ro.Code)
|
||||
}
|
||||
|
||||
if ro.RedirectPath != "/auth/2fa/totp/email/verify" {
|
||||
t.Error("redir path wrong:", ro.RedirectPath)
|
||||
}
|
||||
|
||||
if ro.Failure != "You must first authorize adding 2fa by e-mail." {
|
||||
t.Error("did not get correct failure")
|
||||
}
|
||||
})
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user