From 7c95e4a6565fc5d5eb5f18b1ed3509802a1e291e Mon Sep 17 00:00:00 2001 From: Harry Bairstow Date: Wed, 10 Nov 2021 21:29:09 +0000 Subject: [PATCH 01/13] Initial Slack Tests --- api/external_slack_test.go | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 api/external_slack_test.go diff --git a/api/external_slack_test.go b/api/external_slack_test.go new file mode 100644 index 0000000000..bfe5ab9dfd --- /dev/null +++ b/api/external_slack_test.go @@ -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) +} From f475ca1c75b1aa9c1802859494d39ac3d458cba5 Mon Sep 17 00:00:00 2001 From: Harry Bairstow Date: Wed, 10 Nov 2021 21:30:15 +0000 Subject: [PATCH 02/13] Initial Spotify Tests --- api/external_spotify_test.go | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 api/external_spotify_test.go diff --git a/api/external_spotify_test.go b/api/external_spotify_test.go new file mode 100644 index 0000000000..48bdc78579 --- /dev/null +++ b/api/external_spotify_test.go @@ -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) +} From f5bea92a891d04e27e8de9e1769599c19df467e1 Mon Sep 17 00:00:00 2001 From: Harry Bairstow Date: Wed, 10 Nov 2021 21:57:14 +0000 Subject: [PATCH 03/13] Remove Unnecessary Verified No valid API data, removed from models so that there is no inaccurate data --- api/provider/slack.go | 16 +++++++--------- api/provider/spotify.go | 17 +++++++---------- 2 files changed, 14 insertions(+), 19 deletions(-) diff --git a/api/provider/slack.go b/api/provider/slack.go index 896bc8d09a..b3ec727f88 100644 --- a/api/provider/slack.go +++ b/api/provider/slack.go @@ -74,12 +74,11 @@ 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, @@ -87,9 +86,8 @@ func (g slackProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (*Use 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 } diff --git a/api/provider/spotify.go b/api/provider/spotify.go index 0334b21c6c..b3baae8a8a 100644 --- a/api/provider/spotify.go +++ b/api/provider/spotify.go @@ -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 } From eda6a84ae2b9991cff6e2b327b2b09ca812fdf7e Mon Sep 17 00:00:00 2001 From: Harry Bairstow Date: Wed, 10 Nov 2021 22:01:08 +0000 Subject: [PATCH 04/13] Initial TokTok implementation This isn't working and needs changes for getting user data --- README.md | 4 +- api/external.go | 2 + api/provider/tiktok.go | 84 ++++++++++++++++++++++++++++++++++++++++++ api/settings.go | 2 + api/settings_test.go | 1 + conf/configuration.go | 1 + example.env | 3 ++ hack/test.env | 4 ++ 8 files changed, 99 insertions(+), 2 deletions(-) create mode 100644 api/provider/tiktok.go diff --git a/README.md b/README.md index 335c34939b..4c569605fc 100644 --- a/README.md +++ b/README.md @@ -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. @@ -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= ``` diff --git a/api/external.go b/api/external.go index 81e9818cb4..0bdbd21255 100644 --- a/api/external.go +++ b/api/external.go @@ -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": diff --git a/api/provider/tiktok.go b/api/provider/tiktok.go new file mode 100644 index 0000000000..aa369f131d --- /dev/null +++ b/api/provider/tiktok.go @@ -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 { + 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 +} diff --git a/api/settings.go b/api/settings.go index 56b02a6382..71bc978aff 100644 --- a/api/settings.go +++ b/api/settings.go @@ -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"` @@ -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, diff --git a/api/settings_test.go b/api/settings_test.go index f8698749fc..df38ec8129 100644 --- a/api/settings_test.go +++ b/api/settings_test.go @@ -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) diff --git a/conf/configuration.go b/conf/configuration.go index 0276b6f994..b09cb3d5bf 100644 --- a/conf/configuration.go +++ b/conf/configuration.go @@ -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"` diff --git a/example.env b/example.env index eb4ddc2c05..6cd8c8ad9e 100644 --- a/example.env +++ b/example.env @@ -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="" diff --git a/hack/test.env b/hack/test.env index 6b5b6c25f1..58bfba70d0 100644 --- a/hack/test.env +++ b/hack/test.env @@ -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 From 96d4d653f37834a48835189fc73dde392c9d2a08 Mon Sep 17 00:00:00 2001 From: Harry Bairstow Date: Wed, 10 Nov 2021 22:06:05 +0000 Subject: [PATCH 05/13] Initial TikTok Tests --- api/external_tiktok_test.go | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 api/external_tiktok_test.go diff --git a/api/external_tiktok_test.go b/api/external_tiktok_test.go new file mode 100644 index 0000000000..471481686d --- /dev/null +++ b/api/external_tiktok_test.go @@ -0,0 +1,33 @@ +package api + +import ( + "net/http" + "net/http/httptest" + "net/url" + + jwt "github.com/golang-jwt/jwt" +) + +func (ts *ExternalTestSuite) TestSignupExternalTikTok() { + req := httptest.NewRequest(http.MethodGet, "http://localhost/authorize?provider=tiktok", 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.info.basic", 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("tiktok", claims.Provider) + ts.Equal(ts.Config.SiteURL, claims.SiteURL) +} From 8035aa8997b0b454c6a5a483689a9b0a838600cf Mon Sep 17 00:00:00 2001 From: Harry Bairstow Date: Sat, 13 Nov 2021 21:16:09 +0000 Subject: [PATCH 06/13] Initial WorkOS OAuth Implementation --- README.md | 8 +-- api/external.go | 2 + api/external_workos_test.go | 33 ++++++++++++ api/provider/workos.go | 103 ++++++++++++++++++++++++++++++++++++ api/settings.go | 2 + api/settings_test.go | 1 + conf/configuration.go | 1 + example.env | 3 ++ hack/test.env | 4 ++ 9 files changed, 154 insertions(+), 3 deletions(-) create mode 100644 api/external_workos_test.go create mode 100644 api/provider/workos.go diff --git a/README.md b/README.md index 4c569605fc..29ef4e880a 100644 --- a/README.md +++ b/README.md @@ -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`, `tiktok`, `twitch` and `twitter` for external authentication. +We support `apple`, `azure`, `bitbucket`, `discord`, `facebook`, `github`, `gitlab`, `google`, `spotify`, `slack`, `tiktok`, `twitch`, `twitter` and `workos` for external authentication. Use the names as the keys underneath `external` to configure each separately. @@ -505,7 +505,9 @@ Returns the publicly available settings for this gotrue instance. "slack": true, "spotify": true, "twitch": true, - "twitter": true + "twitter": true, + "tiktok": true, + "workos": true, }, "disable_signup": false, "autoconfirm": false @@ -885,7 +887,7 @@ Get access_token from external oauth provider query params: ``` -provider=apple | azure | bitbucket | discord | facebook | github | gitlab | google | slack | spotify | tiktok | twitch | twitter +provider=apple | azure | bitbucket | discord | facebook | github | gitlab | google | slack | spotify | tiktok | twitch | twitter | workos scopes= ``` diff --git a/api/external.go b/api/external.go index 0bdbd21255..737a9c2cd5 100644 --- a/api/external.go +++ b/api/external.go @@ -393,6 +393,8 @@ func (a *API) Provider(ctx context.Context, name string, scopes string) (provide return provider.NewTwitchProvider(config.External.Twitch, scopes) case "twitter": return provider.NewTwitterProvider(config.External.Twitter, scopes) + case "workos": + return provider.NewWorkOSProvider(config.External.Twitter, scopes) case "saml": return provider.NewSamlProvider(config.External.Saml, a.db, getInstanceID(ctx)) default: diff --git a/api/external_workos_test.go b/api/external_workos_test.go new file mode 100644 index 0000000000..04682b8ec6 --- /dev/null +++ b/api/external_workos_test.go @@ -0,0 +1,33 @@ +package api + +import ( + "net/http" + "net/http/httptest" + "net/url" + + jwt "github.com/golang-jwt/jwt" +) + +func (ts *ExternalTestSuite) TestSignupExternalWorkOS() { + req := httptest.NewRequest(http.MethodGet, "http://localhost/authorize?provider=workos", 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("", 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("workos", claims.Provider) + ts.Equal(ts.Config.SiteURL, claims.SiteURL) +} diff --git a/api/provider/workos.go b/api/provider/workos.go new file mode 100644 index 0000000000..197fe03e5d --- /dev/null +++ b/api/provider/workos.go @@ -0,0 +1,103 @@ +package provider + +import ( + "context" + "fmt" + "strings" + + "github.com/netlify/gotrue/conf" + "golang.org/x/oauth2" +) + +const ( + defaultWorkOSAPIBase = "https://api.workos.com" +) + +type workosProvider struct { + *oauth2.Config + APIPath string +} + +/* +{ + "id": "prof_01DMC79VCBZ0NY2099737PSVF1", + "connection_id": "conn_01E4ZCR3C56J083X43JQXF3JK5", + "connection_type": "okta", + "email": "todd@foo-corp.com", + "first_name": "Todd", + "idp_id": "00u1a0ufowBJlzPlk357", + "last_name": "Rundgren", + "object": "profile", + "raw_attributes": {...} +} +*/ +type workosUser struct { + ID string `json:"id"` + ConnectionId string `json:"connection_id"` + ConnectionType string `json:"connection_type"` + Email string `json:"email"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + Object string `json:"object"` + IdpId string `json:"idp_id"` + RawAttributes map[string]interface{} `json:"raw_attributes"` +} + +// NewWorkOSProvider creates a WorkOS account provider. +func NewWorkOSProvider(ext conf.OAuthProviderConfiguration, scopes string) (OAuthProvider, error) { + if err := ext.Validate(); err != nil { + return nil, err + } + + apiPath := chooseHost(ext.URL, defaultWorkOSAPIBase) + + oauthScopes := []string{} + + if scopes != "" { + oauthScopes = append(oauthScopes, strings.Split(scopes, ",")...) + } + + return &workosProvider{ + Config: &oauth2.Config{ + ClientID: ext.ClientID, + ClientSecret: ext.Secret, + Endpoint: oauth2.Endpoint{ + AuthURL: apiPath + "/sso/authorize", + TokenURL: apiPath + "/sso/token", + }, + Scopes: oauthScopes, + RedirectURL: ext.RedirectURI, + }, + APIPath: apiPath, + }, nil +} + +func (g workosProvider) GetOAuthToken(code string) (*oauth2.Token, error) { + // TODO rework this as the TokenURL returns only an access token and the profile + return g.Exchange(oauth2.NoContext, code) +} + +func (g workosProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (*UserProvidedData, error) { + var u workosUser + // TODO rework this as the only way to get profile is with TokenURL + if err := makeRequest(ctx, tok, g.Config, g.APIPath+"/sso/token", &u); err != nil { + return nil, err + } + + return &UserProvidedData{ + Metadata: &Claims{ + Issuer: g.APIPath, + Subject: u.ID, + Name: u.FirstName, + Email: u.Email, + + // To be deprecated + FullName: fmt.Sprintf("%s %s", u.FirstName, u.LastName), + ProviderId: u.ID, + }, + Emails: []Email{{ + Email: u.Email, + Primary: true, + }}, + }, nil +} diff --git a/api/settings.go b/api/settings.go index 71bc978aff..94deaed8bb 100644 --- a/api/settings.go +++ b/api/settings.go @@ -13,6 +13,7 @@ type ProviderSettings struct { Facebook bool `json:"facebook"` Spotify bool `json:"spotify"` Slack bool `json:"slack"` + WorkOS bool `json:"workos"` TikTok bool `json:"tiktok"` Twitch bool `json:"twitch"` Twitter bool `json:"twitter"` @@ -52,6 +53,7 @@ func (a *API) Settings(w http.ResponseWriter, r *http.Request) error { TikTok: config.External.TikTok.Enabled, Twitch: config.External.Twitch.Enabled, Twitter: config.External.Twitter.Enabled, + WorkOS: config.External.WorkOS.Enabled, Email: config.External.Email.Enabled, Phone: config.External.Phone.Enabled, SAML: config.External.Saml.Enabled, diff --git a/api/settings_test.go b/api/settings_test.go index df38ec8129..76bf44dd83 100644 --- a/api/settings_test.go +++ b/api/settings_test.go @@ -40,6 +40,7 @@ func TestSettings_DefaultProviders(t *testing.T) { require.True(t, p.GitLab) require.True(t, p.SAML) require.True(t, p.Twitch) + require.True(t, p.WorkOS) } func TestSettings_EmailDisabled(t *testing.T) { diff --git a/conf/configuration.go b/conf/configuration.go index b09cb3d5bf..f77a2dfd95 100644 --- a/conf/configuration.go +++ b/conf/configuration.go @@ -93,6 +93,7 @@ type ProviderConfiguration struct { TikTok OAuthProviderConfiguration `json:"tiktok"` Twitter OAuthProviderConfiguration `json:"twitter"` Twitch OAuthProviderConfiguration `json:"twitch"` + WorkOS OAuthProviderConfiguration `json:"workos"` Email EmailProviderConfiguration `json:"email"` Phone PhoneProviderConfiguration `json:"phone"` Saml SamlProviderConfiguration `json:"saml"` diff --git a/example.env b/example.env index 6cd8c8ad9e..802690c8e8 100644 --- a/example.env +++ b/example.env @@ -50,6 +50,9 @@ GOTRUE_EXTERNAL_SLACK_SECRET="" GOTRUE_EXTERNAL_TIKTOK_ENABLED="true" GOTRUE_EXTERNAL_TIKTOK_CLIENT_ID="" GOTRUE_EXTERNAL_TIKTOK_SECRET="" +GOTRUE_EXTERNAL_WORKOS_ENABLED="true" +GOTRUE_EXTERNAL_WORKOS_CLIENT_ID="" +GOTRUE_EXTERNAL_WORKOS_SECRET="" GOTRUE_EXTERNAL_TWITTER_ENABLED="false" GOTRUE_EXTERNAL_TWITTER_CLIENT_ID="" GOTRUE_EXTERNAL_TWITTER_SECRET="" diff --git a/hack/test.env b/hack/test.env index 58bfba70d0..07b424db35 100644 --- a/hack/test.env +++ b/hack/test.env @@ -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_WORKOS_ENABLED=true +GOTRUE_EXTERNAL_WORKOS_CLIENT_ID=testclientid +GOTRUE_EXTERNAL_WORKOS_SECRET=testsecret +GOTRUE_EXTERNAL_WORKOS_REDIRECT_URI=https://identity.services.netlify.com/callback GOTRUE_EXTERNAL_TIKTOK_ENABLED=true GOTRUE_EXTERNAL_TIKTOK_CLIENT_ID=testclientid GOTRUE_EXTERNAL_TIKTOK_SECRET=testsecret From a162887761db24e645e3502bb51f2cd67aaef683 Mon Sep 17 00:00:00 2001 From: Harry Bairstow Date: Sat, 13 Nov 2021 21:18:21 +0000 Subject: [PATCH 07/13] Fix Typo --- api/external.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/external.go b/api/external.go index 737a9c2cd5..e8d55fe6f3 100644 --- a/api/external.go +++ b/api/external.go @@ -394,7 +394,7 @@ func (a *API) Provider(ctx context.Context, name string, scopes string) (provide case "twitter": return provider.NewTwitterProvider(config.External.Twitter, scopes) case "workos": - return provider.NewWorkOSProvider(config.External.Twitter, scopes) + return provider.NewWorkOSProvider(config.External.WorkOS, scopes) case "saml": return provider.NewSamlProvider(config.External.Saml, a.db, getInstanceID(ctx)) default: From c7d3d759eed77fa098aa99d6f0482148aad4720d Mon Sep 17 00:00:00 2001 From: Harry Bairstow Date: Sun, 14 Nov 2021 14:25:59 +0000 Subject: [PATCH 08/13] Initial LinkedIn OAuth Implementation Takes code from #238 and finished implementation, still needs improvements in provider impl file --- README.md | 5 +- api/external.go | 2 + api/external_linkedin_test.go | 33 +++++++ api/provider/linkedin.go | 176 ++++++++++++++++++++++++++++++++++ api/settings.go | 2 + api/settings_test.go | 1 + conf/configuration.go | 1 + example.env | 3 + hack/test.env | 4 + 9 files changed, 225 insertions(+), 2 deletions(-) create mode 100644 api/external_linkedin_test.go create mode 100644 api/provider/linkedin.go diff --git a/README.md b/README.md index 29ef4e880a..9b396ccccf 100644 --- a/README.md +++ b/README.md @@ -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`, `tiktok`, `twitch`, `twitter` and `workos` for external authentication. +We support `apple`, `azure`, `bitbucket`, `discord`, `facebook`, `github`, `gitlab`, `google`, `linkedin`,`spotify`, `slack`, `tiktok`, `twitch`, `twitter` and `workos` for external authentication. Use the names as the keys underneath `external` to configure each separately. @@ -502,6 +502,7 @@ Returns the publicly available settings for this gotrue instance. "github": true, "gitlab": true, "google": true, + "linkedin": true, "slack": true, "spotify": true, "twitch": true, @@ -887,7 +888,7 @@ Get access_token from external oauth provider query params: ``` -provider=apple | azure | bitbucket | discord | facebook | github | gitlab | google | slack | spotify | tiktok | twitch | twitter | workos +provider=apple | azure | bitbucket | discord | facebook | github | gitlab | google | linkedin | slack | spotify | tiktok | twitch | twitter | workos scopes= ``` diff --git a/api/external.go b/api/external.go index e8d55fe6f3..34eefd5ead 100644 --- a/api/external.go +++ b/api/external.go @@ -381,6 +381,8 @@ func (a *API) Provider(ctx context.Context, name string, scopes string) (provide return provider.NewGitlabProvider(config.External.Gitlab, scopes) case "google": return provider.NewGoogleProvider(config.External.Google, scopes) + case "linkedin": + return provider.NewLinkedInProvider(config.External.Google, scopes) case "facebook": return provider.NewFacebookProvider(config.External.Facebook, scopes) case "spotify": diff --git a/api/external_linkedin_test.go b/api/external_linkedin_test.go new file mode 100644 index 0000000000..de91a32a1c --- /dev/null +++ b/api/external_linkedin_test.go @@ -0,0 +1,33 @@ +package api + +import ( + "net/http" + "net/http/httptest" + "net/url" + + jwt "github.com/golang-jwt/jwt" +) + +func (ts *ExternalTestSuite) TestSignupExternalLinkedIn() { + req := httptest.NewRequest(http.MethodGet, "http://localhost/authorize?provider=linkedin", 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.Apple.RedirectURI, q.Get("redirect_uri")) + ts.Equal(ts.Config.External.Apple.ClientID, q.Get("client_id")) + ts.Equal("code", q.Get("response_type")) + ts.Equal("r_emailaddress r_liteprofile", 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("linkedin", claims.Provider) + ts.Equal(ts.Config.SiteURL, claims.SiteURL) +} diff --git a/api/provider/linkedin.go b/api/provider/linkedin.go new file mode 100644 index 0000000000..1d649bfe72 --- /dev/null +++ b/api/provider/linkedin.go @@ -0,0 +1,176 @@ +package provider + +import ( + "context" + "errors" + "strings" + + "github.com/netlify/gotrue/conf" + "golang.org/x/oauth2" +) + +const ( + defaultLinkedinAPIBase = "api.linkedin.com" + endpointLinkedinProfile = "/v2/me?projection=(id,firstName,lastName,profilePicture(displayImage~:playableStreams))" + endpointLinkedinEmail = "/v2/emailAddress?q=members&projection=(elements*(handle~))" +) + +type linkedinProvider struct { + *oauth2.Config + APIPath string + UserInfoURL string + UserEmailUrl string +} + +// This is the json returned by api for profile +// { +// "firstName":{ +// "localized":{ +// "en_US":"Tina" +// }, +// "preferredLocale":{ +// "country":"US", +// "language":"en" +// } +// }, +// "lastName":{ +// "localized":{ +// "en_US":"Belcher" +// }, +// "preferredLocale":{ +// "country":"US", +// "language":"en" +// } +// }, +// } + +// return format for avatarUrl +// {"displayImage~" : { elements: [{identifiers: [ {identifier: "URL"}]}]}} + +// https://docs.microsoft.com/en-us/linkedin/consumer/integrations/self-serve/sign-in-with-linkedin?context=linkedin/consumer/context +type linkedinLocale struct { + Country string `json:"country"` + Language string `json:"language"` +} + +type linkedinName struct { + Localized interface{} `json:"localized"` // try to catch all possible value + PreferredLocale linkedinLocale `json:"preferredLocale"` +} + +type linkedinUser struct { + ID string `json:"id"` + FirstName linkedinName `json:"firstName"` // i tried to parse data but not sure + LastName linkedinName `json:"lastName"` // i tried to parse data but not sure + AvatarURL struct { // I don't know if we can do better than that + DisplayImage struct { + Elements []struct { + Identifiers []struct { + Identifier string `json:"identifier"` + } `json:"identifiers"` + } `json:"elements"` + } `json:"displayImage~"` + } `json:"profilePicture"` +} + +// This is the json returned by api for email +// { +// "handle": "urn:li:emailAddress:3775708763", +// "handle~": { +// "emailAddress": "hsimpson@linkedin.com" +// } +// } + +type linkedinEmail struct { + EmailAddress string `json:"emailAddress"` +} + +type linkedinUserEmail struct { + Handle string `json:"handle"` + Handle_email linkedinEmail `json:"handle~"` +} + +// NewLinkedinProvider creates a Linkedin account provider. +func NewLinkedInProvider(ext conf.OAuthProviderConfiguration, scopes string) (OAuthProvider, error) { + if err := ext.Validate(); err != nil { + return nil, err + } + + // authHost := chooseHost(ext.URL, defaultLinkedinAuthBase) + apiPath := chooseHost(ext.URL, defaultLinkedinAPIBase) + + oauthScopes := []string{ + "r_emailaddress", + "r_liteprofile", + } + + if scopes != "" { + oauthScopes = append(oauthScopes, strings.Split(scopes, ",")...) + } + + return &linkedinProvider{ + Config: &oauth2.Config{ + ClientID: ext.ClientID, + ClientSecret: ext.Secret, + Endpoint: oauth2.Endpoint{ + AuthURL: defaultLinkedinAPIBase + "/oauth/v2/authorization", + TokenURL: defaultLinkedinAPIBase + "/oauth/v2/accessToken", + }, + Scopes: oauthScopes, + RedirectURL: ext.RedirectURI, + }, + APIPath: apiPath, + }, nil +} + +func (g linkedinProvider) GetOAuthToken(code string) (*oauth2.Token, error) { + return g.Exchange(oauth2.NoContext, code) +} + +func GetName(name linkedinName) string { + key := name.PreferredLocale.Language + "_" + name.PreferredLocale.Country + myMap := name.Localized.(map[string]interface{}) // not sure about the cast + return myMap[key].(string) +} + +func (g linkedinProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (*UserProvidedData, error) { + var u linkedinUser + if err := makeRequest(ctx, tok, g.Config, defaultLinkedinAPIBase+endpointLinkedinProfile, &u); err != nil { + return nil, err + } + + data := &UserProvidedData{} + + var email linkedinUserEmail + if err := makeRequest(ctx, tok, g.Config, defaultLinkedinAPIBase+endpointLinkedinEmail, &email); err != nil { + return nil, err + } + + if email.Handle_email.EmailAddress != "" { + data.Emails = append(data.Emails, Email{ + Email: email.Handle_email.EmailAddress, + Verified: true, + Primary: true, + }) + } + + if len(data.Emails) <= 0 { + return nil, errors.New("Unable to find email with Linkedin provider") + } + + data.Metadata = &Claims{ + Issuer: g.APIPath, + Subject: u.ID, + Name: strings.TrimSpace(GetName(u.FirstName) + " " + GetName(u.FirstName)), + Picture: u.AvatarURL.DisplayImage.Elements[0].Identifiers[0].Identifier, + Email: email.Handle_email.EmailAddress, + EmailVerified: true, + + // To be deprecated + AvatarURL: u.AvatarURL.DisplayImage.Elements[0].Identifiers[0].Identifier, + FullName: strings.TrimSpace(GetName(u.FirstName) + " " + GetName(u.FirstName)), + ProviderId: u.ID, + } + + return data, nil +} diff --git a/api/settings.go b/api/settings.go index 94deaed8bb..096993ca0a 100644 --- a/api/settings.go +++ b/api/settings.go @@ -10,6 +10,7 @@ type ProviderSettings struct { GitHub bool `json:"github"` GitLab bool `json:"gitlab"` Google bool `json:"google"` + LinkedIn bool `json:"linkedin"` Facebook bool `json:"facebook"` Spotify bool `json:"spotify"` Slack bool `json:"slack"` @@ -47,6 +48,7 @@ func (a *API) Settings(w http.ResponseWriter, r *http.Request) error { GitHub: config.External.Github.Enabled, GitLab: config.External.Gitlab.Enabled, Google: config.External.Google.Enabled, + LinkedIn: config.External.LinkedIn.Enabled, Facebook: config.External.Facebook.Enabled, Spotify: config.External.Spotify.Enabled, Slack: config.External.Slack.Enabled, diff --git a/api/settings_test.go b/api/settings_test.go index 76bf44dd83..70f884c462 100644 --- a/api/settings_test.go +++ b/api/settings_test.go @@ -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.LinkedIn) require.True(t, p.TikTok) require.True(t, p.Google) require.True(t, p.GitHub) diff --git a/conf/configuration.go b/conf/configuration.go index f77a2dfd95..5eab8d1f1d 100644 --- a/conf/configuration.go +++ b/conf/configuration.go @@ -88,6 +88,7 @@ type ProviderConfiguration struct { Github OAuthProviderConfiguration `json:"github"` Gitlab OAuthProviderConfiguration `json:"gitlab"` Google OAuthProviderConfiguration `json:"google"` + LinkedIn OAuthProviderConfiguration `json:"linkedin"` Spotify OAuthProviderConfiguration `json:"spotify"` Slack OAuthProviderConfiguration `json:"slack"` TikTok OAuthProviderConfiguration `json:"tiktok"` diff --git a/example.env b/example.env index 802690c8e8..b73ae17692 100644 --- a/example.env +++ b/example.env @@ -47,6 +47,9 @@ GOTRUE_EXTERNAL_SPOTIFY_SECRET="" GOTRUE_EXTERNAL_SLACK_ENABLED="true" GOTRUE_EXTERNAL_SLACK_CLIENT_ID="" GOTRUE_EXTERNAL_SLACK_SECRET="" +GOTRUE_EXTERNAL_LINKEDIN_ENABLED="true" +GOTRUE_EXTERNAL_LINKEDIN_CLIENT_ID="" +GOTRUE_EXTERNAL_LINKEDIN_SECRET="" GOTRUE_EXTERNAL_TIKTOK_ENABLED="true" GOTRUE_EXTERNAL_TIKTOK_CLIENT_ID="" GOTRUE_EXTERNAL_TIKTOK_SECRET="" diff --git a/hack/test.env b/hack/test.env index 07b424db35..ce2e90b7bd 100644 --- a/hack/test.env +++ b/hack/test.env @@ -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_LINKEDIN_ENABLED=true +GOTRUE_EXTERNAL_LINKEDIN_CLIENT_ID=testclientid +GOTRUE_EXTERNAL_LINKEDIN_SECRET=testsecret +GOTRUE_EXTERNAL_LINKEDIN_REDIRECT_URI=https://identity.services.netlify.com/callback GOTRUE_EXTERNAL_WORKOS_ENABLED=true GOTRUE_EXTERNAL_WORKOS_CLIENT_ID=testclientid GOTRUE_EXTERNAL_WORKOS_SECRET=testsecret From 1155b7b33f6964e5bedfe4cde3ccf3ff5fe84917 Mon Sep 17 00:00:00 2001 From: Harry Bairstow Date: Sun, 14 Nov 2021 14:32:35 +0000 Subject: [PATCH 09/13] Tidy LinkedIn Provider Impl --- api/provider/linkedin.go | 110 +++++++++++++-------------------------- 1 file changed, 37 insertions(+), 73 deletions(-) diff --git a/api/provider/linkedin.go b/api/provider/linkedin.go index 1d649bfe72..4d2321a9a3 100644 --- a/api/provider/linkedin.go +++ b/api/provider/linkedin.go @@ -10,9 +10,7 @@ import ( ) const ( - defaultLinkedinAPIBase = "api.linkedin.com" - endpointLinkedinProfile = "/v2/me?projection=(id,firstName,lastName,profilePicture(displayImage~:playableStreams))" - endpointLinkedinEmail = "/v2/emailAddress?q=members&projection=(elements*(handle~))" + defaultLinkedInAPIBase = "api.linkedin.com" ) type linkedinProvider struct { @@ -22,42 +20,7 @@ type linkedinProvider struct { UserEmailUrl string } -// This is the json returned by api for profile -// { -// "firstName":{ -// "localized":{ -// "en_US":"Tina" -// }, -// "preferredLocale":{ -// "country":"US", -// "language":"en" -// } -// }, -// "lastName":{ -// "localized":{ -// "en_US":"Belcher" -// }, -// "preferredLocale":{ -// "country":"US", -// "language":"en" -// } -// }, -// } - -// return format for avatarUrl -// {"displayImage~" : { elements: [{identifiers: [ {identifier: "URL"}]}]}} - // https://docs.microsoft.com/en-us/linkedin/consumer/integrations/self-serve/sign-in-with-linkedin?context=linkedin/consumer/context -type linkedinLocale struct { - Country string `json:"country"` - Language string `json:"language"` -} - -type linkedinName struct { - Localized interface{} `json:"localized"` // try to catch all possible value - PreferredLocale linkedinLocale `json:"preferredLocale"` -} - type linkedinUser struct { ID string `json:"id"` FirstName linkedinName `json:"firstName"` // i tried to parse data but not sure @@ -73,13 +36,15 @@ type linkedinUser struct { } `json:"profilePicture"` } -// This is the json returned by api for email -// { -// "handle": "urn:li:emailAddress:3775708763", -// "handle~": { -// "emailAddress": "hsimpson@linkedin.com" -// } -// } +type linkedinLocale struct { + Country string `json:"country"` + Language string `json:"language"` +} + +type linkedinName struct { + Localized interface{} `json:"localized"` // try to catch all possible value + PreferredLocale linkedinLocale `json:"preferredLocale"` +} type linkedinEmail struct { EmailAddress string `json:"emailAddress"` @@ -97,7 +62,7 @@ func NewLinkedInProvider(ext conf.OAuthProviderConfiguration, scopes string) (OA } // authHost := chooseHost(ext.URL, defaultLinkedinAuthBase) - apiPath := chooseHost(ext.URL, defaultLinkedinAPIBase) + apiPath := chooseHost(ext.URL, defaultLinkedInAPIBase) oauthScopes := []string{ "r_emailaddress", @@ -113,8 +78,8 @@ func NewLinkedInProvider(ext conf.OAuthProviderConfiguration, scopes string) (OA ClientID: ext.ClientID, ClientSecret: ext.Secret, Endpoint: oauth2.Endpoint{ - AuthURL: defaultLinkedinAPIBase + "/oauth/v2/authorization", - TokenURL: defaultLinkedinAPIBase + "/oauth/v2/accessToken", + AuthURL: apiPath + "/oauth/v2/authorization", + TokenURL: apiPath + "/oauth/v2/accessToken", }, Scopes: oauthScopes, RedirectURL: ext.RedirectURI, @@ -129,48 +94,47 @@ func (g linkedinProvider) GetOAuthToken(code string) (*oauth2.Token, error) { func GetName(name linkedinName) string { key := name.PreferredLocale.Language + "_" + name.PreferredLocale.Country - myMap := name.Localized.(map[string]interface{}) // not sure about the cast + myMap := name.Localized.(map[string]interface{}) return myMap[key].(string) } func (g linkedinProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (*UserProvidedData, error) { var u linkedinUser - if err := makeRequest(ctx, tok, g.Config, defaultLinkedinAPIBase+endpointLinkedinProfile, &u); err != nil { + if err := makeRequest(ctx, tok, g.Config, g.APIPath+"/v2/me?projection=(id,firstName,lastName,profilePicture(displayImage~:playableStreams))", &u); err != nil { return nil, err } - data := &UserProvidedData{} - var email linkedinUserEmail - if err := makeRequest(ctx, tok, g.Config, defaultLinkedinAPIBase+endpointLinkedinEmail, &email); err != nil { + if err := makeRequest(ctx, tok, g.Config, g.APIPath+"/v2/emailAddress?q=members&projection=(elements*(handle~))", &email); err != nil { return nil, err } + emails := []Email{} + if email.Handle_email.EmailAddress != "" { - data.Emails = append(data.Emails, Email{ - Email: email.Handle_email.EmailAddress, - Verified: true, - Primary: true, + emails = append(emails, Email{ + Email: email.Handle_email.EmailAddress, + Primary: true, }) } - if len(data.Emails) <= 0 { + if len(emails) <= 0 { return nil, errors.New("Unable to find email with Linkedin provider") } - data.Metadata = &Claims{ - Issuer: g.APIPath, - Subject: u.ID, - Name: strings.TrimSpace(GetName(u.FirstName) + " " + GetName(u.FirstName)), - Picture: u.AvatarURL.DisplayImage.Elements[0].Identifiers[0].Identifier, - Email: email.Handle_email.EmailAddress, - EmailVerified: true, - - // To be deprecated - AvatarURL: u.AvatarURL.DisplayImage.Elements[0].Identifiers[0].Identifier, - FullName: strings.TrimSpace(GetName(u.FirstName) + " " + GetName(u.FirstName)), - ProviderId: u.ID, - } - - return data, nil + return &UserProvidedData{ + Metadata: &Claims{ + Issuer: g.APIPath, + Subject: u.ID, + Name: strings.TrimSpace(GetName(u.FirstName) + " " + GetName(u.LastName)), + Picture: u.AvatarURL.DisplayImage.Elements[0].Identifiers[0].Identifier, + Email: email.Handle_email.EmailAddress, + + // To be deprecated + AvatarURL: u.AvatarURL.DisplayImage.Elements[0].Identifiers[0].Identifier, + FullName: strings.TrimSpace(GetName(u.FirstName) + " " + GetName(u.LastName)), + ProviderId: u.ID, + }, + Emails: emails, + }, nil } From c47b9615e5c4a5a80a02743a665f25335f10171c Mon Sep 17 00:00:00 2001 From: Harry Bairstow Date: Sun, 14 Nov 2021 14:47:20 +0000 Subject: [PATCH 10/13] Fix TikTok `GetUserData` --- api/provider/provider.go | 23 +++++++++++++++++++++++ api/provider/tiktok.go | 20 +++++++++++++++++--- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/api/provider/provider.go b/api/provider/provider.go index afb36c5eee..6a1008b862 100644 --- a/api/provider/provider.go +++ b/api/provider/provider.go @@ -1,6 +1,7 @@ package provider import ( + "bytes" "context" "encoding/json" @@ -112,3 +113,25 @@ func makeRequest(ctx context.Context, tok *oauth2.Token, g *oauth2.Config, url s return nil } + +func makePostRequest(ctx context.Context, tok *oauth2.Token, g *oauth2.Config, url string, body interface{}, dst interface{}) error { + client := g.Client(ctx, tok) + + json_data, err := json.Marshal(body) + + if err != nil { + return err + } + + res, err := client.Post(url, "application/json", bytes.NewBuffer(json_data)) + if err != nil { + return err + } + defer res.Body.Close() + + if err := json.NewDecoder(res.Body).Decode(dst); err != nil { + return err + } + + return nil +} diff --git a/api/provider/tiktok.go b/api/provider/tiktok.go index aa369f131d..70ada88336 100644 --- a/api/provider/tiktok.go +++ b/api/provider/tiktok.go @@ -22,11 +22,15 @@ type tiktokUser struct { 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"` } +type tiktokUserRequestBody struct { + Id string `json:"open_id"` + AccessToken string `json:"access_token"` + Fields []string `json:"fields"` +} + // NewTikTokProvider creates a TikTok account provider. func NewTikTokProvider(ext conf.OAuthProviderConfiguration, scopes string) (OAuthProvider, error) { if err := ext.Validate(); err != nil { @@ -64,7 +68,17 @@ func (g tiktokProvider) GetOAuthToken(code string) (*oauth2.Token, error) { 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 { + if err := makePostRequest(ctx, tok, g.Config, g.APIPath+"/user/info", &tiktokUserRequestBody{ + Id: tok.Extra("open_id").(string), + AccessToken: tok.AccessToken, + Fields: []string{ + "open_id", + "union_id", + "display_name", + "avatar_url", + "avatar_large_url", + }, + }, &u); err != nil { return nil, err } From befced2d38f20d8f204bc05382c7394a5549634f Mon Sep 17 00:00:00 2001 From: Harry Bairstow Date: Sun, 14 Nov 2021 14:50:20 +0000 Subject: [PATCH 11/13] Fix WorkOS `GetUserData` --- api/provider/workos.go | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/api/provider/workos.go b/api/provider/workos.go index 197fe03e5d..d968523535 100644 --- a/api/provider/workos.go +++ b/api/provider/workos.go @@ -18,19 +18,6 @@ type workosProvider struct { APIPath string } -/* -{ - "id": "prof_01DMC79VCBZ0NY2099737PSVF1", - "connection_id": "conn_01E4ZCR3C56J083X43JQXF3JK5", - "connection_type": "okta", - "email": "todd@foo-corp.com", - "first_name": "Todd", - "idp_id": "00u1a0ufowBJlzPlk357", - "last_name": "Rundgren", - "object": "profile", - "raw_attributes": {...} -} -*/ type workosUser struct { ID string `json:"id"` ConnectionId string `json:"connection_id"` @@ -73,16 +60,11 @@ func NewWorkOSProvider(ext conf.OAuthProviderConfiguration, scopes string) (OAut } func (g workosProvider) GetOAuthToken(code string) (*oauth2.Token, error) { - // TODO rework this as the TokenURL returns only an access token and the profile return g.Exchange(oauth2.NoContext, code) } func (g workosProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (*UserProvidedData, error) { - var u workosUser - // TODO rework this as the only way to get profile is with TokenURL - if err := makeRequest(ctx, tok, g.Config, g.APIPath+"/sso/token", &u); err != nil { - return nil, err - } + u := tok.Extra("profile").(workosUser) return &UserProvidedData{ Metadata: &Claims{ From dd1e201331c3f658f93438a38bed7e7ec53ec492 Mon Sep 17 00:00:00 2001 From: Harry Bairstow Date: Sun, 14 Nov 2021 16:08:16 +0000 Subject: [PATCH 12/13] Small Bug Fixes - Forgot to change values after copy & paste --- api/external.go | 2 +- api/external_linkedin_test.go | 4 ++-- api/external_tiktok_test.go | 4 ++-- api/external_workos_test.go | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/api/external.go b/api/external.go index 34eefd5ead..c028eb3e67 100644 --- a/api/external.go +++ b/api/external.go @@ -382,7 +382,7 @@ func (a *API) Provider(ctx context.Context, name string, scopes string) (provide case "google": return provider.NewGoogleProvider(config.External.Google, scopes) case "linkedin": - return provider.NewLinkedInProvider(config.External.Google, scopes) + return provider.NewLinkedInProvider(config.External.LinkedIn, scopes) case "facebook": return provider.NewFacebookProvider(config.External.Facebook, scopes) case "spotify": diff --git a/api/external_linkedin_test.go b/api/external_linkedin_test.go index de91a32a1c..4fe0da5913 100644 --- a/api/external_linkedin_test.go +++ b/api/external_linkedin_test.go @@ -16,8 +16,8 @@ func (ts *ExternalTestSuite) TestSignupExternalLinkedIn() { u, err := url.Parse(w.Header().Get("Location")) ts.Require().NoError(err, "redirect url parse failed") q := u.Query() - ts.Equal(ts.Config.External.Apple.RedirectURI, q.Get("redirect_uri")) - ts.Equal(ts.Config.External.Apple.ClientID, q.Get("client_id")) + ts.Equal(ts.Config.External.LinkedIn.RedirectURI, q.Get("redirect_uri")) + ts.Equal(ts.Config.External.LinkedIn.ClientID, q.Get("client_id")) ts.Equal("code", q.Get("response_type")) ts.Equal("r_emailaddress r_liteprofile", q.Get("scope")) diff --git a/api/external_tiktok_test.go b/api/external_tiktok_test.go index 471481686d..06ad11c4e3 100644 --- a/api/external_tiktok_test.go +++ b/api/external_tiktok_test.go @@ -16,8 +16,8 @@ func (ts *ExternalTestSuite) TestSignupExternalTikTok() { 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(ts.Config.External.TikTok.RedirectURI, q.Get("redirect_uri")) + ts.Equal(ts.Config.External.TikTok.ClientID, q.Get("client_id")) ts.Equal("code", q.Get("response_type")) ts.Equal("user.info.basic", q.Get("scope")) diff --git a/api/external_workos_test.go b/api/external_workos_test.go index 04682b8ec6..d7de03b56d 100644 --- a/api/external_workos_test.go +++ b/api/external_workos_test.go @@ -16,8 +16,8 @@ func (ts *ExternalTestSuite) TestSignupExternalWorkOS() { 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(ts.Config.External.WorkOS.RedirectURI, q.Get("redirect_uri")) + ts.Equal(ts.Config.External.WorkOS.ClientID, q.Get("client_id")) ts.Equal("code", q.Get("response_type")) ts.Equal("", q.Get("scope")) From 540829728a9d2af89f0d5bed09b3933ddf42c509 Mon Sep 17 00:00:00 2001 From: Harry Bairstow Date: Mon, 15 Nov 2021 15:23:30 +0000 Subject: [PATCH 13/13] Fix trailing spaces in WorkOS name --- api/provider/workos.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/api/provider/workos.go b/api/provider/workos.go index d968523535..86bdcb8593 100644 --- a/api/provider/workos.go +++ b/api/provider/workos.go @@ -2,7 +2,6 @@ package provider import ( "context" - "fmt" "strings" "github.com/netlify/gotrue/conf" @@ -74,7 +73,7 @@ func (g workosProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (*Us Email: u.Email, // To be deprecated - FullName: fmt.Sprintf("%s %s", u.FirstName, u.LastName), + FullName: strings.TrimSpace(u.FirstName + " " + u.LastName), ProviderId: u.ID, }, Emails: []Email{{