code.gitea.io/gitea@v1.21.7/routers/web/user/setting/security/2fa.go (about)

     1  // Copyright 2014 The Gogs Authors. All rights reserved.
     2  // Copyright 2018 The Gitea Authors. All rights reserved.
     3  // SPDX-License-Identifier: MIT
     4  
     5  package security
     6  
     7  import (
     8  	"bytes"
     9  	"encoding/base64"
    10  	"html/template"
    11  	"image/png"
    12  	"net/http"
    13  	"strings"
    14  
    15  	"code.gitea.io/gitea/models/auth"
    16  	"code.gitea.io/gitea/modules/context"
    17  	"code.gitea.io/gitea/modules/log"
    18  	"code.gitea.io/gitea/modules/setting"
    19  	"code.gitea.io/gitea/modules/web"
    20  	"code.gitea.io/gitea/services/forms"
    21  
    22  	"github.com/pquerna/otp"
    23  	"github.com/pquerna/otp/totp"
    24  )
    25  
    26  // RegenerateScratchTwoFactor regenerates the user's 2FA scratch code.
    27  func RegenerateScratchTwoFactor(ctx *context.Context) {
    28  	ctx.Data["Title"] = ctx.Tr("settings")
    29  	ctx.Data["PageIsSettingsSecurity"] = true
    30  
    31  	t, err := auth.GetTwoFactorByUID(ctx, ctx.Doer.ID)
    32  	if err != nil {
    33  		if auth.IsErrTwoFactorNotEnrolled(err) {
    34  			ctx.Flash.Error(ctx.Tr("settings.twofa_not_enrolled"))
    35  			ctx.Redirect(setting.AppSubURL + "/user/settings/security")
    36  		}
    37  		ctx.ServerError("SettingsTwoFactor: Failed to GetTwoFactorByUID", err)
    38  		return
    39  	}
    40  
    41  	token, err := t.GenerateScratchToken()
    42  	if err != nil {
    43  		ctx.ServerError("SettingsTwoFactor: Failed to GenerateScratchToken", err)
    44  		return
    45  	}
    46  
    47  	if err = auth.UpdateTwoFactor(ctx, t); err != nil {
    48  		ctx.ServerError("SettingsTwoFactor: Failed to UpdateTwoFactor", err)
    49  		return
    50  	}
    51  
    52  	ctx.Flash.Success(ctx.Tr("settings.twofa_scratch_token_regenerated", token))
    53  	ctx.Redirect(setting.AppSubURL + "/user/settings/security")
    54  }
    55  
    56  // DisableTwoFactor deletes the user's 2FA settings.
    57  func DisableTwoFactor(ctx *context.Context) {
    58  	ctx.Data["Title"] = ctx.Tr("settings")
    59  	ctx.Data["PageIsSettingsSecurity"] = true
    60  
    61  	t, err := auth.GetTwoFactorByUID(ctx, ctx.Doer.ID)
    62  	if err != nil {
    63  		if auth.IsErrTwoFactorNotEnrolled(err) {
    64  			ctx.Flash.Error(ctx.Tr("settings.twofa_not_enrolled"))
    65  			ctx.Redirect(setting.AppSubURL + "/user/settings/security")
    66  		}
    67  		ctx.ServerError("SettingsTwoFactor: Failed to GetTwoFactorByUID", err)
    68  		return
    69  	}
    70  
    71  	if err = auth.DeleteTwoFactorByID(ctx, t.ID, ctx.Doer.ID); err != nil {
    72  		if auth.IsErrTwoFactorNotEnrolled(err) {
    73  			// There is a potential DB race here - we must have been disabled by another request in the intervening period
    74  			ctx.Flash.Success(ctx.Tr("settings.twofa_disabled"))
    75  			ctx.Redirect(setting.AppSubURL + "/user/settings/security")
    76  		}
    77  		ctx.ServerError("SettingsTwoFactor: Failed to DeleteTwoFactorByID", err)
    78  		return
    79  	}
    80  
    81  	ctx.Flash.Success(ctx.Tr("settings.twofa_disabled"))
    82  	ctx.Redirect(setting.AppSubURL + "/user/settings/security")
    83  }
    84  
    85  func twofaGenerateSecretAndQr(ctx *context.Context) bool {
    86  	var otpKey *otp.Key
    87  	var err error
    88  	uri := ctx.Session.Get("twofaUri")
    89  	if uri != nil {
    90  		otpKey, err = otp.NewKeyFromURL(uri.(string))
    91  		if err != nil {
    92  			ctx.ServerError("SettingsTwoFactor: Failed NewKeyFromURL: ", err)
    93  			return false
    94  		}
    95  	}
    96  	// Filter unsafe character ':' in issuer
    97  	issuer := strings.ReplaceAll(setting.AppName+" ("+setting.Domain+")", ":", "")
    98  	if otpKey == nil {
    99  		otpKey, err = totp.Generate(totp.GenerateOpts{
   100  			SecretSize:  40,
   101  			Issuer:      issuer,
   102  			AccountName: ctx.Doer.Name,
   103  		})
   104  		if err != nil {
   105  			ctx.ServerError("SettingsTwoFactor: totpGenerate Failed", err)
   106  			return false
   107  		}
   108  	}
   109  
   110  	ctx.Data["TwofaSecret"] = otpKey.Secret()
   111  	img, err := otpKey.Image(320, 240)
   112  	if err != nil {
   113  		ctx.ServerError("SettingsTwoFactor: otpKey image generation failed", err)
   114  		return false
   115  	}
   116  
   117  	var imgBytes bytes.Buffer
   118  	if err = png.Encode(&imgBytes, img); err != nil {
   119  		ctx.ServerError("SettingsTwoFactor: otpKey png encoding failed", err)
   120  		return false
   121  	}
   122  
   123  	ctx.Data["QrUri"] = template.URL("data:image/png;base64," + base64.StdEncoding.EncodeToString(imgBytes.Bytes()))
   124  
   125  	if err := ctx.Session.Set("twofaSecret", otpKey.Secret()); err != nil {
   126  		ctx.ServerError("SettingsTwoFactor: Failed to set session for twofaSecret", err)
   127  		return false
   128  	}
   129  
   130  	if err := ctx.Session.Set("twofaUri", otpKey.String()); err != nil {
   131  		ctx.ServerError("SettingsTwoFactor: Failed to set session for twofaUri", err)
   132  		return false
   133  	}
   134  
   135  	// Here we're just going to try to release the session early
   136  	if err := ctx.Session.Release(); err != nil {
   137  		// we'll tolerate errors here as they *should* get saved elsewhere
   138  		log.Error("Unable to save changes to the session: %v", err)
   139  	}
   140  	return true
   141  }
   142  
   143  // EnrollTwoFactor shows the page where the user can enroll into 2FA.
   144  func EnrollTwoFactor(ctx *context.Context) {
   145  	ctx.Data["Title"] = ctx.Tr("settings")
   146  	ctx.Data["PageIsSettingsSecurity"] = true
   147  
   148  	t, err := auth.GetTwoFactorByUID(ctx, ctx.Doer.ID)
   149  	if t != nil {
   150  		// already enrolled - we should redirect back!
   151  		log.Warn("Trying to re-enroll %-v in twofa when already enrolled", ctx.Doer)
   152  		ctx.Flash.Error(ctx.Tr("settings.twofa_is_enrolled"))
   153  		ctx.Redirect(setting.AppSubURL + "/user/settings/security")
   154  		return
   155  	}
   156  	if err != nil && !auth.IsErrTwoFactorNotEnrolled(err) {
   157  		ctx.ServerError("SettingsTwoFactor: GetTwoFactorByUID", err)
   158  		return
   159  	}
   160  
   161  	if !twofaGenerateSecretAndQr(ctx) {
   162  		return
   163  	}
   164  
   165  	ctx.HTML(http.StatusOK, tplSettingsTwofaEnroll)
   166  }
   167  
   168  // EnrollTwoFactorPost handles enrolling the user into 2FA.
   169  func EnrollTwoFactorPost(ctx *context.Context) {
   170  	form := web.GetForm(ctx).(*forms.TwoFactorAuthForm)
   171  	ctx.Data["Title"] = ctx.Tr("settings")
   172  	ctx.Data["PageIsSettingsSecurity"] = true
   173  
   174  	t, err := auth.GetTwoFactorByUID(ctx, ctx.Doer.ID)
   175  	if t != nil {
   176  		// already enrolled
   177  		ctx.Flash.Error(ctx.Tr("settings.twofa_is_enrolled"))
   178  		ctx.Redirect(setting.AppSubURL + "/user/settings/security")
   179  		return
   180  	}
   181  	if err != nil && !auth.IsErrTwoFactorNotEnrolled(err) {
   182  		ctx.ServerError("SettingsTwoFactor: Failed to check if already enrolled with GetTwoFactorByUID", err)
   183  		return
   184  	}
   185  
   186  	if ctx.HasError() {
   187  		if !twofaGenerateSecretAndQr(ctx) {
   188  			return
   189  		}
   190  		ctx.HTML(http.StatusOK, tplSettingsTwofaEnroll)
   191  		return
   192  	}
   193  
   194  	secretRaw := ctx.Session.Get("twofaSecret")
   195  	if secretRaw == nil {
   196  		ctx.Flash.Error(ctx.Tr("settings.twofa_failed_get_secret"))
   197  		ctx.Redirect(setting.AppSubURL + "/user/settings/security/two_factor/enroll")
   198  		return
   199  	}
   200  
   201  	secret := secretRaw.(string)
   202  	if !totp.Validate(form.Passcode, secret) {
   203  		if !twofaGenerateSecretAndQr(ctx) {
   204  			return
   205  		}
   206  		ctx.Flash.Error(ctx.Tr("settings.passcode_invalid"))
   207  		ctx.Redirect(setting.AppSubURL + "/user/settings/security/two_factor/enroll")
   208  		return
   209  	}
   210  
   211  	t = &auth.TwoFactor{
   212  		UID: ctx.Doer.ID,
   213  	}
   214  	err = t.SetSecret(secret)
   215  	if err != nil {
   216  		ctx.ServerError("SettingsTwoFactor: Failed to set secret", err)
   217  		return
   218  	}
   219  	token, err := t.GenerateScratchToken()
   220  	if err != nil {
   221  		ctx.ServerError("SettingsTwoFactor: Failed to generate scratch token", err)
   222  		return
   223  	}
   224  
   225  	// Now we have to delete the secrets - because if we fail to insert then it's highly likely that they have already been used
   226  	// If we can detect the unique constraint failure below we can move this to after the NewTwoFactor
   227  	if err := ctx.Session.Delete("twofaSecret"); err != nil {
   228  		// tolerate this failure - it's more important to continue
   229  		log.Error("Unable to delete twofaSecret from the session: Error: %v", err)
   230  	}
   231  	if err := ctx.Session.Delete("twofaUri"); err != nil {
   232  		// tolerate this failure - it's more important to continue
   233  		log.Error("Unable to delete twofaUri from the session: Error: %v", err)
   234  	}
   235  	if err := ctx.Session.Release(); err != nil {
   236  		// tolerate this failure - it's more important to continue
   237  		log.Error("Unable to save changes to the session: %v", err)
   238  	}
   239  
   240  	if err = auth.NewTwoFactor(ctx, t); err != nil {
   241  		// FIXME: We need to handle a unique constraint fail here it's entirely possible that another request has beaten us.
   242  		// If there is a unique constraint fail we should just tolerate the error
   243  		ctx.ServerError("SettingsTwoFactor: Failed to save two factor", err)
   244  		return
   245  	}
   246  
   247  	ctx.Flash.Success(ctx.Tr("settings.twofa_enrolled", token))
   248  	ctx.Redirect(setting.AppSubURL + "/user/settings/security")
   249  }