Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update TO to handle Client-Cert-Subject HTTP header for client cert authentication. #8013

Closed
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).

## [unreleased]
### Added
- -[#8013](https://github.com/apache/trafficcontrol/pull/8013) *Traffic Ops* Update TO to handle Client-Cert-Subject HTTP header for client cert authentication.
- [#8014](https://github.com/apache/trafficcontrol/pull/8014) *Traffic Ops* Added logs to indicate which mechanism a client used to login to TO.
- [#7812](https://github.com/apache/trafficcontrol/pull/7812) *Traffic Portal*: Expose the `configUpdateFailed` and `revalUpdateFailed` fields on the server table.
- [#7870](https://github.com/apache/trafficcontrol/pull/7870) *Traffic Portal*: Adds a hyperlink to the DSR page to the DS itself for ease of navigation.
Expand Down
3 changes: 3 additions & 0 deletions experimental/certificate_auth/example/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ func main() {
log.Fatalln(err)
}

// Uncomment the following line to set UID via Client-Cert-Subject header and change the UID value to match the user you want to authenticate
//req.Header.Set("Client-Cert-Subject", "CN=client,OU=client,O=client,L=client,ST=client,C=US,UID=userID")

resp, err := client.Do(req)
if err != nil {
log.Fatalln(err)
Expand Down
2 changes: 1 addition & 1 deletion traffic_ops/traffic_ops_golang/auth/certificate.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ func ParseClientCertificateUID(cert *x509.Certificate) (string, error) {
foundUID = true
}
}
if !foundUID {
if !foundUID || uid == "" {
err = fmt.Errorf("no UID found")
}
return uid, err
Expand Down
56 changes: 43 additions & 13 deletions traffic_ops/traffic_ops_golang/login/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,11 @@ import (
"errors"
"fmt"
"html/template"
"net"
"net/http"
"net/url"
"path/filepath"
"strings"
"time"

"github.com/apache/trafficcontrol/v8/lib/go-log"
Expand Down Expand Up @@ -108,38 +110,47 @@ Subject: {{.InstanceName}} Password Reset Request` + "\r\n\r" + `
</html>
`))

func clientCertAuthentication(w http.ResponseWriter, r *http.Request, db *sqlx.DB, cfg config.Config, dbCtx context.Context, cancelTx context.CancelFunc, form *auth.PasswordForm, authenticated bool) bool {
func clientCertAuthentication(w http.ResponseWriter, r *http.Request, db *sqlx.DB, cfg config.Config, dbCtx context.Context, cancelTx context.CancelFunc, form *auth.PasswordForm, authenticated bool) (bool, bool) {
// No certs provided by the client. Skip to form authentication
if r.TLS == nil || len(r.TLS.PeerCertificates) == 0 {
return false
return false, false
}

// If no configuration is set, skip to form auth
if cfg.ClientCertAuth == nil || len(cfg.ClientCertAuth.RootCertsDir) == 0 {
return false
return false, false
}

// Perform certificate verification to ensure it is valid against Root CAs
err := auth.VerifyClientCertificate(r, cfg.ClientCertAuth.RootCertsDir, cfg.Insecure)
if err != nil {
log.Warnf("client cert auth: error attempting to verify client provided TLS certificate. err: %s\n", err)
return false
return true, false
}

// Client provided a verified certificate. Extract UID value.
// Client provided a verified certificate. Extract UID value. Try Certificate first and then HTTP Header
clientCertSubject := r.Header.Get("Client-Cert-Subject")
remoteIp, _, _ := net.SplitHostPort(r.RemoteAddr)
if username, err := auth.ParseClientCertificateUID(r.TLS.PeerCertificates[0]); err != nil {
log.Errorf("parsing client certificate: %s\n", err)
return false
log.Warnf("parsing client certificate: %s\n", err)
if username, err = extractUID(clientCertSubject); err != nil {
log.Warnf("extracting UID from http header client-cert-subject: %s\n", err)
return true, false
} else {
form.Username = username
log.Infof("extracted UID from http-header client-cert-subject: %s , remoteAddress: %s", form.Username, remoteIp)
}
} else {
form.Username = username
log.Infof("extracted UID from client certificate: %s , remoteAddress: %s", form.Username, remoteIp)
}

// Check if user exists locally (TODB) and has a role.
var blockingErr error
authenticated, err, blockingErr = auth.CheckLocalUserIsAllowed(form.Username, db, dbCtx)
if blockingErr != nil {
api.HandleErr(w, r, nil, http.StatusServiceUnavailable, nil, fmt.Errorf("error checking local user has role: %s", blockingErr.Error()))
return false
return true, false
}
if err != nil {
log.Warnf("client cert auth: checking local user: %s\n", err)
Expand All @@ -153,7 +164,7 @@ func clientCertAuthentication(w http.ResponseWriter, r *http.Request, db *sqlx.D
}
}

return authenticated
return true, authenticated
}

// LoginHandler first attempts to verify and parse user information from an optionally
Expand All @@ -162,6 +173,7 @@ func clientCertAuthentication(w http.ResponseWriter, r *http.Request, db *sqlx.D
func LoginHandler(db *sqlx.DB, cfg config.Config) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
triedAuthentication := false
authenticated := false
form := auth.PasswordForm{}
var resp tc.Alerts
Expand All @@ -171,18 +183,22 @@ func LoginHandler(db *sqlx.DB, cfg config.Config) http.HandlerFunc {
// Attempt to perform client certificate authentication. If fails, goto standard form auth. If the
// certificate was verified, has a UID, and the UID matches an existing user we consider this to
// be a successful login.
authenticated = clientCertAuthentication(w, r, db, cfg, dbCtx, cancelTx, &form, authenticated)
triedAuthentication, authenticated = clientCertAuthentication(w, r, db, cfg, dbCtx, cancelTx, &form, authenticated)

// Failed certificate-based auth, perform standard form auth
if !authenticated {
log.Infof("user %s could not be successfully authenticated using client certificates", form.Username)
if form.Username == "" {
log.Infof("could not extract UID from client certificate or HTTP header & could not successfully authenticate using client certificates")
} else {
log.Infof("user %s could not be successfully authenticated using client certificates", form.Username)
}
// Perform form authentication
if err := json.NewDecoder(r.Body).Decode(&form); err != nil {
api.HandleErr(w, r, nil, http.StatusBadRequest, err, nil)
return
}
if form.Username == "" || form.Password == "" {
api.HandleErr(w, r, nil, http.StatusBadRequest, errors.New("username and password are required"), nil)
api.HandleErr(w, r, nil, http.StatusBadRequest, errors.New("certificate-based auth failed. hence username and password are required"), nil)
return
}

Expand Down Expand Up @@ -225,7 +241,7 @@ func LoginHandler(db *sqlx.DB, cfg config.Config) http.HandlerFunc {
log.Infof("user %s successfully authenticated using LDAP", form.Username)
}
}
} else {
} else if triedAuthentication && authenticated {
log.Infof("user %s successfully authenticated using client certificates", form.Username)
}

Expand Down Expand Up @@ -729,3 +745,17 @@ func ResetPassword(db *sqlx.DB, cfg config.Config) http.HandlerFunc {
api.WriteAndLogErr(w, r, append(respBts, '\n'))
}
}

// Extract UID from the Client-Cert-Subject header
func extractUID(subject string) (string, error) {
err := error(nil)
subjects := strings.Split(subject, ",")
for _, s := range subjects {
if strings.Contains(s, "UID=") {
return strings.TrimSpace(strings.TrimPrefix(s, "UID=")), err
}
}

err = fmt.Errorf("UID not found in Client-Cert-Subject header")
return "", err
}
Loading