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:
parent
76ee29352b
commit
3d0f597b23
@ -24,6 +24,9 @@ import (
|
|||||||
// ErrCannotParseBoolean is returned when a value cannot be parsed as boolean.
|
// ErrCannotParseBoolean is returned when a value cannot be parsed as boolean.
|
||||||
var ErrCannotParseBoolean = errors.New("cannot parse value 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 UserID uint64
|
||||||
|
|
||||||
type Users []User
|
type Users []User
|
||||||
@ -229,6 +232,29 @@ func (u UserView) MarshalZerologObject(e *zerolog.Event) {
|
|||||||
u.ж.MarshalZerologObject(e)
|
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
|
// FlexibleBoolean handles JumpCloud's JSON where email_verified is returned as a
|
||||||
// string "true" or "false" instead of a boolean.
|
// string "true" or "false" instead of a boolean.
|
||||||
// This maps bool to a specific type with a custom unmarshaler to
|
// This maps bool to a specific type with a custom unmarshaler to
|
||||||
@ -269,7 +295,7 @@ type OIDCClaims struct {
|
|||||||
|
|
||||||
// Name is the user's full name.
|
// Name is the user's full name.
|
||||||
Name string `json:"name,omitempty"`
|
Name string `json:"name,omitempty"`
|
||||||
Groups []string `json:"groups,omitempty"`
|
Groups FlexibleStringSlice `json:"groups,omitempty"`
|
||||||
Email string `json:"email,omitempty"`
|
Email string `json:"email,omitempty"`
|
||||||
EmailVerified FlexibleBoolean `json:"email_verified,omitempty"`
|
EmailVerified FlexibleBoolean `json:"email_verified,omitempty"`
|
||||||
ProfilePictureURL string `json:"picture,omitempty"`
|
ProfilePictureURL string `json:"picture,omitempty"`
|
||||||
@ -388,7 +414,7 @@ type OIDCUserInfo struct {
|
|||||||
PreferredUsername string `json:"preferred_username"`
|
PreferredUsername string `json:"preferred_username"`
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
EmailVerified FlexibleBoolean `json:"email_verified,omitempty"`
|
EmailVerified FlexibleBoolean `json:"email_verified,omitempty"`
|
||||||
Groups []string `json:"groups"`
|
Groups FlexibleStringSlice `json:"groups"`
|
||||||
Picture string `json:"picture"`
|
Picture string `json:"picture"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -61,6 +61,57 @@ func TestUnmarshallOIDCClaims(t *testing.T) {
|
|||||||
EmailVerified: false,
|
EmailVerified: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "groups-array",
|
||||||
|
jsonstr: `
|
||||||
|
{
|
||||||
|
"sub": "test4",
|
||||||
|
"email": "test4@test.no",
|
||||||
|
"email_verified": true,
|
||||||
|
"groups": ["Group1", "Group2"]
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
want: OIDCClaims{
|
||||||
|
Sub: "test4",
|
||||||
|
Email: "test4@test.no",
|
||||||
|
EmailVerified: true,
|
||||||
|
Groups: FlexibleStringSlice{"Group1", "Group2"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "groups-single-string",
|
||||||
|
jsonstr: `
|
||||||
|
{
|
||||||
|
"sub": "test5",
|
||||||
|
"email": "test5@test.no",
|
||||||
|
"email_verified": true,
|
||||||
|
"groups": "SingleGroup"
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
want: OIDCClaims{
|
||||||
|
Sub: "test5",
|
||||||
|
Email: "test5@test.no",
|
||||||
|
EmailVerified: true,
|
||||||
|
Groups: FlexibleStringSlice{"SingleGroup"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "groups-empty-array",
|
||||||
|
jsonstr: `
|
||||||
|
{
|
||||||
|
"sub": "test6",
|
||||||
|
"email": "test6@test.no",
|
||||||
|
"email_verified": true,
|
||||||
|
"groups": []
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
want: OIDCClaims{
|
||||||
|
Sub: "test6",
|
||||||
|
Email: "test6@test.no",
|
||||||
|
EmailVerified: true,
|
||||||
|
Groups: FlexibleStringSlice{},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user