Path: blob/main/components/public-api-server/pkg/oidc/router.go
2500 views
// Copyright (c) 2022 Gitpod GmbH. All rights reserved.1// Licensed under the GNU Affero General Public License (AGPL).2// See License.AGPL.txt in the project root for license information.34package oidc56import (7"encoding/base64"8"encoding/json"9"fmt"10"net/http"11"net/url"12"time"1314"github.com/gitpod-io/gitpod/common-go/log"1516"github.com/go-chi/chi/v5"17)1819func Router(s *Service) *chi.Mux {20router := chi.NewRouter()2122router.Route("/start", func(r chi.Router) {23r.Get("/", s.getStartHandler())24})25router.Route("/callback", func(r chi.Router) {26r.Use(s.OAuth2Middleware)27r.Get("/", s.getCallbackHandler())28})2930return router31}3233const (34stateCookieName = "state"35nonceCookieName = "nonce"36verifierCookieName = "verifier"37)3839func (s *Service) getStartHandler() http.HandlerFunc {40return func(rw http.ResponseWriter, r *http.Request) {41useHttpErrors := false42if r.URL.Query().Get("useHttpErrors") != "" {43useHttpErrors = true44}4546config, err := s.getClientConfigFromStartRequest(r)47if err != nil {48log.WithError(err).Warn("Failed to start SSO sign-in flow.")49respondeWithError(rw, r, "We were unable to find the SSO configuration you've requested. Please verify SSO is configured with your Organization owner.", http.StatusNotFound, useHttpErrors)50return51}5253returnToURL := r.URL.Query().Get("returnTo")54if returnToURL == "" {55returnToURL = "/"56}5758activate := false59if r.URL.Query().Get("activate") != "" {60activate = true61}6263// `activate` overrides a `verify` parameter64verify := false65if !activate && r.URL.Query().Get("verify") != "" {66verify = true67}6869redirectURL := getCallbackURL(r.Host)7071startParams, err := s.getStartParams(config, redirectURL, StateParams{72ClientConfigID: config.ID,73ReturnToURL: returnToURL,74Activate: activate,75Verify: verify,76UseHttpErrors: useHttpErrors,77})78if err != nil {79log.WithError(err).Error("Failed to get start parameters for authentication flow.")80respondeWithError(rw, r, "We were unable to start the authentication flow for system reasons.", http.StatusInternalServerError, useHttpErrors)81return82}8384http.SetCookie(rw, newCallbackCookie(r, nonceCookieName, startParams.Nonce))85http.SetCookie(rw, newCallbackCookie(r, stateCookieName, startParams.State))86http.SetCookie(rw, newCallbackCookie(r, verifierCookieName, startParams.CodeVerifier))8788http.Redirect(rw, r, startParams.AuthCodeURL, http.StatusTemporaryRedirect)89}90}9192func getCallbackURL(host string) string {93callbackURL := url.URL{Scheme: "https", Path: "/iam/oidc/callback", Host: host}94return callbackURL.String()95}9697func newCallbackCookie(r *http.Request, name string, value string) *http.Cookie {98return &http.Cookie{99Name: name,100Value: value,101MaxAge: int(10 * time.Minute.Seconds()),102Secure: true,103SameSite: http.SameSiteLaxMode,104HttpOnly: true,105}106}107108// The OIDC callback handler depends on the state produced in the OAuth2 middleware109func (s *Service) getCallbackHandler() http.HandlerFunc {110return func(rw http.ResponseWriter, r *http.Request) {111config, state, err := s.getClientConfigFromCallbackRequest(r)112useHttpErrors := state.UseHttpErrors113if err != nil {114log.WithError(err).Warn("Client SSO config not found")115reportLoginCompleted("failed_client", "sso")116respondeWithError(rw, r, "We were unable to find the SSO configuration you've requested. Please verify SSO is configured with your Organization owner.", http.StatusNotFound, useHttpErrors)117return118}119oauth2Result := GetOAuth2ResultFromContext(r.Context())120if oauth2Result == nil {121reportLoginCompleted("failed_client", "sso")122respondeWithError(rw, r, "OIDC precondition failure", http.StatusInternalServerError, useHttpErrors)123return124}125126// nonce = number used once127nonceCookie, err := r.Cookie(nonceCookieName)128if err != nil {129reportLoginCompleted("failed_client", "sso")130respondeWithError(rw, r, "There was no nonce present on the request. Please try to sign-in in again.", http.StatusBadRequest, useHttpErrors)131return132}133result, err := s.authenticate(r.Context(), authenticateParams{134Config: config,135OAuth2Result: oauth2Result,136NonceCookieValue: nonceCookie.Value,137})138if err != nil {139log.WithError(err).Warn("OIDC authentication failed")140reportLoginCompleted("failed_client", "sso")141responseMsg := "We've not been able to authenticate you with the OIDC Provider."142if celExprErr, ok := err.(*CelExprError); ok {143responseMsg = fmt.Sprintf("%s [%s]", responseMsg, celExprErr.Code)144}145respondeWithError(rw, r, responseMsg, http.StatusInternalServerError, useHttpErrors)146return147}148149log.WithField("id_token", result.IDToken).Trace("User verification was successful")150151if state.Activate {152err = s.activateAndVerifyClientConfig(r.Context(), config)153if err != nil {154log.WithError(err).Warn("Failed to mark OIDC Client Config as active")155reportLoginCompleted("failed", "sso")156respondeWithError(rw, r, "We've been unable to mark the selected OIDC config as active. Please try again.", http.StatusInternalServerError, useHttpErrors)157return158}159}160161if state.Verify {162// Skip the sign-in on verify-only requests.163err = s.markClientConfigAsVerified(r.Context(), config)164if err != nil {165log.Warn("Failed to mark config as verified: " + err.Error())166reportLoginCompleted("failed", "sso")167respondeWithError(rw, r, "Failed to mark config as verified", http.StatusInternalServerError, useHttpErrors)168return169}170} else {171cookies, _, err := s.createSession(r.Context(), result, config)172if err != nil {173log.WithError(err).Warn("Failed to create session from downstream session provider.")174reportLoginCompleted("failed", "sso")175respondeWithError(rw, r, "We were unable to create a user session.", http.StatusInternalServerError, useHttpErrors)176return177}178for _, cookie := range cookies {179http.SetCookie(rw, cookie)180}181}182183reportLoginCompleted("succeeded", "sso")184http.Redirect(rw, r, oauth2Result.ReturnToURL, http.StatusTemporaryRedirect)185}186}187188func respondeWithError(w http.ResponseWriter, r *http.Request, error string, code int, useHttpErrors bool) {189if useHttpErrors {190http.Error(w, error, code)191return192}193194if !useHttpErrors {195jsonString, err := json.Marshal(map[string]interface{}{196"code": code,197"error": error,198})199if err != nil {200log.WithError(err).Warn("Failed to marchal error to be sent to frontend.")201jsonString = []byte("Internal error")202}203errorEncoded := base64.StdEncoding.EncodeToString(jsonString)204url := fmt.Sprintf("/complete-auth?message=error:%s", errorEncoded)205206http.Redirect(w, r, url, http.StatusTemporaryRedirect)207}208}209210211