package provider import ( "context" "strings" "github.com/supabase/auth/internal/conf" "golang.org/x/oauth2" ) const ( defaultLinkedinAPIBase = "api.linkedin.com" ) type linkedinProvider struct { *oauth2.Config APIPath string UserInfoURL string UserEmailUrl string } // 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"` LastName linkedinName `json:"lastName"` AvatarURL struct { DisplayImage struct { Elements []struct { Identifiers []struct { Identifier string `json:"identifier"` } `json:"identifiers"` } `json:"elements"` } `json:"displayImage~"` } `json:"profilePicture"` } func (u *linkedinUser) getAvatarUrl() string { avatarURL := "" if len(u.AvatarURL.DisplayImage.Elements) > 0 { avatarURL = u.AvatarURL.DisplayImage.Elements[0].Identifiers[0].Identifier } return avatarURL } type linkedinName struct { Localized interface{} `json:"localized"` PreferredLocale linkedinLocale `json:"preferredLocale"` } type linkedinLocale struct { Country string `json:"country"` Language string `json:"language"` } // 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. func NewLinkedinProvider(ext conf.OAuthProviderConfiguration, scopes string) (OAuthProvider, error) { if err := ext.ValidateOAuth(); err != nil { return nil, err } 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[0], ClientSecret: ext.Secret, Endpoint: oauth2.Endpoint{ AuthURL: apiPath + "/oauth/v2/authorization", TokenURL: apiPath + "/oauth/v2/accessToken", }, Scopes: oauthScopes, RedirectURL: ext.RedirectURI, }, APIPath: apiPath, }, nil } func (g linkedinProvider) GetOAuthToken(code string) (*oauth2.Token, error) { return g.Exchange(context.Background(), code) } func GetName(name linkedinName) string { key := name.PreferredLocale.Language + "_" + name.PreferredLocale.Country 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, g.APIPath+"/v2/me?projection=(id,firstName,lastName,profilePicture(displayImage~:playableStreams))", &u); err != nil { return nil, err } 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 } data := &UserProvidedData{} if e.Elements[0].HandleTilde.EmailAddress != "" { // linkedin only returns the primary email which is verified for the r_emailaddress scope. data.Emails = []Email{{ Email: e.Elements[0].HandleTilde.EmailAddress, Primary: true, Verified: true, }} } avatarURL := u.getAvatarUrl() data.Metadata = &Claims{ Issuer: g.APIPath, Subject: u.ID, Name: strings.TrimSpace(GetName(u.FirstName) + " " + GetName(u.LastName)), Picture: avatarURL, Email: e.Elements[0].HandleTilde.EmailAddress, EmailVerified: true, // To be deprecated AvatarURL: avatarURL, FullName: strings.TrimSpace(GetName(u.FirstName) + " " + GetName(u.LastName)), ProviderId: u.ID, } return data, nil }