x1ddos / simpleauth

Simple authentication for Python on Google App Engine supporting OAuth 2.0, OAuth 1.0(a) and OpenID
https://simpleauth.appspot.com
327 stars 61 forks source link

LinedIn2, beyond basic profile #21

Closed ghost closed 10 years ago

ghost commented 11 years ago

Hi,

I cannot find any way to get data other than the default profile using LinkedIn2.

For example, if I choose the following scopes (space separated as prescribed by the linkedin api)

r_basicprofile r_emailaddress

the authentication screen correctly displays that the application is requesting the basic profile and the email but the 'data' argument of the '_on_signin' method only contains the basic profile, not the email.

This is true of any other scope that I tried.

Has anybody successfully obtained more than basic profile with LinkedIn2?

Thanks, Eric

x1ddos commented 11 years ago

Hey Eric,

the profile fetch is happening here: https://github.com/crhym3/simpleauth/blob/84620d3db5670a20108d14d5021a42b69020c1e8/simpleauth/handler.py#L435

Unfortunately the field names are hardcoded in the current version but I'm planning to change that, i.e. supporting custom fields.

For now you could just override _get_linkedin2_user_info method in your request handler and do something like this:

def _get_linkedin2_user_info(self, auth_info, **kwargs): fields = 'id,picture-url,public-profile-url,my-field1,my-other-field,...' url = 'https://api.linkedin.com/v1/people/~:(%s)?{0}' % fields resp = self._oauth2_request(url, auth_info['access_token'], token_param='oauth2_access_token') return self._parse_xml_user_info(resp)

(notice 'my-field1' and 'my-other-field' in fields var)

Then you should be able to get all needed field in _on_signin() method.

On Fri, Jul 26, 2013 at 6:27 PM, epichon notifications@github.com wrote:

Hi,

I cannot find any way to get data other than the default profile using LinkedIn2.

For example, if I choose the following scopes (space separated as prescribed by the linkedin api)

r_basicprofile r_emailaddress

the authentication screen correctly displays that the application is requesting the basic profile and the email but the 'data' argument of the '_on_signin' method only contains the basic profile, not the email.

This is true of any other scope that I tried.

Has anybody successfully obtained more than basic profile with LinkedIn2?

Thanks, Eric

— Reply to this email directly or view it on GitHubhttps://github.com/crhym3/simpleauth/issues/21 .

ghost commented 11 years ago

Alex,

Thanks!

As you rightly suggested, all that was necessary to get linkedin to return user email (as requested in the scope) was changing: fields = 'id,first-name,last-name,picture-url,public-profile-url,headline' to: fields_extra = 'email-address' fields = 'id,first-name,last-name,picture-url,public-profile-url,headline' + ',' + fields_extra

If anybody's interested the list of field names is here:

http://developer.linkedin.com/documents/profile-fields

Thanks again, Eric

On Jul 26, 2013, at 18:49 , alex notifications@github.com wrote:

Hey Eric,

the profile fetch is happening here: https://github.com/crhym3/simpleauth/blob/84620d3db5670a20108d14d5021a42b69020c1e8/simpleauth/handler.py#L435

Unfortunately the field names are hardcoded in the current version but I'm planning to change that, i.e. supporting custom fields.

For now you could just override _get_linkedin2_user_info method in your request handler and do something like this:

def _get_linkedin2_user_info(self, auth_info, **kwargs): fields = 'id,picture-url,public-profile-url,my-field1,my-other-field,...' url = 'https://api.linkedin.com/v1/people/~:(%s)?{0}' % fields resp = self._oauth2_request(url, auth_info['access_token'], token_param='oauth2_access_token') return self._parse_xml_user_info(resp)

(notice 'my-field1' and 'my-other-field' in fields var)

Then you should be able to get all needed field in _on_signin() method.

On Fri, Jul 26, 2013 at 6:27 PM, epichon notifications@github.com wrote:

Hi,

I cannot find any way to get data other than the default profile using LinkedIn2.

For example, if I choose the following scopes (space separated as prescribed by the linkedin api)

r_basicprofile r_emailaddress

the authentication screen correctly displays that the application is requesting the basic profile and the email but the 'data' argument of the '_on_signin' method only contains the basic profile, not the email.

This is true of any other scope that I tried.

Has anybody successfully obtained more than basic profile with LinkedIn2?

Thanks, Eric

— Reply to this email directly or view it on GitHubhttps://github.com/crhym3/simpleauth/issues/21 .

— Reply to this email directly or view it on GitHub.

ghost commented 11 years ago

Alex,

And finally, another point: the xml returned by LinkedIn is not flat, for example location can be like:

Paris Area, France fr

Below the changes I made to handle this.

def _get_linkedin2_user_info(self, auth_info, key=None, secret=None):
    # modified to allow for extra fields

    fields_extra = 'email-address,num-connections,location:(name,country:(code))'
    fields = 'id,first-name,last-name,picture-url,public-profile-url,headline' + ',' + fields_extra

    url = 'https://api.linkedin.com/v1/people/~:(%s)?{0}' % fields
    resp = self._oauth2_request(url, auth_info['access_token'],
                                token_param='oauth2_access_token')

    return self._parse_xml_user_info(resp)

def _parse_xml_user_info(self, content):
    # modified to allow for non-flat xml

    try:
        # lxml is one of the third party libs available on App Engine out of the
        # box. See example/app.yaml for more info.
        from lxml import etree
    except ImportError:
        import xml.etree.ElementTree as etree
    elements = etree.fromstring(content)
    uinfo = {}
    for e in elements:
        uinfo.setdefault(e.tag, self._parse_xml_recursive(e))
    return uinfo

def _parse_xml_recursive(self, element):
    if list(element):
        res = {}
        for subelement in list(element):
            res.setdefault(subelement.tag,self._parse_xml_recursive(subelement))
        return res
    else:
        return element.text

With this I was able to successfully get something like:

{'email-address': …, 'first-name': …, 'num-connections': …, etc., 'location': {'name': 'Paris Area, France', 'country': {'code': 'fr'}}}

Thanks again for your work and help.

Eric

On Jul 27, 2013, at 06:52 , Eric PICHON epichon@gmail.com wrote:

Alex,

Thanks!

As you rightly suggested, all that was necessary to get linkedin to return user email (as requested in the scope) was changing: fields = 'id,first-name,last-name,picture-url,public-profile-url,headline' to: fields_extra = 'email-address' fields = 'id,first-name,last-name,picture-url,public-profile-url,headline' + ',' + fields_extra

If anybody's interested the list of field names is here:

http://developer.linkedin.com/documents/profile-fields

Thanks again, Eric

On Jul 26, 2013, at 18:49 , alex notifications@github.com wrote:

Hey Eric,

the profile fetch is happening here: https://github.com/crhym3/simpleauth/blob/84620d3db5670a20108d14d5021a42b69020c1e8/simpleauth/handler.py#L435

Unfortunately the field names are hardcoded in the current version but I'm planning to change that, i.e. supporting custom fields.

For now you could just override _get_linkedin2_user_info method in your request handler and do something like this:

def _get_linkedin2_user_info(self, auth_info, **kwargs): fields = 'id,picture-url,public-profile-url,my-field1,my-other-field,...' url = 'https://api.linkedin.com/v1/people/~:(%s)?{0}' % fields resp = self._oauth2_request(url, auth_info['access_token'], token_param='oauth2_access_token') return self._parse_xml_user_info(resp)

(notice 'my-field1' and 'my-other-field' in fields var)

Then you should be able to get all needed field in _on_signin() method.

On Fri, Jul 26, 2013 at 6:27 PM, epichon notifications@github.com wrote:

Hi,

I cannot find any way to get data other than the default profile using LinkedIn2.

For example, if I choose the following scopes (space separated as prescribed by the linkedin api)

r_basicprofile r_emailaddress

the authentication screen correctly displays that the application is requesting the basic profile and the email but the 'data' argument of the '_on_signin' method only contains the basic profile, not the email.

This is true of any other scope that I tried.

Has anybody successfully obtained more than basic profile with LinkedIn2?

Thanks, Eric

— Reply to this email directly or view it on GitHubhttps://github.com/crhym3/simpleauth/issues/21 .

— Reply to this email directly or view it on GitHub.

ghost commented 11 years ago

PS: a more straightforward manner to get LinkedIn data is to request them in JSON rather than XML:

def _get_linkedin2_user_info(self, auth_info, key=None, secret=None):
    # modified to allow for extra fields
    # modified to get data in JSON rather than XML

    fields_extra = 'email-address,num-connections,location:(name,country:(code)),summary,specialties,positions'
    fields = 'id,first-name,last-name,picture-url,public-profile-url,headline' + ',' + fields_extra

    url = 'https://api.linkedin.com/v1/people/~:(%s)?{0}&format=json' % fields
    resp = self._oauth2_request(url, auth_info['access_token'],
                                token_param='oauth2_access_token')

    return json.loads(resp)

On Jul 27, 2013, at 07:48 , Eric PICHON epichon@gmail.com wrote:

Alex,

And finally, another point: the xml returned by LinkedIn is not flat, for example location can be like:

Paris Area, France fr

Below the changes I made to handle this.

def _get_linkedin2_user_info(self, auth_info, key=None, secret=None):
    # modified to allow for extra fields

    fields_extra = 'email-address,num-connections,location:(name,country:(code))'
    fields = 'id,first-name,last-name,picture-url,public-profile-url,headline' + ',' + fields_extra

    url = 'https://api.linkedin.com/v1/people/~:(%s)?{0}' % fields
    resp = self._oauth2_request(url, auth_info['access_token'],
                                token_param='oauth2_access_token')

    return self._parse_xml_user_info(resp)

def _parse_xml_user_info(self, content):
    # modified to allow for non-flat xml

    try:
        # lxml is one of the third party libs available on App Engine out of the
        # box. See example/app.yaml for more info.
        from lxml import etree
    except ImportError:
        import xml.etree.ElementTree as etree
    elements = etree.fromstring(content)
    uinfo = {}
    for e in elements:
        uinfo.setdefault(e.tag, self._parse_xml_recursive(e))
    return uinfo

def _parse_xml_recursive(self, element):
    if list(element):
        res = {}
        for subelement in list(element):
            res.setdefault(subelement.tag,self._parse_xml_recursive(subelement))
        return res
    else:
        return element.text

With this I was able to successfully get something like:

{'email-address': …, 'first-name': …, 'num-connections': …, etc., 'location': {'name': 'Paris Area, France', 'country': {'code': 'fr'}}}

Thanks again for your work and help.

Eric

On Jul 27, 2013, at 06:52 , Eric PICHON epichon@gmail.com wrote:

Alex,

Thanks!

As you rightly suggested, all that was necessary to get linkedin to return user email (as requested in the scope) was changing: fields = 'id,first-name,last-name,picture-url,public-profile-url,headline' to: fields_extra = 'email-address' fields = 'id,first-name,last-name,picture-url,public-profile-url,headline' + ',' + fields_extra

If anybody's interested the list of field names is here:

http://developer.linkedin.com/documents/profile-fields

Thanks again, Eric

On Jul 26, 2013, at 18:49 , alex notifications@github.com wrote:

Hey Eric,

the profile fetch is happening here: https://github.com/crhym3/simpleauth/blob/84620d3db5670a20108d14d5021a42b69020c1e8/simpleauth/handler.py#L435

Unfortunately the field names are hardcoded in the current version but I'm planning to change that, i.e. supporting custom fields.

For now you could just override _get_linkedin2_user_info method in your request handler and do something like this:

def _get_linkedin2_user_info(self, auth_info, **kwargs): fields = 'id,picture-url,public-profile-url,my-field1,my-other-field,...' url = 'https://api.linkedin.com/v1/people/~:(%s)?{0}' % fields resp = self._oauth2_request(url, auth_info['access_token'], token_param='oauth2_access_token') return self._parse_xml_user_info(resp)

(notice 'my-field1' and 'my-other-field' in fields var)

Then you should be able to get all needed field in _on_signin() method.

On Fri, Jul 26, 2013 at 6:27 PM, epichon notifications@github.com wrote:

Hi,

I cannot find any way to get data other than the default profile using LinkedIn2.

For example, if I choose the following scopes (space separated as prescribed by the linkedin api)

r_basicprofile r_emailaddress

the authentication screen correctly displays that the application is requesting the basic profile and the email but the 'data' argument of the '_on_signin' method only contains the basic profile, not the email.

This is true of any other scope that I tried.

Has anybody successfully obtained more than basic profile with LinkedIn2?

Thanks, Eric

— Reply to this email directly or view it on GitHubhttps://github.com/crhym3/simpleauth/issues/21 .

— Reply to this email directly or view it on GitHub.

x1ddos commented 11 years ago

Awesome! I think I started this long time ago when LinkedIn didn't support JSON. They do now, which is great. I think it'll also remove lxml lib dependency which was required for LinkedIn only.

Thanks, Eric! I'll rename this issue to something like "Use JSON for LinkedIn profile fetch" if you don't mind.

ghost commented 11 years ago

Alex,

This is fine.

Note however that there are some incompatibilities in the current code with non string (eg dict) user data. I had to subclass webapp2 models.User and add some JSONProperty for things to work properly for me. I'm happy to send code snppets if anyone is interested.

Eric

On Jul 29, 2013, at 10:16 , alex notifications@github.com wrote:

Awesome! I think I started this long time ago when LinkedIn didn't support JSON. They do now, which is great.

Thanks, Eric! I'll rename this issue to something like "Use JSON for LinkedIn profile fetch" if you don't mind.

— Reply to this email directly or view it on GitHub.

deeyehgo commented 10 years ago

Hi all,

Is there a way to check if the users phoneNumber exists?

I'm currently using

'phoneNumbers': lambda phoneNumber: ('phone', phoneNumber.get('values')[0].get('phoneNumber'),

to return the users phone number (from ./example/handlers.py - line 133). Unfortunately, if the user has not input a phone number through their LinkedIn profile, its returning this error when signing in: TypeError: 'NoneType' object has no attribute 'getitem'

deeyehgo commented 10 years ago
'phoneNumbers': lambda phoneNumber: ('phone', phoneNumber.get('values', [{'phoneType': '', 'phoneNumber': ''}])[0].get('phoneNumber')),

seems to do the trick

x1ddos commented 10 years ago

The best way to do this currently is to override _get_linkedin2_user_info() method in your handler.

I'm closing this issue for now. If someone comes up with a PR, please reopen.