package provider import ( "context" "fmt" "strconv" "strings" "github.com/supabase/auth/internal/conf" "golang.org/x/oauth2" ) const ( defaultDiscordAPIBase = "discord.com" ) type discordProvider struct { *oauth2.Config APIPath string } type discordUser struct { Avatar string `json:"avatar"` Discriminator string `json:"discriminator"` Email string `json:"email"` ID string `json:"id"` Name string `json:"username"` GlobalName string `json:"global_name"` Verified bool `json:"verified"` } // NewDiscordProvider creates a Discord account provider. func NewDiscordProvider(ext conf.OAuthProviderConfiguration, scopes string) (OAuthProvider, error) { if err := ext.ValidateOAuth(); err != nil { return nil, err } apiPath := chooseHost(ext.URL, defaultDiscordAPIBase) + "/api" oauthScopes := []string{ "email", "identify", } if scopes != "" { oauthScopes = append(oauthScopes, strings.Split(scopes, ",")...) } return &discordProvider{ Config: &oauth2.Config{ ClientID: ext.ClientID[0], ClientSecret: ext.Secret, Endpoint: oauth2.Endpoint{ AuthURL: apiPath + "/oauth2/authorize", TokenURL: apiPath + "/oauth2/token", }, Scopes: oauthScopes, RedirectURL: ext.RedirectURI, }, APIPath: apiPath, }, nil } func (g discordProvider) GetOAuthToken(code string) (*oauth2.Token, error) { return g.Exchange(context.Background(), code) } func (g discordProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (*UserProvidedData, error) { var u discordUser if err := makeRequest(ctx, tok, g.Config, g.APIPath+"/users/@me", &u); err != nil { return nil, err } data := &UserProvidedData{} if u.Email != "" { data.Emails = []Email{{ Email: u.Email, Verified: u.Verified, Primary: true, }} } var avatarURL string extension := "png" if u.Avatar == "" { if intDiscriminator, err := strconv.Atoi(u.Discriminator); err != nil { return nil, err } else { // https://discord.com/developers/docs/reference#image-formatting-cdn-endpoints: // In the case of the Default User Avatar endpoint, the value for // user_discriminator in the path should be the user's discriminator modulo 5 avatarURL = fmt.Sprintf("https://cdn.discordapp.com/embed/avatars/%d.%s", intDiscriminator%5, extension) } } else { // https://discord.com/developers/docs/reference#image-formatting: // "In the case of endpoints that support GIFs, the hash will begin with a_ // if it is available in GIF format." if strings.HasPrefix(u.Avatar, "a_") { extension = "gif" } avatarURL = fmt.Sprintf("https://cdn.discordapp.com/avatars/%s/%s.%s", u.ID, u.Avatar, extension) } data.Metadata = &Claims{ Issuer: g.APIPath, Subject: u.ID, Name: fmt.Sprintf("%v#%v", u.Name, u.Discriminator), Picture: avatarURL, CustomClaims: map[string]interface{}{ "global_name": u.GlobalName, }, // To be deprecated AvatarURL: avatarURL, FullName: u.Name, ProviderId: u.ID, } return data, nil }