Newer
Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// Package linkedin provides authentication strategies using LinkedIn
package linkedin
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"strings"
"golang.org/x/oauth2"
"github.com/coreos/dex/connector"
"github.com/sirupsen/logrus"
)
const (
apiURL = "https://api.linkedin.com/v1"
authURL = "https://www.linkedin.com/oauth/v2/authorization"
tokenURL = "https://www.linkedin.com/oauth/v2/accessToken"
)
// Config holds configuration options for LinkedIn logins.
type Config struct {
ClientID string `json:"clientID"`
ClientSecret string `json:"clientSecret"`
RedirectURI string `json:"redirectURI"`
}
// Open returns a strategy for logging in through LinkedIn
func (c *Config) Open(id string, logger logrus.FieldLogger) (connector.Connector, error) {
return &linkedInConnector{
oauth2Config: &oauth2.Config{
ClientID: c.ClientID,
ClientSecret: c.ClientSecret,
Endpoint: oauth2.Endpoint{
AuthURL: authURL,
TokenURL: tokenURL,
},
Scopes: []string{"r_basicprofile", "r_emailaddress"},
RedirectURL: c.RedirectURI,
},
logger: logger,
}, nil
}
type connectorData struct {
AccessToken string `json:"accessToken"`
}
type linkedInConnector struct {
oauth2Config *oauth2.Config
logger logrus.FieldLogger
}
// LinkedIn doesn't provide refresh tokens, so refresh tokens issued by Dex
// will expire in 60 days (default LinkedIn token lifetime).
var (
_ connector.CallbackConnector = (*linkedInConnector)(nil)
_ connector.RefreshConnector = (*linkedInConnector)(nil)
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
)
// LoginURL returns an access token request URL
func (c *linkedInConnector) LoginURL(scopes connector.Scopes, callbackURL, state string) (string, error) {
if c.oauth2Config.RedirectURL != callbackURL {
return "", fmt.Errorf("expected callback URL %q did not match the URL in the config %q",
callbackURL, c.oauth2Config.RedirectURL)
}
return c.oauth2Config.AuthCodeURL(state), nil
}
// HandleCallback handles HTTP redirect from LinkedIn
func (c *linkedInConnector) HandleCallback(s connector.Scopes, r *http.Request) (identity connector.Identity, err error) {
q := r.URL.Query()
if errType := q.Get("error"); errType != "" {
return identity, &oauth2Error{errType, q.Get("error_description")}
}
ctx := r.Context()
token, err := c.oauth2Config.Exchange(ctx, q.Get("code"))
if err != nil {
return identity, fmt.Errorf("linkedin: get token: %v", err)
}
client := c.oauth2Config.Client(ctx, token)
profile, err := c.profile(ctx, client)
if err != nil {
return identity, fmt.Errorf("linkedin: get profile: %v", err)
}
identity = connector.Identity{
UserID: profile.ID,
Username: profile.fullname(),
Email: profile.Email,
EmailVerified: true,
}
if s.OfflineAccess {
data := connectorData{AccessToken: token.AccessToken}
connData, err := json.Marshal(data)
if err != nil {
return identity, fmt.Errorf("linkedin: marshal connector data: %v", err)
}
identity.ConnectorData = connData
}
return identity, nil
}
func (c *linkedInConnector) Refresh(ctx context.Context, s connector.Scopes, ident connector.Identity) (connector.Identity, error) {
if len(ident.ConnectorData) == 0 {
return ident, fmt.Errorf("linkedin: no upstream access token found")
}
var data connectorData
if err := json.Unmarshal(ident.ConnectorData, &data); err != nil {
return ident, fmt.Errorf("linkedin: unmarshal access token: %v", err)
}
client := c.oauth2Config.Client(ctx, &oauth2.Token{AccessToken: data.AccessToken})
profile, err := c.profile(ctx, client)
if err != nil {
return ident, fmt.Errorf("linkedin: get profile: %v", err)
}
ident.Username = profile.fullname()
ident.Email = profile.Email
return ident, nil
}
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
type profile struct {
ID string `json:"id"`
FirstName string `json:"firstName"`
LastName string `json:"lastName"`
Email string `json:"emailAddress"`
}
// fullname returns a full name of a person, or email if the resulting name is
// empty
func (p profile) fullname() string {
fname := strings.TrimSpace(p.FirstName + " " + p.LastName)
if fname == "" {
return p.Email
}
return fname
}
func (c *linkedInConnector) profile(ctx context.Context, client *http.Client) (p profile, err error) {
// https://developer.linkedin.com/docs/fields/basic-profile
req, err := http.NewRequest("GET", apiURL+"/people/~:(id,first-name,last-name,email-address)", nil)
if err != nil {
return p, fmt.Errorf("new req: %v", err)
}
q := req.URL.Query()
q.Add("format", "json")
req.URL.RawQuery = q.Encode()
resp, err := client.Do(req.WithContext(ctx))
if err != nil {
return p, fmt.Errorf("get URL %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return p, fmt.Errorf("read body: %v", err)
}
return p, fmt.Errorf("%s: %s", resp.Status, body)
}
if err := json.NewDecoder(resp.Body).Decode(&p); err != nil {
return p, fmt.Errorf("JSON decode: %v", err)
}
if p.Email == "" {
return p, fmt.Errorf("email is not set")
}
return p, err
}
type oauth2Error struct {
error string
errorDescription string
}
func (e *oauth2Error) Error() string {
if e.errorDescription == "" {
return e.error
}
return e.error + ": " + e.errorDescription
}