func OauthLoginHandler()

in traffic_ops/traffic_ops_golang/login/login.go [400:585]


func OauthLoginHandler(db *sqlx.DB, cfg config.Config) http.HandlerFunc {
	// The jwk.AutoRefresh and jwk.Whitelist objects only get created once.
	// They are shared between all handlers
	// Note: This assumes two things:
	// 1) that the cfg.ConfigTrafficOpsGolang.WhitelistedOAuthUrls is not updated once it has been initialized
	// 2) OauthLoginHandler is not called conccurently
	if jwksFetcher == nil {
		ar := jwk.NewAutoRefresh(context.TODO())
		wl := &whitelist{urls: cfg.ConfigTrafficOpsGolang.WhitelistedOAuthUrls}
		jwksFetcher = &jwksFetch{
			ar: ar,
			wl: wl,
		}
	}

	return func(w http.ResponseWriter, r *http.Request) {
		defer r.Body.Close()
		resp := struct {
			tc.Alerts
		}{}

		form := auth.PasswordForm{}
		parameters := struct {
			AuthCodeTokenUrl string `json:"authCodeTokenUrl"`
			Code             string `json:"code"`
			ClientId         string `json:"clientId"`
			RedirectUri      string `json:"redirectUri"`
		}{}

		if err := json.NewDecoder(r.Body).Decode(&parameters); err != nil {
			log.Infof("user %s could not be successfully authenticated using SSO", form.Username)
			api.HandleErr(w, r, nil, http.StatusBadRequest, err, nil)
			return
		}

		matched, err := VerifyUrlOnWhiteList(parameters.AuthCodeTokenUrl, cfg.ConfigTrafficOpsGolang.WhitelistedOAuthUrls)
		if err != nil {
			log.Infof("user %s could not be successfully authenticated using SSO", form.Username)
			api.HandleErr(w, r, nil, http.StatusInternalServerError, nil, err)
			return
		}
		if !matched {
			log.Infof("user %s could not be successfully authenticated using SSO", form.Username)
			api.HandleErr(w, r, nil, http.StatusForbidden, nil, errors.New("Key URL from token is not included in the whitelisted urls. Received: "+parameters.AuthCodeTokenUrl))
			return
		}

		data := url.Values{}
		data.Add("code", parameters.Code)
		data.Add("client_id", parameters.ClientId)
		data.Add("grant_type", "authorization_code") // Required by RFC6749 section 4.1.3
		data.Add("redirect_uri", parameters.RedirectUri)

		req, err := http.NewRequest(http.MethodPost, parameters.AuthCodeTokenUrl, bytes.NewBufferString(data.Encode()))
		req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
		if cfg.OAuthClientSecret != "" {
			req.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(parameters.ClientId+":"+cfg.OAuthClientSecret))) // per RFC6749 section 2.3.1
		}
		if err != nil {
			log.Infof("user %s could not be successfully authenticated using SSO", form.Username)
			api.HandleErr(w, r, nil, http.StatusInternalServerError, nil, fmt.Errorf("obtaining token using code from oauth provider: %w", err))
			return
		}

		client := http.Client{
			Timeout: 30 * time.Second,
		}
		response, err := client.Do(req)
		if err != nil {
			log.Infof("user %s could not be successfully authenticated using SSO", form.Username)
			api.HandleErr(w, r, nil, http.StatusInternalServerError, nil, fmt.Errorf("getting an http client: %w", err))
			return
		}
		defer response.Body.Close()

		buf := new(bytes.Buffer)
		buf.ReadFrom(response.Body)
		encodedToken := ""

		var result map[string]interface{}
		if err := json.Unmarshal(buf.Bytes(), &result); err != nil {
			log.Warnf("Error parsing JSON response from OAuth: %s", err)
			encodedToken = buf.String()
		} else if _, ok := result[rfc.IDToken]; !ok {
			sysErr := fmt.Errorf("Missing access token in response: %s\n", buf.String())
			usrErr := errors.New("Bad response from OAuth2.0 provider")
			api.HandleErr(w, r, nil, http.StatusBadGateway, usrErr, sysErr)
			return
		} else {
			switch t := result[rfc.IDToken].(type) {
			case string:
				encodedToken = result[rfc.IDToken].(string)
			default:
				sysErr := fmt.Errorf("Incorrect type of access_token! Expected 'string', got '%v'\n", t)
				usrErr := errors.New("Bad response from OAuth2.0 provider")
				log.Infof("user %s could not be successfully authenticated using SSO", form.Username)
				api.HandleErr(w, r, nil, http.StatusBadGateway, usrErr, sysErr)
				return
			}
		}

		if encodedToken == "" {
			log.Infof("user %s could not be successfully authenticated using SSO", form.Username)
			api.HandleErr(w, r, nil, http.StatusBadRequest, errors.New("Token not found in request but is required"), nil)
			return
		}

		var decodedToken jwt.Token
		if decodedToken, err = jwt.Parse(
			[]byte(encodedToken),
			jwt.WithVerifyAuto(true),
			jwt.WithJWKSetFetcher(jwksFetcher),
		); err != nil {
			if decodedToken, err = jwt.Parse(
				[]byte(encodedToken),
				jwt.WithVerifyAuto(false),
				jwt.WithJWKSetFetcher(jwksFetcher),
			); err != nil {
				log.Infof("user %s could not be successfully authenticated using SSO", form.Username)
				api.HandleErr(w, r, nil, http.StatusInternalServerError, nil, fmt.Errorf("error decoding token with message: %w", err))
				return
			}
		}

		var userIDInterface interface{}
		var userID string
		var ok bool
		if cfg.OAuthUserAttribute != "" {
			attributes := decodedToken.PrivateClaims()
			if userIDInterface, ok = attributes[cfg.OAuthUserAttribute]; !ok {
				log.Infof("user %s could not be successfully authenticated using SSO", form.Username)
				api.HandleErr(w, r, nil, http.StatusInternalServerError, nil, fmt.Errorf("Non-existent OAuth attribute : %s", cfg.OAuthUserAttribute))
				return
			}
			userID = userIDInterface.(string)
		} else {
			userID = decodedToken.Subject()
		}
		form.Username = userID

		dbCtx, cancelTx := context.WithTimeout(r.Context(), time.Duration(cfg.DBQueryTimeoutSeconds)*time.Second)
		defer cancelTx()
		userAllowed, err, blockingErr := auth.CheckLocalUserIsAllowed(form.Username, db, dbCtx)
		if blockingErr != nil {
			log.Infof("user %s could not be successfully authenticated using SSO", form.Username)
			api.HandleErr(w, r, nil, http.StatusServiceUnavailable, nil, fmt.Errorf("error checking local user password: %s\n", blockingErr.Error()))
			return
		}
		if err != nil {
			log.Errorf("checking local user: %s\n", err)
		}

		if userAllowed {
			_, dbErr := db.Exec(UpdateLoginTimeQuery, form.Username)
			if dbErr != nil {
				log.Infof("user %s could not be successfully authenticated using SSO", form.Username)
				dbErr = fmt.Errorf("unable to update authentication time for user '%s': %w", form.Username, dbErr)
				api.HandleErr(w, r, nil, http.StatusInternalServerError, nil, dbErr)
				return
			}
			httpCookie := tocookie.GetCookie(userID, defaultCookieDuration, cfg.Secrets[0])
			http.SetCookie(w, httpCookie)
			resp = struct {
				tc.Alerts
			}{tc.CreateAlerts(tc.SuccessLevel, "Successfully logged in.")}
			log.Infof("user %s successfully authenticated using SSO", form.Username)
		} else {
			resp = struct {
				tc.Alerts
			}{tc.CreateAlerts(tc.ErrorLevel, "Invalid username or password.")}
			log.Infof("user %s could not be successfully authenticated using SSO", form.Username)
		}

		respBts, err := json.Marshal(resp)
		if err != nil {
			api.HandleErr(w, r, nil, http.StatusInternalServerError, nil, fmt.Errorf("encoding response: %w", err))
			return
		}
		w.Header().Set(rfc.ContentType, rfc.ApplicationJSON)
		if !userAllowed {
			w.WriteHeader(http.StatusForbidden)
		}
		fmt.Fprintf(w, "%s", respBts)

	}
}