netbirdio/netbird · error
%s %s
Error message
%s
%s
What it means
Raised in the management service's proxy OAuth callback handler (management/server/http/handlers/proxy/auth.go:92) when golang.org/x/oauth2's Config.Exchange fails while swapping the authorization code for tokens at the IdP token endpoint. The oauth2.Config here carries only ClientID, RedirectURL, and the provider endpoint (no ClientSecret), so Exchange is a public-client PKCE request; any non-2xx IdP response (invalid_grant, invalid_client, invalid_redirect_uri) or transport error surfaces here and the handler answers 500.
Source
Thrown at client/cmd/daemon_error.go:21
import (
"errors"
"fmt"
"strings"
"google.golang.org/genproto/googleapis/rpc/errdetails"
gstatus "google.golang.org/grpc/status"
"github.com/netbirdio/netbird/client/internal/ipcauth"
)
// daemonCallError prepares a daemon error for display. A refusal the daemon
// raised because the operation needs root/administrator is already guidance
// written for the user, so it is surfaced on its own instead of buried under the
// gRPC envelope and the name of the RPC that hit it. Anything else is wrapped
// with context as usual.
func daemonCallError(context string, err error) error {
if guidance, ok := privilegeGuidance(err); ok {
return errors.New(guidance)
}
return fmt.Errorf("%s: %w", context, err)
}
// privilegeGuidance renders the daemon's privilege refusal as a summary and the
// command that performs the operation with the privileges it needs. It reports
// false for any other error.
func privilegeGuidance(err error) (string, bool) {
info, ok := privilegeErrorInfo(err)
if !ok {
return "", false
}
summary := info.GetMetadata()[ipcauth.ErrorMetaSummary]
command := info.GetMetadata()[ipcauth.ErrorMetaCommand]
if summary == "" {
// Detail without a summary: fall back to the status message, which
// carries the same text.View on GitHub (pinned to 93e97f4bf1)
Solutions
- Read the wrapped error in the management log (log.WithError at the same line): 'invalid_grant' means expired/reused code (restart the login flow), 'invalid_client' means the IdP expects a confidential client or the ClientID is wrong, 'invalid_request/redirect_uri' means a URI mismatch.
- Make oidcConfig.CallbackURL identical, character for character, to the redirect URI registered in the IdP application and to the one used when the authorize URL was built.
- Verify egress from the management host: curl -v the IdP token endpoint from inside the management container/process environment.
- If the flow was interrupted (state older than the code TTL or page refresh), restart from the proxy domain so a fresh code + verifier pair is generated.
Example fix
// before: IdP app configured as confidential, exchange built without secret
token, err := (&oauth2.Config{
ClientID: oidcConfig.ClientID,
Endpoint: provider.Endpoint(),
RedirectURL: oidcConfig.CallbackURL,
}).Exchange(r.Context(), r.URL.Query().Get("code"), oauth2.VerifierOption(codeVerifier))
// after: register the IdP app as public/PKCE (no secret), or if confidential,
// send the secret and keep the callback URL byte-identical to the IdP registration:
token, err := (&oauth2.Config{
ClientID: oidcConfig.ClientID,
ClientSecret: oidcConfig.ClientSecret, // only for confidential clients
Endpoint: provider.Endpoint(),
RedirectURL: oidcConfig.CallbackURL, // must equal the registered redirect URI
}).Exchange(r.Context(), r.URL.Query().Get("code"), oauth2.VerifierOption(codeVerifier)) Defensive patterns
Strategy: validation
Validate before calling
// Preflight from the management host before driving users through the flow:
// 1) issuer discovery answers, 2) callback URL matches the registered one.
provider, err := oidc.NewProvider(ctx, cfg.Issuer)
if err != nil {
return fmt.Errorf("issuer unreachable: %w", err)
}
_ = provider.Endpoint() // forces well-known fetch
if cfg.CallbackURL == "" || !strings.HasPrefix(cfg.CallbackURL, "https://") {
return errors.New("proxy OIDC callback URL must be set and use https")
}
// Compare cfg.CallbackURL against the IdP app registration before starting login. Try / catch
token, err := oauth2Cfg.Exchange(ctx, code, oauth2.VerifierOption(verifier))
if err != nil {
var rErr *oauth2.RetrieveError
if errors.As(err, &rErr) {
// rErr.Code / body carry the IdP's error: invalid_grant, invalid_client...
log.Printf("idp rejected exchange: %s %s", rErr.Code, rErr.Body)
}
return fmt.Errorf("exchange code: %w", err)
} Prevention
- Keep the IdP-registered redirect URI and oidcConfig.CallbackURL byte-identical; include scheme, port, and path.
- Register the IdP application as a public PKCE client since the exchange sends no client secret.
- Never bookmark or reload the callback URL; always restart login from the proxy domain for a fresh code+verifier.
- Monitor management logs for 'Failed to exchange code for token' and alert on bursts — they usually mean config drift.
When it happens
Trigger: GET on types.ProxyCallbackEndpoint after ValidateState succeeded, but the IdP rejects the exchange: authorization code expired or already redeemed (callback URL reloaded), PKCE code_verifier from ValidateState does not match the challenge sent at authorize time, RedirectURL (oidcConfig.CallbackURL) differs from the URI used in the authorize request or registered in the IdP app, the IdP app is confidential and demands a client_secret this config never sends, or the management host cannot reach the token endpoint (TLS, DNS, egress firewall).
Common situations: management.json proxy OIDC callback URL not matching the IdP-registered redirect exactly (scheme, port, path); user double-visits or refreshes the callback link; management container has no egress to the IdP; IdP rotated client credentials while management still holds the old ClientID; reverse proxy in front of management rewrites the callback path.
Related errors
- Run `netbird profile list --show-id` to see IDs, then selec
- service domain is required
- identity provider name is required
- identity provider type is required
- unsupported identity provider type
AI-assisted analysis of netbirdio/netbird@93e97f4bf1 (2026-08-16).
Data as JSON: /api/errors/18b33a63b735dcc2.
Report an issue: GitHub.