Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(thirdparty): add discord provider #1353

Merged
merged 7 commits into from
Feb 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading