package handler import ( "crypto/subtle" "html/template" "net/http" "net/url" "strings" "github.com/gin-gonic/gin" "github.com/warmbly/warmbly/internal/app/poollink" "github.com/warmbly/warmbly/internal/errx" "github.com/warmbly/warmbly/internal/models" "github.com/warmbly/warmbly/internal/pkg/crypt" ) // A brokered mailbox sign-in for a linked instance passes through this page on // the cloud's own origin. It names who is asking, and the cookie it sets is // what the provider callback requires, so the sign-in completes only in the // browser where the person read this page and chose to continue. const brokerCookieName = "warmbly_broker" // brokerCookiePath covers the consent page, its continue step and both provider callbacks. const brokerCookiePath = "/addresses/" var brokerConsentPage = template.Must(template.New("broker-consent").Parse(` Connect a mailbox to Warmbly Cloud

Connect a {{.Provider}} mailbox to Warmbly Cloud

The self-hosted Warmbly instance at {{.Host}}{{if .Name}} ({{.Name}}){{end}} asked to connect a mailbox through Warmbly Cloud.

If you continue and sign in, the mailbox is added to the Warmbly Cloud workspace {{if .Workspace}}{{.Workspace}}{{else}}linked to that instance{{end}}, and that instance can send and read mail as it.

Only continue if you started this yourself, from that instance. If someone sent you this link, close this tab.

Continue to {{.Provider}}
`)) func brokerProviderName(p models.InboxProvider) string { if p == models.InboxProviderOutlook { return "Microsoft" } return "Google" } // renderBrokerNotice answers every refused step with a plain page; nothing on it is actionable. func renderBrokerNotice(c *gin.Context, xerr *errx.Error) { status, body := http.StatusBadRequest, "This sign-in link has expired or was already used. Start again from your instance." if xerr != nil && xerr.Code != errx.Internal { body = xerr.Message } else if xerr != nil { status, body = http.StatusInternalServerError, "Something went wrong. Start again from your instance." } renderNotice(c, status, "Sign-in not started", body) } func setBrokerCookie(c *gin.Context, value string, maxAge int) { secure := c.Request.TLS != nil || strings.EqualFold(c.GetHeader("X-Forwarded-Proto"), "https") http.SetCookie(c.Writer, &http.Cookie{ Name: brokerCookieName, Value: value, Path: brokerCookiePath, MaxAge: maxAge, HttpOnly: true, Secure: secure, SameSite: http.SameSiteLaxMode, }) } // PoolLinkOAuthConsentPage is GET /addresses/connect?state=..., the page a brokered sign-in opens on. func (h *Handler) PoolLinkOAuthConsentPage(c *gin.Context) { c.Header("Cache-Control", "no-store") c.Header("Referrer-Policy", "no-referrer") if h.PoolLinkService == nil { renderBrokerNotice(c, nil) return } state := c.Query("state") consent, xerr := h.PoolLinkService.DescribeOAuthConsent(c.Request.Context(), state) if xerr != nil { renderBrokerNotice(c, xerr) return } // One value per browser, so two sign-ins open side by side both still complete. token, _ := c.Cookie(brokerCookieName) if len(token) < 16 { var err error if token, err = crypt.Nonce(); err != nil { renderBrokerNotice(c, errx.InternalError()) return } } setBrokerCookie(c, token, 15*60) data := struct{ Provider, Host, Name, Workspace, ContinueURL string }{ Provider: brokerProviderName(consent.Provider), Host: consent.InstanceHost, Name: consent.InstanceName, Workspace: consent.WorkspaceName, ContinueURL: poollink.BrokerConsentPath + "/continue?" + url.Values{"state": {state}, "t": {token}}.Encode(), } c.Header("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'; frame-ancestors 'none'; base-uri 'none'; form-action 'none'") c.Header("Content-Type", "text/html; charset=utf-8") c.Status(http.StatusOK) _ = brokerConsentPage.Execute(c.Writer, data) } // PoolLinkOAuthContinue is the consent page's button: it binds the sign-in to this browser and opens the provider. func (h *Handler) PoolLinkOAuthContinue(c *gin.Context) { c.Header("Cache-Control", "no-store") c.Header("Referrer-Policy", "no-referrer") if h.PoolLinkService == nil { renderBrokerNotice(c, nil) return } // The link carries the cookie's value, which only the consent page rendered in this browser knows. cookie, _ := c.Cookie(brokerCookieName) if cookie == "" || subtle.ConstantTimeCompare([]byte(cookie), []byte(c.Query("t"))) != 1 { renderBrokerNotice(c, poollink.ErrOAuthBrowser) return } to, xerr := h.PoolLinkService.ContinueOAuth(c.Request.Context(), c.Query("state"), cookie) if xerr != nil { renderBrokerNotice(c, xerr) return } c.Redirect(http.StatusFound, to) }