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 }