mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2025-10-28 15:42:24 -05:00
[feature] 2fa management via CLI (#4368)
Adds 2FA management to the admin CLI. Also does some CLI refactoring so the functions we pass around are exported functions instead of changeable global variables. closes https://codeberg.org/superseriousbusiness/gotosocial/issues/4320 Reviewed-on: https://codeberg.org/superseriousbusiness/gotosocial/pulls/4368 Co-authored-by: kim <grufwub@gmail.com> Co-committed-by: kim <grufwub@gmail.com>
This commit is contained in:
parent
1edc0f7b3c
commit
7f8cb204cd
15 changed files with 224 additions and 113 deletions
|
|
@ -29,12 +29,25 @@ import (
|
|||
"code.superseriousbusiness.org/gotosocial/internal/db/bundb"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/log"
|
||||
userprocessor "code.superseriousbusiness.org/gotosocial/internal/processing/user"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/state"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/util"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/validate"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
var (
|
||||
// check function conformance
|
||||
_ action.GTSAction = Create
|
||||
_ action.GTSAction = List
|
||||
_ action.GTSAction = Confirm
|
||||
_ action.GTSAction = Promote
|
||||
_ action.GTSAction = Demote
|
||||
_ action.GTSAction = Enable
|
||||
_ action.GTSAction = Disable
|
||||
_ action.GTSAction = Password
|
||||
)
|
||||
|
||||
func initState(ctx context.Context) (*state.State, error) {
|
||||
var state state.State
|
||||
state.Caches.Init()
|
||||
|
|
@ -61,7 +74,7 @@ func stopState(state *state.State) error {
|
|||
|
||||
// Create creates a new account and user
|
||||
// in the database using the provided flags.
|
||||
var Create action.GTSAction = func(ctx context.Context) error {
|
||||
func Create(ctx context.Context) error {
|
||||
state, err := initState(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -118,7 +131,7 @@ var Create action.GTSAction = func(ctx context.Context) error {
|
|||
}
|
||||
|
||||
// List returns all existing local accounts.
|
||||
var List action.GTSAction = func(ctx context.Context) error {
|
||||
func List(ctx context.Context) error {
|
||||
state, err := initState(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -156,7 +169,7 @@ var List action.GTSAction = func(ctx context.Context) error {
|
|||
|
||||
// Confirm sets a user to Approved, sets Email to the current
|
||||
// UnconfirmedEmail value, and sets ConfirmedAt to now.
|
||||
var Confirm action.GTSAction = func(ctx context.Context) error {
|
||||
func Confirm(ctx context.Context) error {
|
||||
state, err := initState(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -198,7 +211,7 @@ var Confirm action.GTSAction = func(ctx context.Context) error {
|
|||
}
|
||||
|
||||
// Promote sets admin + moderator flags on a user to true.
|
||||
var Promote action.GTSAction = func(ctx context.Context) error {
|
||||
func Promote(ctx context.Context) error {
|
||||
state, err := initState(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -235,7 +248,7 @@ var Promote action.GTSAction = func(ctx context.Context) error {
|
|||
}
|
||||
|
||||
// Demote sets admin + moderator flags on a user to false.
|
||||
var Demote action.GTSAction = func(ctx context.Context) error {
|
||||
func Demote(ctx context.Context) error {
|
||||
state, err := initState(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -272,7 +285,7 @@ var Demote action.GTSAction = func(ctx context.Context) error {
|
|||
}
|
||||
|
||||
// Disable sets Disabled to true on a user.
|
||||
var Disable action.GTSAction = func(ctx context.Context) error {
|
||||
func Disable(ctx context.Context) error {
|
||||
state, err := initState(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -308,7 +321,7 @@ var Disable action.GTSAction = func(ctx context.Context) error {
|
|||
}
|
||||
|
||||
// Enable sets Disabled to false on a user.
|
||||
var Enable action.GTSAction = func(ctx context.Context) error {
|
||||
func Enable(ctx context.Context) error {
|
||||
state, err := initState(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -344,7 +357,7 @@ var Enable action.GTSAction = func(ctx context.Context) error {
|
|||
}
|
||||
|
||||
// Password sets the password of target account.
|
||||
var Password action.GTSAction = func(ctx context.Context) error {
|
||||
func Password(ctx context.Context) error {
|
||||
state, err := initState(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -389,3 +402,41 @@ var Password action.GTSAction = func(ctx context.Context) error {
|
|||
"encrypted_password",
|
||||
)
|
||||
}
|
||||
|
||||
// Disable2FA disables 2FA for target account.
|
||||
func Disable2FA(ctx context.Context) error {
|
||||
state, err := initState(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
// Ensure state gets stopped on return.
|
||||
if err := stopState(state); err != nil {
|
||||
log.Error(ctx, err)
|
||||
}
|
||||
}()
|
||||
|
||||
username := config.GetAdminAccountUsername()
|
||||
if err := validate.Username(username); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
account, err := state.DB.GetAccountByUsernameDomain(ctx, username, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
user, err := state.DB.GetUserByAccountID(ctx, account.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = userprocessor.TwoFactorDisable(ctx, state, user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("2fa disabled\n")
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,6 +35,10 @@ import (
|
|||
"code.superseriousbusiness.org/gotosocial/internal/state"
|
||||
)
|
||||
|
||||
// check function conformance.
|
||||
var _ action.GTSAction = ListAttachments
|
||||
var _ action.GTSAction = ListEmojis
|
||||
|
||||
type list struct {
|
||||
dbService db.DB
|
||||
state *state.State
|
||||
|
|
@ -155,7 +159,7 @@ func (l *list) shutdown() error {
|
|||
}
|
||||
|
||||
// ListAttachments lists local, remote, or all attachment paths.
|
||||
var ListAttachments action.GTSAction = func(ctx context.Context) error {
|
||||
func ListAttachments(ctx context.Context) error {
|
||||
list, err := setupList(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -214,7 +218,7 @@ var ListAttachments action.GTSAction = func(ctx context.Context) error {
|
|||
}
|
||||
|
||||
// ListEmojis lists local, remote, or all emoji filepaths.
|
||||
var ListEmojis action.GTSAction = func(ctx context.Context) error {
|
||||
func ListEmojis(ctx context.Context) error {
|
||||
list, err := setupList(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
|||
|
|
@ -26,8 +26,11 @@ import (
|
|||
"code.superseriousbusiness.org/gotosocial/internal/log"
|
||||
)
|
||||
|
||||
// check function conformance.
|
||||
var _ action.GTSAction = All
|
||||
|
||||
// All performs all media clean actions
|
||||
var All action.GTSAction = func(ctx context.Context) error {
|
||||
func All(ctx context.Context) error {
|
||||
// Setup pruning utilities.
|
||||
prune, err := setupPrune(ctx)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -26,8 +26,11 @@ import (
|
|||
"code.superseriousbusiness.org/gotosocial/internal/log"
|
||||
)
|
||||
|
||||
// check function conformance.
|
||||
var _ action.GTSAction = Orphaned
|
||||
|
||||
// Orphaned prunes orphaned media from storage.
|
||||
var Orphaned action.GTSAction = func(ctx context.Context) error {
|
||||
func Orphaned(ctx context.Context) error {
|
||||
// Setup pruning utilities.
|
||||
prune, err := setupPrune(ctx)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -27,8 +27,11 @@ import (
|
|||
"code.superseriousbusiness.org/gotosocial/internal/log"
|
||||
)
|
||||
|
||||
// check function conformance.
|
||||
var _ action.GTSAction = Remote
|
||||
|
||||
// Remote prunes old and/or unused remote media.
|
||||
var Remote action.GTSAction = func(ctx context.Context) error {
|
||||
func Remote(ctx context.Context) error {
|
||||
// Setup pruning utilities.
|
||||
prune, err := setupPrune(ctx)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -29,8 +29,11 @@ import (
|
|||
"code.superseriousbusiness.org/gotosocial/internal/trans"
|
||||
)
|
||||
|
||||
// check function conformance.
|
||||
var _ action.GTSAction = Export
|
||||
|
||||
// Export exports info from the database into a file
|
||||
var Export action.GTSAction = func(ctx context.Context) error {
|
||||
func Export(ctx context.Context) error {
|
||||
var state state.State
|
||||
|
||||
// Only set state DB connection.
|
||||
|
|
|
|||
|
|
@ -29,8 +29,11 @@ import (
|
|||
"code.superseriousbusiness.org/gotosocial/internal/trans"
|
||||
)
|
||||
|
||||
// check function conformance.
|
||||
var _ action.GTSAction = Import
|
||||
|
||||
// Import imports info from a file into the database
|
||||
var Import action.GTSAction = func(ctx context.Context) error {
|
||||
func Import(ctx context.Context) error {
|
||||
var state state.State
|
||||
|
||||
// Only set state DB connection.
|
||||
|
|
|
|||
|
|
@ -26,8 +26,11 @@ import (
|
|||
"code.superseriousbusiness.org/gotosocial/internal/config"
|
||||
)
|
||||
|
||||
// check function conformance.
|
||||
var _ action.GTSAction = Config
|
||||
|
||||
// Config just prints the collated config out to stdout as json.
|
||||
var Config action.GTSAction = func(ctx context.Context) (err error) {
|
||||
func Config(ctx context.Context) (err error) {
|
||||
var raw map[string]interface{}
|
||||
|
||||
// Marshal configuration to a raw JSON map
|
||||
|
|
|
|||
|
|
@ -27,8 +27,11 @@ import (
|
|||
"code.superseriousbusiness.org/gotosocial/internal/state"
|
||||
)
|
||||
|
||||
// check function conformance.
|
||||
var _ action.GTSAction = Run
|
||||
|
||||
// Run will initialize the database, running any available migrations.
|
||||
var Run action.GTSAction = func(ctx context.Context) error {
|
||||
func Run(ctx context.Context) error {
|
||||
var state state.State
|
||||
|
||||
defer func() {
|
||||
|
|
|
|||
|
|
@ -70,9 +70,13 @@ import (
|
|||
"go.uber.org/automaxprocs/maxprocs"
|
||||
)
|
||||
|
||||
// check function conformance.
|
||||
var _ action.GTSAction = Maintenance
|
||||
var _ action.GTSAction = Start
|
||||
|
||||
// Maintenance starts and creates a GoToSocial server
|
||||
// in maintenance mode (returns 503 for most requests).
|
||||
var Maintenance action.GTSAction = func(ctx context.Context) error {
|
||||
func Maintenance(ctx context.Context) error {
|
||||
route, err := router.New(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating maintenance router: %w", err)
|
||||
|
|
@ -101,7 +105,7 @@ var Maintenance action.GTSAction = func(ctx context.Context) error {
|
|||
}
|
||||
|
||||
// Start creates and starts a gotosocial server
|
||||
var Start action.GTSAction = func(ctx context.Context) error {
|
||||
func Start(ctx context.Context) error {
|
||||
// Set GOMAXPROCS / GOMEMLIMIT
|
||||
// to match container limits.
|
||||
setLimits(ctx)
|
||||
|
|
|
|||
|
|
@ -19,8 +19,15 @@
|
|||
|
||||
package testrig
|
||||
|
||||
import "code.superseriousbusiness.org/gotosocial/cmd/gotosocial/action"
|
||||
import (
|
||||
"context"
|
||||
|
||||
"code.superseriousbusiness.org/gotosocial/cmd/gotosocial/action"
|
||||
)
|
||||
|
||||
// check function conformance.
|
||||
var _ action.GTSAction = Start
|
||||
|
||||
// Start creates and starts a gotosocial testrig server.
|
||||
// This is only enabled in debug builds, else is nil.
|
||||
var Start action.GTSAction
|
||||
func Start(context.Context) error { return nil }
|
||||
|
|
|
|||
|
|
@ -50,9 +50,12 @@ import (
|
|||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// check function conformance.
|
||||
var _ action.GTSAction = Start
|
||||
|
||||
// Start creates and starts a gotosocial testrig server.
|
||||
// This is only enabled in debug builds, else is nil.
|
||||
var Start action.GTSAction = func(ctx context.Context) error {
|
||||
func Start(ctx context.Context) error {
|
||||
testrig.InitTestConfig()
|
||||
testrig.InitTestLog()
|
||||
|
||||
|
|
|
|||
|
|
@ -146,6 +146,19 @@ func adminCommands() *cobra.Command {
|
|||
config.AddAdminAccountPassword(adminAccountPasswordCmd)
|
||||
adminAccountCmd.AddCommand(adminAccountPasswordCmd)
|
||||
|
||||
adminAccountDisable2FACmd := &cobra.Command{
|
||||
Use: "disable-2fa",
|
||||
Short: "disable 2fa for the given local account",
|
||||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
return preRun(preRunArgs{cmd: cmd})
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return run(cmd.Context(), account.Disable2FA)
|
||||
},
|
||||
}
|
||||
config.AddAdminAccount(adminAccountDisable2FACmd)
|
||||
adminAccountCmd.AddCommand(adminAccountDisable2FACmd)
|
||||
|
||||
adminCmd.AddCommand(adminAccountCmd)
|
||||
|
||||
/*
|
||||
|
|
|
|||
|
|
@ -20,11 +20,12 @@ package main
|
|||
import (
|
||||
"code.superseriousbusiness.org/gotosocial/cmd/gotosocial/action/testrig"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/config"
|
||||
"codeberg.org/gruf/go-debug"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func testrigCommands() *cobra.Command {
|
||||
if testrig.Start != nil {
|
||||
if debug.DEBUG {
|
||||
testrigCmd := &cobra.Command{
|
||||
Use: "testrig",
|
||||
Short: "gotosocial testrig-related tasks",
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import (
|
|||
"code.superseriousbusiness.org/gotosocial/internal/config"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/gtserror"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/state"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/util"
|
||||
"codeberg.org/gruf/go-byteutil"
|
||||
"github.com/pquerna/otp"
|
||||
|
|
@ -43,60 +44,6 @@ import (
|
|||
|
||||
var b32NoPadding = base32.StdEncoding.WithPadding(base32.NoPadding)
|
||||
|
||||
// EncodeQuery is a copy-paste of url.Values.Encode, except it uses
|
||||
// %20 instead of + to encode spaces. This is necessary to correctly
|
||||
// render spaces in some authenticator apps, like Google Authenticator.
|
||||
//
|
||||
// [Note: this func and the above comment are both taken
|
||||
// directly from github.com/pquerna/otp/internal/encode.go.]
|
||||
func encodeQuery(v url.Values) string {
|
||||
if v == nil {
|
||||
return ""
|
||||
}
|
||||
var buf strings.Builder
|
||||
keys := make([]string, 0, len(v))
|
||||
for k := range v {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, k := range keys {
|
||||
vs := v[k]
|
||||
// Changed from url.QueryEscape.
|
||||
keyEscaped := url.PathEscape(k)
|
||||
for _, v := range vs {
|
||||
if buf.Len() > 0 {
|
||||
buf.WriteByte('&')
|
||||
}
|
||||
buf.WriteString(keyEscaped)
|
||||
buf.WriteByte('=')
|
||||
// Changed from url.QueryEscape.
|
||||
buf.WriteString(url.PathEscape(v))
|
||||
}
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// totpURLForUser reconstructs a TOTP URL for the
|
||||
// given user, setting the instance host as issuer.
|
||||
//
|
||||
// See https://github.com/google/google-authenticator/wiki/Key-Uri-Format
|
||||
func totpURLForUser(user *gtsmodel.User) *url.URL {
|
||||
issuer := config.GetHost() + " - GoToSocial"
|
||||
v := url.Values{}
|
||||
v.Set("secret", user.TwoFactorSecret)
|
||||
v.Set("issuer", issuer)
|
||||
v.Set("period", "30") // 30 seconds totp validity.
|
||||
v.Set("algorithm", "SHA1")
|
||||
v.Set("digits", "6") // 6-digit totp.
|
||||
|
||||
return &url.URL{
|
||||
Scheme: "otpauth",
|
||||
Host: "totp",
|
||||
Path: "/" + issuer + ":" + user.Email,
|
||||
RawQuery: encodeQuery(v),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Processor) TwoFactorQRCodePngGet(
|
||||
ctx context.Context,
|
||||
user *gtsmodel.User,
|
||||
|
|
@ -135,14 +82,32 @@ func (p *Processor) TwoFactorQRCodePngGet(
|
|||
}, nil
|
||||
}
|
||||
|
||||
// TwoFactorQRCodeURIGet will generate a new
|
||||
// 2 factor auth secret for user, and return a
|
||||
// URI of expected format for generating a QR code
|
||||
// or inputting into a password manager.
|
||||
//
|
||||
// This may be called multiple times without error
|
||||
// UNTIL the moment the user has finalized enabling
|
||||
// 2FA. i.e. when user.TwoFactorEnabled() == true.
|
||||
// Until this point, the URI may be requested for
|
||||
// both QR code generation, and requesting the URI,
|
||||
// but once 2FA is confirmed enabled it is not safe
|
||||
// to re-share the agreed-upon secret.
|
||||
func (p *Processor) TwoFactorQRCodeURIGet(
|
||||
ctx context.Context,
|
||||
user *gtsmodel.User,
|
||||
) (*url.URL, gtserror.WithCode) {
|
||||
// Check if we need to lazily
|
||||
// generate a new 2fa secret.
|
||||
if user.TwoFactorEnabled() {
|
||||
const errText = "2fa already enabled; not sharing secret again"
|
||||
return nil, gtserror.NewErrorConflict(errors.New(errText), errText)
|
||||
}
|
||||
|
||||
// Only generate new 2FA secret
|
||||
// if not already been generated
|
||||
// during this enabling process.
|
||||
if user.TwoFactorSecret == "" {
|
||||
// We do! Read some random crap.
|
||||
|
||||
// 32 bytes should be plenty entropy.
|
||||
secret := make([]byte, 32)
|
||||
if _, err := io.ReadFull(rand.Reader, secret); err != nil {
|
||||
|
|
@ -156,41 +121,42 @@ func (p *Processor) TwoFactorQRCodeURIGet(
|
|||
err := gtserror.Newf("db error updating user: %w", err)
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
} else if user.TwoFactorEnabled() {
|
||||
// If a secret is already set, and 2fa is
|
||||
// already enabled, we shouldn't share the
|
||||
// secret via QR code again: Someone may
|
||||
// have obtained a token for this user and
|
||||
// is trying to get the 2fa secret so they
|
||||
// can escalate an attack or something.
|
||||
const errText = "2fa already enabled; keeping the secret secret"
|
||||
return nil, gtserror.NewErrorConflict(errors.New(errText), errText)
|
||||
}
|
||||
|
||||
// Recreate the totp key.
|
||||
return totpURLForUser(user), nil
|
||||
// see: https://github.com/google/google-authenticator/wiki/Key-Uri-Format
|
||||
issuer := config.GetHost() + " - GoToSocial"
|
||||
return &url.URL{
|
||||
Scheme: "otpauth",
|
||||
Host: "totp",
|
||||
Path: "/" + issuer + ":" + user.Email,
|
||||
RawQuery: encodeQuery(url.Values{
|
||||
"secret": {user.TwoFactorSecret},
|
||||
"issuer": {issuer},
|
||||
"period": {"30s"}, // 30s totp validity.
|
||||
"digits": {"6"}, // 6-digit totp.
|
||||
"algorithm": {"SHA1"},
|
||||
}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// TwoFactorEnable will enable 2 factor auth for
|
||||
// account, using given TOTP code to validate the
|
||||
// user's 2fa secret before continuing.
|
||||
func (p *Processor) TwoFactorEnable(
|
||||
ctx context.Context,
|
||||
user *gtsmodel.User,
|
||||
code string,
|
||||
) ([]string, gtserror.WithCode) {
|
||||
if user.TwoFactorSecret == "" {
|
||||
// User doesn't have a secret set, which
|
||||
// means they never got the QR code to scan
|
||||
// into their authenticator app. We can safely
|
||||
// return an error from this request.
|
||||
const errText = "no 2fa secret stored yet; read the qr code first"
|
||||
return nil, gtserror.NewErrorForbidden(errors.New(errText), errText)
|
||||
}
|
||||
|
||||
if user.TwoFactorEnabled() {
|
||||
const errText = "2fa already enabled; disable it first then try again"
|
||||
return nil, gtserror.NewErrorConflict(errors.New(errText), errText)
|
||||
}
|
||||
|
||||
if user.TwoFactorSecret == "" {
|
||||
const errText = "no 2fa secret stored; first read qr code / totp secret"
|
||||
return nil, gtserror.NewErrorForbidden(errors.New(errText), errText)
|
||||
}
|
||||
|
||||
// Try validating the provided code and give
|
||||
// a helpful error message if it doesn't work.
|
||||
if !totp.Validate(code, user.TwoFactorSecret) {
|
||||
|
|
@ -222,12 +188,11 @@ func (p *Processor) TwoFactorEnable(
|
|||
err := gtserror.Newf("error encrypting backup codes: %w", err)
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
user.TwoFactorBackups[i] = string(encryptedBackup)
|
||||
}
|
||||
|
||||
if err := p.state.DB.UpdateUser(
|
||||
ctx,
|
||||
// Update user in the database.
|
||||
if err := p.state.DB.UpdateUser(ctx,
|
||||
user,
|
||||
"two_factor_enabled_at",
|
||||
"two_factor_backups",
|
||||
|
|
@ -239,16 +204,12 @@ func (p *Processor) TwoFactorEnable(
|
|||
return backupsClearText, nil
|
||||
}
|
||||
|
||||
// TwoFactorDisable: see TwoFactorDisable().
|
||||
func (p *Processor) TwoFactorDisable(
|
||||
ctx context.Context,
|
||||
user *gtsmodel.User,
|
||||
password string,
|
||||
) gtserror.WithCode {
|
||||
if !user.TwoFactorEnabled() {
|
||||
const errText = "2fa already disabled"
|
||||
return gtserror.NewErrorConflict(errors.New(errText), errText)
|
||||
}
|
||||
|
||||
// Ensure provided password is correct.
|
||||
if err := bcrypt.CompareHashAndPassword(
|
||||
byteutil.S2B(user.EncryptedPassword),
|
||||
|
|
@ -258,13 +219,29 @@ func (p *Processor) TwoFactorDisable(
|
|||
return gtserror.NewErrorUnauthorized(errors.New(errText), errText)
|
||||
}
|
||||
|
||||
// Disable 2fa for this user
|
||||
// and clear backup codes.
|
||||
// Disable 2 factor auth for this account.
|
||||
return TwoFactorDisable(ctx, p.state, user)
|
||||
}
|
||||
|
||||
// TwoFactorDisable disables 2 factor auth
|
||||
// for given user account. Note this should
|
||||
// be gated with password authentication if
|
||||
// accessed via web.
|
||||
func TwoFactorDisable(
|
||||
ctx context.Context,
|
||||
state *state.State,
|
||||
user *gtsmodel.User,
|
||||
) gtserror.WithCode {
|
||||
if !user.TwoFactorEnabled() {
|
||||
const errText = "2fa already disabled"
|
||||
return gtserror.NewErrorConflict(errors.New(errText), errText)
|
||||
}
|
||||
|
||||
// Clear 2FA fields on user account.
|
||||
user.TwoFactorEnabledAt = time.Time{}
|
||||
user.TwoFactorSecret = ""
|
||||
user.TwoFactorBackups = nil
|
||||
if err := p.state.DB.UpdateUser(
|
||||
ctx,
|
||||
if err := state.DB.UpdateUser(ctx,
|
||||
user,
|
||||
"two_factor_enabled_at",
|
||||
"two_factor_secret",
|
||||
|
|
@ -276,3 +253,33 @@ func (p *Processor) TwoFactorDisable(
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
// encodeQuery is a copy-paste of url.Values.Encode, except it uses
|
||||
// %20 instead of + to encode spaces. This is necessary to correctly
|
||||
// render spaces in some authenticator apps, like Google Authenticator.
|
||||
//
|
||||
// [Note: this func and the above comment are both taken
|
||||
// directly from github.com/pquerna/otp/internal/encode.go.]
|
||||
func encodeQuery(v url.Values) string {
|
||||
var buf strings.Builder
|
||||
keys := make([]string, 0, len(v))
|
||||
for k := range v {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, k := range keys {
|
||||
vs := v[k]
|
||||
// Changed from url.QueryEscape.
|
||||
keyEscaped := url.PathEscape(k)
|
||||
for _, v := range vs {
|
||||
if buf.Len() > 0 {
|
||||
buf.WriteByte('&')
|
||||
}
|
||||
buf.WriteString(keyEscaped)
|
||||
buf.WriteByte('=')
|
||||
// Changed from url.QueryEscape.
|
||||
buf.WriteString(url.PathEscape(v))
|
||||
}
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue