{"record":{"id":"2909605e3652e4b4","repo":"gotify/server","slug":"groups-claim-q-contains-a-non-string-element-v","errorCode":null,"errorMessage":"groups claim %q contains a non-string element: %#v","messagePattern":"groups claim %q contains a non-string element: %#v","errorType":"http","errorClass":null,"httpStatus":500,"severity":"error","filePath":"api/oidc.go","lineNumber":571,"sourceCode":"func (a *OIDCAPI) resolvePermission(idTokenClaims, userInfoClaims map[string]any) (bool, int, error) {\n\tif a.GroupsClaim == \"\" {\n\t\treturn false, 0, nil\n\t}\n\n\tgroupsRaw, ok := lookupClaim(a.GroupsClaim, idTokenClaims, userInfoClaims)\n\tif !ok {\n\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}","sourceCodeStart":553,"sourceCodeEnd":589,"githubUrl":"https://github.com/gotify/server/blob/14bfc256276775c425f988d621dccfe705de18ac/api/oidc.go#L553-L589","documentation":"resolvePermission accepts groups as []string, []any of strings, or a single string. If a []any element is not a string (e.g. a number or object), it fails with HTTP 500 'groups claim %q contains a non-string element'. It enforces that every group entry is a plain string before matching against GroupsAdmin/GroupsUser.","triggerScenarios":"The groups claim is an array whose element(s) are non-strings — e.g. Azure AD sends group objects ({\"id\":...}) instead of IDs-as-strings, or a custom mapper emits integers/UUIDs objects.","commonSituations":"Azure/Entra 'groups' claim with SDP properties emitting objects; custom protocol mapper with wrong type; nested group structures from custom claim scripts.","solutions":["Fix the IdP claim mapper to emit a plain array of strings","Change the token configuration so groups are emitted as string IDs (Azure: set group claims to 'Group ID' string form)","Pre-process in a custom mapper/transform, or disable object emission in the claim script","Match the expected type by adjusting GroupsClaim to point at a string-array claim"],"exampleFix":"// before (Azure app manifest)\n\"groupMembershipClaims\": \"ApplicationGroup\"\n// after\n\"groupMembershipClaims\": \"SecurityGroup\" // emits array of group ID strings","handlingStrategy":"type-guard","validationCode":"// verify every group element is a string before login\nraw, _ := claims[\"groups\"].([]any)\nfor _, g := range raw {\n    if _, ok := g.(string); !ok {\n        log.Fatalf(\"groups claim has non-string element: %#v\", g)\n    }\n}","typeGuard":"func isStringSlice(v any) bool {\n    switch t := v.(type) {\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(), \"non-string element\") {\n    // fix IdP mapper to emit string arrays (e.g. Azure Group ID claims)\n    http.Error(w, \"groups claim malformed\", http.StatusInternalServerError)\n    return\n}","preventionTips":["Configure Azure group claims as Group ID (string) not object form","Use the built-in groups mappers instead of custom scripts","Type-check decoded tokens in CI with captured sample tokens","Pin IdP mapper configuration in terraform/automation"],"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"}