Skip to content

Commit 519a5ed

Browse files
feat: added linkedin
1 parent b987ca9 commit 519a5ed

File tree

9 files changed

+452
-2
lines changed

9 files changed

+452
-2
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ making login easier.More third-party authorization are under development.
4848
* Google
4949
* Facebook
5050
* Slack
51+
* Linkedin
5152

5253
## Getting Started
5354

README.zh-Hans.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
* Google
5050
* Facebook
5151
* Slack
52+
* Linkedin
5253

5354
## 快速入门
5455

adapters/linkedin/request.go

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
package linkedin
2+
3+
import (
4+
"encoding/json"
5+
"errors"
6+
"github.com/LeoInnovateLab/gauth"
7+
"github.com/LeoInnovateLab/gauth/config"
8+
"github.com/LeoInnovateLab/gauth/utils"
9+
"github.com/go-resty/resty/v2"
10+
"log"
11+
)
12+
13+
type AuthRequest struct {
14+
*gauth.DefaultAuthRequest
15+
}
16+
17+
func (a *AuthRequest) NewAuthRequest(c *config.AuthConfig) (gauth.AuthRequest, error) {
18+
authRequest := &AuthRequest{}
19+
authRequest.DefaultAuthRequest = gauth.NewDefaultAuthRequest(c, NewLinkedin(), authRequest)
20+
21+
// Ensure that the AuthRequest instance has implemented all methods of the AuthRequest interface
22+
var _ gauth.AuthRequest = authRequest
23+
24+
return authRequest, nil
25+
}
26+
27+
func (a *AuthRequest) Authorize(state string) (string, error) {
28+
url, err := a.DefaultAuthRequest.Authorize(state)
29+
if err != nil {
30+
return "", err
31+
}
32+
scope := Scope{}
33+
return utils.UrlBuilderFromBaseUrl(url).
34+
QueryParam("scope", a.GetScopes(" ", true, scope.GetDefaultScopes())).
35+
Build(false), nil
36+
}
37+
38+
func (a *AuthRequest) GetAccessToken(callback gauth.AuthCallback) (gauth.AuthToken, error) {
39+
accessTokenUrl := a.DefaultAuthRequest.AccessTokenUrl(callback.Code)
40+
41+
resp, err := resty.New().R().
42+
SetHeader("Host", "www.linkedin.com").
43+
Post(accessTokenUrl)
44+
if err != nil {
45+
return gauth.AuthToken{}, gauth.ErrGetAccessToken
46+
}
47+
48+
var responseMap map[string]interface{}
49+
err = json.Unmarshal(resp.Body(), &responseMap)
50+
if err != nil {
51+
return gauth.AuthToken{}, gauth.ErrGetAccessToken
52+
}
53+
54+
if _, ok := responseMap["error"]; ok {
55+
return gauth.AuthToken{}, errors.New(responseMap["error_description"].(string))
56+
}
57+
58+
return gauth.AuthToken{
59+
AccessToken: responseMap["access_token"].(string),
60+
ExpireIn: int(responseMap["expires_in"].(float64)),
61+
RefreshToken: responseMap["refresh_token"].(string),
62+
}, nil
63+
}
64+
65+
func (a *AuthRequest) GetUserInfo(token gauth.AuthToken) (gauth.AuthUser, error) {
66+
userinfoUrl := utils.UrlBuilderFromBaseUrl(a.Source.UserInfo()).
67+
QueryParam("projection", "(id,firstName,lastName,profilePicture(displayImage~:playableStreams))").
68+
Build(false)
69+
70+
resp, err := resty.New().R().
71+
SetHeader("Authorization", "Bearer "+token.AccessToken).
72+
SetHeader("Host", "api.linkedin.com").
73+
SetHeader("Connection", "Keep-Alive").
74+
Post(userinfoUrl)
75+
if err != nil {
76+
return gauth.AuthUser{}, gauth.ErrGetUserInfo
77+
}
78+
79+
var resultMap map[string]interface{}
80+
json.Unmarshal(resp.Body(), &resultMap)
81+
rawUserInfo := string(resp.Body())
82+
83+
if _, ok := resultMap["error"]; ok {
84+
return gauth.AuthUser{}, errors.New(resultMap["error_description"].(string))
85+
}
86+
87+
userName := getUserName(resultMap)
88+
avatar := getAvatar(resultMap)
89+
email, err := getEmail(token.AccessToken)
90+
if err != nil {
91+
log.Printf("Error getting email: %v", err)
92+
}
93+
94+
return gauth.AuthUser{
95+
RawUserInfo: rawUserInfo,
96+
UID: resultMap["id"].(string),
97+
Username: userName,
98+
Nickname: userName,
99+
Avatar: avatar,
100+
Email: email,
101+
Gender: gauth.GenderUnknown,
102+
Token: token,
103+
Source: a.Source.GetSource(),
104+
}, nil
105+
}
106+
107+
func getEmail(accessToken string) (string, error) {
108+
resp, err := resty.New().R().
109+
SetHeader("Authorization", "Bearer "+accessToken).
110+
SetHeader("Host", "api.linkedin.com").
111+
SetHeader("Connection", "Keep-Alive").
112+
Get("https://api.linkedin.com/v2/emailAddress?q=members&projection=(elements*(handle~))")
113+
114+
if err != nil {
115+
return "", gauth.ErrGetUserInfo
116+
}
117+
118+
var resultMap map[string]interface{}
119+
json.Unmarshal(resp.Body(), &resultMap)
120+
121+
if _, ok := resultMap["error"]; ok {
122+
return "", errors.New(resultMap["error_description"].(string))
123+
}
124+
125+
elements, ok := resultMap["elements"].([]interface{})
126+
if !ok || len(elements) == 0 {
127+
return "", gauth.ErrGetUserInfo
128+
}
129+
130+
handleObj, ok := elements[0].(map[string]interface{})
131+
if !ok {
132+
return "", gauth.ErrGetUserInfo
133+
}
134+
135+
emailAddress, ok := handleObj["emailAddress"].(string)
136+
if !ok {
137+
return "", gauth.ErrGetUserInfo
138+
}
139+
140+
return emailAddress, nil
141+
}
142+
143+
func getAvatar(resultMap map[string]interface{}) string {
144+
profilePictureObj, ok := resultMap["profilePicture"].(map[string]interface{})
145+
if !ok {
146+
return ""
147+
}
148+
149+
displayImageObj, ok := profilePictureObj["displayImage~"].(map[string]interface{})
150+
if !ok {
151+
return ""
152+
}
153+
154+
elements, ok := displayImageObj["elements"].([]interface{})
155+
if !ok || len(elements) == 0 {
156+
return ""
157+
}
158+
159+
largestImageObj, ok := elements[len(elements)-1].(map[string]interface{})
160+
if !ok {
161+
return ""
162+
}
163+
164+
identifiers, ok := largestImageObj["identifiers"].([]interface{})
165+
if !ok || len(identifiers) == 0 {
166+
return ""
167+
}
168+
169+
identifierObj, ok := identifiers[0].(map[string]interface{})
170+
if !ok {
171+
return ""
172+
}
173+
174+
identifier, ok := identifierObj["identifier"].(string)
175+
if !ok {
176+
return ""
177+
}
178+
179+
return identifier
180+
}
181+
182+
func getUserName(resultMap map[string]interface{}) string {
183+
firstName, ok := resultMap["localizedFirstName"].(string)
184+
if !ok {
185+
firstName = getUserNameByKey(resultMap, "firstName")
186+
}
187+
188+
lastName, ok := resultMap["localizedLastName"].(string)
189+
if !ok {
190+
lastName = getUserNameByKey(resultMap, "lastName")
191+
}
192+
193+
return firstName + " " + lastName
194+
}
195+
196+
func getUserNameByKey(resultMap map[string]interface{}, nameKey string) string {
197+
nameObj, ok := resultMap[nameKey].(map[string]interface{})
198+
if !ok {
199+
return ""
200+
}
201+
202+
localizedObj, ok := nameObj["localized"].(map[string]interface{})
203+
if !ok {
204+
return ""
205+
}
206+
207+
preferredLocaleObj, ok := nameObj["preferredLocale"].(map[string]interface{})
208+
if !ok {
209+
return ""
210+
}
211+
212+
language, ok := preferredLocaleObj["language"].(string)
213+
if !ok {
214+
return ""
215+
}
216+
217+
country, ok := preferredLocaleObj["country"].(string)
218+
if !ok {
219+
return ""
220+
}
221+
222+
name, ok := localizedObj[language+"_"+country].(string)
223+
if !ok {
224+
return ""
225+
}
226+
227+
return name
228+
}

adapters/linkedin/request_test.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package linkedin_test
2+
3+
import (
4+
"cmp"
5+
"github.com/LeoInnovateLab/gauth"
6+
_ "github.com/LeoInnovateLab/gauth/register"
7+
"github.com/LeoInnovateLab/gauth/utils"
8+
"github.com/joho/godotenv"
9+
"github.com/stretchr/testify/assert"
10+
"log"
11+
"net/url"
12+
"os"
13+
"testing"
14+
)
15+
16+
var (
17+
clientID, clientSecret string
18+
)
19+
20+
func init() {
21+
godotenv.Load("../../.env.local")
22+
23+
clientID = cmp.Or(os.Getenv("LINKIN_CLIENT_ID"), "client_id")
24+
clientSecret = cmp.Or(os.Getenv("LINKIN_SECRET"), "secret")
25+
}
26+
27+
func getMyLinkedin() gauth.AuthRequest {
28+
r, err := gauth.New().
29+
Source("linkedin").
30+
ClientId(clientID).
31+
ClientSecret(clientSecret).
32+
RedirectUrl("http://localhost:8080/auth/linkedin/callback").
33+
Build()
34+
if err != nil {
35+
log.Fatalf("Failed to build request:%v", err)
36+
}
37+
return r
38+
}
39+
40+
func TestLinkedinAuthRequest_Authorize(t *testing.T) {
41+
r := getMyLinkedin()
42+
43+
state := utils.CreateState()
44+
authorizeUrl, err := r.Authorize(state)
45+
46+
assert.Nil(t, err)
47+
48+
u, authErr := url.Parse(authorizeUrl)
49+
assert.NoError(t, authErr)
50+
assert.Equal(t, "www.linkedin.com", u.Host)
51+
values := u.Query()
52+
assert.Equal(t, clientID, values.Get("client_id"))
53+
assert.Equal(t, state, values.Get("state"))
54+
assert.Equal(t, "http://localhost:8080/auth/linkedin/callback", values.Get("redirect_uri"))
55+
assert.Equal(t, "r_liteprofile r_emailaddress w_member_social", values.Get("scope"))
56+
}

0 commit comments

Comments
 (0)