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

     1  package auth
     2  
     3  import (
     4  	"errors"
     5  	"net/http"
     6  	"net/url"
     7  
     8  	"github.com/cozy/cozy-stack/model/bitwarden/settings"
     9  	"github.com/cozy/cozy-stack/model/instance"
    10  	"github.com/cozy/cozy-stack/model/instance/lifecycle"
    11  	"github.com/cozy/cozy-stack/pkg/config/config"
    12  	"github.com/cozy/cozy-stack/pkg/consts"
    13  	"github.com/cozy/cozy-stack/pkg/couchdb"
    14  	"github.com/cozy/cozy-stack/pkg/limits"
    15  	"github.com/cozy/cozy-stack/pkg/realtime"
    16  	"github.com/cozy/cozy-stack/web/middlewares"
    17  	"github.com/labstack/echo/v4"
    18  )
    19  
    20  func confirmForm(c echo.Context) error {
    21  	inst := middlewares.GetInstance(c)
    22  	redirect := c.QueryParam("redirect")
    23  	state := c.QueryParam("state")
    24  	if state == "" {
    25  		return renderError(c, http.StatusBadRequest, "Error No state parameter")
    26  	}
    27  	if inst.HasForcedOIDC() {
    28  		q := url.Values{"redirect": {redirect}, "confirm_state": {state}}
    29  		return c.Redirect(http.StatusSeeOther, inst.PageURL("/oidc/start", q))
    30  	}
    31  
    32  	iterations := 0
    33  	if settings, err := settings.Get(inst); err == nil {
    34  		iterations = settings.PassphraseKdfIterations
    35  	}
    36  	return c.Render(http.StatusOK, "confirm_auth.html", echo.Map{
    37  		"TemplateTitle":  inst.TemplateTitle(),
    38  		"Domain":         inst.ContextualDomain(),
    39  		"ContextName":    inst.ContextName,
    40  		"Locale":         inst.Locale,
    41  		"Iterations":     iterations,
    42  		"Salt":           string(inst.PassphraseSalt()),
    43  		"CSRF":           c.Get("csrf"),
    44  		"Favicon":        middlewares.Favicon(inst),
    45  		"BottomNavBar":   middlewares.BottomNavigationBar(c),
    46  		"CryptoPolyfill": middlewares.CryptoPolyfill(c),
    47  		"State":          state,
    48  		"Redirect":       redirect,
    49  	})
    50  }
    51  
    52  func confirmAuth(c echo.Context) error {
    53  	inst := middlewares.GetInstance(c)
    54  	if inst.HasForcedOIDC() {
    55  		return c.NoContent(http.StatusBadRequest)
    56  	}
    57  	state := c.FormValue("state")
    58  
    59  	// Check passphrase
    60  	passphrase := []byte(c.FormValue("passphrase"))
    61  	if instance.CheckPassphrase(inst, passphrase) != nil {
    62  		errorMessage := inst.Translate(CredentialsErrorKey)
    63  		err := config.GetRateLimiter().CheckRateLimit(inst, limits.AuthType)
    64  		if limits.IsLimitReachedOrExceeded(err) {
    65  			if err = LoginRateExceeded(inst); err != nil {
    66  				inst.Logger().WithNamespace("auth").Warn(err.Error())
    67  			}
    68  		}
    69  		return c.JSON(http.StatusUnauthorized, echo.Map{
    70  			"error": errorMessage,
    71  		})
    72  	}
    73  
    74  	if inst.HasAuthMode(instance.TwoFactorMail) && !isTrustedDevice(c, inst) {
    75  		twoFactorToken, err := lifecycle.SendTwoFactorPasscode(inst)
    76  		if err != nil {
    77  			return err
    78  		}
    79  		v := url.Values{}
    80  		v.Add("two_factor_token", string(twoFactorToken))
    81  		v.Add("state", state)
    82  		v.Add("redirect", c.FormValue("redirect"))
    83  		v.Add("confirm", "true")
    84  		v.Add("trusted_device_checkbox", "false")
    85  
    86  		return c.JSON(http.StatusOK, echo.Map{
    87  			"redirect": inst.PageURL("/auth/twofactor", v),
    88  		})
    89  	}
    90  
    91  	return ConfirmSuccess(c, inst, state)
    92  }
    93  
    94  // ConfirmSuccess can be used to send a response after a successful identity
    95  // confirmation.
    96  func ConfirmSuccess(c echo.Context, inst *instance.Instance, state string) error {
    97  	doc := couchdb.JSONDoc{
    98  		Type: consts.AuthConfirmations,
    99  		M: map[string]interface{}{
   100  			"_id": state,
   101  		},
   102  	}
   103  	realtime.GetHub().Publish(inst, realtime.EventCreate, &doc, nil)
   104  
   105  	redirect, err := checkRedirectToAuthorized(c)
   106  	if err != nil {
   107  		redirect, err = checkRedirectParam(c, inst.DefaultRedirection())
   108  	}
   109  	if err != nil {
   110  		return err
   111  	}
   112  	code, err := GetStore().AddCode(inst)
   113  	if err != nil {
   114  		inst.Logger().Warnf("Cannot add confirm code: %s", err)
   115  		return c.NoContent(http.StatusInternalServerError)
   116  	}
   117  	q := redirect.Query()
   118  	q.Set("code", code)
   119  	q.Set("state", state)
   120  	redirect.RawQuery = q.Encode()
   121  
   122  	if wantsJSON(c) {
   123  		return c.JSON(http.StatusOK, echo.Map{
   124  			"redirect": redirect.String(),
   125  		})
   126  	}
   127  	return c.Redirect(http.StatusSeeOther, redirect.String())
   128  }
   129  
   130  func checkRedirectToAuthorized(c echo.Context) (*url.URL, error) {
   131  	inst := middlewares.GetInstance(c)
   132  	redirect := c.FormValue("redirect")
   133  	u, err := url.Parse(redirect)
   134  	if err != nil {
   135  		return nil, echo.NewHTTPError(http.StatusBadRequest, "bad url: could not parse")
   136  	}
   137  
   138  	if ok := checkRedirectToManager(inst, u); ok {
   139  		return u, nil
   140  	}
   141  
   142  	for _, host := range config.GetConfig().AuthorizedForConfirm {
   143  		if host == u.Host {
   144  			return u, nil
   145  		}
   146  	}
   147  
   148  	return nil, errors.New("not authorized")
   149  }
   150  
   151  func checkRedirectToManager(inst *instance.Instance, redirect *url.URL) bool {
   152  	config, ok := inst.SettingsContext()
   153  	if !ok {
   154  		return false
   155  	}
   156  	managerURL, ok := config["manager_url"].(string)
   157  	if !ok {
   158  		return false
   159  	}
   160  	manager, err := url.Parse(managerURL)
   161  	if err != nil {
   162  		return false
   163  	}
   164  	return redirect.Scheme == manager.Scheme && redirect.Host == manager.Host
   165  }
   166  
   167  func confirmCode(c echo.Context) error {
   168  	inst := middlewares.GetInstance(c)
   169  	code := c.Param("code")
   170  	if code == "" {
   171  		return c.JSON(http.StatusUnauthorized, echo.Map{
   172  			"error": "no code",
   173  		})
   174  	}
   175  	ok, err := GetStore().GetCode(inst, code)
   176  	if err != nil {
   177  		inst.Logger().Warnf("Cannot get confirm code: %s", err)
   178  		return c.NoContent(http.StatusInternalServerError)
   179  	}
   180  	if !ok {
   181  		return c.JSON(http.StatusUnauthorized, echo.Map{
   182  			"error": "invalid code",
   183  		})
   184  	}
   185  
   186  	return c.NoContent(http.StatusNoContent)
   187  }