From 5faadfc0ce7165fca3f18d5f18b7a7ce616a0245 Mon Sep 17 00:00:00 2001 From: Paul Greenberg Date: Fri, 15 Mar 2024 22:29:02 -0400 Subject: [PATCH] feature: add linkedin oauth provider --- pkg/idp/oauth/authenticate.go | 2 +- pkg/idp/oauth/config.go | 11 +++++++++-- pkg/idp/oauth/provider.go | 2 ++ pkg/idp/oauth/user.go | 30 +++++++++++++++++++++++------- 4 files changed, 35 insertions(+), 10 deletions(-) diff --git a/pkg/idp/oauth/authenticate.go b/pkg/idp/oauth/authenticate.go index 21a4a62..d95ecd3 100644 --- a/pkg/idp/oauth/authenticate.go +++ b/pkg/idp/oauth/authenticate.go @@ -125,7 +125,7 @@ func (b *IdentityProvider) Authenticate(r *requests.Request) error { var m map[string]interface{} switch b.config.Driver { - case "github", "gitlab", "facebook", "discord": + case "github", "gitlab", "facebook", "discord", "linkedin": m, err = b.fetchClaims(accessToken) if err != nil { return errors.ErrIdentityProviderOauthFetchClaimsFailed.WithArgs(err) diff --git a/pkg/idp/oauth/config.go b/pkg/idp/oauth/config.go index 0ba5d70..1094797 100644 --- a/pkg/idp/oauth/config.go +++ b/pkg/idp/oauth/config.go @@ -102,8 +102,8 @@ type Config struct { // LoginIcon is the UI login icon attributes. LoginIcon *icons.LoginIcon `json:"login_icon,omitempty" xml:"login_icon,omitempty" yaml:"login_icon,omitempty"` - UserInfoFields []string `json:"user_info_fields,omitempty" xml:"user_info_fields,omitempty" yaml:"user_info_fields,omitempty"` - UserInfoRolesFieldName string `json:"user_info_roles_field_name,omitempty" xml:"user_info_roles_field_name,omitempty" yaml:"user_info_roles_field_name,omitempty"` + UserInfoFields []string `json:"user_info_fields,omitempty" xml:"user_info_fields,omitempty" yaml:"user_info_fields,omitempty"` + UserInfoRolesFieldName string `json:"user_info_roles_field_name,omitempty" xml:"user_info_roles_field_name,omitempty" yaml:"user_info_roles_field_name,omitempty"` // The name of the cookie storing id_token from OAuth provider. IdentityTokenCookieName string `json:"identity_token_cookie_name,omitempty" xml:"identity_token_cookie_name,omitempty" yaml:"identity_token_cookie_name,omitempty"` @@ -161,6 +161,8 @@ func (cfg *Config) Validate() error { cfg.Scopes = []string{"openid", "email", "profile"} case "discord": cfg.Scopes = []string{"identify"} + case "linkedin": + cfg.Scopes = []string{"openid", "email", "profile"} default: cfg.Scopes = []string{"openid", "email", "profile"} } @@ -250,6 +252,11 @@ func (cfg *Config) Validate() error { cfg.AuthorizationURL = "https://discord.com/oauth2/authorize" cfg.TokenURL = "https://discord.com/api/oauth2/token" cfg.RequiredTokenFields = []string{"access_token"} + case "linkedin": + if cfg.BaseAuthURL == "" { + cfg.BaseAuthURL = "https://www.linkedin.com/oauth/" + cfg.MetadataURL = cfg.BaseAuthURL + ".well-known/openid-configuration" + } case "generic": case "": return errors.ErrIdentityProviderConfig.WithArgs("driver name not found") diff --git a/pkg/idp/oauth/provider.go b/pkg/idp/oauth/provider.go index 687bc6f..ca0aef7 100644 --- a/pkg/idp/oauth/provider.go +++ b/pkg/idp/oauth/provider.go @@ -212,6 +212,8 @@ func (b *IdentityProvider) Configure() error { b.disableKeyVerification = true b.disableNonce = true b.enableAcceptHeader = true + case "linkedin": + b.disableNonce = true case "nextcloud": b.disableKeyVerification = true } diff --git a/pkg/idp/oauth/user.go b/pkg/idp/oauth/user.go index 7a7dd97..451ba0f 100644 --- a/pkg/idp/oauth/user.go +++ b/pkg/idp/oauth/user.go @@ -127,6 +127,8 @@ func (b *IdentityProvider) fetchClaims(tokenData map[string]interface{}) (map[st switch b.config.Driver { case "github": userURL = "https://api.github.com/user" + case "linkedin": + userURL = "https://api.linkedin.com/v2/userinfo" case "gitlab": userURL = b.userInfoURL case "facebook": @@ -137,7 +139,7 @@ func (b *IdentityProvider) fetchClaims(tokenData map[string]interface{}) (map[st // Setup http request for the URL. switch b.config.Driver { - case "github", "gitlab", "discord": + case "github", "gitlab", "discord", "linkedin": req, err = http.NewRequest("GET", userURL, nil) if err != nil { return nil, err @@ -165,7 +167,7 @@ func (b *IdentityProvider) fetchClaims(tokenData map[string]interface{}) (map[st switch b.config.Driver { case "github": req.Header.Add("Authorization", "token "+tokenString) - case "gitlab", "discord": + case "gitlab", "discord", "linkedin": req.Header.Add("Authorization", "Bearer "+tokenString) } @@ -193,6 +195,10 @@ func (b *IdentityProvider) fetchClaims(tokenData map[string]interface{}) (map[st } switch b.config.Driver { + case "linkedin": + if _, exists := data["sub"]; !exists { + return nil, fmt.Errorf("failed obtaining user profile with OAuth 2.0 access token, profile field not found") + } case "gitlab": if _, exists := data["profile"]; !exists { return nil, fmt.Errorf("failed obtaining user profile with OAuth 2.0 access token, profile field not found") @@ -205,7 +211,7 @@ func (b *IdentityProvider) fetchClaims(tokenData map[string]interface{}) (map[st return nil, fmt.Errorf("failed obtaining user profile with OAuth 2.0 access token, login field not found") } case "discord": - if _,exists := data["id"]; !exists { + if _, exists := data["id"]; !exists { return nil, fmt.Errorf("failed obtaining user profile with OAuth 2.0 access token, id field not found") } case "facebook": @@ -367,6 +373,16 @@ func (b *IdentityProvider) fetchClaims(tokenData map[string]interface{}) (map[st } m["sub"] = data["id"] m["name"] = data["name"] + case "linkedin": + for _, k := range []string{"name", "picture", "sub", "email"} { + if _, exists := data[k]; !exists { + continue + } + switch v := data[k].(type) { + case string: + m[k] = v + } + } } if len(userGroups) > 0 { @@ -391,7 +407,7 @@ func (b *IdentityProvider) fetchDiscordGuilds(authToken string) (*userData, erro return nil, err } req.Header.Set("Accept", "application/json") - req.Header.Add("Authorization", "Bearer " + authToken) + req.Header.Add("Authorization", "Bearer "+authToken) // Fetch data from the URL. resp, err := cli.Do(req) @@ -443,9 +459,9 @@ func (b *IdentityProvider) fetchDiscordGuilds(authToken string) (*userData, erro "Error converting Guild permissions to integer", zap.Any("error", err), ) - } else if (perm & 0x08) == 0x08 { // Check for admin privileges + } else if (perm & 0x08) == 0x08 { // Check for admin privileges data.Groups = append(data.Groups, fmt.Sprintf("discord.com/%s/admins", guildID)) - } + } } data.Groups = append(data.Groups, fmt.Sprintf("discord.com/%s/members", guildID)) @@ -457,7 +473,7 @@ func (b *IdentityProvider) fetchDiscordGuilds(authToken string) (*userData, erro return nil, err } req.Header.Set("Accept", "application/json") - req.Header.Add("Authorization", "Bearer " + authToken) + req.Header.Add("Authorization", "Bearer "+authToken) resp, err = cli.Do(req) if err != nil {