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, ok

View on GitHub (pinned to 14bfc25627)

Solutions

  1. Point GroupsClaim at a claim that is a string or string array (e.g. top-level 'groups')
  2. Fix the IdP mapper to flatten the claim to []string
  3. If using Keycloak realm_access.roles, add a mapper that hoists roles into a flat claim
  4. 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

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


AI-assisted analysis of gotify/server@14bfc25627 (2026-09-05). Data as JSON: /api/errors/e53613f4cdb005b4. Report an issue: GitHub.