netbirdio/netbird · error
token is not a well-formed JWT
Error message
token is not a well-formed JWT
What it means
validateTokenAudience split the token on '.' and did not get exactly three parts, so the token is not a structurally valid JWT. Many IdPs can issue opaque (non-JWT) access tokens; those can never pass this check because their payload is not base64url-encoded JSON claims.
Source
Thrown at client/internal/auth/util.go:39
}
// validateTokenAudience checks that the token is a well-formed JWT whose
// audience claim matches the expected audience.
//
// It does NOT verify the token's cryptographic signature and therefore must not
// be treated as an authenticity check. The token is obtained by the client
// directly from the IdP token endpoint over TLS, and its signature is verified
// server-side by the management server against the IdP's JWKS
// (see shared/auth/jwt/validator.go). This function is only a client-side
// sanity check that the returned token targets the expected audience.
func validateTokenAudience(token string, audience string) error {
if token == "" {
return fmt.Errorf("token received is empty")
}
parts := strings.Split(token, ".")
if len(parts) != 3 {
return fmt.Errorf("token is not a well-formed JWT")
}
claimsString, err := base64.RawURLEncoding.DecodeString(parts[1])
if err != nil {
return err
}
claims := Claims{}
err = json.Unmarshal(claimsString, &claims)
if err != nil {
return err
}
if claims.Audience == nil {
return fmt.Errorf("required token field audience is absent")
}
// Audience claim of JWT can be a string or an array of stringsView on GitHub (pinned to 93e97f4bf1)
Solutions
- Enable 'Use ID Token' in the NetBird IdP configuration - the id_token is always a JWT and passes the structural check.
- Alternatively, reconfigure the IdP application to issue JWT access tokens (for example register the API so the token carries the aud claim as a JWT).
- Decode a sample token manually: if it has no two dots and no base64url payload, it is opaque and the configuration must change.
- Re-login after changing the IdP configuration so a fresh token is validated.
Defensive patterns
Strategy: type-guard
Validate before calling
// cheap structural check a caller can apply to a token string
func looksLikeJWT(t string) bool {
parts := strings.Split(t, ".")
if len(parts) != 3 {
return false
}
_, err := base64.RawURLEncoding.DecodeString(parts[1])
return err == nil
} Type guard
func isJWT(s string) bool { return len(strings.Split(s, ".")) == 3 } Try / catch
if err := validateTokenAudience(token, audience); err != nil {
if strings.Contains(err.Error(), "not a well-formed JWT") {
// opaque access token: enable Use ID Token or configure the IdP to
// issue JWT access tokens; retrying cannot help
}
} Prevention
- Register the API in the IdP so access tokens are JWTs with an audience.
- Prefer id_token for client-side validation when the provider is opaque-token-only.
- Smoke-test one issued token per environment with a decoder before rollout.
When it happens
Trigger: strings.Split(token, ".") yields a length other than 3: the access token is an opaque random string (common for Azure AD v1-style or custom-issued tokens), a Key Reference/token-exchange artifact, or the wrong token type entirely (refresh token or a SAML assertion) ended up in the field.
Common situations: Azure AD issuing opaque tokens for certain resource configurations; opaque tokens; provider returning a reference token meant only for its introspection endpoint.
Related errors
- validate access token failed with error: %v
- authentication failed: invalid access token - %w
- token received is empty
- required token field audience is absent
- failed reading access token response body with error: %v
AI-assisted analysis of netbirdio/netbird@93e97f4bf1 (2026-08-16).
Data as JSON: /api/errors/f3d86a37240ea48b.
Report an issue: GitHub.