diff --git a/providers/discord/discord.go b/providers/discord/discord.go index 118f5e47..3c088213 100644 --- a/providers/discord/discord.go +++ b/providers/discord/discord.go @@ -205,15 +205,17 @@ func newConfig(p *Provider, scopes []string) *oauth2.Config { AuthURL: authURL, TokenURL: tokenURL, }, - Scopes: []string{}, + // `identify` gates /users/@me itself; supplementary scopes like + // `email` only enrich its response. Always include it so callers + // who request just `email` (or any other scope) still get a usable + // user-fetch flow instead of a 401 from FetchUser (#414). + Scopes: []string{ScopeIdentify}, } - if len(scopes) > 0 { - for _, scope := range scopes { + for _, scope := range scopes { + if scope != ScopeIdentify { c.Scopes = append(c.Scopes, scope) } - } else { - c.Scopes = []string{ScopeIdentify} } return c diff --git a/providers/discord/discord_test.go b/providers/discord/discord_test.go index 8bc077f0..0c532706 100644 --- a/providers/discord/discord_test.go +++ b/providers/discord/discord_test.go @@ -40,6 +40,50 @@ func Test_BeginAuth(t *testing.T) { a.Contains(s.AuthURL, "discord.com/api/oauth2/authorize") } +func Test_DefaultScopeAlwaysIncludesIdentify(t *testing.T) { + // Regression test for #414: passing any custom scope (e.g. only "email") + // used to drop ScopeIdentify from the OAuth2 config, which caused Discord + // to return 401 on /users/@me — so FetchUser failed for everyone who + // hadn't explicitly listed "identify". ScopeIdentify must always be in + // the scope list, and never duplicated when the caller passes it. + t.Parallel() + a := assert.New(t) + + cases := []struct { + name string + scopes []string + want []string + }{ + { + name: "no scopes", + scopes: nil, + want: []string{ScopeIdentify}, + }, + { + name: "email only", + scopes: []string{ScopeEmail}, + want: []string{ScopeIdentify, ScopeEmail}, + }, + { + name: "identify explicit (no dupe)", + scopes: []string{ScopeIdentify, ScopeEmail}, + want: []string{ScopeIdentify, ScopeEmail}, + }, + { + name: "multiple custom", + scopes: []string{ScopeEmail, ScopeConnections, ScopeGuilds}, + want: []string{ScopeIdentify, ScopeEmail, ScopeConnections, ScopeGuilds}, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + p := New("k", "s", "/cb", tc.scopes...) + a.Equal(tc.want, p.config.Scopes, tc.name) + }) + } +} + func Test_SessionFromJSON(t *testing.T) { t.Parallel() a := assert.New(t)