1
0
mirror of https://github.com/volatiletech/authboss.git synced 2024-11-28 08:58:38 +02:00

Add totp2fa module

This commit is contained in:
Aaron L 2018-08-22 21:34:38 -07:00
parent 9aed0c512d
commit 735cbb1ec5
9 changed files with 742 additions and 18 deletions

View File

@ -89,7 +89,7 @@ func (a *Authboss) UpdatePassword(ctx context.Context, user AuthableUser, newPas
//
// If allowHalfAuth is true then half-authed users are allowed through, otherwise a half-authed
// user will not be allowed through.
func Middleware(ab *Authboss, redirectToLogin bool, allowHalfAuth bool) func(http.Handler) http.Handler {
func Middleware(ab *Authboss, redirectToLogin bool, allowHalfAuth bool, force2fa bool) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log := ab.RequestLogger(r)
@ -116,7 +116,7 @@ func Middleware(ab *Authboss, redirectToLogin bool, allowHalfAuth bool) func(htt
w.WriteHeader(http.StatusNotFound)
}
if !allowHalfAuth && !IsFullyAuthed(r) {
if !allowHalfAuth && !IsFullyAuthed(r) || !force2fa && !IsTwoFactored(r) {
fail(w, r)
return
}

View File

@ -15,6 +15,8 @@ const (
SessionHalfAuthKey = "halfauth"
// SessionLastAction is the session key to retrieve the last action of a user.
SessionLastAction = "last_action"
// Session2FA is set when a user has been authenticated with a second factor
Session2FA = "twofactor"
// SessionOAuth2State is the xsrf protection key for oauth.
SessionOAuth2State = "oauth2_state"
// SessionOAuth2Params is the additional settings for oauth like redirection/remember.
@ -221,13 +223,20 @@ func (c *ClientStateResponseWriter) putClientState() error {
return nil
}
// IsFullyAuthed returns false if the user has a HalfAuth
// IsFullyAuthed returns false if the user has a SessionHalfAuth
// in his session.
func IsFullyAuthed(r *http.Request) bool {
_, hasHalfAuth := GetSession(r, SessionHalfAuthKey)
return !hasHalfAuth
}
// IsTwoFactored returns false if the user doesn't have a Session2FA
// in his session.
func IsTwoFactored(r *http.Request) bool {
_, has2fa := GetSession(r, Session2FA)
return has2fa
}
// DelKnownSession deletes all known session variables, effectively
// logging a user out.
func DelKnownSession(w http.ResponseWriter) {

View File

@ -85,6 +85,10 @@ type Config struct {
// OAuth2Providers lists all providers that can be used. See
// OAuthProvider documentation for more details.
OAuth2Providers map[string]OAuth2Provider
// TOTP2FAIssuer is the issuer that appears in the url when scanning a qr code
// for google authenticator.
TOTP2FAIssuer string
}
Mail struct {

View File

@ -76,7 +76,7 @@ func (c *Confirm) PreventAuth(w http.ResponseWriter, r *http.Request, handled bo
cuser := authboss.MustBeConfirmable(user)
if cuser.GetConfirmed() {
logger.Infof("user %s was confirmed, allowing auth", user.GetPID())
logger.Infof("user %s is confirmed, allowing auth", user.GetPID())
return false, nil
}

View File

@ -19,6 +19,7 @@ const (
FormValueConfirm = "cnf"
FormValueToken = "token"
FormValueCode = "code"
)
// UserValues from the login form
@ -92,6 +93,16 @@ func (r RecoverEndValues) GetToken() string { return r.Token }
// GetPassword for recovery
func (r RecoverEndValues) GetPassword() string { return r.NewPassword }
// TwoFA for totp2fa_validate page
type TwoFA struct {
HTTPFormValidator
Code string
}
// GetCode from authenticator
func (r TwoFA) GetCode() string { return r.Code }
// HTTPBodyReader reads forms from various pages and decodes
// them.
type HTTPBodyReader struct {
@ -233,6 +244,11 @@ func (h HTTPBodyReader) Read(page string, r *http.Request) (authboss.Validator,
Token: values[FormValueToken],
NewPassword: values[FormValuePassword],
}, nil
case "totp2fa_validate":
return TwoFA{
HTTPFormValidator: HTTPFormValidator{Values: values, Ruleset: rules, ConfirmFields: confirms},
Code: values[FormValueCode],
}, nil
case "register":
arbitrary := make(map[string]string)

View File

@ -0,0 +1,459 @@
// Package totp2fa implements two factor auth using time-based
// one time passwords.
package totp2fa
import (
"bytes"
"crypto/rand"
"fmt"
"image/png"
"io"
"net/http"
"net/url"
"strings"
"golang.org/x/crypto/bcrypt"
"github.com/pkg/errors"
"github.com/pquerna/otp"
"github.com/pquerna/otp/totp"
"github.com/volatiletech/authboss"
"github.com/volatiletech/authboss/otp/twofactor"
)
const (
otpKeyFormat = "otpauth://totp/%s:%s?issuer=%s&secret=%s"
)
// Session keys
const (
SessionTOTPSecret = "totp_secret"
SessionTOTPPendingPID = "totp_pending"
)
// Pages
const (
PageTOTPValidate = "totp2fa_validate"
PageTOTPValidateSuccess = "totp2fa_validate_success"
)
// Data constants
const (
DataRecoveryCodes = "recovery_codes"
DataValidateMode = "validate_mode"
DataTOTPSecret = SessionTOTPSecret
dataValidate = "validate"
dataValidateSetup = "setup"
dataValidateConfirm = "confirm"
dataValidateRemove = "remove"
)
var (
errNoTOTPEnabled = errors.New("user does not have 2fa enabled")
)
// User for TOTP
type User interface {
twofactor.User
GetTOTPSecretKey() string
PutTOTPSecretKey(string)
}
// TOTP implements time based one time passwords
type TOTP struct {
*authboss.Authboss
}
// Setup the module
func (t *TOTP) Setup() error {
t.Authboss.Core.Router.Get("/2fa/setup/totp", t.Core.ErrorHandler.Wrap(t.GetSetup))
t.Authboss.Core.Router.Post("/2fa/setup/totp", t.Core.ErrorHandler.Wrap(t.PostSetup))
t.Authboss.Core.Router.Get("/2fa/qr/totp", t.Core.ErrorHandler.Wrap(t.GetQRCode))
t.Authboss.Core.Router.Get("/2fa/confirm/totp", t.Core.ErrorHandler.Wrap(t.GetConfirm))
t.Authboss.Core.Router.Post("/2fa/confirm/totp", t.Core.ErrorHandler.Wrap(t.PostConfirm))
t.Authboss.Core.Router.Get("/2fa/remove/totp", t.Core.ErrorHandler.Wrap(t.GetRemove))
t.Authboss.Core.Router.Post("/2fa/remove/totp", t.Core.ErrorHandler.Wrap(t.PostRemove))
t.Authboss.Core.Router.Get("/2fa/validate/totp", t.Core.ErrorHandler.Wrap(t.GetValidate))
t.Authboss.Core.Router.Post("/2fa/validate/totp", t.Core.ErrorHandler.Wrap(t.PostValidate))
t.Authboss.Events.Before(authboss.EventAuth, t.BeforeAuth)
return t.Authboss.Core.ViewRenderer.Load(PageTOTPValidate, PageTOTPValidateSuccess)
}
// BeforeAuth stores the user's pid in a special temporary session variable
// and redirects them to the validation endpoint.
func (t *TOTP) BeforeAuth(w http.ResponseWriter, r *http.Request, handled bool) (bool, error) {
if handled {
return false, nil
}
user := r.Context().Value(authboss.CTXKeyUser).(User)
authboss.PutSession(w, SessionTOTPPendingPID, user.GetPID())
if len(user.GetTOTPSecretKey()) == 0 {
return false, nil
}
var query string
if len(r.URL.RawQuery) != 0 {
query = "?" + r.URL.RawQuery
}
ro := authboss.RedirectOptions{
Code: http.StatusTemporaryRedirect,
RedirectPath: t.Paths.Mount + "/2fa/validate/totp" + query,
}
return true, t.Authboss.Config.Core.Redirector.Redirect(w, r, ro)
}
// GetSetup shows a screen allows a user to opt in to setting up totp 2fa
func (t *TOTP) GetSetup(w http.ResponseWriter, r *http.Request) error {
data := authboss.HTMLData{DataValidateMode: dataValidateSetup}
return t.Core.Responder.Respond(w, r, http.StatusOK, PageTOTPValidate, data)
}
// PostSetup prepares adds a key to the user's session
func (t *TOTP) PostSetup(w http.ResponseWriter, r *http.Request) error {
abUser, err := t.CurrentUser(r)
if err != nil {
return err
}
user := abUser.(User)
key, err := totp.Generate(totp.GenerateOpts{
Issuer: t.Authboss.Config.Modules.TOTP2FAIssuer,
AccountName: user.GetEmail(),
})
if err != nil {
return errors.Wrap(err, "failed to create a totp key")
}
secret := key.Secret()
authboss.PutSession(w, SessionTOTPSecret, secret)
ro := authboss.RedirectOptions{
Code: http.StatusTemporaryRedirect,
RedirectPath: t.Paths.Mount + "/2fa/confirm/totp",
}
return t.Core.Redirector.Redirect(w, r, ro)
}
// GetQRCode responds with a QR code image
func (t *TOTP) GetQRCode(w http.ResponseWriter, r *http.Request) error {
abUser, err := t.CurrentUser(r)
if err != nil {
return err
}
user := abUser.(User)
totpSecret, ok := authboss.GetSession(r, SessionTOTPSecret)
var key *otp.Key
if !ok || len(totpSecret) == 0 {
totpSecret = user.GetTOTPSecretKey()
}
key, err = otp.NewKeyFromURL(
fmt.Sprintf(otpKeyFormat,
url.PathEscape(t.Authboss.Config.Modules.TOTP2FAIssuer),
url.PathEscape(user.GetEmail()),
url.QueryEscape(t.Authboss.Config.Modules.TOTP2FAIssuer),
url.QueryEscape(totpSecret),
))
if err != nil {
return errors.Wrap(err, "failed to reconstruct key from session key: %s")
}
image, err := key.Image(200, 200)
if err != nil {
return errors.Wrap(err, "failed to create totp qr code")
}
buf := &bytes.Buffer{}
if err = png.Encode(buf, image); err != nil {
return errors.Wrap(err, "failed to encode qr code to png")
}
w.Header().Set("Content-Type", "image/png")
w.WriteHeader(http.StatusOK)
_, err = io.Copy(w, buf)
return err
}
// GetConfirm requests a user to enter their totp code
func (t *TOTP) GetConfirm(w http.ResponseWriter, r *http.Request) error {
totpSecret, ok := authboss.GetSession(r, SessionTOTPSecret)
if !ok {
return errors.New("request failed, no totp secret present in session")
}
data := authboss.HTMLData{
DataValidateMode: dataValidateConfirm,
DataTOTPSecret: totpSecret,
}
return t.Core.Responder.Respond(w, r, http.StatusOK, PageTOTPValidate, data)
}
// PostConfirm finally activates totp if the code matches
func (t *TOTP) PostConfirm(w http.ResponseWriter, r *http.Request) error {
abUser, err := t.CurrentUser(r)
if err != nil {
return err
}
user := abUser.(User)
totpSecret, ok := authboss.GetSession(r, SessionTOTPSecret)
if !ok {
return errors.New("request failed, no totp secret present in session")
}
validator, err := t.Authboss.Config.Core.BodyReader.Read(PageTOTPValidate, r)
if err != nil {
return err
}
totpCodeValues := MustHaveTOTPCodeValues(validator)
inputCode := totpCodeValues.GetCode()
ok = totp.Validate(inputCode, totpSecret)
if !ok {
data := authboss.HTMLData{
authboss.DataValidation: map[string][]string{"code": []string{"2fa code was invalid"}},
DataValidateMode: dataValidateConfirm,
DataTOTPSecret: totpSecret,
}
return t.Authboss.Core.Responder.Respond(w, r, http.StatusOK, PageTOTPValidate, data)
}
codes, err := generateRecoveryCodes()
if err != nil {
return err
}
crypted, err := bcryptRecoveryCodes(codes)
if err != nil {
return err
}
// Save the user which activates 2fa
user.PutTOTPSecretKey(totpSecret)
user.PutRecoveryCodes(encodeRecoveryCodes(crypted))
if err = t.Authboss.Config.Storage.Server.Save(r.Context(), user); err != nil {
return err
}
authboss.DelSession(w, SessionTOTPSecret)
logger := t.RequestLogger(r)
logger.Infof("user %s enabled totp", user.GetPID())
data := authboss.HTMLData{
DataRecoveryCodes: codes,
DataValidateMode: dataValidateConfirm,
}
return t.Authboss.Core.Responder.Respond(w, r, http.StatusOK, PageTOTPValidateSuccess, data)
}
// GetRemove starts removal
func (t *TOTP) GetRemove(w http.ResponseWriter, r *http.Request) error {
data := authboss.HTMLData{DataValidateMode: dataValidateRemove}
return t.Authboss.Core.Responder.Respond(w, r, http.StatusOK, PageTOTPValidate, data)
}
// PostRemove removes totp
func (t *TOTP) PostRemove(w http.ResponseWriter, r *http.Request) error {
user, ok, err := t.validate(r)
switch {
case err == errNoTOTPEnabled:
data := authboss.HTMLData{
authboss.DataErr: "totp 2fa not active",
DataValidateMode: dataValidateRemove,
}
return t.Authboss.Core.Responder.Respond(w, r, http.StatusOK, PageTOTPValidate, data)
case err != nil:
return err
case !ok:
data := authboss.HTMLData{
authboss.DataErr: "totp 2fa code incorrect",
DataValidateMode: dataValidateRemove,
}
return t.Authboss.Core.Responder.Respond(w, r, http.StatusOK, PageTOTPValidate, data)
}
user.PutTOTPSecretKey("")
if err = t.Authboss.Config.Storage.Server.Save(r.Context(), user); err != nil {
return err
}
logger := t.RequestLogger(r)
logger.Infof("user %s disabled totp", user.GetPID())
data := authboss.HTMLData{DataValidateMode: dataValidateRemove}
return t.Authboss.Core.Responder.Respond(w, r, http.StatusOK, PageTOTPValidateSuccess, data)
}
// GetValidate shows a page to enter a code into
func (t *TOTP) GetValidate(w http.ResponseWriter, r *http.Request) error {
data := authboss.HTMLData{DataValidateMode: dataValidate}
return t.Authboss.Core.Responder.Respond(w, r, http.StatusOK, PageTOTPValidate, data)
}
// PostValidate redirects on success
func (t *TOTP) PostValidate(w http.ResponseWriter, r *http.Request) error {
logger := t.RequestLogger(r)
user, ok, err := t.validate(r)
switch {
case err == errNoTOTPEnabled:
logger.Infof("user %s totp failure (not enabled)", user.GetPID())
data := authboss.HTMLData{
authboss.DataErr: "totp 2fa not active",
DataValidateMode: dataValidate,
}
return t.Authboss.Core.Responder.Respond(w, r, http.StatusOK, PageTOTPValidate, data)
case err != nil:
return err
case !ok:
logger.Infof("user %s totp failure (wrong code)", user.GetPID())
data := authboss.HTMLData{
authboss.DataErr: "totp 2fa code incorrect",
DataValidateMode: dataValidate,
}
return t.Authboss.Core.Responder.Respond(w, r, http.StatusOK, PageTOTPValidate, data)
}
authboss.PutSession(w, authboss.SessionKey, user.GetPID())
authboss.PutSession(w, authboss.Session2FA, "true")
authboss.DelSession(w, authboss.SessionHalfAuthKey)
authboss.DelSession(w, SessionTOTPPendingPID)
authboss.DelSession(w, SessionTOTPSecret)
logger.Infof("user %s totp success", user.GetPID())
ro := authboss.RedirectOptions{
Code: http.StatusTemporaryRedirect,
Success: "successfully authenticated",
RedirectPath: t.Authboss.Config.Paths.AuthLoginOK,
FollowRedirParam: true,
}
return t.Authboss.Core.Redirector.Redirect(w, r, ro)
}
func (t *TOTP) validate(r *http.Request) (User, bool, error) {
var abUser authboss.User
var err error
if pid, ok := authboss.GetSession(r, SessionTOTPPendingPID); ok && len(pid) != 0 {
abUser, err = t.Authboss.Config.Storage.Server.Load(r.Context(), pid)
} else {
abUser, err = t.CurrentUser(r)
}
if err != nil {
return nil, false, err
}
user := abUser.(User)
secret := user.GetTOTPSecretKey()
if len(secret) == 0 {
return nil, false, errNoTOTPEnabled
}
validator, err := t.Authboss.Config.Core.BodyReader.Read(PageTOTPValidate, r)
if err != nil {
return nil, false, err
}
totpCodeValues := MustHaveTOTPCodeValues(validator)
input := totpCodeValues.GetCode()
return user, totp.Validate(input, secret), nil
}
const alphabet = "abcdefghijklmnopqrstuvwxyz0123456789"
const recoveryCodeLength = 10
// generateRecoveryCodes creates 10 recovery codes of the form:
// abd34-1b24do
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
}
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
}
func encodeRecoveryCodes(codes []string) string { return strings.Join(codes, ",") }
func decodeRecoveryCodes(codes string) []string { return strings.Split(codes, ",") }

View File

@ -0,0 +1,24 @@
package totp2fa
import (
"fmt"
"github.com/volatiletech/authboss"
)
// TOTPCodeValuer returns a code from the body
type TOTPCodeValuer interface {
authboss.Validator
GetCode() string
}
// MustHaveTOTPCodeValues upgrades a validatable set of values
// to ones specific to a user that needs to be recovered.
func MustHaveTOTPCodeValues(v authboss.Validator) TOTPCodeValuer {
if u, ok := v.(TOTPCodeValuer); ok {
return u
}
panic(fmt.Sprintf("bodyreader returned a type that could not be upgraded to TOTPCodeValuer: %T", v))
}

View File

@ -0,0 +1,114 @@
package totp2fa
import (
"regexp"
"strings"
"testing"
)
func TestGenerateRecoveryCodes(t *testing.T) {
t.Parallel()
codes, err := generateRecoveryCodes()
if err != nil {
t.Fatal(err)
}
if len(codes) != 10 {
t.Error("it should create 10 codes, got:", len(codes))
}
rgx := regexp.MustCompile(`^[0-9a-z]{5}-[0-9a-z]{5}$`)
for _, c := range codes {
if !rgx.MatchString(c) {
t.Errorf("code %s did not match regexp", c)
}
}
}
func TestHashRecoveryCodes(t *testing.T) {
t.Parallel()
codes, err := generateRecoveryCodes()
if err != nil {
t.Fatal(err)
}
if len(codes) != 10 {
t.Error("it should create 10 codes, got:", len(codes))
}
cryptedCodes, err := bcryptRecoveryCodes(codes)
if err != nil {
t.Fatal(err)
}
for _, c := range cryptedCodes {
if !strings.HasPrefix(c, "$2a$10$") {
t.Error("code did not look like bcrypt:", c)
}
}
}
func TestUseRecoveryCode(t *testing.T) {
t.Parallel()
codes, err := generateRecoveryCodes()
if err != nil {
t.Fatal(err)
}
if len(codes) != 10 {
t.Error("it should create 10 codes, got:", len(codes))
}
cryptedCodes, err := bcryptRecoveryCodes(codes)
if err != nil {
t.Fatal(err)
}
for _, c := range cryptedCodes {
if !strings.HasPrefix(c, "$2a$10$") {
t.Error("code did not look like bcrypt:", c)
}
}
remaining, ok := useRecoveryCode(cryptedCodes, codes[4])
if !ok {
t.Error("should have used a code")
}
if want, got := len(cryptedCodes)-1, len(remaining); want != got {
t.Error("want:", want, "got:", got)
}
if cryptedCodes[4] == remaining[4] {
t.Error("it should have used number 4")
}
remaining, ok = useRecoveryCode(remaining, codes[0])
if !ok {
t.Error("should have used a code")
}
if want, got := len(cryptedCodes)-2, len(remaining); want != got {
t.Error("want:", want, "got:", got)
}
if cryptedCodes[0] == remaining[0] {
t.Error("it should have used number 0")
}
remaining, ok = useRecoveryCode(remaining, codes[len(codes)-1])
if !ok {
t.Error("should have used a code")
}
if want, got := len(cryptedCodes)-3, len(remaining); want != got {
t.Error("want:", want, "got:", got)
}
if cryptedCodes[len(cryptedCodes)-1] == remaining[len(remaining)-1] {
t.Error("it should have used number 0")
}
}

View File

@ -1,20 +1,8 @@
// Package otp allows authentication via one time passwords
package otp
// Package twofactor allows authentication via one time passwords
package twofactor
import "github.com/volatiletech/authboss"
// Authenticator is a type that implements the basic functionality
// to be able to authenticate via a one time password.
type Authenticator interface {
// Initialize by giving the user a way to enter the first otp
Setup(User)
// Verify the otp for the user
Verify(User, string)
// Remove otp for the user, requires the user be fully authenticated
// and have authenticated with a one time password.
Remove(User, string)
}
// User interface
type User interface {
authboss.User
@ -27,3 +15,113 @@ type User interface {
// PutRecoveryCodes uses a single string to store many bcrypt'd recovery codes
PutRecoveryCodes(codes string)
}
// TOTPUser interface
type TOTPUser interface {
User
GetTOTPSecretKey() string
PutTOTPSecretKey(string)
}
// SMSUser interface
type SMSUser interface {
User
GetPhoneNumber() string
PutPhoneNumber(string)
}
// SMSPhoneGetter retrieves an initial phone number
// to use as the SMS 2fa number.
type SMSPhoneGetter interface {
GetInitialPhoneNumber() string
}
/*
GET /2fa/setup/{sms,totp}
POST /2fa/setup/{sms,totp}
- sms:
- send a 6-8 digit code to the users's phone number
- save this temporary code in the session for the next API call
- totp:
- generate a private key and store it, temporarily in session
GET /2fa/qr/{sms,totp}
- totp:
- send back an image of the secret key that's in the session, fallback to the database
GET /2fa/confirm/{sms,totp}
POST /2fa/confirm/{sms,totp}
- totp:
- post the 2fa code, this finalizes the secret key in the session by storing it into the database
- generate and save 10 recovery codes, return in data
- sms:
- post the sms code delivered to your phone, to finalize that sms phone number
- generate and save 10 recovery codes, return in data
GET /2fa/remove/{sms,totp}
- totp:
- ask for a code
- sms:
- send code to fone
POST /2fa/remove/{sms,totp}
- totp:
- if code matches, remove 2fa
- sms:
- if code matches, remove 2fa
GET /2fa/recovery DOES NOT EXIST LOL, WAT 2 SHO?
- show recovery codes
POST /2fa/recovery/regenerate
- regenerate 10 recovery codes and display them
*/
/*
// Authenticator is a type that implements the basic functionality
// to be able to authenticate via a one time password as a second factor.
type Authenticator interface {
// Setup a secret and generate a code from it so that the 2fa method can be attached
// to the user if they correctly pass back the code. The secret itself
// is stored in the session and will be passed back to be stored on the
// user object in the enable step.
Setup(User) (code string, secret string, err error)
// Enable 2fa on the user, requires the code produced
// by the secret and the secret itself that will have come
// from Setup.
Enable(user User, code string, secret string) error
// Teardown prepares to disable 2fa on the user.
Teardown(User) (code string, err error)
// Disable 2fa on the user, requires a code sent by teardown
// or in some cases that the user will already know.
Disable(user User, code string, secret string) error
// IsActive checks if this authenticator is active on the current user
// This is to ensure that only one authentication method is active at a time
IsActive(User) bool
}
// Authenticator is the basic functionality for a second factor authenticator
type Authenticator interface {
Secret(User) (secret string, err error)
Code(user User, secret string) (code string, err error)
Verify(user User, code, secret string) error
Enabled(User) bool
Enable(User) error
Disable(User) error
}
// QRAuthenticator is able to provide a QR code to represent it's secrets
type QRAuthenticator interface {
// Returns a file as []byte and a mime type
QRCode(User) ([]byte, string)
}
*/