argoproj/argo-workflows · error

failed to validate claims: %w

Error message

failed to validate claims: %w

What it means

The token decrypted successfully, but the claims failed validation against jwt.Expected{Issuer}: the `iss` claim does not match the configured OIDC issuer (or expiry/audience checks fail per Claims.Validate). Argo treats the session as untrusted.

Source

Thrown at server/auth/sso/sso.go:438

func isValidFinalRedirectURL(redirect string) bool {
	// Copied from https://github.com/oauth2-proxy/oauth2-proxy/blob/ab448cf38e7c1f0740b3cc2448284775e39d9661/pkg/app/redirect/validator.go#L47
	return strings.HasPrefix(redirect, "/") && !strings.HasPrefix(redirect, "//") && !invalidRedirectRegex.MatchString(redirect)
}

// authorize verifies a bearer token and pulls user information form the claims.
func (s *sso) Authorize(authorization string) (*types.Claims, error) {
	tok, err := jwt.ParseEncrypted(strings.TrimPrefix(authorization, Prefix), []jose.KeyAlgorithm{jose.DIRECT}, []jose.ContentEncryption{jose.A256GCM})
	if err != nil {
		return nil, fmt.Errorf("failed to parse encrypted token: %w", err)
	}

	c := &types.Claims{}
	if err := tok.Claims(s.encryptionKey, c); err != nil {
		return nil, fmt.Errorf("failed to decrypt token: %w", err)
	}

	if err := c.Validate(jwt.Expected{Issuer: issuer}); err != nil {
		return nil, fmt.Errorf("failed to validate claims: %w", err)
	}
	return c, nil
}

func (s *sso) getRedirectURL(r *http.Request) string {
	if s.config.RedirectURL != "" {
		return s.config.RedirectURL
	}

	proto := "http"

	if r.URL.Scheme != "" {
		proto = r.URL.Scheme
	} else if s.secure {
		proto = "https"
	}

	return fmt.Sprintf("%s://%s%soauth2/callback", proto, r.Host, s.baseHRef)

View on GitHub (pinned to 35bff19146)

Solutions

  1. Re-login to obtain a token issued by the currently configured issuer
  2. Make sure `sso.issuer` matches the provider's actual issuer URL exactly (scheme, host, path)
  3. Check for clock skew between argo-server and the IdP (NTP)
  4. If using issuerAlias, keep it consistent with the value baked into existing tokens

Example fix

// before
sso:
  issuer: http://dex:5556/dex   # token iss is https://sso.example.com/dex
// after
sso:
  issuer: https://sso.example.com/dex
Defensive patterns

Strategy: validation

Validate before calling

// decode the JWE claims client-side and compare iss before calling
u := jwt.Claims{}
tok.Claims(key, &u)
if u.Issuer != expectedIssuer { return errors.New("issuer mismatch, re-login") }

Try / catch

claims, err := sso.Authorize(auth)
if err != nil {
    return status.Error(codes.Unauthenticated, "token rejected (issuer/expiry), re-login")
}

Prevention

When it happens

Trigger: c.Validate(jwt.Expected{Issuer: issuer}) fails because the token's `iss` differs from the configured `sso.issuer` (issuer URL changed, http vs https, trailing slash mismatch, alias mismatch), or the token is expired.

Common situations: Changing the OIDC issuer/alias config while old tokens are still presented; clock skew causing `exp` validation failures; provider behind a proxy exposing a different host than configured.

Related errors


AI-assisted analysis of argoproj/argo-workflows@35bff19146 (2026-09-03). Data as JSON: /api/errors/3bde2c17e93d8d46. Report an issue: GitHub.