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

     1  package auth
     2  
     3  import (
     4  	"crypto/subtle"
     5  	"net/http"
     6  
     7  	"github.com/cozy/cozy-stack/model/bitwarden/settings"
     8  	"github.com/cozy/cozy-stack/model/instance"
     9  	"github.com/cozy/cozy-stack/model/instance/lifecycle"
    10  	"github.com/cozy/cozy-stack/model/oauth"
    11  	"github.com/cozy/cozy-stack/model/session"
    12  	"github.com/cozy/cozy-stack/pkg/config/config"
    13  	"github.com/cozy/cozy-stack/pkg/consts"
    14  	"github.com/cozy/cozy-stack/pkg/couchdb"
    15  	"github.com/cozy/cozy-stack/pkg/jsonapi"
    16  	"github.com/cozy/cozy-stack/pkg/limits"
    17  	"github.com/cozy/cozy-stack/web/middlewares"
    18  	"github.com/labstack/echo/v4"
    19  )
    20  
    21  func sendMagicLink(c echo.Context) error {
    22  	inst := middlewares.GetInstance(c)
    23  
    24  	err := config.GetRateLimiter().CheckRateLimit(inst, limits.MagicLinkType)
    25  	if limits.IsLimitReachedOrExceeded(err) {
    26  		return echo.NewHTTPError(http.StatusTooManyRequests, "Too many requests")
    27  	}
    28  
    29  	redirect := c.FormValue("redirect")
    30  	if err := lifecycle.SendMagicLink(inst, redirect); err != nil {
    31  		return err
    32  	}
    33  	return c.Render(http.StatusOK, "error.html", echo.Map{
    34  		"Domain":       inst.ContextualDomain(),
    35  		"ContextName":  inst.ContextName,
    36  		"Locale":       inst.Locale,
    37  		"Title":        inst.TemplateTitle(),
    38  		"Favicon":      middlewares.Favicon(inst),
    39  		"Inverted":     true,
    40  		"Illustration": "/images/mail-sent.svg",
    41  		"ErrorTitle":   "Magic link has been sent Title",
    42  		"Error":        "Magic link has been sent Body",
    43  		"ErrorDetail":  "Magic link has been sent Detail",
    44  		"SupportEmail": inst.SupportEmailAddress(),
    45  	})
    46  }
    47  
    48  func loginWithMagicLink(c echo.Context) error {
    49  	inst := middlewares.GetInstance(c)
    50  	redirect, err := checkRedirectParam(c, inst.DefaultRedirection())
    51  	if err != nil {
    52  		return err
    53  	}
    54  
    55  	if _, ok := middlewares.GetSession(c); ok {
    56  		return c.Redirect(http.StatusSeeOther, redirect.String())
    57  	}
    58  
    59  	code := c.QueryParam("code") // Login
    60  	if code == "" {
    61  		code = c.QueryParam("magic_code") // Onboarding from the cloudery
    62  	}
    63  	if err := lifecycle.CheckMagicLink(inst, code); err != nil {
    64  		err := config.GetRateLimiter().CheckRateLimit(inst, limits.MagicLinkType)
    65  		if limits.IsLimitReachedOrExceeded(err) {
    66  			return echo.NewHTTPError(http.StatusTooManyRequests, "Too many requests")
    67  		}
    68  		return renderError(c, http.StatusBadRequest, "Error Invalid magic link")
    69  	}
    70  
    71  	if inst.HasAuthMode(instance.TwoFactorMail) {
    72  		iterations := 0
    73  		if settings, err := settings.Get(inst); err == nil {
    74  			iterations = settings.PassphraseKdfIterations
    75  		}
    76  		return c.Render(http.StatusOK, "magic_link_twofactor.html", echo.Map{
    77  			"TemplateTitle":  inst.TemplateTitle(),
    78  			"Domain":         inst.ContextualDomain(),
    79  			"ContextName":    inst.ContextName,
    80  			"Locale":         inst.Locale,
    81  			"Iterations":     iterations,
    82  			"Salt":           string(inst.PassphraseSalt()),
    83  			"CSRF":           c.Get("csrf"),
    84  			"Favicon":        middlewares.Favicon(inst),
    85  			"BottomNavBar":   middlewares.BottomNavigationBar(c),
    86  			"CryptoPolyfill": middlewares.CryptoPolyfill(c),
    87  			"MagicCode":      code,
    88  			"Redirect":       redirect,
    89  		})
    90  	}
    91  
    92  	err = newSession(c, inst, redirect, session.NormalRun, "magic_link")
    93  	if err != nil {
    94  		return err
    95  	}
    96  	return c.Redirect(http.StatusSeeOther, redirect.String())
    97  }
    98  
    99  func loginWithMagicLinkAndPassword(c echo.Context) error {
   100  	inst := middlewares.GetInstance(c)
   101  	code := c.FormValue("magic_code")
   102  
   103  	// Check magic code
   104  	if err := lifecycle.CheckMagicLink(inst, code); err != nil {
   105  		err := config.GetRateLimiter().CheckRateLimit(inst, limits.MagicLinkType)
   106  		if limits.IsLimitReachedOrExceeded(err) {
   107  			return echo.NewHTTPError(http.StatusTooManyRequests, "Too many requests")
   108  		}
   109  		return c.JSON(http.StatusUnauthorized, echo.Map{
   110  			"error": inst.Translate("Error Invalid magic link"),
   111  		})
   112  	}
   113  
   114  	// Check passphrase
   115  	passphrase := []byte(c.FormValue("passphrase"))
   116  	if instance.CheckPassphrase(inst, passphrase) != nil {
   117  		errorMessage := inst.Translate(CredentialsErrorKey)
   118  		err := config.GetRateLimiter().CheckRateLimit(inst, limits.AuthType)
   119  		if limits.IsLimitReachedOrExceeded(err) {
   120  			if err = LoginRateExceeded(inst); err != nil {
   121  				inst.Logger().WithNamespace("auth").Warn(err.Error())
   122  			}
   123  		}
   124  		return c.JSON(http.StatusUnauthorized, echo.Map{
   125  			"error": errorMessage,
   126  		})
   127  	}
   128  
   129  	redirect, err := checkRedirectParam(c, inst.DefaultRedirection())
   130  	if err != nil {
   131  		return err
   132  	}
   133  	err = newSession(c, inst, redirect, session.NormalRun, "magic_link")
   134  	if err != nil {
   135  		return err
   136  	}
   137  	return c.JSON(http.StatusOK, echo.Map{
   138  		"redirect": redirect.String(),
   139  	})
   140  }
   141  
   142  type magicLinkFlagshipParameters struct {
   143  	ClientID     string `json:"client_id"`
   144  	ClientSecret string `json:"client_secret"`
   145  	Code         string `json:"magic_code"`
   146  	Passphrase   string `json:"passphrase"`
   147  }
   148  
   149  func magicLinkFlagship(c echo.Context) error {
   150  	inst := middlewares.GetInstance(c)
   151  
   152  	var args magicLinkFlagshipParameters
   153  	if err := c.Bind(&args); err != nil {
   154  		return jsonapi.Errorf(http.StatusBadRequest, "%s", err)
   155  	}
   156  
   157  	if err := lifecycle.CheckMagicLink(inst, args.Code); err != nil {
   158  		err := config.GetRateLimiter().CheckRateLimit(inst, limits.MagicLinkType)
   159  		if limits.IsLimitReachedOrExceeded(err) {
   160  			return echo.NewHTTPError(http.StatusTooManyRequests, "Too many requests")
   161  		}
   162  		return c.JSON(http.StatusUnauthorized, echo.Map{
   163  			"error": "invalid magic code",
   164  		})
   165  	}
   166  
   167  	if inst.HasAuthMode(instance.TwoFactorMail) {
   168  		if instance.CheckPassphrase(inst, []byte(args.Passphrase)) != nil {
   169  			err := config.GetRateLimiter().CheckRateLimit(inst, limits.AuthType)
   170  			if limits.IsLimitReachedOrExceeded(err) {
   171  				if err = LoginRateExceeded(inst); err != nil {
   172  					inst.Logger().WithNamespace("auth").Warn(err.Error())
   173  				}
   174  			}
   175  			return c.JSON(http.StatusUnauthorized, echo.Map{
   176  				"error": "passphrase is required as second authentication factor",
   177  			})
   178  		}
   179  	}
   180  
   181  	client, err := oauth.FindClient(inst, args.ClientID)
   182  	if err != nil {
   183  		if couchErr, isCouchErr := couchdb.IsCouchError(err); isCouchErr && couchErr.StatusCode >= 500 {
   184  			return err
   185  		}
   186  		return c.JSON(http.StatusBadRequest, echo.Map{
   187  			"error": "the client must be registered",
   188  		})
   189  	}
   190  	if subtle.ConstantTimeCompare([]byte(args.ClientSecret), []byte(client.ClientSecret)) == 0 {
   191  		return c.JSON(http.StatusBadRequest, echo.Map{
   192  			"error": "invalid client_secret",
   193  		})
   194  	}
   195  
   196  	if !client.Flagship {
   197  		return ReturnSessionCode(c, http.StatusAccepted, inst)
   198  	}
   199  
   200  	if client.Pending {
   201  		client.Pending = false
   202  		client.ClientID = ""
   203  		_ = couchdb.UpdateDoc(inst, client)
   204  		client.ClientID = client.CouchID
   205  	}
   206  
   207  	if err := session.SendNewRegistrationNotification(inst, client.ClientID); err != nil {
   208  		return c.JSON(http.StatusInternalServerError, echo.Map{
   209  			"error": err.Error(),
   210  		})
   211  	}
   212  
   213  	out := AccessTokenReponse{
   214  		Type:  "bearer",
   215  		Scope: "*",
   216  	}
   217  	out.Refresh, err = client.CreateJWT(inst, consts.RefreshTokenAudience, out.Scope)
   218  	if err != nil {
   219  		return c.JSON(http.StatusInternalServerError, echo.Map{
   220  			"error": "Can't generate refresh token",
   221  		})
   222  	}
   223  	out.Access, err = client.CreateJWT(inst, consts.AccessTokenAudience, out.Scope)
   224  	if err != nil {
   225  		return c.JSON(http.StatusInternalServerError, echo.Map{
   226  			"error": "Can't generate access token",
   227  		})
   228  	}
   229  	return c.JSON(http.StatusOK, out)
   230  }