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, ¶ms); 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, ¶ms); 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, ¶ms); 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, ¶ms); 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 }