hashicorp/nomad · error

invalid JWT issuer: %v

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.

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)

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.