Skip to content

Commit

Permalink
feat(thirdparty): add discord provider
Browse files Browse the repository at this point in the history
Co-authored-by: Scott Plunkett <[email protected]>
  • Loading branch information
lfleischmann and plunkettscott authored Feb 19, 2024
1 parent cfd11cd commit 7d57bf6
Show file tree
Hide file tree
Showing 14 changed files with 361 additions and 21 deletions.
17 changes: 11 additions & 6 deletions backend/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ package config
import (
"errors"
"fmt"
"log"
"strings"
"time"

"github.com/fatih/structs"
"github.com/gobwas/glob"
"github.com/kelseyhightower/envconfig"
Expand All @@ -12,9 +16,6 @@ import (
zeroLogger "github.com/rs/zerolog/log"
"github.com/teamhanko/hanko/backend/ee/saml/config"
"golang.org/x/exp/slices"
"log"
"strings"
"time"
)

// Config is the central configuration type
Expand Down Expand Up @@ -182,6 +183,9 @@ func DefaultConfig() *Config {
Apple: ThirdPartyProvider{
AllowLinking: true,
},
Discord: ThirdPartyProvider{
AllowLinking: true,
},
},
},
}
Expand Down Expand Up @@ -622,9 +626,10 @@ func (p *ThirdPartyProvider) Validate() error {
}

type ThirdPartyProviders struct {
Google ThirdPartyProvider `yaml:"google" json:"google,omitempty" koanf:"google"`
GitHub ThirdPartyProvider `yaml:"github" json:"github,omitempty" koanf:"github"`
Apple ThirdPartyProvider `yaml:"apple" json:"apple,omitempty" koanf:"apple"`
Google ThirdPartyProvider `yaml:"google" json:"google,omitempty" koanf:"google"`
GitHub ThirdPartyProvider `yaml:"github" json:"github,omitempty" koanf:"github"`
Apple ThirdPartyProvider `yaml:"apple" json:"apple,omitempty" koanf:"apple"`
Discord ThirdPartyProvider `yaml:"discord" json:"discord,omitempty" koanf:"discord"`
}

func (p *ThirdPartyProviders) Validate() error {
Expand Down
38 changes: 38 additions & 0 deletions backend/docs/Config.md
Original file line number Diff line number Diff line change
Expand Up @@ -592,6 +592,44 @@ third_party:
# Default: true
#
allow_linking: true
##
#
# The Discord provider configuration
#
discord:
##
#
# Enable or disable the Discord provider.
#
# Default: false
#
enabled: false
##
#
# The client ID of your Discord OAuth credentials.
# See: https://docs.hanko.io/guides/authentication-methods/oauth/discord
#
# Required if provider is enabled.
#
client_id: "CHANGE_ME"
##
#
# The secret of your Discord OAuth credentials.
# See: https://docs.hanko.io/guides/authentication-methods/oauth/discord
#
# Required if provider is enabled.
#
secret: "CHANGE_ME"
##
#
# Indicates whether accounts can be linked with this provider.
# This option only controls linking for existing accounts. Account registrations
# are not affected (see the 'accounts.allow_signup' option for controlling
# account registration).
#
# Default: true
#
allow_linking: true
log:
## log_health_and_metrics
#
Expand Down
12 changes: 11 additions & 1 deletion backend/handler/thirdparty_auth_test.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
package handler

import (
"github.com/teamhanko/hanko/backend/thirdparty"
"net/http"
"net/http/httptest"
"net/url"
"strings"

"github.com/teamhanko/hanko/backend/thirdparty"
)

func (s *thirdPartySuite) TestThirdPartyHandler_Auth() {
Expand Down Expand Up @@ -47,6 +48,15 @@ func (s *thirdPartySuite) TestThirdPartyHandler_Auth() {
requestedRedirectTo: "https://app.test.example",
expectedBaseURL: thirdparty.AppleAuthEndpoint,
},
{
name: "successful redirect to discord",
referer: "https://login.test.example",
enabledProviders: []string{"discord"},
allowedRedirectURLs: []string{"https://*.test.example"},
requestedProvider: "discord",
requestedRedirectTo: "https://app.test.example",
expectedBaseURL: thirdparty.DiscordOauthAuthEndpoint,
},
{
name: "error redirect on missing provider",
referer: "https://login.test.example",
Expand Down
126 changes: 123 additions & 3 deletions backend/handler/thirdparty_callback_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ package handler

import (
"fmt"
"github.com/h2non/gock"
"github.com/teamhanko/hanko/backend/thirdparty"
"github.com/teamhanko/hanko/backend/utils"
"net/http"
"net/http/httptest"
"testing"

"github.com/h2non/gock"
"github.com/teamhanko/hanko/backend/thirdparty"
"github.com/teamhanko/hanko/backend/utils"
)

func (s *thirdPartySuite) TestThirdPartyHandler_Callback_SignUp_Google() {
Expand Down Expand Up @@ -383,6 +384,125 @@ func (s *thirdPartySuite) TestThirdPartyHandler_Callback_SignIn_Apple() {
}
}

func (s *thirdPartySuite) TestThirdPartyHandler_Callback_SignUp_Discord() {
defer gock.Off()
if testing.Short() {
s.T().Skip("skipping test in short mode.")
}

gock.New(thirdparty.DiscordOauthTokenEndpoint).
Post("/").
Reply(200).
JSON(map[string]string{"access_token": "fakeAccessToken"})

gock.New(thirdparty.DiscordUserInfoEndpoint).
Get("/").
Reply(200).
JSON(&thirdparty.DiscordUser{
ID: "discord_abcde",
Email: "[email protected]",
Verified: true,
})

cfg := s.setUpConfig([]string{"discord"}, []string{"https://example.com"})

state, err := thirdparty.GenerateState(cfg, "discord", "https://example.com")
s.NoError(err)

req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/thirdparty/callback?code=abcde&state=%s", state), nil)
req.AddCookie(&http.Cookie{
Name: utils.HankoThirdpartyStateCookie,
Value: string(state),
})

c, rec := s.setUpContext(req)
handler := s.setUpHandler(cfg)

if s.NoError(handler.Callback(c)) {
s.Equal(http.StatusTemporaryRedirect, rec.Code)

s.assertLocationHeaderHasToken(rec)
s.assertStateCookieRemoved(rec)

email, err := s.Storage.GetEmailPersister().FindByAddress("[email protected]")
s.NoError(err)
s.NotNil(email)
s.True(email.IsPrimary())

user, err := s.Storage.GetUserPersister().Get(*email.UserID)
s.NoError(err)
s.NotNil(user)

identity := email.Identities.GetIdentity("discord", "discord_abcde")
s.NotNil(identity)

logs, lerr := s.Storage.GetAuditLogPersister().List(0, 0, nil, nil, []string{"thirdparty_signup_succeeded"}, user.ID.String(), email.Address, "", "")
s.NoError(lerr)
s.Len(logs, 1)
}
}

func (s *thirdPartySuite) TestThirdPartyHandler_Callback_SignIn_Discord() {
defer gock.Off()
if testing.Short() {
s.T().Skip("skipping test in short mode.")
}

err := s.LoadFixtures("../test/fixtures/thirdparty")
s.NoError(err)

gock.New(thirdparty.DiscordOauthTokenEndpoint).
Post("/").
Reply(200).
JSON(map[string]string{"access_token": "fakeAccessToken"})

gock.New(thirdparty.DiscordUserInfoEndpoint).
Get("/").
Reply(200).
JSON(&thirdparty.DiscordUser{
ID: "discord_abcde",
Email: "[email protected]",
Verified: true,
})

cfg := s.setUpConfig([]string{"discord"}, []string{"https://example.com"})

state, err := thirdparty.GenerateState(cfg, "discord", "https://example.com")
s.NoError(err)

req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/thirdparty/callback?code=abcde&state=%s", state), nil)
req.AddCookie(&http.Cookie{
Name: utils.HankoThirdpartyStateCookie,
Value: string(state),
})

c, rec := s.setUpContext(req)
handler := s.setUpHandler(cfg)

if s.NoError(handler.Callback(c)) {
s.Equal(http.StatusTemporaryRedirect, rec.Code)

s.assertLocationHeaderHasToken(rec)
s.assertStateCookieRemoved(rec)

email, err := s.Storage.GetEmailPersister().FindByAddress("[email protected]")
s.NoError(err)
s.NotNil(email)
s.True(email.IsPrimary())

user, err := s.Storage.GetUserPersister().Get(*email.UserID)
s.NoError(err)
s.NotNil(user)

identity := email.Identities.GetIdentity("discord", "discord_abcde")
s.NotNil(identity)

logs, lerr := s.Storage.GetAuditLogPersister().List(0, 0, nil, nil, []string{"thirdparty_signin_succeeded"}, user.ID.String(), "", "", "")
s.NoError(lerr)
s.Len(logs, 1)
}
}

func (s *thirdPartySuite) TestThirdPartyHandler_Callback_SignUp_WithUnclaimedEmail() {
defer gock.Off()
if testing.Short() {
Expand Down
24 changes: 17 additions & 7 deletions backend/handler/thirdparty_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
package handler

import (
"net/http"
"net/http/httptest"
"net/url"
"strconv"
"testing"
"time"

"github.com/labstack/echo/v4"
"github.com/lestrrat-go/jwx/v2/jwa"
jwk2 "github.com/lestrrat-go/jwx/v2/jwk"
Expand All @@ -13,12 +20,6 @@ import (
"github.com/teamhanko/hanko/backend/session"
"github.com/teamhanko/hanko/backend/test"
"github.com/teamhanko/hanko/backend/utils"
"net/http"
"net/http/httptest"
"net/url"
"strconv"
"testing"
"time"
)

func TestThirdPartySuite(t *testing.T) {
Expand Down Expand Up @@ -72,7 +73,14 @@ func (s *thirdPartySuite) setUpConfig(enabledProviders []string, allowedRedirect
ClientID: "fakeClientID",
Secret: "fakeClientSecret",
AllowLinking: true,
}},
},
Discord: config.ThirdPartyProvider{
Enabled: false,
ClientID: "fakeClientID",
Secret: "fakeClientSecret",
AllowLinking: true,
},
},
ErrorRedirectURL: "https://error.test.example",
RedirectURL: "https://api.test.example/callback",
AllowedRedirectURLS: allowedRedirectURLs,
Expand All @@ -99,6 +107,8 @@ func (s *thirdPartySuite) setUpConfig(enabledProviders []string, allowedRedirect
cfg.ThirdParty.Providers.Google.Enabled = true
case "github":
cfg.ThirdParty.Providers.GitHub.Enabled = true
case "discord":
cfg.ThirdParty.Providers.Discord.Enabled = true
}
}

Expand Down
3 changes: 3 additions & 0 deletions backend/json_schema/hanko.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -709,6 +709,9 @@
},
"apple": {
"$ref": "#/$defs/ThirdPartyProvider"
},
"discord": {
"$ref": "#/$defs/ThirdPartyProvider"
}
},
"additionalProperties": false,
Expand Down
6 changes: 6 additions & 0 deletions backend/test/fixtures/thirdparty/emails.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@
verified: true
created_at: 2020-12-31 23:59:59
updated_at: 2020-12-31 23:59:59
- id: 09f35529-cca6-44a7-ab1d-b07e95a04e3b
user_id: d69bffda-4e4a-4424-a238-fbecc1651d81
address: [email protected]
verified: true
created_at: 2020-12-31 23:59:59
updated_at: 2020-12-31 23:59:59
- id: 527afce8-3b7b-41b6-b1ed-33d408c5a7bb
user_id: 43fb7e88-4d5d-4b2b-9335-391e78d7e472
address: [email protected]
Expand Down
7 changes: 7 additions & 0 deletions backend/test/fixtures/thirdparty/identities.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,10 @@
email_id: 05ab6e1f-8dfb-4329-ae04-22571a68d96b
created_at: 2020-12-31 23:59:59
updated_at: 2020-12-31 23:59:59
- id: 18d61f13-2789-467a-a3a6-4292c0621580
provider_id: "discord_abcde"
provider_name: "discord"
data: '{"email":"[email protected]","email_verified":true,"iss":"https://discord.com/api","sub":"discord_abcde"}'
email_id: 09f35529-cca6-44a7-ab1d-b07e95a04e3b
created_at: 2020-12-31 23:59:59
updated_at: 2020-12-31 23:59:59
5 changes: 5 additions & 0 deletions backend/test/fixtures/thirdparty/primary_emails.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@
user_id: b3537e49-de92-4e16-8981-ae4beb44c447
created_at: 2020-12-31 23:59:59
updated_at: 2020-12-31 23:59:59
- id: f2eb0190-77f8-42a4-b45f-6f932a98995c
email_id: 09f35529-cca6-44a7-ab1d-b07e95a04e3b
user_id: d69bffda-4e4a-4424-a238-fbecc1651d81
created_at: 2020-12-31 23:59:59
updated_at: 2020-12-31 23:59:59
- id: c57715eb-0c63-4910-b429-9b6165c50fab
email_id: 527afce8-3b7b-41b6-b1ed-33d408c5a7bb
user_id: 43fb7e88-4d5d-4b2b-9335-391e78d7e472
Expand Down
4 changes: 4 additions & 0 deletions backend/test/fixtures/thirdparty/users.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@
- id: b3537e49-de92-4e16-8981-ae4beb44c447
created_at: 2020-12-31 23:59:59
updated_at: 2020-12-31 23:59:59
# user with email and discord identity
- id: d69bffda-4e4a-4424-a238-fbecc1651d81
created_at: 2020-12-31 23:59:59
updated_at: 2020-12-31 23:59:59
# user with email, no identity
- id: 43fb7e88-4d5d-4b2b-9335-391e78d7e472
created_at: 2020-12-31 23:59:59
Expand Down
9 changes: 6 additions & 3 deletions backend/thirdparty/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@ import (
"encoding/json"
"errors"
"fmt"
"github.com/fatih/structs"
"github.com/teamhanko/hanko/backend/config"
"golang.org/x/oauth2"
"io"
"net/http"
"strings"
"time"

"github.com/fatih/structs"
"github.com/teamhanko/hanko/backend/config"
"golang.org/x/oauth2"
)

type UserData struct {
Expand Down Expand Up @@ -85,6 +86,8 @@ func GetProvider(config config.ThirdParty, name string) (OAuthProvider, error) {
return NewGithubProvider(config.Providers.GitHub, config.RedirectURL)
case "apple":
return NewAppleProvider(config.Providers.Apple, config.RedirectURL)
case "discord":
return NewDiscordProvider(config.Providers.Discord, config.RedirectURL)
default:
return nil, fmt.Errorf("provider '%s' is not supported", name)
}
Expand Down
Loading

0 comments on commit 7d57bf6

Please sign in to comment.