From 1484315d9025be38ec92f28ef0b14349d71caf81 Mon Sep 17 00:00:00 2001 From: Martin DONADIEU Date: Sun, 17 Oct 2021 12:20:58 +0200 Subject: [PATCH 01/10] feaT: linkedin provider WIP --- api/provider/linkedin.go | 183 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 183 insertions(+) create mode 100644 api/provider/linkedin.go diff --git a/api/provider/linkedin.go b/api/provider/linkedin.go new file mode 100644 index 0000000000..a1b8d70eb3 --- /dev/null +++ b/api/provider/linkedin.go @@ -0,0 +1,183 @@ +package provider + +import ( + "context" + "errors" + "strings" + + "github.com/netlify/gotrue/conf" + "golang.org/x/oauth2" +) + +const ( + defaultLinkedinAPIBase = "api.linkedin.com" + endpointProfile = "/v2/me?projection=(id,firstName,lastName,profilePicture(displayImage~:playableStreams))" + endpointEmail = "/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 linkedinLocalized struct { + En_US string `json:"en_US"` // how to modelize catch all for lang ? +} +type linkedinName struct { + Localized linkedinLocalized `json:"localized"` + PreferredLocale linkedinLocale `json:"preferredLocale"` +} + +type +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: authHost + "/oauth/v2/authorization", + TokenURL: authHost + "/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) { + return name.localized[name.preferredLocale.language + "_" + name.preferredLocale.country] +} + +nameRes.localized[`${nameRes.preferredLocale.language}_${nameRes.preferredLocale.country}`] + +func (g linkedinProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (*UserProvidedData, error) { + var u linkedinUser + if err := makeRequest(ctx, tok, g.Config, g.APIPath + endpointProfile, &u); err != nil { + return nil, err + } + + data := &UserProvidedData{} + + var email githubUserEmail + if err := makeRequest(ctx, tok, g.Config, g.APIHost + endpointEmail, &email); err != nil { + return nil, err + } + + if email != "" { + 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, + FullName: strings.TrimSpace(GetName(u.FirstName) + " " + GetName(u.FirstName)), + ProviderId: u.ID, + } + + return data, nil +} From e712382a5f5c8b36c6608ab446c5bd6ecf149175 Mon Sep 17 00:00:00 2001 From: Martin DONADIEU Date: Mon, 18 Oct 2021 00:00:26 +0200 Subject: [PATCH 02/10] fix: use interface{} for catch all value --- api/provider/linkedin.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/api/provider/linkedin.go b/api/provider/linkedin.go index a1b8d70eb3..2e01d62424 100644 --- a/api/provider/linkedin.go +++ b/api/provider/linkedin.go @@ -55,11 +55,8 @@ type linkedinLocale struct { Language string `json:"language"` } -type linkedinLocalized struct { - En_US string `json:"en_US"` // how to modelize catch all for lang ? -} type linkedinName struct { - Localized linkedinLocalized `json:"localized"` + Localized interface{} `json:"localized"` // try to catch all possible value PreferredLocale linkedinLocale `json:"preferredLocale"` } From 062fa65f5d878fca02ac3bdc472a3decece3bce5 Mon Sep 17 00:00:00 2001 From: martindonadieu Date: Mon, 18 Oct 2021 15:44:14 +0100 Subject: [PATCH 03/10] fix: lint issue --- api/provider/linkedin.go | 66 +++++++++++++++++++--------------------- 1 file changed, 31 insertions(+), 35 deletions(-) diff --git a/api/provider/linkedin.go b/api/provider/linkedin.go index 2e01d62424..0243733b63 100644 --- a/api/provider/linkedin.go +++ b/api/provider/linkedin.go @@ -10,19 +10,18 @@ import ( ) const ( - defaultLinkedinAPIBase = "api.linkedin.com" - endpointProfile = "/v2/me?projection=(id,firstName,lastName,profilePicture(displayImage~:playableStreams))" - endpointEmail = "/v2/emailAddress?q=members&projection=(elements*(handle~))" + defaultLinkedinAPIBase = "api.linkedin.com" + endpointProfile = "/v2/me?projection=(id,firstName,lastName,profilePicture(displayImage~:playableStreams))" + endpointEmail = "/v2/emailAddress?q=members&projection=(elements*(handle~))" ) type linkedinProvider struct { *oauth2.Config - APIPath string - UserInfoURL string - UserEmailUrl string + APIPath string + UserInfoURL string + UserEmailUrl string } - // This is the json returned by api for profile // { // "firstName":{ @@ -48,35 +47,32 @@ type linkedinProvider struct { // 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"` + 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"` + Localized interface{} `json:"localized"` // try to catch all possible value + PreferredLocale linkedinLocale `json:"preferredLocale"` } -type 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 + 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"` + 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", @@ -86,12 +82,12 @@ type linkedinUser struct { // } type linkedinEmail struct { - EmailAddress string `json:"emailAddress"` + EmailAddress string `json:"emailAddress"` } type linkedinUserEmail struct { - Handle string `json:"handle"` - Handle_email linkedinEmail `json:"handle~"` + Handle string `json:"handle"` + Handle_email linkedinEmail `json:"handle~"` } // NewLinkedinProvider creates a Linkedin account provider. @@ -117,8 +113,8 @@ func NewLinkedinProvider(ext conf.OAuthProviderConfiguration, scopes string) (OA ClientID: ext.ClientID, ClientSecret: ext.Secret, Endpoint: oauth2.Endpoint{ - AuthURL: authHost + "/oauth/v2/authorization", - TokenURL: authHost + "/oauth/v2/accessToken", + AuthURL: defaultLinkedinAPIBase + "/oauth/v2/authorization", + TokenURL: defaultLinkedinAPIBase + "/oauth/v2/accessToken", }, Scopes: oauthScopes, RedirectURL: ext.RedirectURI, @@ -131,26 +127,26 @@ func (g linkedinProvider) GetOAuthToken(code string) (*oauth2.Token, error) { return g.Exchange(oauth2.NoContext, code) } -func GetName(name linkedinName) (string) { - return name.localized[name.preferredLocale.language + "_" + name.preferredLocale.country] +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) } -nameRes.localized[`${nameRes.preferredLocale.language}_${nameRes.preferredLocale.country}`] - func (g linkedinProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (*UserProvidedData, error) { var u linkedinUser - if err := makeRequest(ctx, tok, g.Config, g.APIPath + endpointProfile, &u); err != nil { + if err := makeRequest(ctx, tok, g.Config, defaultLinkedinAPIBase+endpointProfile, &u); err != nil { return nil, err } data := &UserProvidedData{} - var email githubUserEmail - if err := makeRequest(ctx, tok, g.Config, g.APIHost + endpointEmail, &email); err != nil { + var email linkedinUserEmail + if err := makeRequest(ctx, tok, g.Config, defaultLinkedinAPIBase+endpointEmail, &email); err != nil { return nil, err } - if email != "" { + if email.Handle_email.EmailAddress != "" { data.Emails = append(data.Emails, Email{ Email: email.Handle_email.EmailAddress, Verified: true, @@ -171,7 +167,7 @@ func (g linkedinProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (* EmailVerified: true, // To be deprecated - AvatarURL: u.AvatarURL, + AvatarURL: u.AvatarURL.DisplayImage.Elements[0].Identifiers[0].Identifier, FullName: strings.TrimSpace(GetName(u.FirstName) + " " + GetName(u.FirstName)), ProviderId: u.ID, } From 086917e6fca72ed050f37e860c24895caa62a90d Mon Sep 17 00:00:00 2001 From: martindonadieu Date: Mon, 18 Oct 2021 15:51:01 +0100 Subject: [PATCH 04/10] feat: add WIP test for linkedin --- api/external_linkedin_test.go | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 api/external_linkedin_test.go diff --git a/api/external_linkedin_test.go b/api/external_linkedin_test.go new file mode 100644 index 0000000000..726b648006 --- /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) +} From 025ed85ed33df4495d3e2c18c1ad4867a5f8772c Mon Sep 17 00:00:00 2001 From: martindonadieu Date: Fri, 22 Oct 2021 06:40:45 +0100 Subject: [PATCH 05/10] fix: redeaclaration error --- api/provider/linkedin.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/api/provider/linkedin.go b/api/provider/linkedin.go index 0243733b63..d0fd7b46de 100644 --- a/api/provider/linkedin.go +++ b/api/provider/linkedin.go @@ -10,9 +10,9 @@ import ( ) const ( - defaultLinkedinAPIBase = "api.linkedin.com" - endpointProfile = "/v2/me?projection=(id,firstName,lastName,profilePicture(displayImage~:playableStreams))" - endpointEmail = "/v2/emailAddress?q=members&projection=(elements*(handle~))" + 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 { @@ -135,14 +135,14 @@ func GetName(name linkedinName) string { func (g linkedinProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (*UserProvidedData, error) { var u linkedinUser - if err := makeRequest(ctx, tok, g.Config, defaultLinkedinAPIBase+endpointProfile, &u); err != nil { + 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+endpointEmail, &email); err != nil { + if err := makeRequest(ctx, tok, g.Config, defaultLinkedinAPIBase+endpointLinkedinEmail, &email); err != nil { return nil, err } From b7d150d75de8eab996085a8f542e9def40ab7a8a Mon Sep 17 00:00:00 2001 From: martindonadieu Date: Sun, 14 Nov 2021 14:47:36 +0000 Subject: [PATCH 06/10] fix: add missing env vars --- README.md | 2 +- example.env | 4 ++++ hack/test.env | 4 ++++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 335c34939b..dbf6776a95 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`, `twitch`, `linkedin` and `twitter` for external authentication. Use the names as the keys underneath `external` to configure each separately. diff --git a/example.env b/example.env index eb4ddc2c05..de0519c982 100644 --- a/example.env +++ b/example.env @@ -32,6 +32,9 @@ GOTRUE_EXTERNAL_PHONE_ENABLED="true" GOTRUE_EXTERNAL_GITHUB_ENABLED="false" GOTRUE_EXTERNAL_GITHUB_CLIENT_ID="" GOTRUE_EXTERNAL_GITHUB_SECRET="" +GOTRUE_EXTERNAL_LINKEDIN_ENABLED="false" +GOTRUE_EXTERNAL_LINKEDIN__CLIENT_ID="" +GOTRUE_EXTERNAL_LINKEDIN__SECRET="" GOTRUE_EXTERNAL_BITBUCKET_ENABLED="false" GOTRUE_EXTERNAL_BITBUCKET_CLIENT_ID="" GOTRUE_EXTERNAL_BITBUCKET_SECRET="" @@ -77,6 +80,7 @@ GOTRUE_MAILER_TEMPLATES_EMAIL_CHANGE="https://app.supabase.io/api/auth/example-p GOTRUE_EXTERNAL_DISCORD_REDIRECT_URI="http://example.com/callback" GOTRUE_EXTERNAL_GOOGLE_REDIRECT_URI="http://example.com/callback" GOTRUE_EXTERNAL_GITHUB_REDIRECT_URI="http://example.com/callback" +GOTRUE_EXTERNAL_LINKEDIN_REDIRECT_URI="http://example.com/callback" GOTRUE_EXTERNAL_BITBUCKET_REDIRECT_URI="http://example.com/callback" GOTRUE_EXTERNAL_AZURE_REDIRECT_URI="http://example.com/callback" GOTRUE_EXTERNAL_FACEBOOK_REDIRECT_URI="http://example.com/callback" diff --git a/hack/test.env b/hack/test.env index 6b5b6c25f1..dbd2ca92d9 100644 --- a/hack/test.env +++ b/hack/test.env @@ -37,6 +37,10 @@ GOTRUE_EXTERNAL_GITHUB_ENABLED=true GOTRUE_EXTERNAL_GITHUB_CLIENT_ID=testclientid GOTRUE_EXTERNAL_GITHUB_SECRET=testsecret GOTRUE_EXTERNAL_GITHUB_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_GITLAB_ENABLED=true GOTRUE_EXTERNAL_GITLAB_CLIENT_ID=testclientid GOTRUE_EXTERNAL_GITLAB_SECRET=testsecret From 433a8b46480fd7a4ec23cc9d3fc6bafbdefd3fda Mon Sep 17 00:00:00 2001 From: Martin DONADIEU Date: Fri, 19 Nov 2021 18:55:28 +0100 Subject: [PATCH 07/10] fix: update branch with @HarryET changes --- api/external.go | 2 + api/external_linkedin_test.go | 8 +-- api/provider/linkedin.go | 114 ++++++++++++---------------------- api/settings.go | 2 + api/settings_test.go | 1 + conf/configuration.go | 1 + example.env | 6 +- 7 files changed, 52 insertions(+), 82 deletions(-) diff --git a/api/external.go b/api/external.go index 81e9818cb4..f56fff6c39 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.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 726b648006..8866219123 100644 --- a/api/external_linkedin_test.go +++ b/api/external_linkedin_test.go @@ -8,7 +8,7 @@ import ( jwt "github.com/golang-jwt/jwt" ) -func (ts *ExternalTestSuite) TestSignupExternalLinkedin() { +func (ts *ExternalTestSuite) TestSignupExternalLinkedIn() { req := httptest.NewRequest(http.MethodGet, "http://localhost/authorize?provider=linkedin", nil) w := httptest.NewRecorder() ts.API.handler.ServeHTTP(w, req) @@ -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")) @@ -30,4 +30,4 @@ func (ts *ExternalTestSuite) TestSignupExternalLinkedin() { ts.Equal("linkedin", claims.Provider) ts.Equal(ts.Config.SiteURL, claims.SiteURL) -} +} \ No newline at end of file diff --git a/api/provider/linkedin.go b/api/provider/linkedin.go index d0fd7b46de..65f3844f7f 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"` @@ -91,13 +56,13 @@ type linkedinUserEmail struct { } // NewLinkedinProvider creates a Linkedin account provider. -func NewLinkedinProvider(ext conf.OAuthProviderConfiguration, scopes string) (OAuthProvider, error) { +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) + 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 +} \ No newline at end of file diff --git a/api/settings.go b/api/settings.go index 56b02a6382..71d53b1dfe 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"` @@ -45,6 +46,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 f8698749fc..7c6d8b998e 100644 --- a/api/settings_test.go +++ b/api/settings_test.go @@ -35,6 +35,7 @@ func TestSettings_DefaultProviders(t *testing.T) { require.True(t, p.Spotify) require.True(t, p.Slack) require.True(t, p.Google) + require.True(t, p.LinkedIn) require.True(t, p.GitHub) require.True(t, p.GitLab) require.True(t, p.SAML) diff --git a/conf/configuration.go b/conf/configuration.go index 0276b6f994..e126487d0a 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"` Twitter OAuthProviderConfiguration `json:"twitter"` diff --git a/example.env b/example.env index de0519c982..ce02e5bb68 100644 --- a/example.env +++ b/example.env @@ -32,9 +32,9 @@ GOTRUE_EXTERNAL_PHONE_ENABLED="true" GOTRUE_EXTERNAL_GITHUB_ENABLED="false" GOTRUE_EXTERNAL_GITHUB_CLIENT_ID="" GOTRUE_EXTERNAL_GITHUB_SECRET="" -GOTRUE_EXTERNAL_LINKEDIN_ENABLED="false" -GOTRUE_EXTERNAL_LINKEDIN__CLIENT_ID="" -GOTRUE_EXTERNAL_LINKEDIN__SECRET="" +GOTRUE_EXTERNAL_LINKEDIN_ENABLED="true" +GOTRUE_EXTERNAL_LINKEDIN_CLIENT_ID="" +GOTRUE_EXTERNAL_LINKEDIN_SECRET="" GOTRUE_EXTERNAL_BITBUCKET_ENABLED="false" GOTRUE_EXTERNAL_BITBUCKET_CLIENT_ID="" GOTRUE_EXTERNAL_BITBUCKET_SECRET="" From 7b4bf1d6e8c9f9ff747c0dcde72702dc85568901 Mon Sep 17 00:00:00 2001 From: Kang Ming Date: Tue, 4 Jan 2022 14:34:45 +0800 Subject: [PATCH 08/10] fix: invalid linkedin emailAddress struct format --- api/provider/linkedin.go | 54 +++++++++++++++++++++------------------- 1 file changed, 29 insertions(+), 25 deletions(-) diff --git a/api/provider/linkedin.go b/api/provider/linkedin.go index 65f3844f7f..5a71aa4fd4 100644 --- a/api/provider/linkedin.go +++ b/api/provider/linkedin.go @@ -20,12 +20,13 @@ type linkedinProvider struct { UserEmailUrl string } -// https://docs.microsoft.com/en-us/linkedin/consumer/integrations/self-serve/sign-in-with-linkedin?context=linkedin/consumer/context +// See https://docs.microsoft.com/en-us/linkedin/consumer/integrations/self-serve/sign-in-with-linkedin?context=linkedin/consumer/context +// for retrieving a member's profile. This requires the r_liteprofile scope. 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 + FirstName linkedinName `json:"firstName"` + LastName linkedinName `json:"lastName"` + AvatarURL struct { DisplayImage struct { Elements []struct { Identifiers []struct { @@ -36,23 +37,25 @@ type linkedinUser struct { } `json:"profilePicture"` } -type linkedinLocale struct { - Country string `json:"country"` - Language string `json:"language"` -} - type linkedinName struct { - Localized interface{} `json:"localized"` // try to catch all possible value + Localized interface{} `json:"localized"` PreferredLocale linkedinLocale `json:"preferredLocale"` } -type linkedinEmail struct { - EmailAddress string `json:"emailAddress"` +type linkedinLocale struct { + Country string `json:"country"` + Language string `json:"language"` } -type linkedinUserEmail struct { - Handle string `json:"handle"` - Handle_email linkedinEmail `json:"handle~"` +// See https://docs.microsoft.com/en-us/linkedin/consumer/integrations/self-serve/sign-in-with-linkedin?context=linkedin/consumer/context#retrieving-member-email-address +// for retrieving a member email address. This requires the r_email_address scope. +type linkedinElements struct { + Elements []struct { + Handle string `json:"handle"` + HandleTilde struct { + EmailAddress string `json:"emailAddress"` + } `json:"handle~"` + } `json:"elements"` } // NewLinkedinProvider creates a Linkedin account provider. @@ -104,31 +107,32 @@ func (g linkedinProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (* return nil, err } - var email linkedinUserEmail - if err := makeRequest(ctx, tok, g.Config, g.APIPath+"/v2/emailAddress?q=members&projection=(elements*(handle~))", &email); err != nil { + var e linkedinElements + // Note: Use primary contact api for handling phone numbers + if err := makeRequest(ctx, tok, g.Config, g.APIPath+"/v2/emailAddress?q=members&projection=(elements*(handle~))", &e); err != nil { return nil, err } + if len(e.Elements) <= 0 { + return nil, errors.New("Unable to find email with Linkedin provider") + } + emails := []Email{} - if email.Handle_email.EmailAddress != "" { + if e.Elements[0].HandleTilde.EmailAddress != "" { emails = append(emails, Email{ - Email: email.Handle_email.EmailAddress, + Email: e.Elements[0].HandleTilde.EmailAddress, Primary: true, }) } - if len(emails) <= 0 { - return nil, errors.New("Unable to find email with Linkedin provider") - } - 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, + Email: e.Elements[0].HandleTilde.EmailAddress, // To be deprecated AvatarURL: u.AvatarURL.DisplayImage.Elements[0].Identifiers[0].Identifier, @@ -137,4 +141,4 @@ func (g linkedinProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (* }, Emails: emails, }, nil -} \ No newline at end of file +} From 097d37664a3ebe78115a4171d8c437ff52b321af Mon Sep 17 00:00:00 2001 From: Kang Ming Date: Tue, 4 Jan 2022 16:34:53 +0800 Subject: [PATCH 09/10] refactor: change linkedin to lowercase --- api/external.go | 2 +- api/provider/linkedin.go | 11 ++++++----- api/settings.go | 4 ++-- api/settings_test.go | 2 +- conf/configuration.go | 2 +- 5 files changed, 11 insertions(+), 10 deletions(-) diff --git a/api/external.go b/api/external.go index ae55437e58..63db5f0b30 100644 --- a/api/external.go +++ b/api/external.go @@ -376,7 +376,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.LinkedIn, scopes) + return provider.NewLinkedinProvider(config.External.Linkedin, scopes) case "facebook": return provider.NewFacebookProvider(config.External.Facebook, scopes) case "spotify": diff --git a/api/provider/linkedin.go b/api/provider/linkedin.go index 5a71aa4fd4..6cbb5d5467 100644 --- a/api/provider/linkedin.go +++ b/api/provider/linkedin.go @@ -10,7 +10,7 @@ import ( ) const ( - defaultLinkedInAPIBase = "api.linkedin.com" + defaultLinkedinAPIBase = "api.linkedin.com" ) type linkedinProvider struct { @@ -59,13 +59,13 @@ type linkedinElements struct { } // NewLinkedinProvider creates a Linkedin account provider. -func NewLinkedInProvider(ext conf.OAuthProviderConfiguration, scopes string) (OAuthProvider, error) { +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) + apiPath := chooseHost(ext.URL, defaultLinkedinAPIBase) oauthScopes := []string{ "r_emailaddress", @@ -121,8 +121,9 @@ func (g linkedinProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (* if e.Elements[0].HandleTilde.EmailAddress != "" { emails = append(emails, Email{ - Email: e.Elements[0].HandleTilde.EmailAddress, - Primary: true, + Email: e.Elements[0].HandleTilde.EmailAddress, + Primary: true, + Verified: true, }) } diff --git a/api/settings.go b/api/settings.go index 71d53b1dfe..ab988eb0b4 100644 --- a/api/settings.go +++ b/api/settings.go @@ -10,7 +10,7 @@ type ProviderSettings struct { GitHub bool `json:"github"` GitLab bool `json:"gitlab"` Google bool `json:"google"` - LinkedIn bool `json:"linkedin"` + Linkedin bool `json:"linkedin"` Facebook bool `json:"facebook"` Spotify bool `json:"spotify"` Slack bool `json:"slack"` @@ -46,7 +46,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, + 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 7c6d8b998e..683232acd6 100644 --- a/api/settings_test.go +++ b/api/settings_test.go @@ -35,7 +35,7 @@ func TestSettings_DefaultProviders(t *testing.T) { require.True(t, p.Spotify) require.True(t, p.Slack) require.True(t, p.Google) - require.True(t, p.LinkedIn) + require.True(t, p.Linkedin) require.True(t, p.GitHub) require.True(t, p.GitLab) require.True(t, p.SAML) diff --git a/conf/configuration.go b/conf/configuration.go index faefa51328..b74e1ccba5 100644 --- a/conf/configuration.go +++ b/conf/configuration.go @@ -88,7 +88,7 @@ type ProviderConfiguration struct { Github OAuthProviderConfiguration `json:"github"` Gitlab OAuthProviderConfiguration `json:"gitlab"` Google OAuthProviderConfiguration `json:"google"` - LinkedIn OAuthProviderConfiguration `json:"linkedin"` + Linkedin OAuthProviderConfiguration `json:"linkedin"` Spotify OAuthProviderConfiguration `json:"spotify"` Slack OAuthProviderConfiguration `json:"slack"` Twitter OAuthProviderConfiguration `json:"twitter"` From bbabc41e46eb96f6a4f2125a0df0bd9a3b37bc5e Mon Sep 17 00:00:00 2001 From: Kang Ming Date: Tue, 4 Jan 2022 17:10:21 +0800 Subject: [PATCH 10/10] test: update linkedin provider tests --- api/external_linkedin_test.go | 133 +++++++++++++++++++++++++++++++++- 1 file changed, 129 insertions(+), 4 deletions(-) diff --git a/api/external_linkedin_test.go b/api/external_linkedin_test.go index 8866219123..cb53924827 100644 --- a/api/external_linkedin_test.go +++ b/api/external_linkedin_test.go @@ -1,6 +1,7 @@ package api import ( + "fmt" "net/http" "net/http/httptest" "net/url" @@ -8,7 +9,13 @@ import ( jwt "github.com/golang-jwt/jwt" ) -func (ts *ExternalTestSuite) TestSignupExternalLinkedIn() { +const ( + linkedinUser string = `{"id":"linkedinTestId","firstName":{"localized":{"en_US":"Linkedin"},"preferredLocale":{"country":"US","language":"en"}},"lastName":{"localized":{"en_US":"Test"},"preferredLocale":{"country":"US","language":"en"}},"profilePicture":{"displayImage~":{"elements":[{"identifiers":[{"identifier":"http://example.com/avatar"}]}]}}}` + linkedinEmail string = `{"elements": [{"handle": "","handle~": {"emailAddress": "linkedin@example.com"}}]}` + linkedinWrongEmail string = `{"elements": [{"handle": "","handle~": {"emailAddress": "other@example.com"}}]}` +) + +func (ts *ExternalTestSuite) TestSignupExternalLinkedin() { req := httptest.NewRequest(http.MethodGet, "http://localhost/authorize?provider=linkedin", nil) w := httptest.NewRecorder() ts.API.handler.ServeHTTP(w, req) @@ -16,8 +23,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.LinkedIn.RedirectURI, q.Get("redirect_uri")) - ts.Equal(ts.Config.External.LinkedIn.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")) @@ -30,4 +37,122 @@ func (ts *ExternalTestSuite) TestSignupExternalLinkedIn() { ts.Equal("linkedin", claims.Provider) ts.Equal(ts.Config.SiteURL, claims.SiteURL) -} \ No newline at end of file +} + +func LinkedinTestSignupSetup(ts *ExternalTestSuite, tokenCount *int, userCount *int, code string, user string, email string) *httptest.Server { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/oauth/v2/accessToken": + *tokenCount++ + ts.Equal(code, r.FormValue("code")) + ts.Equal("authorization_code", r.FormValue("grant_type")) + ts.Equal(ts.Config.External.Linkedin.RedirectURI, r.FormValue("redirect_uri")) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprint(w, `{"access_token":"linkedin_token","expires_in":100000}`) + case "/v2/me": + *userCount++ + w.Header().Add("Content-Type", "application/json") + fmt.Fprint(w, user) + case "/v2/emailAddress": + w.Header().Add("Content-Type", "application/json") + fmt.Fprint(w, email) + default: + w.WriteHeader(500) + ts.Fail("unknown linkedin oauth call %s", r.URL.Path) + } + })) + + ts.Config.External.Linkedin.URL = server.URL + + return server +} + +func (ts *ExternalTestSuite) TestSignupExternalLinkedin_AuthorizationCode() { + ts.Config.DisableSignup = false + tokenCount, userCount := 0, 0 + code := "authcode" + server := LinkedinTestSignupSetup(ts, &tokenCount, &userCount, code, linkedinUser, linkedinEmail) + defer server.Close() + + u := performAuthorization(ts, "linkedin", code, "") + + assertAuthorizationSuccess(ts, u, tokenCount, userCount, "linkedin@example.com", "Linkedin Test", "linkedinTestId", "http://example.com/avatar") +} + +func (ts *ExternalTestSuite) TestSignupExternalLinkedinDisableSignupErrorWhenNoUser() { + ts.Config.DisableSignup = true + + tokenCount, userCount := 0, 0 + code := "authcode" + server := LinkedinTestSignupSetup(ts, &tokenCount, &userCount, code, linkedinUser, linkedinEmail) + defer server.Close() + + u := performAuthorization(ts, "linkedin", code, "") + + assertAuthorizationFailure(ts, u, "Signups not allowed for this instance", "access_denied", "linkedin@example.com") +} + +func (ts *ExternalTestSuite) TestSignupExternalLinkedinDisableSignupSuccessWithPrimaryEmail() { + ts.Config.DisableSignup = true + + ts.createUser("linkedinTestId", "linkedin@example.com", "Linkedin Test", "http://example.com/avatar", "") + + tokenCount, userCount := 0, 0 + code := "authcode" + server := LinkedinTestSignupSetup(ts, &tokenCount, &userCount, code, linkedinUser, linkedinEmail) + defer server.Close() + + u := performAuthorization(ts, "linkedin", code, "") + + assertAuthorizationSuccess(ts, u, tokenCount, userCount, "linkedin@example.com", "Linkedin Test", "linkedinTestId", "http://example.com/avatar") +} + +func (ts *ExternalTestSuite) TestInviteTokenExternalLinkedinSuccessWhenMatchingToken() { + // name and avatar should be populated from Linkedin API + ts.createUser("linkedinTestId", "linkedin@example.com", "", "", "invite_token") + + tokenCount, userCount := 0, 0 + code := "authcode" + server := LinkedinTestSignupSetup(ts, &tokenCount, &userCount, code, linkedinUser, linkedinEmail) + defer server.Close() + + u := performAuthorization(ts, "linkedin", code, "invite_token") + + assertAuthorizationSuccess(ts, u, tokenCount, userCount, "linkedin@example.com", "Linkedin Test", "linkedinTestId", "http://example.com/avatar") +} + +func (ts *ExternalTestSuite) TestInviteTokenExternalLinkedinErrorWhenNoMatchingToken() { + tokenCount, userCount := 0, 0 + code := "authcode" + server := LinkedinTestSignupSetup(ts, &tokenCount, &userCount, code, linkedinUser, linkedinEmail) + defer server.Close() + + w := performAuthorizationRequest(ts, "linkedin", "invite_token") + ts.Require().Equal(http.StatusNotFound, w.Code) +} + +func (ts *ExternalTestSuite) TestInviteTokenExternalLinkedinErrorWhenWrongToken() { + ts.createUser("linkedinTestId", "linkedin@example.com", "", "", "invite_token") + + tokenCount, userCount := 0, 0 + code := "authcode" + server := LinkedinTestSignupSetup(ts, &tokenCount, &userCount, code, linkedinUser, linkedinEmail) + defer server.Close() + + w := performAuthorizationRequest(ts, "linkedin", "wrong_token") + ts.Require().Equal(http.StatusNotFound, w.Code) +} + +func (ts *ExternalTestSuite) TestInviteTokenExternalLinkedinErrorWhenEmailDoesntMatch() { + ts.createUser("linkedinTestId", "linkedin@example.com", "", "", "invite_token") + + tokenCount, userCount := 0, 0 + code := "authcode" + server := LinkedinTestSignupSetup(ts, &tokenCount, &userCount, code, linkedinUser, linkedinWrongEmail) + defer server.Close() + + u := performAuthorization(ts, "linkedin", code, "invite_token") + + assertAuthorizationFailure(ts, u, "Invited email does not match emails from external provider", "invalid_request", "") +}