gotify/server · error
groups claim %q is not a string or string array: %#v
Error message
groups claim %q is not a string or string array: %#v
What it means
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.
Source
Thrown at api/oidc.go:578
return false, http.StatusInternalServerError, fmt.Errorf("groups claim %q is missing", a.GroupsClaim)
}
var groups []string
switch groupsRaw := groupsRaw.(type) {
case []string:
groups = groupsRaw
case []any:
for _, groupRaw := range groupsRaw {
group, ok := groupRaw.(string)
if !ok {
return false, http.StatusInternalServerError, fmt.Errorf("groups claim %q contains a non-string element: %#v", a.GroupsClaim, groupRaw)
}
groups = append(groups, group)
}
case string:
groups = append(groups, groupsRaw)
default:
return false, http.StatusInternalServerError, fmt.Errorf("groups claim %q is not a string or string array: %#v", a.GroupsClaim, groupsRaw)
}
switch {
case containsAny(a.GroupsAdmin, groups):
return true, 0, nil
case len(a.GroupsUser) == 0 || containsAny(a.GroupsUser, groups):
return false, 0, nil
default:
return false, http.StatusForbidden, errors.New("user is not in any allowed group")
}
}
func lookupClaim(name string, idTokenClaims, userInfoClaims map[string]any) (any, bool) {
if value, ok := idTokenClaims[name]; ok {
return value, true
}
value, ok := userInfoClaims[name]
return value, okView on GitHub (pinned to 14bfc25627)
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
Example fix
// before OIDC_GROUPS_CLAIM=realm_access // after OIDC_GROUPS_CLAIM=groups // flat string array emitted by mapper
Defensive patterns
Strategy: type-guard
Validate before calling
// assert the claim shape at startup
v, ok := claims[groupsClaim]
if !ok || !(isStringOrStringArray(v)) {
log.Fatalf("groups claim %q has unsupported type %T; must be string or []string", groupsClaim, v)
} Type guard
func isStringOrStringArray(v any) bool {
switch t := v.(type) {
case string:
return true
case []string:
return true
case []any:
for _, e := range t {
if _, ok := e.(string); !ok { return false }
}
return true
default:
return false
}
} Try / catch
user, status, err := resolveUser(...)
if err != nil && strings.Contains(err.Error(), "not a string or string array") {
// GroupsClaim points at a wrong-shaped claim; repoint or flatten via mapper
http.Error(w, "groups claim malformed", http.StatusInternalServerError)
return
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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).
Related errors
- groups claim %q contains a non-string element: %#v
- groups claim %q is missing
- issuer claim was empty
- subject claim was empty
- username claim was empty
AI-assisted analysis of gotify/server@14bfc25627 (2026-09-05).
Data as JSON: /api/errors/e53613f4cdb005b4.
Report an issue: GitHub.