Skip to content

Commit

Permalink
LinkedIn: Switch to using OAuth2 (beaulebens#59)
Browse files Browse the repository at this point in the history
Co-authored-by: Gary Pendergast <[email protected]>
  • Loading branch information
2 people authored and Beau Lebens committed Feb 28, 2021
1 parent 1dc72ba commit 61b8b51
Show file tree
Hide file tree
Showing 2 changed files with 94 additions and 37 deletions.
130 changes: 93 additions & 37 deletions includes/services/extended/linkedin.php
Original file line number Diff line number Diff line change
@@ -1,97 +1,153 @@
<?php

/**
* LinkedIn service definition for Keyring. Clean implementation of OAuth1
* LinkedIn service definition for Keyring. Clean implementation of OAuth2
*/

class Keyring_Service_LinkedIn extends Keyring_Service_OAuth1 {
class Keyring_Service_LinkedIn extends Keyring_Service_OAuth2 {
const NAME = 'linkedin';
const LABEL = 'LinkedIn';

var $person = array();

function __construct() {
parent::__construct();

$this->authorization_header = true;
$this->authorization_realm = "api.linkedin.com";

// Enable "basic" UI for entering key/secret
if ( ! KEYRING__HEADLESS_MODE ) {
add_action( 'keyring_linkedin_manage_ui', array( $this, 'basic_ui' ) );
add_filter( 'keyring_linkedin_basic_ui_intro', array( $this, 'basic_ui_intro' ) );
}

$this->set_endpoint( 'request_token', 'https://api.linkedin.com/uas/oauth/requestToken', 'POST' );
$this->set_endpoint( 'authorize', 'https://api.linkedin.com/uas/oauth/authenticate', 'GET' );
$this->set_endpoint( 'access_token', 'https://api.linkedin.com/uas/oauth/accessToken', 'GET' );
$this->set_endpoint( 'authorize', 'https://www.linkedin.com/oauth/v2/authorization', 'GET' );
$this->set_endpoint( 'access_token', 'https://www.linkedin.com/oauth/v2/accessToken', 'POST' );
$this->set_endpoint( 'self', 'https://api.linkedin.com/v2/me', 'GET' );
$this->set_endpoint( 'profile_pic', 'https://api.linkedin.com/v2/me/picture-urls::(original)/', 'GET' );

$creds = $this->get_credentials();
$this->app_id = $creds['app_id'];
$this->key = $creds['key'];
$this->secret = $creds['secret'];

$this->consumer = new OAuthConsumer( $this->key, $this->secret, $this->callback_url );
$this->signature_method = new OAuthSignatureMethod_HMAC_SHA1;
$this->consumer = new OAuthConsumer( $this->key, $this->secret, $this->callback_url );
$this->signature_method = new OAuthSignatureMethod_HMAC_SHA1;
$this->authorization_header = 'Bearer';

add_filter( 'keyring_linkedin_request_scope', array( $this, 'member_permissions' ) );
add_filter( 'keyring_' . self::NAME . '_request_scope', array( $this, 'member_permissions' ) );
}

function basic_ui_intro() {
echo '<p>' . sprintf( __( "To connect to LinkedIn, you'll first need to <a href='%s'>create an app</a>. A lot of the details are required, but they're not actually important to the operation of your app, since Keyring will override any important settings.", 'keyring' ), 'https://www.linkedin.com/secure/developer?newapp=' ) . '</p>';
echo '<p>' . __( "Once you've created your app, go down to the <strong>OAuth Keys</strong> section and copy the <strong>API Key</strong> value into the <strong>API Key</strong> field below, and the <strong>Secret Key</strong> value into the <strong>API Secret</strong> field and click save (you don't need an App ID value for LinkedIn).", 'keyring' ) . '</p>';
echo '<p>' . __( "Once you've created your app, go down to the <strong>Auth</strong> section and copy the <strong>Client ID</strong> value into the <strong>API Key</strong> field below, and the <strong>Client Secret</strong> value into the <strong>API Secret</strong> field", 'keyring' ) . '</p>';
echo '<p>' . sprintf( __( "In the LinkedIn <strong>Redirect URLs:</strong> box, enter the URL <code>%s</code>.", 'keyring' ), Keyring_Util::admin_url( $this->get_name(), array( 'action' => 'verify' ) ) ) . '</p>';
echo '<p>' . __( "Then click save (you don't need an App ID value for LinkedIn).", 'keyring' ) . '</p>';
}

function parse_response( $response ) {
if ( '<?xml' == substr( $response, 0, 5 ) ) { // Errors always come back as XML
return simplexml_load_string( $response );
} else {
return json_decode( $response );
}
/**
* Add in the `scope` parameter when authorizing.
* r_liteprofile Grants access to first name, last name, id, and profile picture.
* w_member_social Grants access to post on behalf of the user.
*
* @param string $scope
* @return string
*/
function member_permissions( $scope ) {
$scope = 'r_liteprofile w_member_social';
return $scope;
}

function member_permissions( $permissions = '' ) {
$permissions = 'r_basicprofile';
return $permissions;
/**
* By adding the `x-li-format: json` header here, we can avoid having to append `?format=json` to all requests.
*
* https://developer.linkedin.com/docs/rest-api#hero-par_longformtext_4_longform-text-content-par_resourceparagraph
*
* @param string $url
* @param array $params
* @return array|Keyring_Error|mixed|object|string
*/
function request( $url, array $params = array() ) {
$params['headers']['x-li-format'] = 'json';
return parent::request( $url, $params );
}

/**
* Build the meta for the token.
*
* @param mixed $token
* @return false|int|mixed
*/
function build_token_meta( $token ) {
// Set the token so that we can make requests using it
$this->set_token(
new Keyring_Access_Token(
$this->get_name(),
new OAuthToken(
$token['oauth_token'],
$token['oauth_token_secret']
)
$token['access_token'],
array()
)
);

// Get user profile information
$response = $this->request( "https://api.linkedin.com/v1/people/~:(id,formatted-name,picture-url)?format=json" );
$response = $this->request(
$this->self_url . '?projection=(id,firstName,lastName,profilePicture(displayImage~:playableStreams))',
array( 'method' => $this->self_method )
);

if ( Keyring_Util::is_error( $response ) ) {
$meta = array();
} else {
$this->person = $response;

$firstName = $this->person->firstName;
$lastName = $this->person->lastName;
$lfirst = "{$firstName->preferredLocale->language}_{$firstName->preferredLocale->country}";
$llast = "{$lastName->preferredLocale->language}_{$lastName->preferredLocale->country}";

$profilePicture = $this->person->profilePicture;

$meta = array(
'user_id' => $this->person->id,
'name' => $this->person->formattedName,
'picture' => $this->person->pictureUrl,
'name' => $firstName->localized->{$lfirst} . ' ' . $lastName->localized->{$llast},
'picture' => $profilePicture->{'displayImage~'}->elements[0]->identifiers[0]->identifier,
);
}

return apply_filters( 'keyring_access_token_meta', $meta, $this->get_name(), $token, $response, $this );
return apply_filters( 'keyring_access_token_meta', $meta, self::NAME, $token, $response, $this );
}

function get_display( Keyring_Access_Token $token ) {
return $token->get_meta( 'name' );
}

function test_connection() {
$res = $this->request( "https://api.linkedin.com/v1/people/~:(id,formatted-name)?format=json" );
if ( ! Keyring_Util::is_error( $res ) ) {
return true;
}
/**
* Get profile picture.
*
* @return string|mixed
*/
function fetch_profile_picture () {
$response = $this->request(
$this->self_url . '?projection=(profilePicture(displayImage~:playableStreams))',
array( 'method' => $this->self_method )
);

return $res;
if ( Keyring_Util::is_error( $response ) ) {
return new WP_Error( 'missing-profile_picture', __( 'Could not find profile picture.', 'keyring' ) );
}

return $response->profilePicture->{'displayImage~'}->elements[0]->identifiers[0]->identifier;
}

/**
* Test whether the connection has not been voided or expired.
*
* @return array|bool|Keyring_Error|mixed|object|string
*/
function test_connection() {
$res = $this->request(
$this->self_url,
array( 'method' => $this->self_method )
);
if ( ! Keyring_Util::is_error( $res ) ) {
return true;
}
return $res;
}
}

Expand Down
1 change: 1 addition & 0 deletions readme.txt
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ Add files to includes/services/extended/ that either implement one of the includ
== Changelog ==

= =
* Enhancement BREAKING: LinkedIn now uses OAuth2. Props @glendaviesnz.
* Enhancement: Added a GitHub Service definition, props @alperakgun.
* Enhancement: Added a Google Drive Service definition, props @scruffian.
* Enhancement: Trim spaces off API keys etc to avoid mistakes when copy/pasting. Props @kbrown9.
Expand Down

0 comments on commit 61b8b51

Please sign in to comment.