From 2a9cec75efa9f36682e65b675cc8826d3460f455 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Thu, 26 Mar 2026 16:58:37 -0600 Subject: [PATCH] fix(jwt): accept JSON array for SpaceDelimited scope claim Some issuers (e.g. PaperOS) emit `scope` as a JSON array (`[]` or `["openid","profile"]`) instead of the RFC 6749 space-delimited string. SpaceDelimited.UnmarshalJSON now accepts both forms; a JSON array is converted to the equivalent slice. Other non-string, non-array values still return an error. Adds test cases: array_values and array_empty. --- auth/jwt/coverage_test.go | 17 +++++++++++++++++ auth/jwt/types.go | 26 ++++++++++++++++++++------ 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/auth/jwt/coverage_test.go b/auth/jwt/coverage_test.go index 059491e..afff514 100644 --- a/auth/jwt/coverage_test.go +++ b/auth/jwt/coverage_test.go @@ -220,6 +220,23 @@ func TestCov_SpaceDelimited_UnmarshalJSON(t *testing.T) { t.Fatalf("expected empty, got %v", s) } }) + t.Run("array_values", func(t *testing.T) { + var s SpaceDelimited + json.Unmarshal([]byte(`["openid","profile"]`), &s) + if len(s) != 2 || s[0] != "openid" || s[1] != "profile" { + t.Fatalf("got %v", s) + } + }) + t.Run("array_empty", func(t *testing.T) { + var s SpaceDelimited + json.Unmarshal([]byte(`[]`), &s) + if s == nil { + t.Fatal("expected non-nil empty SpaceDelimited, got nil") + } + if len(s) != 0 { + t.Fatalf("expected empty, got %v", s) + } + }) t.Run("invalid", func(t *testing.T) { var s SpaceDelimited if err := json.Unmarshal([]byte(`123`), &s); err == nil { diff --git a/auth/jwt/types.go b/auth/jwt/types.go index 5c61542..536a7f9 100644 --- a/auth/jwt/types.go +++ b/auth/jwt/types.go @@ -89,16 +89,30 @@ type SpaceDelimited []string // UnmarshalJSON decodes a space-separated string into a slice. // An empty string "" unmarshals to a non-nil empty SpaceDelimited{}, // preserving the distinction from a nil (absent) SpaceDelimited. +// +// As a compatibility extension, it also accepts a JSON array of strings, +// because some issuers (e.g. PaperOS) emit scope as [] instead of "". func (s *SpaceDelimited) UnmarshalJSON(data []byte) error { var str string - if err := json.Unmarshal(data, &str); err != nil { - return fmt.Errorf("space-delimited must be a string: %w: %w", ErrInvalidPayload, err) - } - if str == "" { - *s = SpaceDelimited{} + if err := json.Unmarshal(data, &str); err == nil { + if str == "" { + *s = SpaceDelimited{} + return nil + } + *s = strings.Fields(str) return nil } - *s = strings.Fields(str) + + // Fallback: accept a JSON array of strings (non-standard but used in the wild). + var ss []string + if err := json.Unmarshal(data, &ss); err != nil { + return fmt.Errorf("space-delimited must be a string or array of strings: %w: %w", ErrInvalidPayload, err) + } + if ss == nil { + *s = SpaceDelimited{} + } else { + *s = SpaceDelimited(ss) + } return nil }