adonisjs / ally

AdonisJS Social Authentication Provider
MIT License
159 stars 53 forks source link

LinkedIn login not working (api v2) #69

Closed chernovpavl closed 3 years ago

chernovpavl commented 5 years ago

LinkedIn login not working for api v2

The LinkedIn driver uses the v1 version of the API. And new applications do not work with this version of api.

All new applications created on the LinkedIn Developer Platform as of January 14, 2019 can use LinkedIn's v2 APIs. Starting May 1, 2019, LinkedIn will deprecate use of its v1 APIs. If your developer application currently depends on LinkedIn v1 APIs, see the frequently asked questions below before migrating to LinkedIn v2 APIs.

Source: https://docs.microsoft.com/en-us/linkedin/consumer/integrations/self-serve/migration-faq?context=linkedin/consumer/context

twigs67 commented 5 years ago

Just checking to see if there are any plans to update this or if we should find another workaround?

chernovpavl commented 5 years ago

I just added custom driver for linkedin at this moment

twigs67 commented 5 years ago

@CHERiPP cool, did you use any documentation on creating a custom driver? I've only seen this thread and the instructions are lacking for me.

chernovpavl commented 5 years ago

No, I didn't find any documentation. I wrote Linkedinv2 driver and extended AllyManager in hooks.js file. I also had to rewrite AllyUser and use it, because linkedin api has a slightly different set of fields

const { hooks } = require('@adonisjs/ignitor')
const AllyManager = require('../node_modules/@adonisjs/ally/src/AllyManager')
const LinkedInV2 = require('../app/Classes/Ally/Drivers/LinkedinV2')

hooks.before.providersBooted(() => {
  AllyManager.extend('linkedinv2', LinkedInV2)
})
RomainLanz commented 5 years ago

Hey all, 👋

May you provide a PR with the needed change @CHERiPP?

chernovpavl commented 5 years ago

I don't have dev application in linkedin that would have access to any information other than first name, last name and email of user. Therefore, my version of the driver is limited in functionality and does not support, for example, downloading a picture profile. I don't think that this option is acceptable for PR.

zoran-php commented 5 years ago

I don't have dev application in linkedin that would have access to any information other than first name, last name and email of user. Therefore, my version of the driver is limited in functionality and does not support, for example, downloading a picture profile. I don't think that this option is acceptable for PR.

Your version of the driver would suit my needs. Can you please send it to me at zorandavidovic@live.com. Thanks in advance CHERiPP!

chernovpavl commented 5 years ago

This is my driver implementation:

'use strict'

const got = require('got')
const loc = '../../../../node_modules/@adonisjs/ally/'
const CE = require(loc + 'src/Exceptions')
const OAuth2Scheme = require(loc + 'src/Schemes/OAuth2')
const utils = require(loc + 'lib/utils')
const AllyUser = require('../AllyUser')
const _ = require('lodash')

/**
 * LinkedIn driver to authenticating users via OAuth2Scheme.
 *
 * @class LinkedIn
 * @constructor
 */
class LinkedInV2 extends OAuth2Scheme {
  constructor (Config) {
    const config = Config.get('services.ally.linkedinv2')

    utils.validateDriverConfig('linkedinv2', config)
    utils.debug('linkedinv2', config)

    super(config.clientId, config.clientSecret, config.headers)

    this._redirectUri = config.redirectUri
    this._redirectUriOptions = _.merge({ response_type: 'code' }, config.options)

    this.scope = _.size(config.scope) ? config.scope : ['r_liteprofile', 'r_emailaddress']
    this.fields = _.size(config.fields) ? config.fields : ['id', 'firstName', 'lastName']
  }

  /**
   * Injections to be made by the IoC container.
   *
   * @attribute inject
   *
   * @return {Array}
   */
  static get inject () {
    return ['Adonis/Src/Config']
  }

  /**
   * Returns a boolean telling if driver supports
   * state
   *
   * @method supportStates
   *
   * @return {Boolean}
   */
  get supportStates () {
    return true
  }

  /**
   * Scope seperator for seperating multiple
   * scopes.
   *
   * @attribute scopeSeperator
   *
   * @return {String}
   */
  get scopeSeperator () {
    return ' '
  }

  /**
   * Base url to be used for constructing
   * linkedin oauth urls.
   *
   * @attribute baseUrl
   *
   * @return {String}
   */
  get baseUrl () {
    return 'https://www.linkedin.com/oauth/v2'
  }

  /**
   * Relative url to be used for redirecting
   * user.
   *
   * @attribute authorizeUrl
   *
   * @return {String}
   */
  get authorizeUrl () {
    return 'authorization'
  }

  /**
   * Relative url to be used for exchanging
   * access token.
   *
   * @attribute accessTokenUrl
   *
   * @return {String}
   */
  get accessTokenUrl () {
    return 'accessToken'
  }

  /**
   * Returns the user profile as an object using the
   * access token.
   *
   * @attribute _getUserProfile
   *
   * @param   {String} accessToken
   *
   * @return  {Object}
   *
   * @private
   */
  async _getUserProfile (accessToken) {
    const profileUrl = `https://api.linkedin.com/v2/me?fields=${this.fields.join(',')}`

    const response = await got(profileUrl, {
      headers: {
        'x-li-format': 'json',
        Authorization: `Bearer ${accessToken}`
      },
      json: true
    })

    return response.body
  }

  /**
   * Returns the user email as an object using the
   * access token.
   *
   * @attribute _getUserEmail
   *
   * @param   {String} accessToken
   *
   * @return  {Object}
   *
   * @private
   */
  async _getUserEmail (accessToken) {
    const emailUrl =
      'https://api.linkedin.com/v2/clientAwareMemberHandles?q=members&projection=(elements*(primary,type,handle~))'

    const response = await got(emailUrl, {
      headers: {
        'x-li-format': 'json',
        Authorization: `Bearer ${accessToken}`
      },
      json: true
    })

    return response.body
  }

  /**
   * Normalize the user profile response and build an Ally user.
   *
   * @param {object} userProfile
   * @param {object} accessTokenResponse
   *
   * @return {object}
   *
   * @private
   */
  _buildAllyUser (userProfile, email, accessTokenResponse) {
    const user = new AllyUser()
    const expires = _.get(accessTokenResponse, 'result.expires_in')
    const emailAddress = email.elements.find(e => e.type === 'EMAIL')

    user
      .setOriginal(userProfile)
      .setFields(
        userProfile.id,
        userProfile.firstName.localized[Object.keys(userProfile.firstName.localized)[0]],
        userProfile.lastName.localized[Object.keys(userProfile.lastName.localized)[0]],
        emailAddress && emailAddress['handle~'] && emailAddress['handle~'].emailAddress,
        null,
        null
      )
      .setToken(
        accessTokenResponse.accessToken,
        accessTokenResponse.refreshToken,
        null,
        expires ? Number(expires) : null
      )

    return user
  }

  /**
   * Returns the redirect url for a given provider.
   *
   * @method getRedirectUrl
   *
   * @param {String} [state]
   *
   * @return {String}
   */
  async getRedirectUrl (state) {
    const options = state
      ? Object.assign(this._redirectUriOptions, { state })
      : this._redirectUriOptions

    return this.getUrl(this._redirectUri, this.scope, options)
  }

  /**
   * Parses the redirect errors returned by linkedin
   * and returns the error message.
   *
   * @method parseRedirectError
   *
   * @param  {Object} queryParams
   *
   * @return {String}
   */
  parseRedirectError (queryParams) {
    return queryParams.error_description || 'Oauth failed during redirect'
  }

  /**
   * Returns the user profile with it's access token, refresh token
   * and token expiry.
   *
   * @method getUser
   * @async
   *
   * @param {Object} queryParams
   * @param {String} [originalState]
   *
   * @return {Object}
   */
  async getUser (queryParams, originalState) {
    const code = queryParams.code
    const state = queryParams.state

    /**
     * Throw an exception when query string does not have
     * code.
     */
    if (!code) {
      const errorMessage = this.parseRedirectError(queryParams)
      throw CE.OAuthException.tokenExchangeException(errorMessage, null, errorMessage)
    }

    /**
     * Valid state with original state
     */
    if (state && originalState !== state) {
      throw CE.OAuthException.invalidState()
    }

    const accessTokenResponse = await this.getAccessToken(code, this._redirectUri, {
      grant_type: 'authorization_code'
    })

    const userProfile = await this._getUserProfile(accessTokenResponse.accessToken)
    const emailAddress = await this._getUserEmail(accessTokenResponse.accessToken)
    return this._buildAllyUser(userProfile, emailAddress, accessTokenResponse)
  }

  /**
   * Get user by access token
   *
   * @method getUserByToken
   *
   * @param  {String}       accessToken
   *
   * @return {void}
   */
  async getUserByToken (accessToken) {
    const userProfile = await this._getUserProfile(accessToken)

    return this._buildAllyUser(userProfile, null, { accessToken, refreshToken: null })
  }
}

module.exports = LinkedInV2
zoran-php commented 5 years ago

Thank you, Pavel

🙂


From: Pavel Chernov notifications@github.com Sent: Tuesday, June 4, 2019 2:37 PM To: adonisjs/adonis-ally Cc: Zoran Davidović; Comment Subject: Re: [adonisjs/adonis-ally] LinkedIn login not working (api v2) (#69)

This is my driver implementation:

'use strict'

const got = require('got') const loc = '../../../../nodemodules/@adonisjs/ally/' const CE = require(loc + 'src/Exceptions') const OAuth2Scheme = require(loc + 'src/Schemes/OAuth2') const utils = require(loc + 'lib/utils') const AllyUser = require('../AllyUser') const = require('lodash')

/**

module.exports = LinkedInV2

— You are receiving this because you commented. Reply to this email directly, view it on GitHubhttps://github.com/adonisjs/adonis-ally/issues/69?email_source=notifications&email_token=AHGEB5CUBI5DWWYHAXGG6JDPYZOW7A5CNFSM4HLIBJTKYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGODW4NOXI#issuecomment-498653021, or mute the threadhttps://github.com/notifications/unsubscribe-auth/AHGEB5ASME46M7LA4U42VG3PYZOW7ANCNFSM4HLIBJTA.

jacobaparecio commented 5 years ago

can't use linked driver how to fix?

chernovpavl commented 5 years ago

Can you provide any details or errors?

jacobaparecio commented 5 years ago

how do you implement the linkedInV2 ??

chernovpavl commented 5 years ago

All described in these messages: https://github.com/adonisjs/adonis-ally/issues/69#issuecomment-494258851 https://github.com/adonisjs/adonis-ally/issues/69#issuecomment-498653021

vivex commented 4 years ago

any update on this..?

Kuzzy commented 4 years ago

@CHERiPP thanks for the solution.

For any who also need profile picture:

// in constructor add profilePicture field
this.fields = _.size(config.fields) ?
      config.fields :
      ['id', 'firstName', 'lastName', 'profilePicture(displayImage~:playableStreams)']

// in _getUserProfile method change request from:
const profileUrl = `https://api.linkedin.com/v2/me?fields=${this.fields.join(',')}`

// to:
const profileUrl = `https://api.linkedin.com/v2/me?projection=(${this.fields.join(',')})`

// in _buildAllyUser method, get image url from userProfile response
const avatarUrl = _.get(userProfile, 'profilePicture[displayImage~].elements[0].identifiers[0].identifier')
// yeah it is buried very deep: https://docs.microsoft.com/en-us/linkedin/shared/references/v2/profile/profile-picture

// then pass it to the setFields method, my version:
user
      .setOriginal(userProfile)
      .setFields(
        userProfile.id,
        userProfile.firstName.localized[Object.keys(userProfile.firstName.localized)[0]],
        userProfile.lastName.localized[Object.keys(userProfile.lastName.localized)[0]],
        emailAddress && emailAddress['handle~'] && emailAddress['handle~'].emailAddress,
        avatarUrl,
      )
      .setToken(
        accessTokenResponse.accessToken,
        accessTokenResponse.refreshToken,
        null,
        expires ? Number(expires) : null
      )

Also, few changes are required in AllyUser class:

// in constructor, update _userFields object:
this._userFields = {
      id: '',
      name: '',
      email: '',
      avatar: '',
    }

// Update setFields method:
setFields(id, firstName, lastName, email, avatar) {
    this._userFields = { id, name: `${firstName} ${lastName}`, email, avatar }
    return this
  }
stale[bot] commented 3 years ago

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.