gotify/server · error

failed to get user info: %w

Error message

failed to get user info: %w

What it means

After a successful token exchange, ExternalTokenHandler calls `rp.Userinfo` to fetch claims about the end user from the provider's userinfo endpoint. If that call fails, the handler wraps the cause as 'failed to get user info: %w' and returns 500.

Source

Thrown at api/oidc.go:392

		return
	}
	session, ok := a.popPendingSession(req.State)
	if !ok {
		ctx.AbortWithError(http.StatusBadRequest, errors.New("unknown or expired state"))
		return
	}
	exchangeOpts := []rp.CodeExchangeOpt{
		rp.CodeExchangeOpt(rp.WithURLParam("redirect_uri", session.RedirectURI)),
		rp.WithCodeVerifier(req.CodeVerifier),
	}
	tokens, err := rp.CodeExchange[*oidc.IDTokenClaims](ctx.Request.Context(), req.Code, a.Provider, exchangeOpts...)
	if err != nil {
		ctx.AbortWithError(http.StatusUnauthorized, fmt.Errorf("token exchange failed: %w", err))
		return
	}
	info, err := rp.Userinfo[*oidc.UserInfo](ctx.Request.Context(), tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.GetSubject(), a.Provider)
	if err != nil {
		ctx.AbortWithError(http.StatusInternalServerError, fmt.Errorf("failed to get user info: %w", err))
		return
	}
	user, status, resolveErr := a.resolveUser(tokens.IDTokenClaims, info)
	if resolveErr != nil {
		ctx.AbortWithError(status, resolveErr)
		return
	}
	client, err := a.createClient(session.ClientName, user.ID)
	if err != nil {
		ctx.AbortWithError(http.StatusInternalServerError, err)
		return
	}
	ctx.JSON(http.StatusOK, &model.OIDCExternalTokenResponse{
		Token: client.Token,
		User:  &model.UserExternal{ID: user.ID, Name: user.Name, Admin: user.Admin},
	})
}

View on GitHub (pinned to 14bfc25627)

Solutions

  1. Check the wrapped cause for the provider's HTTP status
  2. Verify OIDC discovery (issuer/.well-known/openid-configuration) exposes a valid userinfo_endpoint
  3. Ensure network/TLS access from the server to the IdP
  4. Retry after confirming the access token is valid and unexpired
Defensive patterns

Strategy: retry

Validate before calling

// check discovery exposes userinfo
const disco = await fetch(`${issuer}/.well-known/openid-configuration`).then(r => r.json());
if (!disco.userinfo_endpoint) throw new Error('provider has no userinfo endpoint');

Try / catch

try {
  info = await userinfo(accessToken, tokenType, subject);
} catch (err) {
  if (isTransientNetwork(err)) return retryWithBackoff();
  if (isUnauthorized(err)) return startNewLoginFlow();
  throw err;
}

Prevention

When it happens

Trigger: Access token rejected/expired at the userinfo endpoint, provider userinfo URL misconfigured or unreachable, network failure, or the provider returning an error status for the given subject/token type.

Common situations: Provider outage or internal network/DNS issues; token type mismatch (e.g. opaque vs JWT); discovery document missing userinfo_endpoint; TLS problems to the IdP.

Related errors


AI-assisted analysis of gotify/server@14bfc25627 (2026-09-05). Data as JSON: /api/errors/3d33197332601185. Report an issue: GitHub.