{"record":{"id":"e53613f4cdb005b4","repo":"gotify/server","slug":"groups-claim-q-is-not-a-string-or-string-array","errorCode":null,"errorMessage":"groups claim %q is not a string or string array: %#v","messagePattern":"groups claim %q is not a string or string array: %#v","errorType":"http","errorClass":null,"httpStatus":500,"severity":"error","filePath":"api/oidc.go","lineNumber":578,"sourceCode":"\t\treturn false, http.StatusInternalServerError, fmt.Errorf(\"groups claim %q is missing\", a.GroupsClaim)\n\t}\n\n\tvar groups []string\n\tswitch groupsRaw := groupsRaw.(type) {\n\tcase []string:\n\t\tgroups = groupsRaw\n\tcase []any:\n\t\tfor _, groupRaw := range groupsRaw {\n\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","sourceCodeStart":560,"sourceCodeEnd":596,"githubUrl":"https://github.com/gotify/server/blob/14bfc256276775c425f988d621dccfe705de18ac/api/oidc.go#L560-L596","documentation":"resolvePermission's type switch has no matching case: the groups claim value is neither a string nor an array of strings, so it returns HTTP 500 'groups claim %q is not a string or string array' with the value printed via %#v. The library only supports those shapes for role resolution.","triggerScenarios":"GroupsClaim resolves to an unexpected JSON type — e.g. a single object, a boolean, a number, or a nested array (e.g. [[\"admins\"]]) emitted by a misconfigured mapper.","commonSituations":"Custom claim mapper returning JSON objects; IdP emitting groups as nested structure; pointing GroupsClaim at the wrong claim (e.g. a JSON blob like 'realm_access' without navigating to .roles).","solutions":["Point GroupsClaim at a claim that is a string or string array (e.g. top-level 'groups')","Fix the IdP mapper to flatten the claim to []string","If using Keycloak realm_access.roles, add a mapper that hoists roles into a flat claim","Decode the token to confirm the claim's actual JSON type"],"exampleFix":"// before\nOIDC_GROUPS_CLAIM=realm_access\n// after\nOIDC_GROUPS_CLAIM=groups // flat string array emitted by mapper","handlingStrategy":"type-guard","validationCode":"// assert the claim shape at startup\nv, ok := claims[groupsClaim]\nif !ok || !(isStringOrStringArray(v)) {\n    log.Fatalf(\"groups claim %q has unsupported type %T; must be string or []string\", groupsClaim, v)\n}","typeGuard":"func isStringOrStringArray(v any) bool {\n    switch t := v.(type) {\n    case string:\n        return true\n    case []string:\n        return true\n    case []any:\n        for _, e := range t {\n            if _, ok := e.(string); !ok { return false }\n        }\n        return true\n    default:\n        return false\n    }\n}","tryCatchPattern":"user, status, err := resolveUser(...)\nif err != nil && strings.Contains(err.Error(), \"not a string or string array\") {\n    // GroupsClaim points at a wrong-shaped claim; repoint or flatten via mapper\n    http.Error(w, \"groups claim malformed\", http.StatusInternalServerError)\n    return\n}","preventionTips":["Point GroupsClaim at a flat string-array claim, not nested objects like realm_access","Add a mapper to flatten nested roles into a top-level claim","Validate token claim types in a post-IdP-change smoke test","Document supported claim shapes for operators"],"tags":["oidc","claims","type-mismatch","authorization"],"backgroundTag":"jwt-claim-type-mismatch","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"}