{"record":{"id":"6c9ad77dfe675614","repo":"gotify/server","slug":"user-is-not-in-any-allowed-group","errorCode":null,"errorMessage":"user is not in any allowed group","messagePattern":"user is not in any allowed group","errorType":"http","errorClass":null,"httpStatus":403,"severity":"warning","filePath":"api/oidc.go","lineNumber":587,"sourceCode":"\t\t\tgroup, ok := groupRaw.(string)\n\t\t\tif !ok {\n\t\t\t\treturn false, http.StatusInternalServerError, fmt.Errorf(\"groups claim %q contains a non-string element: %#v\", a.GroupsClaim, groupRaw)\n\t\t\t}\n\t\t\tgroups = append(groups, group)\n\t\t}\n\tcase string:\n\t\tgroups = append(groups, groupsRaw)\n\tdefault:\n\t\treturn false, http.StatusInternalServerError, fmt.Errorf(\"groups claim %q is not a string or string array: %#v\", a.GroupsClaim, groupsRaw)\n\t}\n\n\tswitch {\n\tcase containsAny(a.GroupsAdmin, groups):\n\t\treturn true, 0, nil\n\tcase len(a.GroupsUser) == 0 || containsAny(a.GroupsUser, groups):\n\t\treturn false, 0, nil\n\tdefault:\n\t\treturn false, http.StatusForbidden, errors.New(\"user is not in any allowed group\")\n\t}\n}\n\nfunc lookupClaim(name string, idTokenClaims, userInfoClaims map[string]any) (any, bool) {\n\tif value, ok := idTokenClaims[name]; ok {\n\t\treturn value, true\n\t}\n\tvalue, ok := userInfoClaims[name]\n\treturn value, ok\n}\n\nfunc containsAny(configured, actual []string) bool {\n\tfor _, value := range actual {\n\t\tif slices.Contains(configured, value) {\n\t\t\treturn true\n\t\t}\n\t}\n","sourceCodeStart":569,"sourceCodeEnd":605,"githubUrl":"https://github.com/gotify/server/blob/14bfc256276775c425f988d621dccfe705de18ac/api/oidc.go#L569-L605","documentation":"resolvePermission throws 403 'user is not in any allowed group' when the OIDC user's group/role claims match none of the configured allowed groups: not in GOTIFY_OIDC_GROUPS_ADMIN and not in GOTIFY_OIDC_GROUPS_USER (unless the user-group list is empty, which permits everyone).","triggerScenarios":"resolveUser calls resolvePermission with the token's groups; the groups claim is present but contains none of the configured admin/user groups, or the groups claim itself is missing/empty so containsAny never matches while a user-group allowlist is configured.","commonSituations":"IdP emits groups under a different claim name (e.g. 'roles') than Gotify expects; group names renamed at the provider; client not granted the groups scope; typo in GOTIFY_OIDC_GROUPS_USER values.","solutions":["Update GOTIFY_OIDC_GROUPS_USER to include the group names your provider actually issues.","Ensure the token includes a groups claim (configure the groups scope / claim mapping at the IdP).","Clear GOTIFY_OIDC_GROUPS_USER to allow any authenticated OIDC user as a normal user.","Match exact group names (case/whitespace) between IdP and Gotify config."],"exampleFix":"// before\nGOTIFY_OIDC_GROUPS_USER=gotify-users // IdP emits 'users'\n// after\nGOTIFY_OIDC_GROUPS_USER=gotify-users,users","handlingStrategy":"validation","validationCode":"const groups = decodeJwt(idToken).groups ?? [];\nconst allowed = (process.env.GOTIFY_OIDC_GROUPS_USER ?? '').split(',').filter(Boolean);\nif (allowed.length > 0 && !groups.some(g => allowed.includes(g))) throw new Error('token groups do not match GOTIFY_OIDC_GROUPS_USER');","typeGuard":"function inAnyGroup(tokenGroups, allowed) { return Array.isArray(tokenGroups) && tokenGroups.some(g => allowed.includes(g)); }","tryCatchPattern":null,"preventionTips":["Compare exact group names from a decoded token against config.","Ensure the groups scope/claim mapping is enabled at the IdP.","Leave GOTIFY_OIDC_GROUPS_USER empty to allow all authenticated users.","Re-verify after group renames at the provider."],"tags":["oidc","http-403","authorization","groups"],"backgroundTag":"oidc-group-authorization-failed","analyzedSha":"14bfc256276775c425f988d621dccfe705de18ac","analyzedAt":"2026-09-05T12:52:36.781Z","contentChangedAt":"2026-09-05T12:52:36.781Z","schemaVersion":2},"datasetVersion":"2026-09-12T17:17:11.597Z"}