{"record":{"id":"18ab2b1379cd83e8","repo":"knadh/listmonk","slug":"error-fetching-user-info-from-oidc","errorCode":null,"errorMessage":"error fetching user info from OIDC","messagePattern":"error fetching user info from OIDC","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/auth/auth.go","lineNumber":272,"sourceCode":"\tif idTk.Nonce != nonce {\n\t\treturn \"\", OIDCclaim{}, echo.NewHTTPError(http.StatusUnauthorized, \"nonce did not match\")\n\t}\n\n\tvar claims OIDCclaim\n\tif err := idTk.Claims(&claims); err != nil {\n\t\treturn \"\", OIDCclaim{}, errors.New(\"error getting user from OIDC\")\n\t}\n\n\t// If claims doesn't have the e-mail, attempt to fetch it from the userinfo endpoint.\n\tif claims.Email == \"\" {\n\t\tprovider, err := o.getProvider()\n\t\tif err != nil {\n\t\t\treturn \"\", OIDCclaim{}, fmt.Errorf(\"error getting provider: %v\", err)\n\t\t}\n\n\t\tuserInfo, err := provider.UserInfo(context.TODO(), oauth2.StaticTokenSource(tk))\n\t\tif err != nil {\n\t\t\treturn \"\", OIDCclaim{}, errors.New(\"error fetching user info from OIDC\")\n\t\t}\n\n\t\t// Parse the UserInfo claims into the claims struct\n\t\tif err := userInfo.Claims(&claims); err != nil {\n\t\t\treturn \"\", OIDCclaim{}, errors.New(\"error parsing user info claims\")\n\t\t}\n\t}\n\n\treturn rawIDTk, claims, nil\n}\n\n// Middleware is the HTTP middleware used for wrapping HTTP handlers registered on the echo router.\n// It authorizes token (BasicAuth/token) based and cookie based sessions and on successful auth,\n// sets the authenticated User{} on the echo context on the key UserKey. On failure, it sets an Error{}\n// instead on the same key.\nfunc (o *Auth) Middleware(next echo.HandlerFunc) echo.HandlerFunc {\n\treturn func(c echo.Context) error {\n\t\t// It's an `Authorization` header request.","sourceCodeStart":254,"sourceCodeEnd":290,"githubUrl":"https://github.com/knadh/listmonk/blob/670c01717d48647093335cc23a6be6f4b79c3b6b/internal/auth/auth.go#L254-L290","documentation":"This error is returned when the call to provider.UserInfo() — which fetches the OpenID Connect UserInfo endpoint using the exchanged OAuth2 token — fails. It only happens when the ID token claims had no email, so the library falls back to the userinfo endpoint. The underlying HTTP/network/protocol error is discarded, so the actual cause is hidden.","triggerScenarios":"After a successful token exchange, claims.Email is empty, and provider.UserInfo(context.TODO(), oauth2.StaticTokenSource(tk)) returns an error: network failure, OIDC discovery/metadata problems, expired/invalid access token rejected by the userinfo endpoint, or the provider lacks a userinfo endpoint.","commonSituations":"The IdP did not issue an email claim because the email scope was not requested or not consented; the access token was rejected by userinfo; corporate proxy/firewall blocks the IdP's userinfo URL; the issuer's well-known discovery lacks userinfo_endpoint.","solutions":["Request the email and profile scopes in the OAuth2 config so the ID token itself contains the email and the userinfo call is skipped entirely.","Decode the discarded err and log it (fmt.Errorf with %w) to see if it is a 401 on userinfo (token/scopes) vs a network error.","Verify the IdP discovery document exposes a userinfo_endpoint and that the server can reach that URL.","Confirm the access token from cfg.Exchange is valid and not expired at userinfo call time."],"exampleFix":"// before\nuserInfo, err := provider.UserInfo(context.TODO(), oauth2.StaticTokenSource(tk))\nif err != nil {\n\treturn \"\", OIDCclaim{}, errors.New(\"error fetching user info from OIDC\")\n}\n// after\nuserInfo, err := provider.UserInfo(context.TODO(), oauth2.StaticTokenSource(tk))\nif err != nil {\n\treturn \"\", OIDCclaim{}, fmt.Errorf(\"error fetching user info from OIDC: %w\", err)\n}","handlingStrategy":"try-catch","validationCode":"// ensure email scope is granted before exchange\nif !strings.Contains(cfg.Scopes(\"token\")[0], \"email\") {\n\tcfg = cfg.Scopes(\"openid\", \"email\", \"profile\")\n}","typeGuard":"func claimsHaveEmail(c auth.OIDCclaim) bool { return c.Email != \"\" }","tryCatchPattern":"raw, claims, err := auth.ExchangeOIDCToken(code, nonce)\nif err != nil {\n\tif strings.Contains(err.Error(), \"error fetching user info from OIDC\") {\n\t\t// check network reachability of IdP userinfo endpoint, token validity\n\t\tlog.Printf(\"userinfo fetch failed: %v\", err)\n\t}\n\treturn err\n}","preventionTips":["Always request the email scope so the userinfo fallback rarely runs.","Monitor egress to the IdP's userinfo endpoint from your server.","Verify the discovery document exposes userinfo_endpoint for your issuer.","Wrap and log the discarded underlying error in a fork/patch."],"tags":["oidc","network","userinfo-endpoint"],"backgroundTag":"userinfo-endpoint-fetch-failed","analyzedSha":"670c01717d48647093335cc23a6be6f4b79c3b6b","analyzedAt":"2026-09-01T03:39:35.452Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}