coder/enterprise/coderd/identityprovider/middleware.go

150 lines
5.2 KiB
Go

package identityprovider
import (
"net/http"
"net/url"
"github.com/coder/coder/v2/coderd/httpapi"
"github.com/coder/coder/v2/coderd/httpmw"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/site"
)
// authorizeMW serves to remove some code from the primary authorize handler.
// It decides when to show the html allow page, and when to just continue.
func authorizeMW(accessURL *url.URL) func(next http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
origin := r.Header.Get(httpmw.OriginHeader)
originU, err := url.Parse(origin)
if err != nil {
httpapi.Write(r.Context(), rw, http.StatusBadRequest, codersdk.Response{
Message: "Invalid origin header.",
Detail: err.Error(),
})
return
}
referer := r.Referer()
refererU, err := url.Parse(referer)
if err != nil {
httpapi.Write(r.Context(), rw, http.StatusBadRequest, codersdk.Response{
Message: "Invalid referer header.",
Detail: err.Error(),
})
return
}
app := httpmw.OAuth2ProviderApp(r)
ua := httpmw.UserAuthorization(r)
// url.Parse() allows empty URLs, which is fine because the origin is not
// always set by browsers (or other tools like cURL). If the origin does
// exist, we will make sure it matches. We require `referer` to be set at
// a minimum in order to detect whether "allow" has been pressed, however.
cameFromSelf := (origin == "" || originU.Hostname() == accessURL.Hostname()) &&
refererU.Hostname() == accessURL.Hostname() &&
refererU.Path == "/login/oauth2/authorize"
// If we were redirected here from this same page it means the user
// pressed the allow button so defer to the authorize handler which
// generates the code, otherwise show the HTML allow page.
// TODO: Skip this step if the user has already clicked allow before, and
// we can just reuse the token.
if cameFromSelf {
next.ServeHTTP(rw, r)
return
}
// TODO: For now only browser-based auth flow is officially supported but
// in a future PR we should support a cURL-based flow where we output text
// instead of HTML.
if r.URL.Query().Get("redirected") != "" {
// When the user first comes into the page, referer might be blank which
// is OK. But if they click "allow" and their browser has *still* not
// sent the referer header, we have no way of telling whether they
// actually clicked the button. "Redirected" means they *might* have
// pressed it, but it could also mean an app added it for them as part
// of their redirect, so we cannot use it as a replacement for referer
// and the best we can do is error.
if referer == "" {
site.RenderStaticErrorPage(rw, r, site.ErrorPageData{
Status: http.StatusInternalServerError,
HideStatus: false,
Title: "Referer header missing",
Description: "We cannot continue authorization because your client has not sent the referer header.",
RetryEnabled: false,
DashboardURL: accessURL.String(),
Warnings: nil,
})
return
}
site.RenderStaticErrorPage(rw, r, site.ErrorPageData{
Status: http.StatusInternalServerError,
HideStatus: false,
Title: "Oauth Redirect Loop",
Description: "Oauth redirect loop detected.",
RetryEnabled: false,
DashboardURL: accessURL.String(),
Warnings: nil,
})
return
}
callbackURL, err := url.Parse(app.CallbackURL)
if err != nil {
site.RenderStaticErrorPage(rw, r, site.ErrorPageData{
Status: http.StatusInternalServerError,
HideStatus: false,
Title: "Internal Server Error",
Description: err.Error(),
RetryEnabled: false,
DashboardURL: accessURL.String(),
Warnings: nil,
})
return
}
// Extract the form parameters for two reasons:
// 1. We need the redirect URI to build the cancel URI.
// 2. Since validation will run once the user clicks "allow", it is
// better to validate now to avoid wasting the user's time clicking a
// button that will just error anyway.
params, validationErrs, err := extractAuthorizeParams(r, callbackURL)
if err != nil {
errStr := make([]string, len(validationErrs))
for i, err := range validationErrs {
errStr[i] = err.Detail
}
site.RenderStaticErrorPage(rw, r, site.ErrorPageData{
Status: http.StatusBadRequest,
HideStatus: false,
Title: "Invalid Query Parameters",
Description: "One or more query parameters are missing or invalid.",
RetryEnabled: false,
DashboardURL: accessURL.String(),
Warnings: errStr,
})
return
}
cancel := params.redirectURL
cancelQuery := params.redirectURL.Query()
cancelQuery.Add("error", "access_denied")
cancel.RawQuery = cancelQuery.Encode()
redirect := r.URL
vals := redirect.Query()
vals.Add("redirected", "true") // For loop detection.
r.URL.RawQuery = vals.Encode()
site.RenderOAuthAllowPage(rw, r, site.RenderOAuthAllowData{
AppIcon: app.Icon,
AppName: app.Name,
CancelURI: cancel.String(),
RedirectURI: r.URL.String(),
Username: ua.ActorName,
})
})
}
}