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(¶meters); 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)
}
}