diff --git a/internal/api/external.go b/internal/api/external.go index 7ad21f851f..6d8bed8c20 100644 --- a/internal/api/external.go +++ b/internal/api/external.go @@ -540,6 +540,8 @@ func (a *API) Provider(ctx context.Context, name string, scopes string) (provide return provider.NewKeycloakProvider(config.External.Keycloak, scopes) case "linkedin": return provider.NewLinkedinProvider(config.External.Linkedin, scopes) + case "linkedin_oidc": + return provider.NewLinkedinOIDCProvider(config.External.LinkedinOIDC, scopes) case "notion": return provider.NewNotionProvider(config.External.Notion) case "spotify": diff --git a/internal/api/provider/linkedin_oidc.go b/internal/api/provider/linkedin_oidc.go new file mode 100644 index 0000000000..48c1a14a85 --- /dev/null +++ b/internal/api/provider/linkedin_oidc.go @@ -0,0 +1,97 @@ +package provider + +import ( + "context" + "strings" + + "github.com/supabase/gotrue/internal/conf" + "golang.org/x/oauth2" +) + +const ( + defaultLinkedinOIDCAPIBase = "api.linkedin.com" +) + +type linkedinOIDCProvider struct { + *oauth2.Config + APIPath string + UserInfoURL string + UserEmailUrl string +} + +// See https://learn.microsoft.com/en-us/linkedin/consumer/integrations/self-serve/sign-in-with-linkedin-v2 +// for retrieving a member's profile. This requires the profile, openid, and email scope. +type linkedinOIDCUser struct { + Sub string `json:"sub"` + Email string `json:"email"` + Name string `json:"name"` + Picture string `json:"picture"` + GivenName string `json:"given_name"` + FamilyName string `json:"family_name"` + EmailVerified bool `json:"email_verified"` +} + +// NewLinkedinOIDCProvider creates a Linkedin account provider via OIDC. +func NewLinkedinOIDCProvider(ext conf.OAuthProviderConfiguration, scopes string) (OAuthProvider, error) { + if err := ext.ValidateOAuth(); err != nil { + return nil, err + } + + apiPath := chooseHost(ext.URL, defaultLinkedinOIDCAPIBase) + + oauthScopes := []string{ + "openid", + "email", + "profile", + } + + if scopes != "" { + oauthScopes = append(oauthScopes, strings.Split(scopes, ",")...) + } + + return &linkedinOIDCProvider{ + 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 linkedinOIDCProvider) GetOAuthToken(code string) (*oauth2.Token, error) { + return g.Exchange(context.Background(), code) +} + +func (g linkedinOIDCProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (*UserProvidedData, error) { + var u linkedinOIDCUser + if err := makeRequest(ctx, tok, g.Config, g.APIPath+"/v2/userinfo", &u); err != nil { + return nil, err + } + + return &UserProvidedData{ + Metadata: &Claims{ + Issuer: g.APIPath, + Subject: u.Sub, + Name: strings.TrimSpace(u.GivenName + " " + u.FamilyName), + Picture: u.Picture, + Email: u.Email, + EmailVerified: u.EmailVerified, + + // To be deprecated + AvatarURL: u.Picture, + FullName: strings.TrimSpace(u.GivenName + " " + u.FamilyName), + ProviderId: u.Sub, + }, + Emails: []Email{{ + Email: u.Email, + Verified: u.EmailVerified, + Primary: true, + }}, + }, nil +} diff --git a/internal/api/settings.go b/internal/api/settings.go index a24690eb31..d9517d2b3d 100644 --- a/internal/api/settings.go +++ b/internal/api/settings.go @@ -3,27 +3,28 @@ package api import "net/http" type ProviderSettings struct { - Apple bool `json:"apple"` - Azure bool `json:"azure"` - Bitbucket bool `json:"bitbucket"` - Discord bool `json:"discord"` - Facebook bool `json:"facebook"` - Figma bool `json:"figma"` - GitHub bool `json:"github"` - GitLab bool `json:"gitlab"` - Google bool `json:"google"` - Keycloak bool `json:"keycloak"` - Kakao bool `json:"kakao"` - Linkedin bool `json:"linkedin"` - Notion bool `json:"notion"` - Spotify bool `json:"spotify"` - Slack bool `json:"slack"` - WorkOS bool `json:"workos"` - Twitch bool `json:"twitch"` - Twitter bool `json:"twitter"` - Email bool `json:"email"` - Phone bool `json:"phone"` - Zoom bool `json:"zoom"` + Apple bool `json:"apple"` + Azure bool `json:"azure"` + Bitbucket bool `json:"bitbucket"` + Discord bool `json:"discord"` + Facebook bool `json:"facebook"` + Figma bool `json:"figma"` + GitHub bool `json:"github"` + GitLab bool `json:"gitlab"` + Google bool `json:"google"` + Keycloak bool `json:"keycloak"` + Kakao bool `json:"kakao"` + Linkedin bool `json:"linkedin"` + LinkedinOIDC bool `json:"linkedin_oidc"` + Notion bool `json:"notion"` + Spotify bool `json:"spotify"` + Slack bool `json:"slack"` + WorkOS bool `json:"workos"` + Twitch bool `json:"twitch"` + Twitter bool `json:"twitter"` + Email bool `json:"email"` + Phone bool `json:"phone"` + Zoom bool `json:"zoom"` } type Settings struct { @@ -41,27 +42,28 @@ func (a *API) Settings(w http.ResponseWriter, r *http.Request) error { return sendJSON(w, http.StatusOK, &Settings{ ExternalProviders: ProviderSettings{ - Apple: config.External.Apple.Enabled, - Azure: config.External.Azure.Enabled, - Bitbucket: config.External.Bitbucket.Enabled, - Discord: config.External.Discord.Enabled, - Facebook: config.External.Facebook.Enabled, - Figma: config.External.Figma.Enabled, - GitHub: config.External.Github.Enabled, - GitLab: config.External.Gitlab.Enabled, - Google: config.External.Google.Enabled, - Kakao: config.External.Kakao.Enabled, - Keycloak: config.External.Keycloak.Enabled, - Linkedin: config.External.Linkedin.Enabled, - Notion: config.External.Notion.Enabled, - Spotify: config.External.Spotify.Enabled, - Slack: config.External.Slack.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, - Zoom: config.External.Zoom.Enabled, + Apple: config.External.Apple.Enabled, + Azure: config.External.Azure.Enabled, + Bitbucket: config.External.Bitbucket.Enabled, + Discord: config.External.Discord.Enabled, + Facebook: config.External.Facebook.Enabled, + Figma: config.External.Figma.Enabled, + GitHub: config.External.Github.Enabled, + GitLab: config.External.Gitlab.Enabled, + Google: config.External.Google.Enabled, + Kakao: config.External.Kakao.Enabled, + Keycloak: config.External.Keycloak.Enabled, + Linkedin: config.External.Linkedin.Enabled, + LinkedinOIDC: config.External.LinkedinOIDC.Enabled, + Notion: config.External.Notion.Enabled, + Spotify: config.External.Spotify.Enabled, + Slack: config.External.Slack.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, + Zoom: config.External.Zoom.Enabled, }, DisableSignup: config.DisableSignup, diff --git a/internal/conf/configuration.go b/internal/conf/configuration.go index 588b53ccae..f2846e2815 100644 --- a/internal/conf/configuration.go +++ b/internal/conf/configuration.go @@ -196,6 +196,7 @@ type ProviderConfiguration struct { Notion OAuthProviderConfiguration `json:"notion"` Keycloak OAuthProviderConfiguration `json:"keycloak"` Linkedin OAuthProviderConfiguration `json:"linkedin"` + LinkedinOIDC OAuthProviderConfiguration `json:"linkedin_oidc" envconfig:"LINKEDIN_OIDC"` Spotify OAuthProviderConfiguration `json:"spotify"` Slack OAuthProviderConfiguration `json:"slack"` Twitter OAuthProviderConfiguration `json:"twitter"`