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 strings

View on GitHub (pinned to 93e97f4bf1)

Solutions

  1. Enable 'Use ID Token' in the NetBird IdP configuration - the id_token is always a JWT and passes the structural check.
  2. 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).
  3. Decode a sample token manually: if it has no two dots and no base64url payload, it is opaque and the configuration must change.
  4. 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

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


AI-assisted analysis of netbirdio/netbird@93e97f4bf1 (2026-08-16). Data as JSON: /api/errors/f3d86a37240ea48b. Report an issue: GitHub.