github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/web/auth/passphrase.go (about)

     1  package auth
     2  
     3  import (
     4  	"encoding/hex"
     5  	"errors"
     6  	"net/http"
     7  	"net/url"
     8  	"strconv"
     9  
    10  	"github.com/cozy/cozy-stack/model/bitwarden"
    11  	"github.com/cozy/cozy-stack/model/bitwarden/settings"
    12  	"github.com/cozy/cozy-stack/model/instance"
    13  	"github.com/cozy/cozy-stack/model/instance/lifecycle"
    14  	"github.com/cozy/cozy-stack/model/sharing"
    15  	"github.com/cozy/cozy-stack/pkg/config/config"
    16  	"github.com/cozy/cozy-stack/pkg/consts"
    17  	"github.com/cozy/cozy-stack/pkg/couchdb"
    18  	"github.com/cozy/cozy-stack/pkg/crypto"
    19  	"github.com/cozy/cozy-stack/pkg/limits"
    20  	"github.com/cozy/cozy-stack/web/middlewares"
    21  	"github.com/labstack/echo/v4"
    22  )
    23  
    24  func passphraseResetForm(c echo.Context) error {
    25  	instance := middlewares.GetInstance(c)
    26  	if !instance.OnboardingFinished {
    27  		return middlewares.RenderNeedOnboarding(c, instance)
    28  	}
    29  
    30  	hasHint := false
    31  	if setting, err := settings.Get(instance); err == nil {
    32  		hasHint = setting.PassphraseHint != ""
    33  	}
    34  	hasCiphers := true
    35  	if resp, err := couchdb.NormalDocs(instance, consts.BitwardenCiphers, 0, 1, "", false); err == nil {
    36  		hasCiphers = resp.Total > 0
    37  	}
    38  	backButton := ""
    39  	from := c.QueryParam("from")
    40  	if from != "" {
    41  		backButton = instance.SubDomain(from).String()
    42  	} else if c.QueryParam("hideBackButton") != "true" {
    43  		backButton = instance.PageURL("/auth/login", nil)
    44  	}
    45  	forcedOIDC := instance.HasForcedOIDC()
    46  	return c.Render(http.StatusOK, "passphrase_reset.html", echo.Map{
    47  		"Domain":      instance.ContextualDomain(),
    48  		"ContextName": instance.ContextName,
    49  		"Locale":      instance.Locale,
    50  		"Title":       instance.TemplateTitle(),
    51  		"Favicon":     middlewares.Favicon(instance),
    52  		"CSRF":        c.Get("csrf"),
    53  		"Redirect":    c.QueryParam("redirect"),
    54  		"HasHint":     hasHint,
    55  		"HasCiphers":  hasCiphers,
    56  		"CozyPass":    forcedOIDC,
    57  		"From":        from,
    58  		"BackButton":  backButton,
    59  	})
    60  }
    61  
    62  func passphraseForm(c echo.Context) error {
    63  	inst := middlewares.GetInstance(c)
    64  	registerToken := c.QueryParams().Get("registerToken")
    65  	if inst.OnboardingFinished {
    66  		redirect := inst.DefaultRedirection()
    67  		return c.Redirect(http.StatusSeeOther, redirect.String())
    68  	}
    69  
    70  	if registerToken == "" || !middlewares.CheckRegisterToken(c, inst) {
    71  		return middlewares.RenderNeedOnboarding(c, inst)
    72  	}
    73  
    74  	cryptoPolyfill := middlewares.CryptoPolyfill(c)
    75  	iterations := crypto.DefaultPBKDF2Iterations
    76  	if cryptoPolyfill {
    77  		iterations = crypto.MinPBKDF2Iterations
    78  	}
    79  
    80  	return c.Render(http.StatusOK, "passphrase_choose.html", echo.Map{
    81  		"Domain":         inst.ContextualDomain(),
    82  		"ContextName":    inst.ContextName,
    83  		"Locale":         inst.Locale,
    84  		"Title":          inst.TemplateTitle(),
    85  		"Favicon":        middlewares.Favicon(inst),
    86  		"Action":         "/settings/passphrase",
    87  		"Iterations":     iterations,
    88  		"Salt":           string(inst.PassphraseSalt()),
    89  		"RegisterToken":  registerToken,
    90  		"CryptoPolyfill": cryptoPolyfill,
    91  	})
    92  }
    93  
    94  func sendHint(c echo.Context) error {
    95  	i := middlewares.GetInstance(c)
    96  	if err := config.GetRateLimiter().CheckRateLimit(i, limits.SendHintByMail); err == nil {
    97  		if err := lifecycle.SendHint(i); err != nil {
    98  			return err
    99  		}
   100  	}
   101  	var u url.Values
   102  	if redirect := c.FormValue("redirect"); redirect != "" {
   103  		u = url.Values{"redirect": {redirect}}
   104  	}
   105  	return c.Render(http.StatusOK, "error.html", echo.Map{
   106  		"Domain":       i.ContextualDomain(),
   107  		"ContextName":  i.ContextName,
   108  		"Locale":       i.Locale,
   109  		"Title":        i.TemplateTitle(),
   110  		"Favicon":      middlewares.Favicon(i),
   111  		"Inverted":     true,
   112  		"Illustration": "/images/mail-sent.svg",
   113  		"ErrorTitle":   "Hint sent Title",
   114  		"Error":        "Hint sent Body",
   115  		"ErrorDetail":  "Hint sent Detail",
   116  		"SupportEmail": i.SupportEmailAddress(),
   117  		"Button":       "Hint sent Login Button",
   118  		"ButtonURL":    i.PageURL("/auth/login", u),
   119  	})
   120  }
   121  
   122  func passphraseReset(c echo.Context) error {
   123  	i := middlewares.GetInstance(c)
   124  	from := c.FormValue("from")
   125  	if err := lifecycle.RequestPassphraseReset(i, from); err != nil && !errors.Is(err, instance.ErrResetAlreadyRequested) {
   126  		return err
   127  	}
   128  	// Disconnect the user if it is logged in. The idea is that if the user
   129  	// (maybe by accident) asks for a passphrase reset while logged in, we log
   130  	// him out to be able to re-go through the process of logging back-in. It is
   131  	// more a UX choice than a "security" one.
   132  	session, ok := middlewares.GetSession(c)
   133  	if ok {
   134  		c.SetCookie(session.Delete(i))
   135  	}
   136  	var u url.Values
   137  	if redirect := c.FormValue("redirect"); redirect != "" {
   138  		u = url.Values{"redirect": {redirect}}
   139  	}
   140  	return c.Render(http.StatusOK, "error.html", echo.Map{
   141  		"Domain":       i.ContextualDomain(),
   142  		"ContextName":  i.ContextName,
   143  		"Locale":       i.Locale,
   144  		"Title":        i.TemplateTitle(),
   145  		"Favicon":      middlewares.Favicon(i),
   146  		"Inverted":     true,
   147  		"Illustration": "/images/mail-sent.svg",
   148  		"ErrorTitle":   "Passphrase is reset Title",
   149  		"Error":        "Passphrase is reset Body",
   150  		"ErrorDetail":  "Passphrase is reset Detail",
   151  		"SupportEmail": i.SupportEmailAddress(),
   152  		"Button":       "Passphrase is reset Login Button",
   153  		"ButtonURL":    i.PageURL("/auth/login", u),
   154  	})
   155  }
   156  
   157  func passphraseRenewForm(c echo.Context) error {
   158  	inst := middlewares.GetInstance(c)
   159  	if middlewares.IsLoggedIn(c) {
   160  		redirect := inst.DefaultRedirection().String()
   161  		return c.Redirect(http.StatusSeeOther, redirect)
   162  	}
   163  
   164  	// Check that the token is actually defined and well encoded. The actual
   165  	// token value checking is also done on the passphraseRenew handler.
   166  	token, err := hex.DecodeString(c.QueryParam("token"))
   167  	if err != nil || len(token) == 0 {
   168  		return renderError(c, http.StatusBadRequest, "Error Invalid reset token")
   169  	}
   170  	if err = lifecycle.CheckPassphraseRenewToken(inst, token); err != nil {
   171  		if errors.Is(err, instance.ErrMissingToken) {
   172  			return renderError(c, http.StatusBadRequest, "Error Invalid reset token")
   173  		}
   174  		return c.JSON(http.StatusBadRequest, echo.Map{
   175  			"error": "invalid_token",
   176  		})
   177  	}
   178  
   179  	cryptoPolyfill := middlewares.CryptoPolyfill(c)
   180  	iterations := crypto.DefaultPBKDF2Iterations
   181  	if cryptoPolyfill {
   182  		iterations = crypto.MinPBKDF2Iterations
   183  	}
   184  
   185  	return c.Render(http.StatusOK, "passphrase_choose.html", echo.Map{
   186  		"Domain":         inst.ContextualDomain(),
   187  		"ContextName":    inst.ContextName,
   188  		"Locale":         inst.Locale,
   189  		"Title":          inst.TemplateTitle(),
   190  		"Favicon":        middlewares.Favicon(inst),
   191  		"Action":         "/auth/passphrase_renew",
   192  		"From":           c.QueryParam("from"),
   193  		"Iterations":     iterations,
   194  		"Salt":           string(inst.PassphraseSalt()),
   195  		"ResetToken":     hex.EncodeToString(token),
   196  		"CSRF":           c.Get("csrf"),
   197  		"CryptoPolyfill": cryptoPolyfill,
   198  	})
   199  }
   200  
   201  func passphraseRenew(c echo.Context) error {
   202  	inst := middlewares.GetInstance(c)
   203  	if middlewares.IsLoggedIn(c) {
   204  		redirect := inst.DefaultRedirection().String()
   205  		if wantsJSON(c) {
   206  			return c.JSON(http.StatusOK, echo.Map{"redirect": redirect})
   207  		}
   208  		return c.Redirect(http.StatusSeeOther, redirect)
   209  	}
   210  	pass := []byte(c.FormValue("passphrase"))
   211  	iterations, _ := strconv.Atoi(c.FormValue("iterations"))
   212  	token, err := hex.DecodeString(c.FormValue("passphrase_reset_token"))
   213  	if err != nil {
   214  		if wantsJSON(c) {
   215  			return c.JSON(http.StatusUnauthorized, echo.Map{
   216  				"error": "Invalid reset token",
   217  			})
   218  		}
   219  		return renderError(c, http.StatusBadRequest, "Error Invalid reset token")
   220  	}
   221  	err = lifecycle.PassphraseRenew(inst, token, lifecycle.PassParameters{
   222  		Pass:       pass,
   223  		Iterations: iterations,
   224  		Key:        c.FormValue("key"),
   225  		PublicKey:  c.FormValue("public_key"),
   226  		PrivateKey: c.FormValue("private_key"),
   227  		Hint:       c.FormValue("hint"),
   228  	})
   229  	if err != nil {
   230  		if errors.Is(err, instance.ErrMissingToken) {
   231  			if wantsJSON(c) {
   232  				return c.JSON(http.StatusUnauthorized, echo.Map{
   233  					"error": "Invalid reset token",
   234  				})
   235  			}
   236  			return renderError(c, http.StatusBadRequest, "Error Invalid reset token")
   237  		}
   238  		return c.JSON(http.StatusBadRequest, echo.Map{
   239  			"error": "invalid_token",
   240  		})
   241  	}
   242  	// Before deleting the ciphers, it will revoke the sharings to avoid deleting
   243  	// the ciphers on the Cozy instances of the other members.
   244  	if err := sharing.RevokeCipherSharings(inst); err == nil {
   245  		if err := bitwarden.DeleteUnrecoverableCiphers(inst); err != nil {
   246  			inst.Logger().WithNamespace("bitwarden").
   247  				Warnf("Error on ciphers deletion after password reset: %s", err)
   248  		}
   249  	}
   250  
   251  	redirect := inst.PageURL("/auth/login", nil)
   252  	if c.FormValue("from") == consts.SettingsSlug {
   253  		u := inst.SubDomain(consts.SettingsSlug)
   254  		u.Fragment = "/profile/email"
   255  		redirect = u.String()
   256  	}
   257  	if wantsJSON(c) {
   258  		return c.JSON(http.StatusOK, echo.Map{"redirect": redirect})
   259  	}
   260  	return c.Redirect(http.StatusSeeOther, redirect)
   261  }