oidc: handle groups claim as string or array (FlexibleStringSlice)

Some OIDC providers (notably JumpCloud) return the `groups` claim as
a plain string when the user belongs to a single group, rather than
a single-element array:

  Single group:    {"groups": "MyGroup"}
  Multiple groups: {"groups": ["Group1", "Group2"]}

This causes `json.Unmarshal` to fail with:

  cannot unmarshal string into Go struct field OIDCClaims.groups of type []string

This is the same class of issue as juanfont#2293 (FlexibleBoolean for
email_verified). The fix follows the same pattern: introduce a
FlexibleStringSlice type with a custom UnmarshalJSON that accepts
both a string and a []string, and use it for the Groups field in
both OIDCClaims and OIDCUserInfo.
This commit is contained in:
primewildy 2026-05-04 14:26:53 +01:00 committed by GitHub
parent 76ee29352b
commit 3d0f597b23
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 83 additions and 6 deletions

View file

@ -24,6 +24,9 @@ import (
// ErrCannotParseBoolean is returned when a value cannot be parsed as boolean.
var ErrCannotParseBoolean = errors.New("cannot parse value as boolean")
// ErrCannotParseStringSlice is returned when a value cannot be parsed as string or []string.
var ErrCannotParseStringSlice = errors.New("cannot parse value as string or []string")
type UserID uint64
type Users []User
@ -229,6 +232,29 @@ func (u UserView) MarshalZerologObject(e *zerolog.Event) {
u.ж.MarshalZerologObject(e)
}
// FlexibleStringSlice handles OIDC providers (e.g. JumpCloud) that return the
// groups claim as a plain string when the user belongs to a single group,
// instead of a single-element array.
type FlexibleStringSlice []string
func (f *FlexibleStringSlice) UnmarshalJSON(data []byte) error {
var arr []string
err := json.Unmarshal(data, &arr)
if err == nil {
*f = arr
return nil
}
var single string
err = json.Unmarshal(data, &single)
if err == nil {
*f = []string{single}
return nil
}
return fmt.Errorf("%w: %s", ErrCannotParseStringSlice, string(data))
}
// FlexibleBoolean handles JumpCloud's JSON where email_verified is returned as a
// string "true" or "false" instead of a boolean.
// This maps bool to a specific type with a custom unmarshaler to
@ -269,9 +295,9 @@ type OIDCClaims struct {
// Name is the user's full name.
Name string `json:"name,omitempty"`
Groups []string `json:"groups,omitempty"`
Email string `json:"email,omitempty"`
EmailVerified FlexibleBoolean `json:"email_verified,omitempty"`
Groups FlexibleStringSlice `json:"groups,omitempty"`
Email string `json:"email,omitempty"`
EmailVerified FlexibleBoolean `json:"email_verified,omitempty"`
ProfilePictureURL string `json:"picture,omitempty"`
Username string `json:"preferred_username,omitempty"`
}
@ -387,9 +413,9 @@ type OIDCUserInfo struct {
FamilyName string `json:"family_name"`
PreferredUsername string `json:"preferred_username"`
Email string `json:"email"`
EmailVerified FlexibleBoolean `json:"email_verified,omitempty"`
Groups []string `json:"groups"`
Picture string `json:"picture"`
EmailVerified FlexibleBoolean `json:"email_verified,omitempty"`
Groups FlexibleStringSlice `json:"groups"`
Picture string `json:"picture"`
}
// FromClaim overrides a User from OIDC claims.