coder/enterprise/coderd/identityprovider/authorize.go

141 lines
4.6 KiB
Go

package identityprovider
import (
"database/sql"
"errors"
"net/http"
"net/url"
"time"
"github.com/google/uuid"
"golang.org/x/xerrors"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbtime"
"github.com/coder/coder/v2/coderd/httpapi"
"github.com/coder/coder/v2/coderd/httpmw"
"github.com/coder/coder/v2/codersdk"
)
type authorizeParams struct {
clientID string
redirectURL *url.URL
responseType codersdk.OAuth2ProviderResponseType
scope []string
state string
}
func extractAuthorizeParams(r *http.Request, callbackURL *url.URL) (authorizeParams, []codersdk.ValidationError, error) {
p := httpapi.NewQueryParamParser()
vals := r.URL.Query()
p.RequiredNotEmpty("state", "response_type", "client_id")
params := authorizeParams{
clientID: p.String(vals, "", "client_id"),
redirectURL: p.RedirectURL(vals, callbackURL, "redirect_uri"),
responseType: httpapi.ParseCustom(p, vals, "", "response_type", httpapi.ParseEnum[codersdk.OAuth2ProviderResponseType]),
scope: p.Strings(vals, []string{}, "scope"),
state: p.String(vals, "", "state"),
}
// We add "redirected" when coming from the authorize page.
_ = p.String(vals, "", "redirected")
p.ErrorExcessParams(vals)
if len(p.Errors) > 0 {
return authorizeParams{}, p.Errors, xerrors.Errorf("invalid query params: %w", p.Errors)
}
return params, nil, nil
}
// Authorize displays an HTML page for authorizing an application when the user
// has first been redirected to this path and generates a code and redirects to
// the app's callback URL after the user clicks "allow" on that page, which is
// detected via the origin and referer headers.
func Authorize(db database.Store, accessURL *url.URL) http.HandlerFunc {
handler := func(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
apiKey := httpmw.APIKey(r)
app := httpmw.OAuth2ProviderApp(r)
callbackURL, err := url.Parse(app.CallbackURL)
if err != nil {
httpapi.Write(r.Context(), rw, http.StatusInternalServerError, codersdk.Response{
Message: "Failed to validate query parameters.",
Detail: err.Error(),
})
return
}
params, validationErrs, err := extractAuthorizeParams(r, callbackURL)
if err != nil {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "Invalid query params.",
Detail: err.Error(),
Validations: validationErrs,
})
return
}
// TODO: Ignoring scope for now, but should look into implementing.
code, err := GenerateSecret()
if err != nil {
httpapi.Write(r.Context(), rw, http.StatusInternalServerError, codersdk.Response{
Message: "Failed to generate OAuth2 app authorization code.",
})
return
}
err = db.InTx(func(tx database.Store) error {
// Delete any previous codes.
err = tx.DeleteOAuth2ProviderAppCodesByAppAndUserID(ctx, database.DeleteOAuth2ProviderAppCodesByAppAndUserIDParams{
AppID: app.ID,
UserID: apiKey.UserID,
})
if err != nil && !errors.Is(err, sql.ErrNoRows) {
return xerrors.Errorf("delete oauth2 app codes: %w", err)
}
// Insert the new code.
_, err = tx.InsertOAuth2ProviderAppCode(ctx, database.InsertOAuth2ProviderAppCodeParams{
ID: uuid.New(),
CreatedAt: dbtime.Now(),
// TODO: Configurable expiration? Ten minutes matches GitHub.
// This timeout is only for the code that will be exchanged for the
// access token, not the access token itself. It does not need to be
// long-lived because normally it will be exchanged immediately after it
// is received. If the application does wait before exchanging the
// token (for example suppose they ask the user to confirm and the user
// has left) then they can just retry immediately and get a new code.
ExpiresAt: dbtime.Now().Add(time.Duration(10) * time.Minute),
SecretPrefix: []byte(code.Prefix),
HashedSecret: []byte(code.Hashed),
AppID: app.ID,
UserID: apiKey.UserID,
})
if err != nil {
return xerrors.Errorf("insert oauth2 authorization code: %w", err)
}
return nil
}, nil)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Failed to generate OAuth2 authorization code.",
Detail: err.Error(),
})
return
}
newQuery := params.redirectURL.Query()
newQuery.Add("code", code.Formatted)
newQuery.Add("state", params.state)
params.redirectURL.RawQuery = newQuery.Encode()
http.Redirect(rw, r, params.redirectURL.String(), http.StatusTemporaryRedirect)
}
// Always wrap with its custom mw.
return authorizeMW(accessURL)(http.HandlerFunc(handler)).ServeHTTP
}