hashicorp/nomad · error

invalid JWT issuer

Error message

invalid JWT issuer: %v

What it means

The JWT validator rejects a token because its `iss` (issuer) claim is not present in the auth method's configured BoundIssuers list. Nomad requires that every JWT used for login be signed by a trusted issuer; this check runs only when the auth method specifies BoundIssuers. The token itself was decodable, but its issuer is not one of the allowed values.

Solutions

  1. Decode the token (e.g. jwt.io or `nomad acl login`) and read its `iss` claim, then add that exact string to the auth method's BoundIssuers via `nomad acl auth-method update`.
  2. Verify the auth method's BoundIssuers values match the provider's documented issuer URL exactly (scheme, host, trailing slash).
  3. If the issuer is dynamic/unexpected, confirm the client is fetching tokens from the intended OIDC provider and not a stale or test IdP.

Example fix

// before
authMethod := &structs.ACLAuthMethod{
  Name: "keycloak",
  Config: &structs.ACLAuthMethodConfig{
    BoundIssuers: []string{"https://keycloak.example.com/realms/old"},
  },
}
// after: issuer updated to match the token's iss claim
authMethod := &structs.ACLAuthMethod{
  Name: "keycloak",
  Config: &structs.ACLAuthMethodConfig{
    BoundIssuers: []string{"https://keycloak.example.com/realms/prod"},
  },
}
Defensive patterns

Strategy: validation

Validate before calling

// decode token's iss claim client-side before login
parts := strings.Split(token, ".")
if len(parts) != 3 { return fmt.Errorf("not a JWT") }
payload, _ := base64.RawURLEncoding.DecodeString(parts[1])
var claims map[string]interface{}
json.Unmarshal(payload, &claims)
iss, _ := claims["iss"].(string)
allowed := authMethodConfig.BoundIssuers
if len(allowed) > 0 && !slices.Contains(allowed, iss) {
  return fmt.Errorf("issuer %q not in BoundIssuers %v", iss, allowed)
}

Type guard

func hasTrustedIssuer(claims map[string]interface{}, bound []string) bool {
  iss, ok := claims["iss"].(string)
  return ok && iss != "" && slices.Contains(bound, iss)
}

Prevention

When it happens

Trigger: ACL auth method login with JWT auth: `nomad acl login -jwt <token>` against an auth method with JWTAuthMethod.BoundIssuers set, where the token's `iss` claim does not exactly match any entry in BoundIssuer.

Common situations: Misconfigured auth method (typo'd or missing issuer in BoundIssuers, e.g. `https://accounts.google.com` vs `accounts.google.com`); pointing clients at the wrong OIDC provider; provider changed its issuer URL after a tenant/region migration; using a token from a different environment's IdP.

Related errors


AI-assisted analysis of hashicorp/nomad@482b49bf1a (2026-09-04). Data as JSON: /api/errors/fb997e64427db9d4. Report an issue: GitHub.

Appendix: source

Thrown at lib/auth/jwt/validator.go:82

		return nil, err
	}

	claims, err := validator.Validate(ctx, token, expected)
	if err != nil {
		return nil, fmt.Errorf("unable to verify signature of JWT token: %v", err)
	}

	// validate issuer manually, because we allow users to specify an array
	if len(methodConf.BoundIssuer) > 0 {
		if _, ok := claims["iss"]; !ok {
			return nil, fmt.Errorf(
				"auth method specifies BoundIssuers but the provided token does not contain issuer information",
			)
		}
		if iss, ok := claims["iss"].(string); !ok {
			return nil, fmt.Errorf("unable to read iss property of provided token")
		} else if !slices.Contains(methodConf.BoundIssuer, iss) {
			return nil, fmt.Errorf("invalid JWT issuer: %v", claims["iss"])
		}
	}

	return claims, nil
}

func usingStaticKeys(keys []string) (jwt.KeySet, error) {
	var parsedKeys []crypto.PublicKey
	for _, v := range keys {
		key, err := jwt.ParsePublicKeyPEM([]byte(v))
		parsedKeys = append(parsedKeys, key)
		if err != nil {
			return nil, fmt.Errorf("unable to parse public key for JWT auth: %v", err)
		}
	}
	return jwt.NewStaticKeySet(parsedKeys)
}

View on GitHub (pinned to 482b49bf1a)