2022-02-13 23:39:27 +08:00
// Copyright 2021 The Casdoor Authors. All Rights Reserved.
2021-07-03 20:53:38 +08:00
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package idp
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
"golang.org/x/oauth2"
)
type LinkedInIdProvider struct {
Client * http . Client
Config * oauth2 . Config
}
func NewLinkedInIdProvider ( clientId string , clientSecret string , redirectUrl string ) * LinkedInIdProvider {
idp := & LinkedInIdProvider { }
config := idp . getConfig ( clientId , clientSecret , redirectUrl )
idp . Config = config
return idp
}
func ( idp * LinkedInIdProvider ) SetHttpClient ( client * http . Client ) {
idp . Client = client
}
// getConfig return a point of Config, which describes a typical 3-legged OAuth2 flow
func ( idp * LinkedInIdProvider ) getConfig ( clientId string , clientSecret string , redirectUrl string ) * oauth2 . Config {
2022-08-07 12:26:14 +08:00
endpoint := oauth2 . Endpoint {
2021-07-03 20:53:38 +08:00
TokenURL : "https://www.linkedIn.com/oauth/v2/accessToken" ,
}
2022-08-07 12:26:14 +08:00
config := & oauth2 . Config {
2021-07-03 20:53:38 +08:00
Scopes : [ ] string { "email,public_profile" } ,
Endpoint : endpoint ,
ClientID : clientId ,
ClientSecret : clientSecret ,
RedirectURL : redirectUrl ,
}
return config
}
type LinkedInAccessToken struct {
2022-08-07 12:26:14 +08:00
AccessToken string ` json:"access_token" ` // Interface call credentials
ExpiresIn int64 ` json:"expires_in" ` // access_token interface call credential timeout time, unit (seconds)
2021-07-03 20:53:38 +08:00
}
// GetToken use code get access_token (*operation of getting code ought to be done in front)
// get more detail via: https://docs.microsoft.com/en-us/linkedIn/shared/authentication/authorization-code-flow?context=linkedIn%2Fcontext&tabs=HTTPS
func ( idp * LinkedInIdProvider ) GetToken ( code string ) ( * oauth2 . Token , error ) {
params := url . Values { }
params . Add ( "grant_type" , "authorization_code" )
params . Add ( "redirect_uri" , idp . Config . RedirectURL )
params . Add ( "client_id" , idp . Config . ClientID )
params . Add ( "client_secret" , idp . Config . ClientSecret )
params . Add ( "code" , code )
accessTokenUrl := fmt . Sprintf ( "%s?%s" , idp . Config . Endpoint . TokenURL , params . Encode ( ) )
bs , _ := json . Marshal ( params . Encode ( ) )
req , _ := http . NewRequest ( "POST" , accessTokenUrl , strings . NewReader ( string ( bs ) ) )
req . Header . Set ( "Content-Type" , "application/x-www-form-urlencoded" )
resp , err := http . DefaultClient . Do ( req )
if err != nil {
return nil , err
}
2022-08-09 16:50:49 +08:00
rbs , err := io . ReadAll ( resp . Body )
2021-07-03 20:53:38 +08:00
if err != nil {
return nil , err
}
2021-08-07 22:02:56 +08:00
2021-07-03 20:53:38 +08:00
tokenResp := LinkedInAccessToken { }
if err = json . Unmarshal ( rbs , & tokenResp ) ; err != nil {
return nil , err
}
2021-08-07 22:02:56 +08:00
2021-07-03 20:53:38 +08:00
token := & oauth2 . Token {
2021-08-07 22:02:56 +08:00
AccessToken : tokenResp . AccessToken ,
2021-07-03 20:53:38 +08:00
TokenType : "Bearer" ,
2021-08-07 22:02:56 +08:00
Expiry : time . Unix ( time . Now ( ) . Unix ( ) + tokenResp . ExpiresIn , 0 ) ,
2021-07-03 20:53:38 +08:00
}
2021-08-07 22:02:56 +08:00
2021-07-03 20:53:38 +08:00
return token , nil
}
/ *
{
"firstName" : {
"localized" : {
"zh_CN" : "继坤"
} ,
"preferredLocale" : {
"country" : "CN" ,
"language" : "zh"
}
} ,
"lastName" : {
"localized" : {
"zh_CN" : "刘"
} ,
"preferredLocale" : {
"country" : "CN" ,
"language" : "zh"
}
} ,
"profilePicture" : {
"displayImage" : "urn:li:digitalmediaAsset:C5603AQHbdR8RkG62yg" ,
"displayImage~" : {
"paging" : {
"count" : 10 ,
"start" : 0 ,
"links" : [ ]
} ,
"elements" : [
{
"artifact" : "urn:li:digitalmediaMediaArtifact:(urn:li:digitalmediaAsset:C5603AQHbdR8RkG62yg,urn:li:digitalmediaMediaArtifactClass:profile-displayphoto-shrink_100_100)" ,
"authorizationMethod" : "PUBLIC" ,
"data" : {
"com.linkedin.digitalmedia.mediaartifact.StillImage" : {
"mediaType" : "image/jpeg" ,
"rawCodecSpec" : {
"name" : "jpeg" ,
"type" : "image"
} ,
"displaySize" : {
"width" : 100.0 ,
"uom" : "PX" ,
"height" : 100.0
} ,
"storageSize" : {
"width" : 100 ,
"height" : 100
} ,
"storageAspectRatio" : {
"widthAspect" : 1.0 ,
"heightAspect" : 1.0 ,
"formatted" : "1.00:1.00"
} ,
"displayAspectRatio" : {
"widthAspect" : 1.0 ,
"heightAspect" : 1.0 ,
"formatted" : "1.00:1.00"
}
}
} ,
"identifiers" : [
{
"identifier" : "https://media.licdn.cn/dms/image/C5603AQHbdR8RkG62yg/profile-displayphoto-shrink_100_100/0/1625279434135?e=1630540800&v=beta&t=Z-bQKf_jFv8L1uwr6X5AJLoTQRWZrueT7qrITDSvxWM" ,
"index" : 0 ,
"mediaType" : "image/jpeg" ,
"file" : "urn:li:digitalmediaFile:(urn:li:digitalmediaAsset:C5603AQHbdR8RkG62yg,urn:li:digitalmediaMediaArtifactClass:profile-displayphoto-shrink_100_100,0)" ,
"identifierType" : "EXTERNAL_URL" ,
"identifierExpiresInSeconds" : 1630540800
}
]
} ,
// ...
}
]
}
} ,
"id" : "vvMfLsLIRs"
}
* /
type LinkedInUserInfo struct {
FirstName struct {
2021-08-07 22:02:56 +08:00
Localized map [ string ] string ` json:"localized" `
2021-07-03 20:53:38 +08:00
PreferredLocale struct {
Country string ` json:"country" `
Language string ` json:"language" `
} ` json:"preferredLocale" `
} ` json:"firstName" `
LastName struct {
2021-08-07 22:02:56 +08:00
Localized map [ string ] string ` json:"localized" `
2021-07-03 20:53:38 +08:00
PreferredLocale struct {
Country string ` json:"country" `
Language string ` json:"language" `
} ` json:"preferredLocale" `
} ` json:"lastName" `
ProfilePicture struct {
DisplayImage string ` json:"displayImage" `
DisplayImage1 struct {
Paging struct {
Count int ` json:"count" `
Start int ` json:"start" `
Links [ ] interface { } ` json:"links" `
} ` json:"paging" `
Elements [ ] struct {
Artifact string ` json:"artifact" `
AuthorizationMethod string ` json:"authorizationMethod" `
Data struct {
ComLinkedinDigitalmediaMediaartifactStillImage struct {
MediaType string ` json:"mediaType" `
RawCodecSpec struct {
Name string ` json:"name" `
Type string ` json:"type" `
} ` json:"rawCodecSpec" `
DisplaySize struct {
Width float64 ` json:"width" `
Uom string ` json:"uom" `
Height float64 ` json:"height" `
} ` json:"displaySize" `
StorageSize struct {
Width int ` json:"width" `
Height int ` json:"height" `
} ` json:"storageSize" `
StorageAspectRatio struct {
WidthAspect float64 ` json:"widthAspect" `
HeightAspect float64 ` json:"heightAspect" `
Formatted string ` json:"formatted" `
} ` json:"storageAspectRatio" `
DisplayAspectRatio struct {
WidthAspect float64 ` json:"widthAspect" `
HeightAspect float64 ` json:"heightAspect" `
Formatted string ` json:"formatted" `
} ` json:"displayAspectRatio" `
} ` json:"com.linkedin.digitalmedia.mediaartifact.StillImage" `
} ` json:"data" `
Identifiers [ ] struct {
Identifier string ` json:"identifier" `
Index int ` json:"index" `
MediaType string ` json:"mediaType" `
File string ` json:"file" `
IdentifierType string ` json:"identifierType" `
IdentifierExpiresInSeconds int ` json:"identifierExpiresInSeconds" `
} ` json:"identifiers" `
} ` json:"elements" `
} ` json:"displayImage~" `
} ` json:"profilePicture" `
Id string ` json:"id" `
}
/ *
{
"handle" : "urn:li:emailAddress:3775708763" ,
"handle~" : {
"emailAddress" : "hsimpson@linkedin.com"
}
}
* /
type LinkedInUserEmail struct {
Elements [ ] struct {
Handle struct {
EmailAddress string ` json:"emailAddress" `
} ` json:"handle~" `
Handle1 string ` json:"handle" `
} ` json:"elements" `
}
// GetUserInfo use LinkedInAccessToken gotten before return LinkedInUserInfo
// get more detail via: https://docs.microsoft.com/en-us/linkedin/consumer/integrations/self-serve/sign-in-with-linkedin?context=linkedin/consumer/context
func ( idp * LinkedInIdProvider ) GetUserInfo ( token * oauth2 . Token ) ( * UserInfo , error ) {
var linkedInUserInfo LinkedInUserInfo
bs , err := idp . GetUrlRespWithAuthorization ( "https://api.linkedIn.com/v2/me?projection=(id,firstName,lastName,profilePicture(displayImage~:playableStreams))" , token . AccessToken )
if err != nil {
return nil , err
}
if err = json . Unmarshal ( bs , & linkedInUserInfo ) ; err != nil {
return nil , err
}
var linkedInUserEmail LinkedInUserEmail
bs , err = idp . GetUrlRespWithAuthorization ( "https://api.linkedIn.com/v2/emailAddress?q=members&projection=(elements*(handle~))" , token . AccessToken )
if err != nil {
return nil , err
}
if err = json . Unmarshal ( bs , & linkedInUserEmail ) ; err != nil {
return nil , err
}
username := ""
for _ , name := range linkedInUserInfo . FirstName . Localized {
username += name
}
for _ , name := range linkedInUserInfo . LastName . Localized {
username += name
}
userInfo := UserInfo {
Id : linkedInUserInfo . Id ,
DisplayName : username ,
2021-08-07 22:02:56 +08:00
Username : username ,
2021-07-03 20:53:38 +08:00
Email : linkedInUserEmail . Elements [ 0 ] . Handle . EmailAddress ,
AvatarUrl : linkedInUserInfo . ProfilePicture . DisplayImage1 . Elements [ 0 ] . Identifiers [ 0 ] . Identifier ,
}
return & userInfo , nil
}
func ( idp * LinkedInIdProvider ) GetUrlRespWithAuthorization ( url , token string ) ( [ ] byte , error ) {
req , _ := http . NewRequest ( "GET" , url , nil )
req . Header . Set ( "Authorization" , fmt . Sprintf ( "Bearer %s" , token ) )
resp , err := http . DefaultClient . Do ( req )
if err != nil {
return nil , err
}
defer func ( Body io . ReadCloser ) {
err := Body . Close ( )
if err != nil {
return
}
} ( resp . Body )
2022-08-09 16:50:49 +08:00
bs , err := io . ReadAll ( resp . Body )
2021-07-03 20:53:38 +08:00
if err != nil {
return nil , err
}
return bs , nil
}