gotify/server · error
groups claim %q contains a non-string element: %#v
Error message
groups claim %q contains a non-string element: %#v
What it means
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.
Source
Thrown at api/oidc.go:571
func (a *OIDCAPI) resolvePermission(idTokenClaims, userInfoClaims map[string]any) (bool, int, error) {
if a.GroupsClaim == "" {
return false, 0, nil
}
groupsRaw, ok := lookupClaim(a.GroupsClaim, idTokenClaims, userInfoClaims)
if !ok {
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")
}
}View on GitHub (pinned to 14bfc25627)
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
Example fix
// before (Azure app manifest) "groupMembershipClaims": "ApplicationGroup" // after "groupMembershipClaims": "SecurityGroup" // emits array of group ID strings
Defensive patterns
Strategy: type-guard
Validate before calling
// verify every group element is a string before login
raw, _ := claims["groups"].([]any)
for _, g := range raw {
if _, ok := g.(string); !ok {
log.Fatalf("groups claim has non-string element: %#v", g)
}
} Type guard
func isStringSlice(v any) bool {
switch t := v.(type) {
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(), "non-string element") {
// fix IdP mapper to emit string arrays (e.g. Azure Group ID claims)
http.Error(w, "groups claim malformed", http.StatusInternalServerError)
return
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: Azure/Entra 'groups' claim with SDP properties emitting objects; custom protocol mapper with wrong type; nested group structures from custom claim scripts.
Related errors
- groups claim %q is not a string or string array: %#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/2909605e3652e4b4.
Report an issue: GitHub.