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

     1  package auth
     2  
     3  import (
     4  	"net/http"
     5  	"net/url"
     6  	"strconv"
     7  
     8  	"github.com/cozy/cozy-stack/model/instance"
     9  	"github.com/cozy/cozy-stack/model/session"
    10  	"github.com/cozy/cozy-stack/pkg/config/config"
    11  	"github.com/cozy/cozy-stack/pkg/limits"
    12  	"github.com/cozy/cozy-stack/web/middlewares"
    13  	"github.com/labstack/echo/v4"
    14  )
    15  
    16  func renderTwoFactorForm(c echo.Context, i *instance.Instance, code int, credsError string, twoFactorToken []byte) error {
    17  	title := i.Translate("Login Two factor title")
    18  
    19  	longRunSession, err := getTwoFactorLongRunSession(c)
    20  	if err != nil {
    21  		return err
    22  	}
    23  	redirect, err := getTwoFactorRedirect(c)
    24  	if err != nil {
    25  		return err
    26  	}
    27  
    28  	trustedDeviceCheckBox := true
    29  	trustedDeviceCheckBoxParam := c.QueryParam("trusted_device_checkbox")
    30  	if trustedDeviceCheckBoxParam != "" {
    31  		if b, err := strconv.ParseBool(trustedDeviceCheckBoxParam); err == nil {
    32  			trustedDeviceCheckBox = b
    33  		}
    34  	}
    35  
    36  	oauth := hasRedirectToAuthorize(i, redirect)
    37  	trustedCheckbox := !oauth && trustedDeviceCheckBox
    38  
    39  	return c.Render(code, "twofactor.html", echo.Map{
    40  		"Domain":                i.ContextualDomain(),
    41  		"ContextName":           i.ContextName,
    42  		"Locale":                i.Locale,
    43  		"Title":                 title,
    44  		"Favicon":               middlewares.Favicon(i),
    45  		"CredentialsError":      credsError,
    46  		"Redirect":              redirect.String(),
    47  		"Confirm":               c.FormValue("confirm"),
    48  		"State":                 c.FormValue("state"),
    49  		"ClientID":              c.FormValue("client_id"),
    50  		"LongRunSession":        longRunSession,
    51  		"TwoFactorToken":        string(twoFactorToken),
    52  		"TrustedDeviceCheckBox": trustedCheckbox,
    53  	})
    54  }
    55  
    56  func getTwoFactorLongRunSession(c echo.Context) (bool, error) {
    57  	if longRunParam := c.QueryParam("long-run-session"); longRunParam != "" {
    58  		longRun, err := strconv.ParseBool(longRunParam)
    59  		if err != nil {
    60  			return false, err
    61  		}
    62  		return longRun, nil
    63  	}
    64  	return false, nil
    65  }
    66  
    67  func getTwoFactorRedirect(c echo.Context) (*url.URL, error) {
    68  	inst := middlewares.GetInstance(c)
    69  	if c.FormValue("client_id") != "" {
    70  		return url.Parse(c.FormValue("redirect"))
    71  	}
    72  	return checkRedirectParam(c, inst.DefaultRedirection())
    73  }
    74  
    75  // twoFactorForm handles the twoFactor from GET request
    76  func twoFactorForm(c echo.Context) error {
    77  	inst := middlewares.GetInstance(c)
    78  	twoFactorTokenParam := c.QueryParam("two_factor_token")
    79  	if twoFactorTokenParam == "" {
    80  		return c.JSON(http.StatusBadRequest, "Missing twoFactorToken")
    81  	}
    82  	twoFactorToken := []byte(twoFactorTokenParam)
    83  
    84  	return renderTwoFactorForm(c, inst, http.StatusOK, "", twoFactorToken)
    85  }
    86  
    87  // twoFactor handles a the twoFactor POST request
    88  func twoFactor(c echo.Context) error {
    89  	inst := middlewares.GetInstance(c)
    90  	if !inst.HasAuthMode(instance.TwoFactorMail) {
    91  		errorMessage := inst.Translate(TwoFactorErrorKey)
    92  		return c.JSON(http.StatusUnauthorized, echo.Map{
    93  			"error": errorMessage,
    94  		})
    95  	}
    96  
    97  	// Retreiving data from request
    98  	token := []byte(c.FormValue("two-factor-token"))
    99  	passcode := c.FormValue("two-factor-passcode")
   100  	generateTrustedDeviceToken, _ := strconv.ParseBool(c.FormValue("two-factor-generate-trusted-device-token"))
   101  
   102  	// Handle 2FA failed
   103  	correctPasscode := inst.ValidateTwoFactorPasscode(token, passcode)
   104  	if !correctPasscode {
   105  		return twoFactorFailed(c, inst, token)
   106  	}
   107  
   108  	// Special case when the 2FA validation is for confirming authentication,
   109  	// not creating a new session.
   110  	if c.FormValue("confirm") == "true" {
   111  		return ConfirmSuccess(c, inst, c.FormValue("state"))
   112  	}
   113  
   114  	// Special case when the 2FA validation if for moving a Cozy to this
   115  	// instance.
   116  	if c.FormValue("client_id") != "" {
   117  		u, err := moveSuccessURI(c)
   118  		if err != nil {
   119  			return err
   120  		}
   121  		return c.JSON(http.StatusOK, echo.Map{
   122  			"redirect": u,
   123  		})
   124  	}
   125  
   126  	// Generate a new session
   127  	longRunSession, err := getTwoFactorLongRunSession(c)
   128  	if err != nil {
   129  		return err
   130  	}
   131  	redirect, err := getTwoFactorRedirect(c)
   132  	if err != nil {
   133  		return err
   134  	}
   135  	duration := session.NormalRun
   136  	if longRunSession {
   137  		duration = session.LongRun
   138  	} else if hasRedirectToAuthorize(inst, redirect) {
   139  		duration = session.ShortRun
   140  	}
   141  	if err := newSession(c, inst, redirect, duration, "2FA"); err != nil {
   142  		return err
   143  	}
   144  
   145  	// Check if the user trusts its device
   146  	var generatedTrustedDeviceToken []byte
   147  	if generateTrustedDeviceToken {
   148  		generatedTrustedDeviceToken, _ = inst.GenerateTwoFactorTrustedDeviceSecret(c.Request())
   149  	}
   150  	if wantsJSON(c) {
   151  		result := echo.Map{"redirect": redirect.String()}
   152  		if len(generatedTrustedDeviceToken) > 0 {
   153  			result["two_factor_trusted_device_token"] = string(generatedTrustedDeviceToken)
   154  		}
   155  		return c.JSON(http.StatusOK, result)
   156  	}
   157  
   158  	return c.Redirect(http.StatusSeeOther, redirect.String())
   159  }
   160  
   161  // twoFactorFailed returns the 2FA form with an error message
   162  func twoFactorFailed(c echo.Context, inst *instance.Instance, token []byte) error {
   163  	errorMessage := inst.Translate(TwoFactorErrorKey)
   164  	errCheckRateLimit := config.GetRateLimiter().CheckRateLimit(inst, limits.TwoFactorType)
   165  	if errCheckRateLimit == limits.ErrRateLimitExceeded {
   166  		if err := TwoFactorRateExceeded(inst); err != nil {
   167  			inst.Logger().WithNamespace("auth").Warn(err.Error())
   168  			errorMessage = inst.Translate(TwoFactorExceededErrorKey)
   169  		}
   170  	}
   171  
   172  	// Render either the passcode page or a JSON message
   173  	if wantsJSON(c) {
   174  		return c.JSON(http.StatusUnauthorized, echo.Map{
   175  			"error": errorMessage,
   176  		})
   177  	}
   178  	return renderTwoFactorForm(c, inst, http.StatusUnauthorized, errorMessage, token)
   179  }