// Copyright © 2023 Ory Corp // SPDX-License-Identifier: Apache-2.0 package oidc import ( "context" "encoding/json" "net/http" "net/url" "github.com/ory/x/otelx" "github.com/hashicorp/go-retryablehttp" "github.com/pkg/errors" "golang.org/x/oauth2" "golang.org/x/oauth2/linkedin" "github.com/ory/herodot" "github.com/ory/x/httpx" ) type LinkedInProfile struct { LocalizedLastName string `json:"localizedLastName"` LocalizedFirstName string `json:"localizedFirstName"` ProfilePicture *struct { DisplayImage struct { Elements []struct { Identifiers []struct { Identifier string `json:"identifier"` } `json:"identifiers"` } `json:"elements"` } `json:"displayImage~"` } `json:"profilePicture,omitempty"` ID string `json:"id"` } type LinkedInEmail struct { Elements []struct { Handle struct { EmailAddress string `json:"emailAddress"` } `json:"handle~"` HandleUrn string `json:"handle"` } `json:"elements"` } type LinkedInIntrospection struct { Active bool `json:"active"` ClientID string `json:"client_id"` AuthorizedAt uint32 `json:"authorized_at"` CreatedAt uint32 `json:"created_at"` ExpiresAt uint32 `json:"expires_at"` Status string `json:"status"` Scope string `json:"scope"` AuthType string `json:"auth_type"` } // type APIUrl string const ( ProfileURL string = "https://api.linkedin.com/v2/me?projection=(id,localizedFirstName,localizedLastName,profilePicture(displayImage~digitalmediaAsset:playableStreams))" EmailURL string = "https://api.linkedin.com/v2/emailAddress?q=members&projection=(elements*(handle~))" IntrospectionURL string = "https://www.linkedin.com/oauth/v2/introspectToken" ) var _ OAuth2Provider = (*ProviderLinkedIn)(nil) type ProviderLinkedIn struct { config *Configuration reg Dependencies } func NewProviderLinkedIn( config *Configuration, reg Dependencies, ) Provider { return &ProviderLinkedIn{ config: config, reg: reg, } } func (l *ProviderLinkedIn) Config() *Configuration { return l.config } func (l *ProviderLinkedIn) oauth2(ctx context.Context) *oauth2.Config { return &oauth2.Config{ ClientID: l.config.ClientID, ClientSecret: l.config.ClientSecret, Endpoint: linkedin.Endpoint, Scopes: l.config.Scope, RedirectURL: l.config.Redir(l.reg.Config().OIDCRedirectURIBase(ctx)), } } func (l *ProviderLinkedIn) OAuth2(ctx context.Context) (*oauth2.Config, error) { return l.oauth2(ctx), nil } func (l *ProviderLinkedIn) AuthCodeURLOptions(r ider) []oauth2.AuthCodeOption { return []oauth2.AuthCodeOption{} } func (l *ProviderLinkedIn) fetch(ctx context.Context, client *retryablehttp.Client, url string, result interface{}) (err error) { ctx, span := l.reg.Tracer(ctx).Tracer().Start(ctx, "selfservice.strategy.oidc.ProviderLinkedIn.fetch") defer otelx.End(span, &err) req, err := retryablehttp.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return errors.WithStack(err) } res, err := client.Do(req) if err != nil { return errors.WithStack(err) } defer func() { _ = res.Body.Close() }() if err := logUpstreamError(l.reg.Logger(), res); err != nil { return err } if err := json.NewDecoder(res.Body).Decode(result); err != nil { return errors.WithStack(err) } return nil } func (l *ProviderLinkedIn) Profile(ctx context.Context, client *retryablehttp.Client) (*LinkedInProfile, error) { var result LinkedInProfile if err := l.fetch(ctx, client, ProfileURL, &result); err != nil { return nil, errors.WithStack(err) } return &result, nil } func (l *ProviderLinkedIn) Email(ctx context.Context, client *retryablehttp.Client) (*LinkedInEmail, error) { var result LinkedInEmail if err := l.fetch(ctx, client, EmailURL, &result); err != nil { return nil, errors.WithStack(err) } return &result, nil } func (l *ProviderLinkedIn) ProfilePicture(profile *LinkedInProfile) string { if profile.ProfilePicture == nil { return "" } elements := profile.ProfilePicture.DisplayImage.Elements i := len(elements) if i == 0 { return "" } else if i > 3 { i = 3 } identifiers := elements[i-1].Identifiers if len(identifiers) == 0 { return "" } return identifiers[0].Identifier } func (l *ProviderLinkedIn) Claims(ctx context.Context, exchange *oauth2.Token, query url.Values) (_ *Claims, err error) { ctx, span := l.reg.Tracer(ctx).Tracer().Start(ctx, "selfservice.strategy.oidc.ProviderLinkedIn.Claims") defer otelx.End(span, &err) o, err := l.OAuth2(ctx) if err != nil { return nil, err } ctx, client := httpx.SetOAuth2(ctx, l.reg.HTTPClient(ctx), o, exchange) profile, err := l.Profile(ctx, client) if err != nil { return nil, errors.WithStack(herodot.ErrUpstreamError().WithWrap(err).WithReasonf("%s", err)) } email, err := l.Email(ctx, client) if err != nil { return nil, errors.WithStack(herodot.ErrUpstreamError().WithWrap(err).WithReasonf("%s", err)) } claims := &Claims{ Subject: profile.ID, Issuer: "https://login.linkedin.com/", Email: email.Elements[0].Handle.EmailAddress, GivenName: profile.LocalizedFirstName, LastName: profile.LocalizedLastName, Picture: l.ProfilePicture(profile), } return claims, nil }