netbirdio/netbird · error

authentication failed: invalid access token - %w

Error message

authentication failed: invalid access token - %w

What it means

After a successful token exchange, the client-side audience sanity check (validateTokenAudience) rejected the token that would be used for management login. The wrapped error says which check failed: token empty, not a three-part JWT, missing aud claim, or an aud matching neither the configured Audience nor - when Audience is empty - the ClientID fallback. This is a configuration sanity check, not a signature verification (that happens server-side against the IdP JWKS).

Source

Thrown at client/internal/auth/pkce_flow.go:310

	tokenInfo := TokenInfo{
		AccessToken:  token.AccessToken,
		RefreshToken: token.RefreshToken,
		TokenType:    token.TokenType,
		ExpiresIn:    token.Expiry.Second(),
		UseIDToken:   p.providerConfig.UseIDToken,
	}
	if idToken, ok := token.Extra("id_token").(string); ok {
		tokenInfo.IDToken = idToken
	}

	// if a provider doesn't support an audience, use the Client ID for token verification
	audience := p.providerConfig.Audience
	if audience == "" {
		audience = p.providerConfig.ClientID
	}

	if err := validateTokenAudience(tokenInfo.GetTokenToUse(), audience); err != nil {
		return TokenInfo{}, fmt.Errorf("authentication failed: invalid access token - %w", err)
	}

	email, err := parseEmailFromIDToken(tokenInfo.IDToken)
	if err != nil {
		log.Warnf("failed to parse email from ID token: %v", err)
	} else {
		tokenInfo.Email = email
	}

	return tokenInfo, nil
}

// parseEmailFromIDToken extracts the email (or name) claim from an ID token
// without verifying its signature. The value is best-effort and used only as a
// UX convenience (login hint prefill and display); it never drives an
// authorization decision. The authoritative identity is established server-side
// from the signature-verified token.
func parseEmailFromIDToken(token string) (string, error) {

View on GitHub (pinned to 93e97f4bf1)

Solutions

  1. Decode the failing JWT (base64url-decode the middle dot-separated part) and compare its aud claim with the Audience configured in NetBird's IdP settings; make them match.
  2. If the provider has no audience concept, ensure the token's aud equals the ClientID - that is the fallback checked when Audience is empty.
  3. If the access token is opaque (not a JWT), enable 'Use ID Token' in the IdP configuration so the always-JWT id_token is validated instead.
  4. Re-login after the administrator fixes the IdP configuration; the check runs on every fresh token retrieval.
Defensive patterns

Strategy: validation

Validate before calling

// decode the aud claim of a sample token and compare with what NetBird will check
func tokenAudienceMatches(token, expected string) bool {
    parts := strings.Split(token, ".")
    if len(parts) != 3 {
        return false
    }
    payload, err := base64.RawURLEncoding.DecodeString(parts[1])
    if err != nil {
        return false
    }
    var claims struct {
        Audience json.RawMessage `json:"aud"`
    }
    if json.Unmarshal(payload, &claims) != nil || len(claims.Audience) == 0 {
        return false
    }
    var auds []string
    if json.Unmarshal(claims.Audience, &auds) != nil {
        var single string
        if json.Unmarshal(claims.Audience, &single) == nil {
            auds = []string{single}
        }
    }
    for _, a := range auds {
        if a == expected {
            return true
        }
    }
    return false
}

Type guard

func isJWT(s string) bool { return len(strings.Split(s, ".")) == 3 }

Try / catch

tokenInfo, err := flow.WaitToken(ctx, info)
if err != nil {
    if strings.Contains(err.Error(), "invalid access token") {
        // configuration mismatch between IdP audience and NetBird settings;
        // fix the Audience (or enable Use ID Token) before retrying
    }
}

Prevention

When it happens

Trigger: parseOAuthToken calls validateTokenAudience(tokenInfo.GetTokenToUse(), audience): the IdP issued the token for a different API identifier than configured; the access token is opaque rather than a JWT; UseIDToken is set but no id_token came back so the checked string is empty; Audience is unset so ClientID is compared but the token's aud is a resource URI.

Common situations: Auth0/Azure AD API identifier (aud) differs from the Audience entered in NetBird's IdP settings; provider does not support the audience parameter so the ClientID fallback is used while the token still targets a resource scope; some IdP configurations issue opaque access tokens unless the app registration is adjusted; UseIDToken enabled on a flow where the scopes omit openid so no id_token is returned.

Understand the failure class

Related errors


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