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

     1  // Package auth provides register and login handlers
     2  package auth
     3  
     4  import (
     5  	"errors"
     6  	"fmt"
     7  	"net/http"
     8  	"net/url"
     9  	"strconv"
    10  	"strings"
    11  
    12  	"github.com/cozy/cozy-stack/model/app"
    13  	"github.com/cozy/cozy-stack/model/bitwarden/settings"
    14  	"github.com/cozy/cozy-stack/model/instance"
    15  	"github.com/cozy/cozy-stack/model/instance/lifecycle"
    16  	"github.com/cozy/cozy-stack/model/session"
    17  	csettings "github.com/cozy/cozy-stack/model/settings"
    18  	build "github.com/cozy/cozy-stack/pkg/config"
    19  	"github.com/cozy/cozy-stack/pkg/config/config"
    20  	"github.com/cozy/cozy-stack/pkg/crypto"
    21  	"github.com/cozy/cozy-stack/pkg/limits"
    22  	"github.com/cozy/cozy-stack/web/middlewares"
    23  	"github.com/labstack/echo/v4"
    24  )
    25  
    26  const (
    27  	// CredentialsErrorKey is the key for translating the message showed to the
    28  	// user when he/she enters incorrect credentials
    29  	CredentialsErrorKey = "Login Credentials error"
    30  	// TwoFactorErrorKey is the key for translating the message showed to the
    31  	// user when he/she enters incorrect two factor secret
    32  	TwoFactorErrorKey = "Login Two factor error"
    33  	// TwoFactorExceededErrorKey is the key for translating the message showed to the
    34  	// user when there were too many attempts
    35  	TwoFactorExceededErrorKey = "Login Two factor attempts error"
    36  )
    37  
    38  func wantsJSON(c echo.Context) bool {
    39  	return c.Request().Header.Get(echo.HeaderAccept) == echo.MIMEApplicationJSON
    40  }
    41  
    42  func renderError(c echo.Context, code int, msg string) error {
    43  	instance := middlewares.GetInstance(c)
    44  	return c.Render(code, "error.html", echo.Map{
    45  		"Domain":       instance.ContextualDomain(),
    46  		"ContextName":  instance.ContextName,
    47  		"Locale":       instance.Locale,
    48  		"Title":        instance.TemplateTitle(),
    49  		"Favicon":      middlewares.Favicon(instance),
    50  		"Illustration": "/images/generic-error.svg",
    51  		"Error":        msg,
    52  		"SupportEmail": instance.SupportEmailAddress(),
    53  	})
    54  }
    55  
    56  // Home is the handler for /
    57  // It redirects to the login page is the user is not yet authentified
    58  // Else, it redirects to its home application (or onboarding)
    59  func Home(c echo.Context) error {
    60  	instance := middlewares.GetInstance(c)
    61  
    62  	if len(instance.RegisterToken) > 0 && !instance.OnboardingFinished {
    63  		if !middlewares.CheckRegisterToken(c, instance) {
    64  			return middlewares.RenderNeedOnboarding(c, instance)
    65  		}
    66  		return c.Redirect(http.StatusSeeOther, instance.PageURL("/auth/passphrase", c.QueryParams()))
    67  	}
    68  
    69  	if middlewares.IsLoggedIn(c) {
    70  		redirect := instance.DefaultRedirection()
    71  		return c.Redirect(http.StatusSeeOther, redirect.String())
    72  	}
    73  
    74  	// Onboarding to a specific app when authentication via OIDC is enabled
    75  	redirection := c.QueryParam("redirection")
    76  	if redirection != "" && instance.HasForcedOIDC() {
    77  		splits := strings.SplitN(redirection, "#", 2)
    78  		parts := strings.SplitN(splits[0], "/", 2)
    79  		if _, err := app.GetWebappBySlug(instance, parts[0]); err == nil {
    80  			u := instance.SubDomain(parts[0])
    81  			if len(parts) == 2 {
    82  				u.Path = parts[1]
    83  			}
    84  			if len(splits) == 2 {
    85  				u.Fragment = splits[1]
    86  			}
    87  			q := url.Values{"redirect": {u.String()}}
    88  			return c.Redirect(http.StatusSeeOther, instance.PageURL("/oidc/start", q))
    89  		}
    90  	}
    91  
    92  	params := make(url.Values)
    93  	if jwt := c.QueryParam("jwt"); jwt != "" {
    94  		params.Add("jwt", jwt)
    95  	}
    96  	if code := c.QueryParam("email_verified_code"); code != "" {
    97  		params.Add("email_verified_code", code)
    98  	}
    99  	return c.Redirect(http.StatusSeeOther, instance.PageURL("/auth/login", params))
   100  }
   101  
   102  // SetCookieForNewSession creates a new session and sets the cookie on echo context
   103  func SetCookieForNewSession(c echo.Context, duration session.Duration) (string, error) {
   104  	instance := middlewares.GetInstance(c)
   105  	session, err := session.New(instance, duration)
   106  	if err != nil {
   107  		return "", err
   108  	}
   109  	cookie, err := session.ToCookie()
   110  	if err != nil {
   111  		return "", err
   112  	}
   113  	c.SetCookie(cookie)
   114  	return session.ID(), nil
   115  }
   116  
   117  // isTrustedDevice checks if a device of an instance is trusted
   118  func isTrustedDevice(c echo.Context, inst *instance.Instance) bool {
   119  	trustedDeviceToken := []byte(c.FormValue("trusted-device-token"))
   120  	return inst.ValidateTwoFactorTrustedDeviceSecret(c.Request(), trustedDeviceToken)
   121  }
   122  
   123  // hasEmailVerified checks if the email has already been verified, and if it is
   124  // the case, the stack can skip the 2FA by email.
   125  func hasEmailVerified(c echo.Context, inst *instance.Instance) bool {
   126  	code := c.FormValue("email_verified_code")
   127  	return inst.CheckEmailVerifiedCode(code)
   128  }
   129  
   130  func getLogoutURL(context string) string {
   131  	auth := config.GetConfig().Authentication
   132  	delegated, _ := auth[context].(map[string]interface{})
   133  	oidc, _ := delegated["oidc"].(map[string]interface{})
   134  	u, _ := oidc["logout_url"].(string)
   135  	return u
   136  }
   137  
   138  func redirectOIDC(c echo.Context, inst *instance.Instance) error {
   139  	if u := getLogoutURL(inst.ContextName); u != "" {
   140  		cookie, err := c.Cookie("logout")
   141  		if err == nil && cookie.Value == "1" {
   142  			c.SetCookie(&http.Cookie{
   143  				Name:   "logout",
   144  				Value:  "",
   145  				MaxAge: -1,
   146  				Domain: session.CookieDomain(inst),
   147  			})
   148  			return c.Redirect(http.StatusSeeOther, u)
   149  		}
   150  	}
   151  
   152  	var q url.Values
   153  	if redirect := c.QueryParam("redirect"); redirect != "" {
   154  		q = url.Values{"redirect": {redirect}}
   155  	}
   156  	return c.Redirect(http.StatusSeeOther, inst.PageURL("/oidc/start", q))
   157  }
   158  
   159  func renderLoginForm(c echo.Context, i *instance.Instance, code int, credsErrors string, redirect *url.URL) error {
   160  	if i.HasForcedOIDC() {
   161  		return redirectOIDC(c, i)
   162  	}
   163  	hasFranceConnect := i.FranceConnectID != ""
   164  
   165  	publicName, err := csettings.PublicName(i)
   166  	if err != nil {
   167  		publicName = ""
   168  	}
   169  
   170  	var redirectStr string
   171  	var hasOAuth, hasSharing bool
   172  	if redirect != nil {
   173  		redirectStr = redirect.String()
   174  		hasOAuth = hasRedirectToAuthorize(i, redirect)
   175  		hasSharing = hasRedirectToAuthorizeSharing(i, redirect)
   176  	}
   177  
   178  	var title, help string
   179  	if c.QueryParam("msg") == "passphrase-reset-requested" {
   180  		title = i.Translate("Login Connect after reset requested title")
   181  		help = i.Translate("Login Connect after reset requested help")
   182  	} else if strings.Contains(redirectStr, "reconnect") {
   183  		title = i.Translate("Login Reconnect title")
   184  		help = i.Translate("Login Reconnect help")
   185  	} else if hasSharing {
   186  		title = i.Translate("Login Connect from sharing title", publicName)
   187  		help = i.Translate("Login Connect from sharing help")
   188  	} else {
   189  		if publicName == "" {
   190  			title = i.Translate("Login Welcome")
   191  		} else {
   192  			title = i.Translate("Login Welcome name", publicName)
   193  		}
   194  		help = i.Translate("Login Password help")
   195  	}
   196  
   197  	iterations := 0
   198  	if settings, err := settings.Get(i); err == nil {
   199  		iterations = settings.PassphraseKdfIterations
   200  	}
   201  
   202  	// When we have an email_verified_code, we need to ask the user their
   203  	// password, not send them an email with a magic link
   204  	emailVerifiedCode := c.QueryParam("email_verified_code")
   205  	magicLink := i.MagicLink
   206  	if emailVerifiedCode != "" {
   207  		magicLink = false
   208  	}
   209  
   210  	return c.Render(code, "login.html", echo.Map{
   211  		"TemplateTitle":     i.TemplateTitle(),
   212  		"Domain":            i.ContextualDomain(),
   213  		"ContextName":       i.ContextName,
   214  		"Locale":            i.Locale,
   215  		"Favicon":           middlewares.Favicon(i),
   216  		"CryptoPolyfill":    middlewares.CryptoPolyfill(c),
   217  		"BottomNavBar":      middlewares.BottomNavigationBar(c),
   218  		"Iterations":        iterations,
   219  		"Salt":              string(i.PassphraseSalt()),
   220  		"Title":             title,
   221  		"PasswordHelp":      help,
   222  		"CredentialsError":  credsErrors,
   223  		"Redirect":          redirectStr,
   224  		"CSRF":              c.Get("csrf"),
   225  		"EmailVerifiedCode": emailVerifiedCode,
   226  		"MagicLink":         magicLink,
   227  		"OAuth":             hasOAuth,
   228  		"FranceConnect":     hasFranceConnect,
   229  	})
   230  }
   231  
   232  func loginForm(c echo.Context) error {
   233  	instance := middlewares.GetInstance(c)
   234  
   235  	redirect, err := checkRedirectParam(c, nil)
   236  	if err != nil {
   237  		return err
   238  	}
   239  
   240  	if middlewares.IsLoggedIn(c) {
   241  		if redirect == nil {
   242  			redirect = instance.DefaultRedirection()
   243  		}
   244  		return c.Redirect(http.StatusSeeOther, redirect.String())
   245  	}
   246  	// Delegated JWT
   247  	if token := c.QueryParam("jwt"); token != "" {
   248  		err := session.CheckDelegatedJWT(instance, token)
   249  		if err != nil {
   250  			instance.Logger().Warnf("Delegated token check failed: %s", err)
   251  		} else {
   252  			sessionID, err := SetCookieForNewSession(c, session.NormalRun)
   253  			if err != nil {
   254  				return err
   255  			}
   256  			if err = session.StoreNewLoginEntry(instance, sessionID, "", c.Request(), "JWT", true); err != nil {
   257  				instance.Logger().Errorf("Could not store session history %q: %s", sessionID, err)
   258  			}
   259  			if redirect == nil {
   260  				redirect = instance.DefaultRedirection()
   261  			}
   262  			return c.Redirect(http.StatusSeeOther, redirect.String())
   263  		}
   264  	}
   265  	return renderLoginForm(c, instance, http.StatusOK, "", redirect)
   266  }
   267  
   268  // newSession generates a new session, and puts a cookie for it
   269  func newSession(c echo.Context, inst *instance.Instance, redirect *url.URL, duration session.Duration, logMessage string) error {
   270  	var clientID string
   271  	if hasRedirectToAuthorize(inst, redirect) {
   272  		// NOTE: the login scope is used by external clients for authentication.
   273  		// Typically, these clients are used for internal purposes, like
   274  		// authenticating to an external system via the cozy. For these clients
   275  		// we do not push a "client" notification, we only store a new login
   276  		// history.
   277  		clientID = redirect.Query().Get("client_id")
   278  		duration = session.ShortRun
   279  	}
   280  
   281  	sessionID, err := SetCookieForNewSession(c, duration)
   282  	if err != nil {
   283  		return err
   284  	}
   285  
   286  	if err = session.StoreNewLoginEntry(inst, sessionID, clientID, c.Request(), logMessage, true); err != nil {
   287  		inst.Logger().Errorf("Could not store session history %q: %s", sessionID, err)
   288  	}
   289  
   290  	return nil
   291  }
   292  
   293  func migrateToHashedPassphrase(inst *instance.Instance, settings *settings.Settings, passphrase []byte, iterations int) {
   294  	salt := inst.PassphraseSalt()
   295  	pass, masterKey := crypto.HashPassWithPBKDF2(passphrase, salt, iterations)
   296  	hash, err := crypto.GenerateFromPassphrase(pass)
   297  	if err != nil {
   298  		inst.Logger().Errorf("Could not hash the passphrase: %s", err.Error())
   299  		return
   300  	}
   301  	inst.PassphraseHash = hash
   302  	settings.PassphraseKdfIterations = iterations
   303  	settings.PassphraseKdf = instance.PBKDF2_SHA256
   304  	settings.SecurityStamp = lifecycle.NewSecurityStamp()
   305  	key, encKey, err := lifecycle.CreatePassphraseKey(masterKey)
   306  	if err != nil {
   307  		inst.Logger().Errorf("Could not create passphrase key: %s", err.Error())
   308  		return
   309  	}
   310  	settings.Key = key
   311  	pubKey, privKey, err := lifecycle.CreateKeyPair(encKey)
   312  	if err != nil {
   313  		inst.Logger().Errorf("Could not create key pair: %s", err.Error())
   314  		return
   315  	}
   316  	settings.PublicKey = pubKey
   317  	settings.PrivateKey = privKey
   318  	if err := instance.Update(inst); err != nil {
   319  		inst.Logger().Errorf("Could not update: %s", err.Error())
   320  	}
   321  	if err := settings.Save(inst); err != nil {
   322  		inst.Logger().Errorf("Could not update: %s", err.Error())
   323  	}
   324  }
   325  
   326  func login(c echo.Context) error {
   327  	inst := middlewares.GetInstance(c)
   328  
   329  	redirect, err := checkRedirectParam(c, inst.DefaultRedirection())
   330  	if err != nil {
   331  		return err
   332  	}
   333  
   334  	passphrase := []byte(c.FormValue("passphrase"))
   335  	longRunSession, _ := strconv.ParseBool(c.FormValue("long-run-session"))
   336  
   337  	var sessionID string
   338  	sess, ok := middlewares.GetSession(c)
   339  	if ok { // The user was already logged-in
   340  		sessionID = sess.ID()
   341  	} else if instance.CheckPassphrase(inst, passphrase) == nil {
   342  		iterations := crypto.DefaultPBKDF2Iterations
   343  		settings, err := settings.Get(inst)
   344  		// If the passphrase was not yet hashed on the client side, migrate it
   345  		if err == nil && settings.PassphraseKdfIterations == 0 {
   346  			migrateToHashedPassphrase(inst, settings, passphrase, iterations)
   347  		}
   348  
   349  		// In case the second factor authentication mode is "mail", we also
   350  		// check that the mail has been confirmed. If not, 2FA is not
   351  		// activated.
   352  		// If device is trusted, skip the 2FA.
   353  		// If the email has already been verified, skip the 2FA too.
   354  		if inst.HasAuthMode(instance.TwoFactorMail) && !isTrustedDevice(c, inst) && !hasEmailVerified(c, inst) {
   355  			twoFactorToken, err := lifecycle.SendTwoFactorPasscode(inst)
   356  			if err != nil {
   357  				return err
   358  			}
   359  			v := url.Values{}
   360  			v.Add("two_factor_token", string(twoFactorToken))
   361  			v.Add("long_run_session", strconv.FormatBool(longRunSession))
   362  			if loc := c.FormValue("redirect"); loc != "" {
   363  				v.Add("redirect", loc)
   364  			}
   365  
   366  			if wantsJSON(c) {
   367  				return c.JSON(http.StatusOK, echo.Map{
   368  					"redirect": inst.PageURL("/auth/twofactor", v),
   369  				})
   370  			}
   371  			return c.Redirect(http.StatusSeeOther, inst.PageURL("/auth/twofactor", v))
   372  		}
   373  	} else { // Bad login passphrase
   374  		errorMessage := inst.Translate(CredentialsErrorKey)
   375  		err := config.GetRateLimiter().CheckRateLimit(inst, limits.AuthType)
   376  		if limits.IsLimitReachedOrExceeded(err) {
   377  			if err = LoginRateExceeded(inst); err != nil {
   378  				inst.Logger().WithNamespace("auth").Warn(err.Error())
   379  			}
   380  		}
   381  		if wantsJSON(c) {
   382  			return c.JSON(http.StatusUnauthorized, echo.Map{
   383  				"error": errorMessage,
   384  			})
   385  		}
   386  		return renderLoginForm(c, inst, http.StatusUnauthorized, errorMessage, redirect)
   387  	}
   388  
   389  	// Successful authentication
   390  	// User is now logged-in, generate a new session
   391  	if sessionID == "" {
   392  		duration := session.NormalRun
   393  		if longRunSession {
   394  			duration = session.LongRun
   395  		}
   396  		err := newSession(c, inst, redirect, duration, "password")
   397  		if err != nil {
   398  			return err
   399  		}
   400  	}
   401  	if wantsJSON(c) {
   402  		return c.JSON(http.StatusOK, echo.Map{
   403  			"redirect": redirect.String(),
   404  		})
   405  	}
   406  
   407  	return c.Redirect(http.StatusSeeOther, redirect.String())
   408  }
   409  
   410  // addLogoutCookie adds a cookie for logged-out users on instances in a context
   411  // where OIDC is configured. It allows to redirects the user on the next request
   412  // to a special page instead of sending them to the OIDC page (which can logs
   413  // in the user again automatically).
   414  func addLogoutCookie(c echo.Context, inst *instance.Instance) {
   415  	if u := getLogoutURL(inst.ContextName); u == "" {
   416  		return
   417  	}
   418  	c.SetCookie(&http.Cookie{
   419  		Name:     "logout",
   420  		Value:    "1",
   421  		MaxAge:   10,
   422  		Domain:   session.CookieDomain(inst),
   423  		Secure:   !build.IsDevRelease(),
   424  		HttpOnly: true,
   425  	})
   426  }
   427  
   428  func logout(c echo.Context) error {
   429  	res := c.Response()
   430  	origin := c.Request().Header.Get(echo.HeaderOrigin)
   431  	res.Header().Set(echo.HeaderAccessControlAllowOrigin, origin)
   432  	res.Header().Set(echo.HeaderAccessControlAllowCredentials, "true")
   433  
   434  	inst := middlewares.GetInstance(c)
   435  	if !middlewares.AllowLogout(c) {
   436  		return c.JSON(http.StatusUnauthorized, echo.Map{
   437  			"error": "The user can logout only from client-side apps",
   438  		})
   439  	}
   440  
   441  	session, ok := middlewares.GetSession(c)
   442  	if ok {
   443  		c.SetCookie(session.Delete(inst))
   444  	}
   445  
   446  	addLogoutCookie(c, inst)
   447  
   448  	return c.NoContent(http.StatusNoContent)
   449  }
   450  
   451  func logoutOthers(c echo.Context) error {
   452  	res := c.Response()
   453  	origin := c.Request().Header.Get(echo.HeaderOrigin)
   454  	res.Header().Set(echo.HeaderAccessControlAllowOrigin, origin)
   455  	res.Header().Set(echo.HeaderAccessControlAllowCredentials, "true")
   456  
   457  	instance := middlewares.GetInstance(c)
   458  	if !middlewares.AllowLogout(c) {
   459  		return c.JSON(http.StatusUnauthorized, echo.Map{
   460  			"error": "The user can logout only from client-side apps",
   461  		})
   462  	}
   463  
   464  	sess, ok := middlewares.GetSession(c)
   465  	if !ok {
   466  		return c.JSON(http.StatusUnauthorized, echo.Map{
   467  			"error": "Could not retrieve session",
   468  		})
   469  	}
   470  	if err := session.DeleteOthers(instance, sess.ID()); err != nil {
   471  		return err
   472  	}
   473  
   474  	return c.NoContent(http.StatusNoContent)
   475  }
   476  
   477  func logoutPreflight(c echo.Context) error {
   478  	req := c.Request()
   479  	res := c.Response()
   480  	origin := req.Header.Get(echo.HeaderOrigin)
   481  
   482  	res.Header().Add(echo.HeaderVary, echo.HeaderOrigin)
   483  	res.Header().Add(echo.HeaderVary, echo.HeaderAccessControlRequestMethod)
   484  	res.Header().Add(echo.HeaderVary, echo.HeaderAccessControlRequestHeaders)
   485  	res.Header().Set(echo.HeaderAccessControlAllowOrigin, origin)
   486  	res.Header().Set(echo.HeaderAccessControlAllowMethods, echo.DELETE)
   487  	res.Header().Set(echo.HeaderAccessControlAllowCredentials, "true")
   488  	res.Header().Set(echo.HeaderAccessControlMaxAge, middlewares.MaxAgeCORS)
   489  	if h := req.Header.Get(echo.HeaderAccessControlRequestHeaders); h != "" {
   490  		res.Header().Set(echo.HeaderAccessControlAllowHeaders, h)
   491  	}
   492  
   493  	return c.NoContent(http.StatusNoContent)
   494  }
   495  
   496  // checkRedirectParam returns the optional redirect query parameter. If not
   497  // empty, we check that the redirect is a subdomain of the cozy-instance.
   498  func checkRedirectParam(c echo.Context, defaultRedirect *url.URL) (*url.URL, error) {
   499  	instance := middlewares.GetInstance(c)
   500  	redirect := c.FormValue("redirect")
   501  	if redirect == "" {
   502  		redirect = c.QueryParam("redirect")
   503  	}
   504  
   505  	// If the Cozy was moved from another address and the owner had a vault,
   506  	// we will show them instructions about how to import their vault.
   507  	settings, err := instance.SettingsDocument()
   508  	if err == nil && settings.M["import_vault"] == true {
   509  		u := url.URL{
   510  			Scheme: instance.Scheme(),
   511  			Host:   instance.ContextualDomain(),
   512  			Path:   "/move/vault",
   513  		}
   514  		return &u, nil
   515  	}
   516  
   517  	if redirect == "" {
   518  		if defaultRedirect == nil {
   519  			return defaultRedirect, nil
   520  		}
   521  		redirect = defaultRedirect.String()
   522  	}
   523  
   524  	u, err := url.Parse(redirect)
   525  	if err != nil || u.Scheme == "" {
   526  		u, err = AppRedirection(instance, redirect)
   527  	}
   528  	if err != nil {
   529  		return nil, echo.NewHTTPError(http.StatusBadRequest,
   530  			"bad url: could not parse")
   531  	}
   532  
   533  	if u.Scheme != "http" && u.Scheme != "https" {
   534  		return nil, echo.NewHTTPError(http.StatusBadRequest,
   535  			"bad url: bad scheme")
   536  	}
   537  
   538  	if !instance.HasDomain(u.Host) {
   539  		instanceHost, appSlug, _ := config.SplitCozyHost(u.Host)
   540  		if !instance.HasDomain(instanceHost) || appSlug == "" {
   541  			return nil, echo.NewHTTPError(http.StatusBadRequest,
   542  				"bad url: should be subdomain")
   543  		}
   544  		return u, nil
   545  	}
   546  
   547  	// To protect against stealing authorization code with redirection, the
   548  	// fragment is always overridden. Most browsers keep URI fragments upon
   549  	// redirects, to make sure to override them, we put an empty one.
   550  	//
   551  	// see: oauthsecurity.com/#provider-in-the-middle
   552  	// see: 7.4.2 OAuth2 in Action
   553  	u.Fragment = "="
   554  	return u, nil
   555  }
   556  
   557  func AppRedirection(inst *instance.Instance, redirect string) (*url.URL, error) {
   558  	splits := strings.SplitN(redirect, "#", 2)
   559  	parts := strings.SplitN(splits[0], "/", 2)
   560  	if _, err := app.GetWebappBySlug(inst, parts[0]); err != nil {
   561  		return nil, err
   562  	}
   563  	u := inst.SubDomain(parts[0])
   564  	if len(parts) == 2 {
   565  		u.Path = parts[1]
   566  	}
   567  	if len(splits) == 2 {
   568  		u.Fragment = splits[1]
   569  	}
   570  	return u, nil
   571  }
   572  
   573  func resendActivationMail(c echo.Context) error {
   574  	inst := middlewares.GetInstance(c)
   575  	rate := config.GetRateLimiter()
   576  	if err := rate.CheckRateLimit(inst, limits.ResendOnboardingMailType); err == nil {
   577  		client := instance.APIManagerClient(inst)
   578  		if len(inst.RegisterToken) == 0 || inst.UUID == "" || client == nil {
   579  			return errors.New("cannot resend activation link")
   580  		}
   581  		url := fmt.Sprintf("/api/v1/instances/%s/resend", url.PathEscape(inst.UUID))
   582  		if err := client.Post(url, nil); err != nil {
   583  			return errors.New("cannot resend activation link")
   584  		}
   585  	}
   586  	return c.Render(http.StatusOK, "error.html", echo.Map{
   587  		"Domain":       inst.ContextualDomain(),
   588  		"ContextName":  inst.ContextName,
   589  		"Locale":       inst.Locale,
   590  		"Title":        inst.TemplateTitle(),
   591  		"Favicon":      middlewares.Favicon(inst),
   592  		"Inverted":     true,
   593  		"Illustration": "/images/mail-sent.svg",
   594  		"ErrorTitle":   "Onboarding Resend activation Title",
   595  		"Error":        "Onboarding Resend activation Body",
   596  		"ErrorDetail":  "Onboarding Resend activation Detail",
   597  		"SupportEmail": inst.SupportEmailAddress(),
   598  	})
   599  }
   600  
   601  // Routes sets the routing for the status service
   602  func Routes(router *echo.Group) {
   603  	noCSRF := middlewares.CSRFWithConfig(middlewares.CSRFConfig{
   604  		TokenLookup:    "form:csrf_token",
   605  		CookieMaxAge:   3600, // 1 hour
   606  		CookieHTTPOnly: true,
   607  		CookieSecure:   !build.IsDevRelease(),
   608  		CookieSameSite: http.SameSiteStrictMode,
   609  		CookiePath:     "/auth",
   610  	})
   611  
   612  	// Login/logout
   613  	router.GET("/login", loginForm, noCSRF, middlewares.CheckOnboardingNotFinished)
   614  	router.POST("/login", login, noCSRF, middlewares.CheckOnboardingNotFinished)
   615  	router.POST("/login/flagship", loginFlagship, middlewares.CheckOnboardingNotFinished)
   616  	router.DELETE("/login/others", logoutOthers)
   617  	router.OPTIONS("/login/others", logoutPreflight)
   618  	router.DELETE("/login", logout)
   619  	router.OPTIONS("/login", logoutPreflight)
   620  
   621  	// Magic links
   622  	router.POST("/magic_link", sendMagicLink, noCSRF)
   623  	router.GET("/magic_link", loginWithMagicLink, noCSRF)
   624  	router.POST("/magic_link/twofactor", loginWithMagicLinkAndPassword, noCSRF)
   625  	router.POST("/magic_link/flagship", magicLinkFlagship)
   626  
   627  	// Passphrase
   628  	router.GET("/passphrase_reset", passphraseResetForm, noCSRF)
   629  	router.POST("/passphrase_reset", passphraseReset, noCSRF)
   630  	router.GET("/passphrase_renew", passphraseRenewForm, noCSRF)
   631  	router.POST("/passphrase_renew", passphraseRenew, noCSRF)
   632  	router.GET("/passphrase", passphraseForm, noCSRF)
   633  	router.POST("/hint", sendHint)
   634  	router.POST("/onboarding/resend", resendActivationMail)
   635  
   636  	// Confirmation by typing
   637  	router.GET("/confirm", confirmForm, noCSRF)
   638  	router.POST("/confirm", confirmAuth, noCSRF)
   639  	router.GET("/confirm/:code", confirmCode)
   640  
   641  	// Register OAuth clients
   642  	router.POST("/register", registerClient, middlewares.AcceptJSON, middlewares.ContentTypeJSON)
   643  	router.GET("/register/:client-id", readClient, middlewares.AcceptJSON, checkRegistrationToken)
   644  	router.PUT("/register/:client-id", updateClient, middlewares.AcceptJSON, middlewares.ContentTypeJSON)
   645  	router.DELETE("/register/:client-id", deleteClient)
   646  	router.POST("/clients/:client-id/challenge", postChallenge, checkRegistrationToken)
   647  	router.POST("/clients/:client-id/attestation", postAttestation)
   648  	router.POST("/clients/:client-id/flagship", confirmFlagship)
   649  
   650  	// OAuth flow
   651  	authHandler := NewAuthorizeHandler(config.GetConfig().DeprecatedApps)
   652  	authHandler.Register(router.Group("/authorize", noCSRF))
   653  
   654  	router.POST("/access_token", accessToken)
   655  
   656  	// Flagship app
   657  	router.POST("/session_code", CreateSessionCode)
   658  	router.POST("/tokens/konnectors/:slug", buildKonnectorToken)
   659  
   660  	// 2FA
   661  	router.GET("/twofactor", twoFactorForm)
   662  	router.POST("/twofactor", twoFactor)
   663  
   664  	// Share by link protected by password
   665  	router.POST("/share-by-link/password", checkPasswordForShareByLink)
   666  }