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

     1  package auth
     2  
     3  import (
     4  	"crypto/sha256"
     5  	"crypto/subtle"
     6  	"encoding/base64"
     7  	"encoding/json"
     8  	"errors"
     9  	"fmt"
    10  	"html/template"
    11  	"net/http"
    12  	"net/url"
    13  	"strconv"
    14  	"strings"
    15  	"time"
    16  
    17  	"github.com/cozy/cozy-stack/model/app"
    18  	"github.com/cozy/cozy-stack/model/bitwarden/settings"
    19  	"github.com/cozy/cozy-stack/model/feature"
    20  	"github.com/cozy/cozy-stack/model/instance"
    21  	"github.com/cozy/cozy-stack/model/instance/lifecycle"
    22  	"github.com/cozy/cozy-stack/model/move"
    23  	"github.com/cozy/cozy-stack/model/oauth"
    24  	"github.com/cozy/cozy-stack/model/permission"
    25  	"github.com/cozy/cozy-stack/model/session"
    26  	csettings "github.com/cozy/cozy-stack/model/settings"
    27  	"github.com/cozy/cozy-stack/model/sharing"
    28  	"github.com/cozy/cozy-stack/model/vfs"
    29  	"github.com/cozy/cozy-stack/pkg/config/config"
    30  	"github.com/cozy/cozy-stack/pkg/consts"
    31  	"github.com/cozy/cozy-stack/pkg/couchdb"
    32  	"github.com/cozy/cozy-stack/pkg/limits"
    33  	"github.com/cozy/cozy-stack/pkg/registry"
    34  	"github.com/cozy/cozy-stack/web/middlewares"
    35  	"github.com/labstack/echo/v4"
    36  	"github.com/mssola/user_agent"
    37  )
    38  
    39  type webappParams struct {
    40  	Name string
    41  	Slug string
    42  }
    43  
    44  type authorizeParams struct {
    45  	instance        *instance.Instance
    46  	state           string
    47  	clientID        string
    48  	redirectURI     string
    49  	scope           string
    50  	resType         string
    51  	challenge       string
    52  	challengeMethod string
    53  	client          *oauth.Client
    54  	webapp          *webappParams
    55  }
    56  
    57  type AuthorizeHTTPHandler struct {
    58  	deprecatedApps *DeprecatedAppList
    59  }
    60  
    61  // NewAuthorizeHandler instantiates a new [AuthHTTPHandler].
    62  func NewAuthorizeHandler(deprecatedAppsCfg config.DeprecatedAppsCfg) *AuthorizeHTTPHandler {
    63  	return &AuthorizeHTTPHandler{
    64  		deprecatedApps: NewDeprecatedAppList(deprecatedAppsCfg),
    65  	}
    66  }
    67  
    68  func (a *AuthorizeHTTPHandler) Register(router *echo.Group) {
    69  	router.GET("", a.authorizeForm)
    70  	router.POST("", a.authorize)
    71  	router.GET("/sharing", a.authorizeSharingForm)
    72  	router.POST("/sharing", a.authorizeSharing)
    73  	router.GET("/sharing/:sharing-id/cancel", a.cancelAuthorizeSharing)
    74  	router.GET("/move", a.authorizeMoveForm)
    75  	router.POST("/move", a.authorizeMove)
    76  }
    77  
    78  func checkAuthorizeParams(c echo.Context, params *authorizeParams) (bool, error) {
    79  	if params.state == "" {
    80  		return true, renderError(c, http.StatusBadRequest, "Error No state parameter")
    81  	}
    82  	if params.clientID == "" {
    83  		return true, renderError(c, http.StatusBadRequest, "Error No client_id parameter")
    84  	}
    85  	if params.redirectURI == "" {
    86  		return true, renderError(c, http.StatusBadRequest, "Error No redirect_uri parameter")
    87  	}
    88  	if params.resType != "code" {
    89  		return true, renderError(c, http.StatusBadRequest, "Error Invalid response type")
    90  	}
    91  	if params.challenge != "" && params.challengeMethod != "S256" {
    92  		return true, renderError(c, http.StatusBadRequest, "Error Invalid challenge code method")
    93  	}
    94  	if params.challengeMethod == "S256" && params.challenge == "" {
    95  		return true, renderError(c, http.StatusBadRequest, "Error No challenge code")
    96  	}
    97  
    98  	client, err := oauth.FindClient(params.instance, params.clientID)
    99  	if err != nil {
   100  		return true, renderError(c, http.StatusBadRequest, "Error No registered client")
   101  	}
   102  	params.client = client
   103  	if !params.client.AcceptRedirectURI(params.redirectURI) {
   104  		return true, renderError(c, http.StatusBadRequest, "Error Incorrect redirect_uri")
   105  	}
   106  
   107  	params.scope = strings.TrimSpace(params.scope)
   108  	if params.scope == "*" {
   109  		if params.challenge == "" {
   110  			return true, renderError(c, http.StatusBadRequest, "Error No challenge code")
   111  		}
   112  		instance := middlewares.GetInstance(c)
   113  		context := instance.ContextName
   114  		if context == "" {
   115  			context = config.DefaultInstanceContext
   116  		}
   117  		cfg := config.GetConfig().Flagship.Contexts[context]
   118  		skipCertification := false
   119  		if cfg, ok := cfg.(map[string]interface{}); ok {
   120  			skipCertification = cfg["skip_certification"] == true
   121  		}
   122  		if !skipCertification && !params.client.Flagship {
   123  			return true, renderConfirmFlagship(c, params.clientID)
   124  		}
   125  		return false, nil
   126  	}
   127  
   128  	if appSlug := oauth.GetLinkedAppSlug(params.client.SoftwareID); appSlug != "" {
   129  		webapp, err := registry.GetLatestVersion(appSlug, "stable", params.instance.Registries())
   130  
   131  		if err != nil {
   132  			return true, renderError(c, http.StatusBadRequest, "Cannot find application on instance registries")
   133  		}
   134  
   135  		var manifest struct {
   136  			Slug        string         `json:"slug"`
   137  			Name        string         `json:"name"`
   138  			Permissions permission.Set `json:"permissions"`
   139  		}
   140  		err = json.Unmarshal(webapp.Manifest, &manifest)
   141  		if err != nil {
   142  			return true, renderError(c, http.StatusBadRequest, "Cannot decode application manifest")
   143  		}
   144  
   145  		params.scope, err = manifest.Permissions.MarshalScopeString()
   146  		if err != nil {
   147  			return true, renderError(c, http.StatusBadRequest, "Cannot marshal scope permissions")
   148  		}
   149  
   150  		params.webapp = &webappParams{
   151  			Slug: manifest.Slug,
   152  			Name: manifest.Name,
   153  		}
   154  	}
   155  
   156  	if params.scope == "" {
   157  		return true, renderError(c, http.StatusBadRequest, "Error No scope parameter")
   158  	}
   159  	if params.scope == oauth.ScopeLogin && !params.client.AllowLoginScope {
   160  		return true, renderError(c, http.StatusBadRequest, "Error No scope parameter")
   161  	}
   162  
   163  	return false, nil
   164  }
   165  
   166  func (a *AuthorizeHTTPHandler) authorizeForm(c echo.Context) error {
   167  	inst := middlewares.GetInstance(c)
   168  	params := authorizeParams{
   169  		instance:        inst,
   170  		state:           c.QueryParam("state"),
   171  		clientID:        c.QueryParam("client_id"),
   172  		redirectURI:     c.QueryParam("redirect_uri"),
   173  		scope:           c.QueryParam("scope"),
   174  		resType:         c.QueryParam("response_type"),
   175  		challenge:       c.QueryParam("code_challenge"),
   176  		challengeMethod: c.QueryParam("code_challenge_method"),
   177  	}
   178  
   179  	isLoggedIn := middlewares.IsLoggedIn(c)
   180  	if code := c.QueryParam("session_code"); code != "" {
   181  		// XXX we should always clear the session code to avoid it being
   182  		// reused, even if the user is already logged in and we don't want to
   183  		// create a new session
   184  		if checked := inst.CheckAndClearSessionCode(code); checked && !isLoggedIn {
   185  			sessionID, err := SetCookieForNewSession(c, session.ShortRun)
   186  			req := c.Request()
   187  			if err == nil {
   188  				if err = session.StoreNewLoginEntry(inst, sessionID, "", req, "session_code", false); err != nil {
   189  					inst.Logger().Errorf("Could not store session history %q: %s", sessionID, err)
   190  				}
   191  			}
   192  			redirect := req.URL
   193  			q := redirect.Query()
   194  			q.Del("session_code")
   195  			redirect.RawQuery = q.Encode()
   196  			return c.Redirect(http.StatusSeeOther, redirect.String())
   197  		}
   198  	}
   199  
   200  	if hasError, err := checkAuthorizeParams(c, &params); hasError {
   201  		return err
   202  	}
   203  
   204  	if a.deprecatedApps.IsDeprecated(params.client) {
   205  		return c.Render(http.StatusOK, "new_app_available.html", a.deprecatedApps.RenderArgs(params.client, inst, c.Request().UserAgent()))
   206  	}
   207  
   208  	if !isLoggedIn {
   209  		u := inst.PageURL("/auth/login", url.Values{
   210  			"redirect": {inst.FromURL(c.Request().URL)},
   211  		})
   212  		return c.Redirect(http.StatusSeeOther, u)
   213  	}
   214  
   215  	// For a scope "login": such client is only used to transmit authentication
   216  	// for the manager. It does not require any authorization from the user, and
   217  	// generate a code without asking any permission.
   218  	if params.scope == oauth.ScopeLogin {
   219  		access, err := oauth.CreateAccessCode(params.instance, params.client, "" /* = scope */, "" /* = challenge */)
   220  		if err != nil {
   221  			return err
   222  		}
   223  
   224  		u, err := url.ParseRequestURI(params.redirectURI)
   225  		if err != nil {
   226  			return renderError(c, http.StatusBadRequest, "Error Invalid redirect_uri")
   227  		}
   228  
   229  		q := u.Query()
   230  		// We should be sending "code" only, but for compatibility reason, we keep
   231  		// the access_code parameter that we used to send in our first impl.
   232  		q.Set("access_code", access.Code)
   233  		q.Set("code", access.Code)
   234  		q.Set("state", params.state)
   235  		u.RawQuery = q.Encode()
   236  		u.Fragment = ""
   237  
   238  		return c.Redirect(http.StatusFound, u.String()+"#")
   239  	}
   240  
   241  	if !params.client.Flagship {
   242  		flags, err := feature.GetFlags(inst)
   243  		if err != nil {
   244  			return err
   245  		}
   246  
   247  		if clientsLimit, ok := flags.M["cozy.oauthclients.max"].(float64); ok && clientsLimit >= 0 {
   248  			limit := int(clientsLimit)
   249  
   250  			clients, _, err := oauth.GetConnectedUserClients(inst, 100, "")
   251  			if err != nil {
   252  				return fmt.Errorf("Could not get user OAuth clients: %w", err)
   253  			}
   254  			count := len(clients)
   255  
   256  			if count >= limit {
   257  				var manageDevicesURL, premiumURL string
   258  
   259  				connectedDevicesURL := inst.SubDomain(consts.SettingsSlug)
   260  				connectedDevicesURL.Fragment = "/connectedDevices"
   261  				manageDevicesURL = connectedDevicesURL.String()
   262  
   263  				if inst.HasPremiumLinksEnabled() {
   264  					if premiumURL, err = inst.ManagerURL(instance.ManagerPremiumURL); err != nil {
   265  						inst.Logger().Errorf("Could not get instance Premium Manager URL: %s", err.Error())
   266  					}
   267  				}
   268  
   269  				sess, _ := middlewares.GetSession(c)
   270  				settingsToken := inst.BuildAppToken(consts.SettingsSlug, sess.ID())
   271  
   272  				return c.Render(http.StatusOK, "oauth_clients_limit_exceeded.html", echo.Map{
   273  					"Domain":            inst.ContextualDomain(),
   274  					"ContextName":       inst.ContextName,
   275  					"Locale":            inst.Locale,
   276  					"Title":             inst.TemplateTitle(),
   277  					"Favicon":           middlewares.Favicon(inst),
   278  					"ClientsCount":      strconv.Itoa(count),
   279  					"ClientsLimit":      strconv.Itoa(limit),
   280  					"OpenLinksInNewTab": true,
   281  					"ManageDevicesURL":  manageDevicesURL,
   282  					"PremiumURL":        premiumURL,
   283  					"SettingsToken":     settingsToken,
   284  				})
   285  			}
   286  		}
   287  	}
   288  
   289  	permissions, err := permission.UnmarshalScopeString(params.scope)
   290  	if err != nil {
   291  		context := inst.ContextName
   292  		if context == "" {
   293  			context = config.DefaultInstanceContext
   294  		}
   295  		cfg := config.GetConfig().Flagship.Contexts[context]
   296  		skipCertification := false
   297  		if cfg, ok := cfg.(map[string]interface{}); ok {
   298  			skipCertification = cfg["skip_certification"] == true
   299  		}
   300  		if params.scope != "*" || (!skipCertification && !params.client.Flagship) {
   301  			return renderError(c, http.StatusBadRequest, "Error Invalid scope")
   302  		}
   303  		permissions = permission.MaximalSet()
   304  	}
   305  	readOnly := true
   306  	for _, p := range permissions {
   307  		if !p.Verbs.ReadOnly() {
   308  			readOnly = false
   309  		}
   310  	}
   311  	params.client.ClientID = params.client.CouchID
   312  
   313  	u, err := url.ParseRequestURI(params.redirectURI)
   314  	if err != nil {
   315  		return renderError(c, http.StatusBadRequest, "Error Invalid redirect_uri")
   316  	}
   317  	q := u.Query()
   318  	if params.client.CreatedAtOnboarding {
   319  		return createAccessCode(c, params, u, q)
   320  	}
   321  	q.Set("error", "access_denied")
   322  	u.RawQuery = q.Encode()
   323  	closeURI := template.URL("/")
   324  	if u.Scheme == "http" || u.Scheme == "https" || u.Scheme == "cozy" {
   325  		closeURI = template.URL(u.String())
   326  	}
   327  
   328  	var clientDomain string
   329  	clientURL, err := url.Parse(params.client.ClientURI)
   330  	if err != nil {
   331  		clientDomain = params.client.ClientURI
   332  	} else {
   333  		clientDomain = clientURL.Hostname()
   334  	}
   335  
   336  	// This Content-Security-Policy (CSP) nonce is here to allow the display of
   337  	// logos for OAuth clients on the authorize page.
   338  	if logoURI := params.client.LogoURI; logoURI != "" {
   339  		logoURL, err := url.Parse(logoURI)
   340  		if err == nil {
   341  			csp := c.Response().Header().Get(echo.HeaderContentSecurityPolicy)
   342  			if !strings.Contains(csp, "img-src") {
   343  				c.Response().Header().Set(echo.HeaderContentSecurityPolicy,
   344  					fmt.Sprintf("%simg-src 'self' https://%s;", csp, logoURL.Hostname()+logoURL.EscapedPath()))
   345  			}
   346  		}
   347  	}
   348  
   349  	slugname, instanceDomain := inst.SlugAndDomain()
   350  
   351  	return c.Render(http.StatusOK, "authorize.html", echo.Map{
   352  		"Domain":           inst.ContextualDomain(),
   353  		"ContextName":      inst.ContextName,
   354  		"Locale":           inst.Locale,
   355  		"Title":            inst.TemplateTitle(),
   356  		"Favicon":          middlewares.Favicon(inst),
   357  		"InstanceSlugName": slugname,
   358  		"InstanceDomain":   instanceDomain,
   359  		"ClientDomain":     clientDomain,
   360  		"Client":           params.client,
   361  		"State":            params.state,
   362  		"RedirectURI":      params.redirectURI,
   363  		"CloseURI":         closeURI,
   364  		"Scope":            params.scope,
   365  		"Challenge":        params.challenge,
   366  		"ChallengeMethod":  params.challengeMethod,
   367  		"Permissions":      permissions,
   368  		"ReadOnly":         readOnly,
   369  		"CSRF":             c.Get("csrf"),
   370  		"Webapp":           params.webapp,
   371  	})
   372  }
   373  
   374  func (a *AuthorizeHTTPHandler) authorize(c echo.Context) error {
   375  	instance := middlewares.GetInstance(c)
   376  	params := authorizeParams{
   377  		instance:        instance,
   378  		state:           c.FormValue("state"),
   379  		clientID:        c.FormValue("client_id"),
   380  		redirectURI:     c.FormValue("redirect_uri"),
   381  		scope:           c.FormValue("scope"),
   382  		resType:         c.FormValue("response_type"),
   383  		challenge:       c.FormValue("code_challenge"),
   384  		challengeMethod: c.FormValue("code_challenge_method"),
   385  	}
   386  
   387  	if hasError, err := checkAuthorizeParams(c, &params); hasError {
   388  		return err
   389  	}
   390  
   391  	if !middlewares.IsLoggedIn(c) {
   392  		return renderError(c, http.StatusUnauthorized, "Error Must be authenticated")
   393  	}
   394  
   395  	u, err := url.ParseRequestURI(params.redirectURI)
   396  	if err != nil {
   397  		return renderError(c, http.StatusBadRequest, "Error Invalid redirect_uri")
   398  	}
   399  	q := u.Query()
   400  
   401  	// Install the application in case of mobile client
   402  	softwareID := params.client.SoftwareID
   403  	if oauth.IsLinkedApp(softwareID) {
   404  		manifest, err := GetLinkedApp(instance, softwareID)
   405  		if err != nil {
   406  			return err
   407  		}
   408  		slug := manifest.Slug()
   409  		installer, err := app.NewInstaller(instance, app.Copier(consts.WebappType, instance), &app.InstallerOptions{
   410  			Operation:  app.Install,
   411  			Type:       consts.WebappType,
   412  			SourceURL:  softwareID,
   413  			Slug:       slug,
   414  			Registries: instance.Registries(),
   415  		})
   416  		if !errors.Is(err, app.ErrAlreadyExists) {
   417  			if err != nil {
   418  				return err
   419  			}
   420  			go installer.Run()
   421  		}
   422  		params.scope = oauth.BuildLinkedAppScope(slug)
   423  		if u.Scheme == "http" || u.Scheme == "https" {
   424  			q.Set("fallback", instance.SubDomain(slug).String())
   425  		}
   426  	}
   427  
   428  	// Fill the client_os of the OAuth client
   429  	rawUserAgent := c.Request().UserAgent()
   430  	ua := user_agent.New(rawUserAgent)
   431  	params.client.ClientOS = ua.OS()
   432  	_ = couchdb.UpdateDoc(instance, params.client)
   433  
   434  	return createAccessCode(c, params, u, q)
   435  }
   436  
   437  func createAccessCode(c echo.Context, params authorizeParams, u *url.URL, q url.Values) error {
   438  	q.Set("state", params.state)
   439  
   440  	access, err := oauth.CreateAccessCode(params.instance, params.client, params.scope, params.challenge)
   441  	if err != nil {
   442  		return err
   443  	}
   444  	var ip string
   445  	if forwardedFor := c.Request().Header.Get(echo.HeaderXForwardedFor); forwardedFor != "" {
   446  		ip = strings.TrimSpace(strings.SplitN(forwardedFor, ",", 2)[0])
   447  	}
   448  	if ip == "" {
   449  		ip = strings.Split(c.Request().RemoteAddr, ":")[0]
   450  	}
   451  	params.instance.Logger().WithNamespace("loginaudit").
   452  		Infof("Access code created from %s at %s with scope %s", ip, time.Now(), access.Scope)
   453  
   454  	// We should be sending "code" only, but for compatibility reason, we keep
   455  	// the access_code parameter that we used to send in our first impl.
   456  	q.Set("access_code", access.Code)
   457  	q.Set("code", access.Code)
   458  
   459  	u.RawQuery = q.Encode()
   460  	u.Fragment = ""
   461  	location := u.String() + "#"
   462  
   463  	wantsJSON := c.Request().Header.Get(echo.HeaderAccept) == echo.MIMEApplicationJSON
   464  	if wantsJSON {
   465  		return c.JSON(http.StatusOK, echo.Map{"deeplink": location})
   466  	}
   467  	return c.Redirect(http.StatusFound, location)
   468  }
   469  
   470  func renderConfirmFlagship(c echo.Context, clientID string) error {
   471  	inst := middlewares.GetInstance(c)
   472  
   473  	if !middlewares.IsLoggedIn(c) {
   474  		u := inst.PageURL("/auth/login", url.Values{
   475  			"redirect": {inst.FromURL(c.Request().URL)},
   476  		})
   477  		return c.Redirect(http.StatusSeeOther, u)
   478  	}
   479  
   480  	err := config.GetRateLimiter().CheckRateLimit(inst, limits.ConfirmFlagshipType)
   481  	if limits.IsLimitReachedOrExceeded(err) {
   482  		return renderError(c, http.StatusTooManyRequests, err.Error())
   483  	}
   484  
   485  	token, err := oauth.SendConfirmFlagshipCode(inst, clientID)
   486  	if err != nil {
   487  		return renderError(c, http.StatusInternalServerError, err.Error())
   488  	}
   489  
   490  	email, _ := inst.SettingsEMail()
   491  	return c.Render(http.StatusOK, "confirm_flagship.html", echo.Map{
   492  		"Domain":       inst.ContextualDomain(),
   493  		"ContextName":  inst.ContextName,
   494  		"Locale":       inst.Locale,
   495  		"Title":        inst.TemplateTitle(),
   496  		"Favicon":      middlewares.Favicon(inst),
   497  		"Email":        email,
   498  		"SupportEmail": inst.SupportEmailAddress(),
   499  		"Token":        string(token),
   500  		"ClientID":     clientID,
   501  	})
   502  }
   503  
   504  type authorizeSharingParams struct {
   505  	instance  *instance.Instance
   506  	state     string
   507  	sharingID string
   508  }
   509  
   510  func checkAuthorizeSharingParams(c echo.Context, params *authorizeSharingParams) (bool, error) {
   511  	if params.state == "" {
   512  		return true, renderError(c, http.StatusBadRequest, "Error No state parameter")
   513  	}
   514  	if params.sharingID == "" {
   515  		return true, renderError(c, http.StatusBadRequest, "Error No sharing_id parameter")
   516  	}
   517  	return false, nil
   518  }
   519  
   520  func (a *AuthorizeHTTPHandler) authorizeSharingForm(c echo.Context) error {
   521  	instance := middlewares.GetInstance(c)
   522  	params := authorizeSharingParams{
   523  		instance:  instance,
   524  		state:     c.QueryParam("state"),
   525  		sharingID: c.QueryParam("sharing_id"),
   526  	}
   527  
   528  	if hasError, err := checkAuthorizeSharingParams(c, &params); hasError {
   529  		return err
   530  	}
   531  
   532  	if !middlewares.IsLoggedIn(c) {
   533  		u := instance.PageURL("/auth/login", url.Values{
   534  			"redirect": {instance.FromURL(c.Request().URL)},
   535  		})
   536  		return c.Redirect(http.StatusSeeOther, u)
   537  	}
   538  
   539  	s, err := sharing.FindSharing(instance, params.sharingID)
   540  	if err != nil || s.Owner || s.Active || len(s.Members) < 2 {
   541  		return renderError(c, http.StatusUnauthorized, "Error Invalid sharing")
   542  	}
   543  
   544  	hasShortcut := s.ShortcutID != ""
   545  	var sharerDomain, targetType string
   546  	sharerURL, err := url.Parse(s.Members[0].Instance)
   547  	if err != nil {
   548  		sharerDomain = s.Members[0].Instance
   549  	} else {
   550  		sharerDomain = sharerURL.Host
   551  	}
   552  	if s.Rules[0].DocType == consts.BitwardenOrganizations {
   553  		targetType = instance.Translate("Notification Sharing Type Organization")
   554  		hasShortcut = true
   555  		s.Rules[0].Mime = "organization"
   556  		if len(s.Rules) == 2 && s.Rules[1].DocType == consts.BitwardenCiphers {
   557  			s.Rules = s.Rules[:1]
   558  		}
   559  	} else if s.Rules[0].DocType != consts.Files {
   560  		targetType = instance.Translate("Notification Sharing Type Document")
   561  	} else if s.Rules[0].Mime == "" {
   562  		targetType = instance.Translate("Notification Sharing Type Directory")
   563  	} else {
   564  		targetType = instance.Translate("Notification Sharing Type File")
   565  	}
   566  
   567  	return c.Render(http.StatusOK, "authorize_sharing.html", echo.Map{
   568  		"Domain":       instance.ContextualDomain(),
   569  		"ContextName":  instance.ContextName,
   570  		"Locale":       instance.Locale,
   571  		"Title":        instance.TemplateTitle(),
   572  		"Favicon":      middlewares.Favicon(instance),
   573  		"SharerDomain": sharerDomain,
   574  		"SharerName":   s.Members[0].PrimaryName(),
   575  		"State":        params.state,
   576  		"Sharing":      s,
   577  		"CSRF":         c.Get("csrf"),
   578  		"HasShortcut":  hasShortcut,
   579  		"TargetType":   targetType,
   580  	})
   581  }
   582  
   583  func (a *AuthorizeHTTPHandler) authorizeSharing(c echo.Context) error {
   584  	instance := middlewares.GetInstance(c)
   585  	params := authorizeSharingParams{
   586  		instance:  instance,
   587  		state:     c.FormValue("state"),
   588  		sharingID: c.FormValue("sharing_id"),
   589  	}
   590  
   591  	if hasError, err := checkAuthorizeSharingParams(c, &params); hasError {
   592  		return err
   593  	}
   594  
   595  	if !middlewares.IsLoggedIn(c) {
   596  		return renderError(c, http.StatusUnauthorized, "Error Must be authenticated")
   597  	}
   598  
   599  	s, err := sharing.FindSharing(instance, params.sharingID)
   600  	if err != nil {
   601  		return err
   602  	}
   603  	if s.Owner || len(s.Members) < 2 {
   604  		return sharing.ErrInvalidSharing
   605  	}
   606  
   607  	if c.FormValue("synchronize") == "" {
   608  		if err = s.AddShortcut(instance, params.state); err != nil {
   609  			return err
   610  		}
   611  		u := instance.SubDomain(consts.DriveSlug)
   612  		u.RawQuery = "sharing=" + s.SID
   613  		u.Fragment = "/folder/" + consts.SharedWithMeDirID
   614  		return c.Redirect(http.StatusSeeOther, u.String())
   615  	}
   616  
   617  	if !s.Active {
   618  		if err = s.SendAnswer(instance, params.state); err != nil {
   619  			return err
   620  		}
   621  	}
   622  	redirect := s.RedirectAfterAuthorizeURL(instance)
   623  	return c.Redirect(http.StatusSeeOther, redirect.String())
   624  }
   625  
   626  func (a *AuthorizeHTTPHandler) cancelAuthorizeSharing(c echo.Context) error {
   627  	if !middlewares.IsLoggedIn(c) {
   628  		return renderError(c, http.StatusUnauthorized, "Error Must be authenticated")
   629  	}
   630  
   631  	inst := middlewares.GetInstance(c)
   632  	s, err := sharing.FindSharing(inst, c.Param("sharing-id"))
   633  	if err != nil || s.Owner || len(s.Members) < 2 {
   634  		return c.Redirect(http.StatusSeeOther, inst.SubDomain(consts.HomeSlug).String())
   635  	}
   636  
   637  	previewURL, err := s.GetPreviewURL(inst, c.QueryParam("state"))
   638  	if err != nil {
   639  		return c.Redirect(http.StatusSeeOther, inst.SubDomain(consts.HomeSlug).String())
   640  	}
   641  	return c.Redirect(http.StatusSeeOther, previewURL)
   642  }
   643  
   644  func (a *AuthorizeHTTPHandler) authorizeMoveForm(c echo.Context) error {
   645  	inst := middlewares.GetInstance(c)
   646  	state := c.QueryParam("state")
   647  	if state == "" {
   648  		return renderError(c, http.StatusBadRequest, "Error No state parameter")
   649  	}
   650  	clientID := c.QueryParam("client_id")
   651  	if clientID == "" {
   652  		return renderError(c, http.StatusBadRequest, "Error No client_id parameter")
   653  	}
   654  	redirectURI := c.QueryParam("redirect_uri")
   655  	if redirectURI == "" {
   656  		return renderError(c, http.StatusBadRequest, "Error No redirect_uri parameter")
   657  	}
   658  	client := oauth.Client{}
   659  	if err := couchdb.GetDoc(inst, consts.OAuthClients, clientID, &client); err != nil {
   660  		return renderError(c, http.StatusBadRequest, "Error No registered client")
   661  	}
   662  	if !client.AcceptRedirectURI(redirectURI) {
   663  		return renderError(c, http.StatusBadRequest, "Error Incorrect redirect_uri")
   664  	}
   665  
   666  	if inst.HasForcedOIDC() {
   667  		if !middlewares.IsLoggedIn(c) {
   668  			u := c.Request().URL
   669  			redirect := inst.PageURL(u.Path, u.Query())
   670  			q := url.Values{"redirect": {redirect}}
   671  			return c.Redirect(http.StatusSeeOther, inst.PageURL("/oidc/start", q))
   672  		}
   673  		twoFactorToken, err := lifecycle.SendTwoFactorPasscode(inst)
   674  		if err != nil {
   675  			return err
   676  		}
   677  		mail, _ := inst.SettingsEMail()
   678  		return c.Render(http.StatusOK, "move_delegated_auth.html", echo.Map{
   679  			"Domain":           inst.ContextualDomain(),
   680  			"ContextName":      inst.ContextName,
   681  			"Favicon":          middlewares.Favicon(inst),
   682  			"TwoFactorToken":   string(twoFactorToken),
   683  			"CredentialsError": "",
   684  			"Email":            mail,
   685  			"State":            state,
   686  			"ClientID":         clientID,
   687  			"Redirect":         redirectURI,
   688  		})
   689  	}
   690  
   691  	publicName, err := csettings.PublicName(inst)
   692  	if err != nil {
   693  		publicName = ""
   694  	}
   695  	var title string
   696  	if publicName == "" {
   697  		title = inst.Translate("Login Welcome")
   698  	} else {
   699  		title = inst.Translate("Login Welcome name", publicName)
   700  	}
   701  	help := inst.Translate("Login Password help")
   702  	iterations := 0
   703  	if settings, err := settings.Get(inst); err == nil {
   704  		iterations = settings.PassphraseKdfIterations
   705  	}
   706  
   707  	return c.Render(http.StatusOK, "authorize_move.html", echo.Map{
   708  		"TemplateTitle":  inst.TemplateTitle(),
   709  		"Domain":         inst.ContextualDomain(),
   710  		"ContextName":    inst.ContextName,
   711  		"Locale":         inst.Locale,
   712  		"Iterations":     iterations,
   713  		"Salt":           string(inst.PassphraseSalt()),
   714  		"Title":          title,
   715  		"PasswordHelp":   help,
   716  		"CSRF":           c.Get("csrf"),
   717  		"Favicon":        middlewares.Favicon(inst),
   718  		"BottomNavBar":   middlewares.BottomNavigationBar(c),
   719  		"CryptoPolyfill": middlewares.CryptoPolyfill(c),
   720  		"State":          state,
   721  		"ClientID":       clientID,
   722  		"RedirectURI":    redirectURI,
   723  	})
   724  }
   725  
   726  func (a *AuthorizeHTTPHandler) authorizeMove(c echo.Context) error {
   727  	inst := middlewares.GetInstance(c)
   728  	if inst.HasForcedOIDC() {
   729  		if !middlewares.IsLoggedIn(c) {
   730  			return renderError(c, http.StatusUnauthorized, "Error Must be authenticated")
   731  		}
   732  		token := []byte(c.FormValue("two-factor-token"))
   733  		passcode := c.FormValue("two-factor-passcode")
   734  		correctPasscode := inst.ValidateTwoFactorPasscode(token, passcode)
   735  		if !correctPasscode {
   736  			errorMessage := inst.Translate(TwoFactorErrorKey)
   737  			mail, _ := inst.SettingsEMail()
   738  			return c.Render(http.StatusOK, "move_delegated_auth.html", echo.Map{
   739  				"Domain":           inst.ContextualDomain(),
   740  				"ContextName":      inst.ContextName,
   741  				"Favicon":          middlewares.Favicon(inst),
   742  				"TwoFactorToken":   string(token),
   743  				"CredentialsError": errorMessage,
   744  				"Email":            mail,
   745  				"State":            c.FormValue("state"),
   746  				"ClientID":         c.FormValue("client_id"),
   747  				"Redirect":         c.FormValue("redirect"),
   748  			})
   749  		}
   750  		u, err := moveSuccessURI(c)
   751  		if err != nil {
   752  			return err
   753  		}
   754  		return c.Redirect(http.StatusSeeOther, u)
   755  	}
   756  
   757  	// Check passphrase
   758  	passphrase := []byte(c.FormValue("passphrase"))
   759  	if instance.CheckPassphrase(inst, passphrase) != nil {
   760  		errorMessage := inst.Translate(CredentialsErrorKey)
   761  		err := config.GetRateLimiter().CheckRateLimit(inst, limits.AuthType)
   762  		if limits.IsLimitReachedOrExceeded(err) {
   763  			if err = LoginRateExceeded(inst); err != nil {
   764  				inst.Logger().WithNamespace("auth").Warn(err.Error())
   765  			}
   766  		}
   767  		return c.JSON(http.StatusUnauthorized, echo.Map{
   768  			"error": errorMessage,
   769  		})
   770  	}
   771  
   772  	if inst.HasAuthMode(instance.TwoFactorMail) && !isTrustedDevice(c, inst) {
   773  		twoFactorToken, err := lifecycle.SendTwoFactorPasscode(inst)
   774  		if err != nil {
   775  			return err
   776  		}
   777  		v := url.Values{}
   778  		v.Add("two_factor_token", string(twoFactorToken))
   779  		v.Add("state", c.FormValue("state"))
   780  		v.Add("client_id", c.FormValue("client_id"))
   781  		v.Add("redirect", c.FormValue("redirect"))
   782  		v.Add("trusted_device_checkbox", "false")
   783  
   784  		return c.JSON(http.StatusOK, echo.Map{
   785  			"redirect": inst.PageURL("/auth/twofactor", v),
   786  		})
   787  	}
   788  
   789  	u, err := moveSuccessURI(c)
   790  	if err != nil {
   791  		return err
   792  	}
   793  	return c.JSON(http.StatusOK, echo.Map{
   794  		"redirect": u,
   795  	})
   796  }
   797  
   798  func moveSuccessURI(c echo.Context) (string, error) {
   799  	u, err := url.Parse(c.FormValue("redirect"))
   800  	if err != nil {
   801  		return "", echo.NewHTTPError(http.StatusBadRequest, "bad url: could not parse")
   802  	}
   803  
   804  	inst := middlewares.GetInstance(c)
   805  	vault := settings.HasVault(inst)
   806  	used, quota, err := DiskInfo(inst.VFS())
   807  	if err != nil {
   808  		return "", err
   809  	}
   810  
   811  	client, err := oauth.FindClient(inst, c.FormValue("client_id"))
   812  	if err != nil {
   813  		return "", err
   814  	}
   815  	access, err := oauth.CreateAccessCode(inst, client, move.MoveScope, "")
   816  	if err != nil {
   817  		return "", err
   818  	}
   819  
   820  	q := u.Query()
   821  	q.Set("state", c.FormValue("state"))
   822  	q.Set("code", access.Code)
   823  	q.Set("vault", strconv.FormatBool(vault))
   824  	q.Set("used", used)
   825  	if quota != "" {
   826  		q.Set("quota", quota)
   827  	}
   828  	u.RawQuery = q.Encode()
   829  	return u.String(), nil
   830  }
   831  
   832  // DiskInfo returns the used and quota disk space for the given VFS.
   833  func DiskInfo(fs vfs.VFS) (string, string, error) {
   834  	versions, err := fs.VersionsUsage()
   835  	if err != nil {
   836  		return "", "", err
   837  	}
   838  	files, err := fs.FilesUsage()
   839  	if err != nil {
   840  		return "", "", err
   841  	}
   842  
   843  	used := fmt.Sprintf("%d", files+versions)
   844  	var quota string
   845  	if q := fs.DiskQuota(); q > 0 {
   846  		quota = fmt.Sprintf("%d", q)
   847  	}
   848  	return used, quota, nil
   849  }
   850  
   851  // AccessTokenReponse is the stuct used for serializing to JSON the response
   852  // for an access token.
   853  type AccessTokenReponse struct {
   854  	Type    string `json:"token_type"`
   855  	Scope   string `json:"scope"`
   856  	Access  string `json:"access_token"`
   857  	Refresh string `json:"refresh_token,omitempty"`
   858  }
   859  
   860  func LockOAuthClient(inst *instance.Instance, clientID string) func() {
   861  	mu := config.Lock().ReadWrite(inst, "oauth/"+clientID)
   862  	_ = mu.Lock()
   863  	return mu.Unlock
   864  }
   865  
   866  func accessToken(c echo.Context) error {
   867  	grant := c.FormValue("grant_type")
   868  	clientID := c.FormValue("client_id")
   869  	clientSecret := c.FormValue("client_secret")
   870  	verifier := c.FormValue("code_verifier")
   871  	instance := middlewares.GetInstance(c)
   872  
   873  	if grant == "" {
   874  		return c.JSON(http.StatusBadRequest, echo.Map{
   875  			"error": "the grant_type parameter is mandatory",
   876  		})
   877  	}
   878  	if clientID == "" {
   879  		return c.JSON(http.StatusBadRequest, echo.Map{
   880  			"error": "the client_id parameter is mandatory",
   881  		})
   882  	}
   883  	if clientSecret == "" {
   884  		return c.JSON(http.StatusBadRequest, echo.Map{
   885  			"error": "the client_secret parameter is mandatory",
   886  		})
   887  	}
   888  	defer LockOAuthClient(instance, clientID)()
   889  
   890  	client, err := oauth.FindClient(instance, clientID)
   891  	if err != nil {
   892  		if couchErr, isCouchErr := couchdb.IsCouchError(err); isCouchErr && couchErr.StatusCode >= 500 {
   893  			return err
   894  		}
   895  		return c.JSON(http.StatusBadRequest, echo.Map{
   896  			"error": "the client must be registered",
   897  		})
   898  	}
   899  	if subtle.ConstantTimeCompare([]byte(clientSecret), []byte(client.ClientSecret)) == 0 {
   900  		return c.JSON(http.StatusBadRequest, echo.Map{
   901  			"error": "invalid client_secret",
   902  		})
   903  	}
   904  	out := AccessTokenReponse{
   905  		Type: "bearer",
   906  	}
   907  
   908  	slug := oauth.GetLinkedAppSlug(client.SoftwareID)
   909  	if slug != "" {
   910  		if err := CheckLinkedAppInstalled(instance, slug); err != nil {
   911  			return err
   912  		}
   913  	}
   914  
   915  	switch grant {
   916  	case "authorization_code":
   917  		code := c.FormValue("code")
   918  		if code == "" {
   919  			return c.JSON(http.StatusBadRequest, echo.Map{
   920  				"error": "the code parameter is mandatory",
   921  			})
   922  		}
   923  		accessCode := &oauth.AccessCode{}
   924  		if err = couchdb.GetDoc(instance, consts.OAuthAccessCodes, code, accessCode); err != nil {
   925  			return c.JSON(http.StatusBadRequest, echo.Map{
   926  				"error": "invalid code",
   927  			})
   928  		}
   929  		if accessCode.Challenge != "" {
   930  			sum := sha256.Sum256([]byte(verifier))
   931  			challenge := base64.RawURLEncoding.EncodeToString(sum[:])
   932  			if challenge != accessCode.Challenge {
   933  				return c.JSON(http.StatusBadRequest, echo.Map{
   934  					"error": "invalid code_verifier",
   935  				})
   936  			}
   937  		}
   938  		out.Scope = accessCode.Scope
   939  		out.Refresh, err = client.CreateJWT(instance, consts.RefreshTokenAudience, out.Scope)
   940  		if err != nil {
   941  			return c.JSON(http.StatusInternalServerError, echo.Map{
   942  				"error": "Can't generate refresh token",
   943  			})
   944  		}
   945  		// Delete the access code, it can be used only once
   946  		err = couchdb.DeleteDoc(instance, accessCode)
   947  		if err != nil {
   948  			instance.Logger().Errorf(
   949  				"[oauth] Failed to delete the access code: %s", err)
   950  		}
   951  
   952  	case "refresh_token":
   953  		token := c.FormValue("refresh_token")
   954  		claims, ok := client.ValidToken(instance, consts.RefreshTokenAudience, token)
   955  		if !ok && client.ClientKind == "sharing" {
   956  			out.Refresh, claims, ok = sharing.TryTokenForMovedSharing(instance, client, token)
   957  		}
   958  		if !ok {
   959  			return c.JSON(http.StatusBadRequest, echo.Map{
   960  				"error": "invalid refresh token",
   961  			})
   962  		}
   963  
   964  		// Code below is used to transform an old OAuth client token scope to
   965  		// the new linked-app scope
   966  		if slug != "" {
   967  			out.Scope = oauth.BuildLinkedAppScope(slug)
   968  		} else {
   969  			out.Scope = claims.Scope
   970  		}
   971  
   972  	default:
   973  		return c.JSON(http.StatusBadRequest, echo.Map{
   974  			"error": "invalid grant type",
   975  		})
   976  	}
   977  
   978  	out.Access, err = client.CreateJWT(instance, consts.AccessTokenAudience, out.Scope)
   979  	if err != nil {
   980  		return c.JSON(http.StatusInternalServerError, echo.Map{
   981  			"error": "Can't generate access token",
   982  		})
   983  	}
   984  
   985  	// Update the last_refreshed_at field of the OAuth client
   986  	client.LastRefreshedAt = time.Now()
   987  	_ = couchdb.UpdateDoc(instance, client)
   988  
   989  	_ = session.RemoveLoginRegistration(instance.ContextualDomain(), clientID)
   990  	return c.JSON(http.StatusOK, out)
   991  }
   992  
   993  func buildKonnectorToken(c echo.Context) error {
   994  	inst := middlewares.GetInstance(c)
   995  	slug := c.Param("slug")
   996  
   997  	if err := middlewares.AllowMaximal(c); err != nil {
   998  		return c.JSON(http.StatusForbidden, err)
   999  	}
  1000  
  1001  	_, err := app.GetBySlug(inst, slug, consts.KonnectorType)
  1002  	if err != nil {
  1003  		return c.JSON(http.StatusNotFound, err)
  1004  	}
  1005  
  1006  	token := inst.BuildKonnectorToken(slug)
  1007  
  1008  	return c.JSON(http.StatusCreated, token)
  1009  }
  1010  
  1011  // CheckLinkedAppInstalled checks if a linked webapp has been installed to the
  1012  // instance
  1013  func CheckLinkedAppInstalled(inst *instance.Instance, slug string) error {
  1014  	_, err := app.GetWebappBySlugAndUpdate(inst, slug,
  1015  		app.Copier(consts.WebappType, inst), inst.Registries())
  1016  	if err == nil {
  1017  		return nil
  1018  	}
  1019  
  1020  	const nbRetries = 10
  1021  	for i := 0; i < nbRetries; i++ {
  1022  		time.Sleep(3 * time.Second)
  1023  		if _, err := app.GetWebappBySlug(inst, slug); err == nil {
  1024  			return nil
  1025  		}
  1026  	}
  1027  	return fmt.Errorf("%s is not installed", slug)
  1028  }
  1029  
  1030  // GetLinkedApp fetches the app manifest on the registry
  1031  func GetLinkedApp(instance *instance.Instance, softwareID string) (*app.WebappManifest, error) {
  1032  	var webappManifest app.WebappManifest
  1033  	appSlug := oauth.GetLinkedAppSlug(softwareID)
  1034  	webapp, err := registry.GetLatestVersion(appSlug, "stable", instance.Registries())
  1035  	if err != nil {
  1036  		return nil, err
  1037  	}
  1038  	err = json.Unmarshal(webapp.Manifest, &webappManifest)
  1039  	if err != nil {
  1040  		return nil, err
  1041  	}
  1042  	return &webappManifest, nil
  1043  }
  1044  
  1045  func hasRedirectToAuthorize(inst *instance.Instance, redirect *url.URL) bool {
  1046  	if !inst.HasDomain(redirect.Host) {
  1047  		return false
  1048  	}
  1049  	if redirect.Path != "/auth/authorize" {
  1050  		return false
  1051  	}
  1052  
  1053  	redirectQuery := redirect.Query()
  1054  	scopes := redirectQuery["scope"]
  1055  	for _, scope := range scopes {
  1056  		if scope == oauth.ScopeLogin {
  1057  			return false
  1058  		}
  1059  	}
  1060  	return true
  1061  }
  1062  
  1063  func hasRedirectToAuthorizeSharing(inst *instance.Instance, redirect *url.URL) bool {
  1064  	if !inst.HasDomain(redirect.Host) {
  1065  		return false
  1066  	}
  1067  	return redirect.Path == "/auth/authorize/sharing"
  1068  }