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: extra auth providers & tests for previously added providers #269

Closed
wants to merge 13 commits into from
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ The default group to assign all new users to.

### External Authentication Providers

We support `apple`, `azure`, `bitbucket`, `discord`, `facebook`, `github`, `gitlab`, `google`, `spotify`, `slack`, `twitch` and `twitter` for external authentication.
We support `apple`, `azure`, `bitbucket`, `discord`, `facebook`, `github`, `gitlab`, `google`, `spotify`, `slack`, `tiktok`, `twitch` and `twitter` for external authentication.

Use the names as the keys underneath `external` to configure each separately.

Expand Down Expand Up @@ -885,7 +885,7 @@ Get access_token from external oauth provider
query params:

```
provider=apple | azure | bitbucket | discord | facebook | github | gitlab | google | slack | spotify | twitch | twitter
provider=apple | azure | bitbucket | discord | facebook | github | gitlab | google | slack | spotify | tiktok | twitch | twitter
scopes=<optional additional scopes depending on the provider (email and name are requested by default)>
```

Expand Down
2 changes: 2 additions & 0 deletions api/external.go
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,8 @@ func (a *API) Provider(ctx context.Context, name string, scopes string) (provide
return provider.NewSpotifyProvider(config.External.Spotify, scopes)
case "slack":
return provider.NewSlackProvider(config.External.Slack, scopes)
case "tiktok":
return provider.NewTikTokProvider(config.External.TikTok, scopes)
case "twitch":
return provider.NewTwitchProvider(config.External.Twitch, scopes)
case "twitter":
Expand Down
33 changes: 33 additions & 0 deletions api/external_slack_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package api

import (
"net/http"
"net/http/httptest"
"net/url"

jwt "github.com/golang-jwt/jwt"
)

func (ts *ExternalTestSuite) TestSignupExternalSlack() {
req := httptest.NewRequest(http.MethodGet, "http://localhost/authorize?provider=slack", nil)
w := httptest.NewRecorder()
ts.API.handler.ServeHTTP(w, req)
ts.Require().Equal(http.StatusFound, w.Code)
u, err := url.Parse(w.Header().Get("Location"))
ts.Require().NoError(err, "redirect url parse failed")
q := u.Query()
ts.Equal(ts.Config.External.Slack.RedirectURI, q.Get("redirect_uri"))
ts.Equal(ts.Config.External.Slack.ClientID, q.Get("client_id"))
ts.Equal("code", q.Get("response_type"))
ts.Equal("profile email openid", q.Get("scope"))

claims := ExternalProviderClaims{}
p := jwt.Parser{ValidMethods: []string{jwt.SigningMethodHS256.Name}}
_, err = p.ParseWithClaims(q.Get("state"), &claims, func(token *jwt.Token) (interface{}, error) {
return []byte(ts.Config.JWT.Secret), nil
})
ts.Require().NoError(err)

ts.Equal("slack", claims.Provider)
ts.Equal(ts.Config.SiteURL, claims.SiteURL)
}
33 changes: 33 additions & 0 deletions api/external_spotify_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package api

import (
"net/http"
"net/http/httptest"
"net/url"

jwt "github.com/golang-jwt/jwt"
)

func (ts *ExternalTestSuite) TestSignupExternalSpotify() {
req := httptest.NewRequest(http.MethodGet, "http://localhost/authorize?provider=spotify", nil)
w := httptest.NewRecorder()
ts.API.handler.ServeHTTP(w, req)
ts.Require().Equal(http.StatusFound, w.Code)
u, err := url.Parse(w.Header().Get("Location"))
ts.Require().NoError(err, "redirect url parse failed")
q := u.Query()
ts.Equal(ts.Config.External.Spotify.RedirectURI, q.Get("redirect_uri"))
ts.Equal(ts.Config.External.Spotify.ClientID, q.Get("client_id"))
ts.Equal("code", q.Get("response_type"))
ts.Equal("user-read-email", q.Get("scope"))

claims := ExternalProviderClaims{}
p := jwt.Parser{ValidMethods: []string{jwt.SigningMethodHS256.Name}}
_, err = p.ParseWithClaims(q.Get("state"), &claims, func(token *jwt.Token) (interface{}, error) {
return []byte(ts.Config.JWT.Secret), nil
})
ts.Require().NoError(err)

ts.Equal("spotify", claims.Provider)
ts.Equal(ts.Config.SiteURL, claims.SiteURL)
}
16 changes: 7 additions & 9 deletions api/provider/slack.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,22 +74,20 @@ func (g slackProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (*Use

return &UserProvidedData{
Metadata: &Claims{
Issuer: g.APIPath,
Subject: u.ID,
Name: u.Name,
Picture: u.AvatarURL,
Email: u.Email,
EmailVerified: true, // Slack dosen't provide data on if email is verified.
Issuer: g.APIPath,
Subject: u.ID,
Name: u.Name,
Picture: u.AvatarURL,
Email: u.Email,

// To be deprecated
AvatarURL: u.AvatarURL,
FullName: u.Name,
ProviderId: u.ID,
},
Emails: []Email{{
Email: u.Email,
Verified: true, // Slack dosen't provide data on if email is verified.
Primary: true,
Email: u.Email,
Primary: true,
}},
}, nil
}
17 changes: 7 additions & 10 deletions api/provider/spotify.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,22 +86,19 @@ func (g spotifyProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (*U

return &UserProvidedData{
Metadata: &Claims{
Issuer: g.APIPath,
Subject: u.ID,
Name: u.DisplayName,
Picture: avatarURL,
Email: u.Email,
EmailVerified: true, // Spotify dosen't provide data on if email is verified.

Issuer: g.APIPath,
Subject: u.ID,
Name: u.DisplayName,
Picture: avatarURL,
Email: u.Email,
// To be deprecated
AvatarURL: avatarURL,
FullName: u.DisplayName,
ProviderId: u.ID,
},
Emails: []Email{{
Email: u.Email,
Verified: true, // Spotify dosen't provide data on if email is verified.
Primary: true,
Email: u.Email,
Primary: true,
}},
}, nil
}
84 changes: 84 additions & 0 deletions api/provider/tiktok.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package provider

import (
"context"
"strings"

"github.com/netlify/gotrue/conf"
"golang.org/x/oauth2"
)

const (
defaultTikTokAPIBase = "https://open-api.tiktok.com/"
)

type tiktokProvider struct {
*oauth2.Config
APIPath string
}

type tiktokUser struct {
ID string `json:"open_id"`
UnionID string `json:"union_id"`
DisplayName string `json:"display_name"`
AvatarUrl string `json:"avatar_url"`
AvatarUrl100 string `json:"avatar_url_100"`
AvatarUrl200 string `json:"avatar_url_200"`
AvatarUrlLarge string `json:"avatar_large_url"`
}

// NewTikTokProvider creates a TikTok account provider.
func NewTikTokProvider(ext conf.OAuthProviderConfiguration, scopes string) (OAuthProvider, error) {
if err := ext.Validate(); err != nil {
return nil, err
}

apiPath := chooseHost(ext.URL, defaultTikTokAPIBase)

oauthScopes := []string{
"user.info.basic",
}

if scopes != "" {
oauthScopes = append(oauthScopes, strings.Split(scopes, ",")...)
}

return &tiktokProvider{
Config: &oauth2.Config{
ClientID: ext.ClientID,
ClientSecret: ext.Secret,
Endpoint: oauth2.Endpoint{
AuthURL: apiPath + "/platform/oauth/connect",
TokenURL: apiPath + "/oauth/access_token/",
},
Scopes: oauthScopes,
RedirectURL: ext.RedirectURI,
},
APIPath: apiPath,
}, nil
}

func (g tiktokProvider) GetOAuthToken(code string) (*oauth2.Token, error) {
return g.Exchange(oauth2.NoContext, code)
}

func (g tiktokProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (*UserProvidedData, error) {
var u tiktokUser
if err := makeRequest(ctx, tok, g.Config, g.APIPath+"/user/info", &u); err != nil {
HarryET marked this conversation as resolved.
Show resolved Hide resolved
return nil, err
}

return &UserProvidedData{
Metadata: &Claims{
Issuer: g.APIPath,
Subject: u.ID,
Name: u.DisplayName,
Picture: u.AvatarUrl,

// To be deprecated
AvatarURL: u.AvatarUrl,
FullName: u.DisplayName,
ProviderId: u.ID,
},
}, nil
}
2 changes: 2 additions & 0 deletions api/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ type ProviderSettings struct {
Facebook bool `json:"facebook"`
Spotify bool `json:"spotify"`
Slack bool `json:"slack"`
TikTok bool `json:"tiktok"`
Twitch bool `json:"twitch"`
Twitter bool `json:"twitter"`
Email bool `json:"email"`
Expand Down Expand Up @@ -48,6 +49,7 @@ func (a *API) Settings(w http.ResponseWriter, r *http.Request) error {
Facebook: config.External.Facebook.Enabled,
Spotify: config.External.Spotify.Enabled,
Slack: config.External.Slack.Enabled,
TikTok: config.External.TikTok.Enabled,
Twitch: config.External.Twitch.Enabled,
Twitter: config.External.Twitter.Enabled,
Email: config.External.Email.Enabled,
Expand Down
1 change: 1 addition & 0 deletions api/settings_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ func TestSettings_DefaultProviders(t *testing.T) {
require.True(t, p.Facebook)
require.True(t, p.Spotify)
require.True(t, p.Slack)
require.True(t, p.TikTok)
require.True(t, p.Google)
require.True(t, p.GitHub)
require.True(t, p.GitLab)
Expand Down
1 change: 1 addition & 0 deletions conf/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ type ProviderConfiguration struct {
Google OAuthProviderConfiguration `json:"google"`
Spotify OAuthProviderConfiguration `json:"spotify"`
Slack OAuthProviderConfiguration `json:"slack"`
TikTok OAuthProviderConfiguration `json:"tiktok"`
Twitter OAuthProviderConfiguration `json:"twitter"`
Twitch OAuthProviderConfiguration `json:"twitch"`
Email EmailProviderConfiguration `json:"email"`
Expand Down
3 changes: 3 additions & 0 deletions example.env
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ GOTRUE_EXTERNAL_SPOTIFY_SECRET=""
GOTRUE_EXTERNAL_SLACK_ENABLED="true"
GOTRUE_EXTERNAL_SLACK_CLIENT_ID=""
GOTRUE_EXTERNAL_SLACK_SECRET=""
GOTRUE_EXTERNAL_TIKTOK_ENABLED="true"
GOTRUE_EXTERNAL_TIKTOK_CLIENT_ID=""
GOTRUE_EXTERNAL_TIKTOK_SECRET=""
GOTRUE_EXTERNAL_TWITTER_ENABLED="false"
GOTRUE_EXTERNAL_TWITTER_CLIENT_ID=""
GOTRUE_EXTERNAL_TWITTER_SECRET=""
Expand Down
4 changes: 4 additions & 0 deletions hack/test.env
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ GOTRUE_EXTERNAL_SLACK_ENABLED=true
GOTRUE_EXTERNAL_SLACK_CLIENT_ID=testclientid
GOTRUE_EXTERNAL_SLACK_SECRET=testsecret
GOTRUE_EXTERNAL_SLACK_REDIRECT_URI=https://identity.services.netlify.com/callback
GOTRUE_EXTERNAL_TIKTOK_ENABLED=true
GOTRUE_EXTERNAL_TIKTOK_CLIENT_ID=testclientid
GOTRUE_EXTERNAL_TIKTOK_SECRET=testsecret
GOTRUE_EXTERNAL_TIKTOK_REDIRECT_URI=https://identity.services.netlify.com/callback
GOTRUE_EXTERNAL_TWITCH_ENABLED=true
GOTRUE_EXTERNAL_TWITCH_CLIENT_ID=testclientid
GOTRUE_EXTERNAL_TWITCH_SECRET=testsecret
Expand Down