Compare commits

...

3 Commits

Author SHA1 Message Date
Max Kotliar
b86347f10d app/vmauth: rm unused 2026-02-25 16:19:10 +02:00
Max Kotliar
8cd01e0040 lib/jwt: fixes 2026-02-25 16:04:08 +02:00
Max Kotliar
1840a3838e app/vmauth: log sub\iss claims as username when needed
Currently, when vmauth logs messages and calls `ui.name()` to include
user context, it returns an empty string for the JWT authentication
method. As a result, logs lack any user-identifying information, which
makes debugging and incident analysis harder.

This PR improves the logging behavior for JWT-based auth:
- Use the `sub` claim as the primary user identifier.
- Fall back to the `iss` claim if sub is not present.
- If neither claim is available, explicitly indicate that the
authentication method is JWT.

This ensures logs always contain meaningful context about the
authenticated entity and avoids silent empty user fields.

Related to
https://github.com/VictoriaMetrics/VictoriaMetrics/issues/9439,
https://github.com/VictoriaMetrics/VictoriaMetrics/issues/10445
2026-02-25 15:47:30 +02:00
3 changed files with 54 additions and 24 deletions

View File

@@ -20,6 +20,7 @@ import (
"sync/atomic"
"time"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/jwt"
"github.com/VictoriaMetrics/metrics"
"github.com/cespare/xxhash/v2"
"gopkg.in/yaml.v2"
@@ -127,11 +128,11 @@ func (ui *UserInfo) beginConcurrencyLimit(ctx context.Context) error {
// The current request couldn't be executed until the request timeout.
ui.concurrencyLimitReached.Inc()
return fmt.Errorf("cannot start executing the request during -maxQueueDuration=%s because %d concurrent requests from the user %s are executed",
*maxQueueDuration, ui.getMaxConcurrentRequests(), ui.name())
*maxQueueDuration, ui.getMaxConcurrentRequests(), userName(ui, nil))
}
return fmt.Errorf("cannot start executing the request because %d concurrent requests from the user %s are executed: %w",
ui.getMaxConcurrentRequests(), ui.name(), err)
ui.getMaxConcurrentRequests(), userName(ui, nil), err)
}
}
}
@@ -1010,7 +1011,7 @@ func parseAuthConfigUsers(ac *AuthConfig) (map[string]*UserInfo, error) {
var labelNameRegexp = regexp.MustCompile("^[a-zA-Z_:.][a-zA-Z0-9_:.]*$")
func (ui *UserInfo) getMetricLabels() (string, error) {
name := ui.name()
name := userName(ui, nil)
if len(name) == 0 && len(ui.MetricLabels) == 0 {
// fast path
return "", nil
@@ -1115,24 +1116,6 @@ func (ui *UserInfo) initURLs() error {
return nil
}
func (ui *UserInfo) name() string {
if ui.Name != "" {
return ui.Name
}
if ui.Username != "" {
return ui.Username
}
if ui.BearerToken != "" {
h := xxhash.Sum64([]byte(ui.BearerToken))
return fmt.Sprintf("bearer_token:hash:%016X", h)
}
if ui.AuthToken != "" {
h := xxhash.Sum64([]byte(ui.AuthToken))
return fmt.Sprintf("auth_token:hash:%016X", h)
}
return ""
}
func getAuthTokens(authToken, bearerToken, username, password string) ([]string, error) {
if authToken != "" {
if bearerToken != "" {
@@ -1236,3 +1219,36 @@ func sanitizeURLPrefix(urlPrefix *url.URL) (*url.URL, error) {
}
return urlPrefix, nil
}
func userName(ui *UserInfo, tkn *jwt.Token) string {
if tkn != nil {
if sub := tkn.Subject(); sub != "" {
return "jwt:sub:" + sub
}
if iss := tkn.Issuer(); iss != "" {
return "jwt:iss:" + iss
}
return "jwt:unknown"
}
if ui.Name != "" {
return ui.Name
}
if ui.Username != "" {
return ui.Username
}
if ui.BearerToken != "" {
h := xxhash.Sum64([]byte(ui.BearerToken))
return fmt.Sprintf("bearer_token:hash:%016X", h)
}
if ui.AuthToken != "" {
h := xxhash.Sum64([]byte(ui.AuthToken))
return fmt.Sprintf("auth_token:hash:%016X", h)
}
if ui.JWT != nil {
return "jwt"
}
return ""
}

View File

@@ -188,7 +188,7 @@ func requestHandler(w http.ResponseWriter, r *http.Request) bool {
}
if ui, tkn := getUserInfoByJWTToken(ats); ui != nil {
if tkn == nil {
logger.Panicf("BUG: unexpected nil jwt token for user %q", ui.name())
logger.Panicf("BUG: unexpected nil jwt token for user %q", userName(ui, nil))
}
processUserRequest(w, r, ui, tkn)
@@ -253,7 +253,7 @@ func processUserRequest(w http.ResponseWriter, r *http.Request, ui *UserInfo, tk
}
// Read the initial chunk for the request body.
userName := ui.name()
userName := userName(ui, tkn)
if userName == "" {
userName = "unauthorized"
}
@@ -412,7 +412,7 @@ func processRequest(w http.ResponseWriter, r *http.Request, ui *UserInfo, tkn *j
ui.backendErrors.Inc()
}
err := &httpserver.ErrorWithStatusCode{
Err: fmt.Errorf("all the %d backends for the user %q are unavailable for proxying the request - check previous WARN logs to see the exact error for each failed backend", up.getBackendsCount(), ui.name()),
Err: fmt.Errorf("all the %d backends for the user %q are unavailable for proxying the request - check previous WARN logs to see the exact error for each failed backend", up.getBackendsCount(), userName(ui, tkn)),
StatusCode: http.StatusBadGateway,
}
httpserver.Errorf(w, r, "%s", err)

View File

@@ -50,6 +50,8 @@ type body struct {
Iat int64 `json:"iat"`
Jti string `json:"jti,omitempty"`
Scope string `json:"scope,omitempty"`
Sub string `json:"sub,omitempty"`
Iss string `json:"iss,omitempty"`
VMAccess *VMAccessClaim `json:"vm_access"`
}
@@ -207,6 +209,14 @@ func (t *Token) VMAccess() *VMAccessClaim {
return t.body.VMAccess
}
func (t *Token) Subject() string {
return t.body.Sub
}
func (t *Token) Issuer() string {
return t.body.Iss
}
func parseJWTHeader(data string) (*header, error) {
var jh header
decoded, err := decodeB64([]byte(data))
@@ -226,6 +236,8 @@ func parseJWTBody(data string) (*body, error) {
// issued at time unix_ts
Iat int64 `json:"iat"`
Jti string `json:"jti,omitempty"`
Sub string `json:"sub,omitempty"`
Iss string `json:"iss,omitempty"`
Scope json.RawMessage `json:"scope,omitempty"`
// store as raw message to support different types
VMAccess *json.RawMessage `json:"vm_access"`
@@ -274,6 +286,8 @@ func parseJWTBody(data string) (*body, error) {
Exp: tb.Exp,
Iat: tb.Iat,
Jti: tb.Jti,
Sub: tb.Sub,
Iss: tb.Iss,
Scope: scope,
VMAccess: &a,
}