Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added support for NGPVAN ActionID OpenID #750

Merged
merged 2 commits into from
Mar 28, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ or current ones extended):
* `Moves app`_ OAuth2 https://dev.moves-app.com/docs/authentication
* `Mozilla Persona`_
* NaszaKlasa_ OAuth2
* `NGPVAN ActionID`_ OpenId
* Odnoklassniki_ OAuth2 and Application Auth
* OpenId_
* OpenStreetMap_ OAuth1 http://wiki.openstreetmap.org/wiki/OAuth
Expand Down Expand Up @@ -267,6 +268,7 @@ check `django-social-auth LICENSE`_ for details:
.. _Moves app: https://dev.moves-app.com/docs/
.. _Mozilla Persona: http://www.mozilla.org/persona/
.. _NaszaKlasa: https://developers.nk.pl/
.. _NGPVAN ActionID: http://developers.ngpvan.com/action-id
.. _Odnoklassniki: http://www.odnoklassniki.ru
.. _Pocket: http://getpocket.com
.. _Podio: https://podio.com
Expand Down
1 change: 1 addition & 0 deletions docs/backends/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ Social backends
moves
naszaklasa
nationbuilder
ngpvan_actionid
odnoklassnikiru
openstreetmap
orbi
Expand Down
36 changes: 36 additions & 0 deletions docs/backends/ngpvan_actionid.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
NGP VAN ActionID
================

`NGP VAN`_'s ActionID_ service provides an OpenID 1.1 endpoint, which provides
first name, last name, email address, and phone number.

ActionID doesn't require major settings beside being defined on
``AUTHENTICATION_BACKENDS``

.. code-block:: python

SOCIAL_AUTH_AUTHENTICATION_BACKENDS = (
...
'social.backends.ngpvan.ActionIDOpenID',
...
)


If you want to be able to access the "phone" attribute offered by NGP VAN
within ``extra_data`` you can add the following to your settings:

.. code-block:: python

SOCIAL_AUTH_ACTIONID_OPENID_AX_EXTRA_DATA = [
('http://openid.net/schema/contact/phone/business', 'phone')
]


NGP VAN offers the ability to have your domain whitelisted, which will disable
the "{domain} is requesting a link to your ActionID" warning when your app
attempts to login using an ActionID account. Contact
`NGP VAN Developer Support`_ for more information

.. _NGP VAN: http://www.ngpvan.com/
.. _ActionID: http://developers.ngpvan.com/action-id
.. _NGP VAN Developer Support: http://developers.ngpvan.com/support/contact
2 changes: 2 additions & 0 deletions docs/intro.rst
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ or extend current one):
* Mixcloud_ OAuth2
* `Mozilla Persona`_
* NaszaKlasa_ OAuth2
* `NGPVAN ActionID`_ OpenId
* Odnoklassniki_ OAuth2 and Application Auth
* OpenId_
* Podio_ OAuth2
Expand Down Expand Up @@ -141,6 +142,7 @@ section.
.. _Mixcloud: https://www.mixcloud.com
.. _Mozilla Persona: http://www.mozilla.org/persona/
.. _NaszaKlasa: https://developers.nk.pl/
.. _NGPVAN ActionID: http://developers.ngpvan.com/action-id
.. _Odnoklassniki: http://www.odnoklassniki.ru
.. _Podio: https://podio.com
.. _Shopify: http://shopify.com
Expand Down
61 changes: 61 additions & 0 deletions social/backends/ngpvan.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
"""
NGP VAN's `ActionID` Provider

http://developers.ngpvan.com/action-id
"""
from social.backends.open_id import OpenIdAuth
from openid.extensions import ax


class ActionIDOpenID(OpenIdAuth):
"""
NGP VAN's ActionID OpenID 1.1 authentication backend
"""
name = 'actionid-openid'
URL = 'https://accounts.ngpvan.com/Home/Xrds'
USERNAME_KEY = 'email'

def get_ax_attributes(self):
"""
Return the AX attributes that ActionID responds with, as well as the
user data result that it must map to.
"""
return [
('http://openid.net/schema/contact/internet/email', 'email'),
('http://openid.net/schema/contact/phone/business', 'phone'),
('http://openid.net/schema/namePerson/first', 'first_name'),
('http://openid.net/schema/namePerson/last', 'last_name'),
('http://openid.net/schema/namePerson', 'fullname'),
]

def setup_request(self, params=None):
"""
Setup the OpenID request

Because ActionID does not advertise the availiability of AX attributes
nor use standard attribute aliases, we need to setup the attributes
manually instead of rely on the parent OpenIdAuth.setup_request()
"""
request = self.openid_request(params)

fetch_request = ax.FetchRequest()
fetch_request.add(ax.AttrInfo(
'http://openid.net/schema/contact/internet/email',
alias='ngpvanemail',
required=True))

fetch_request.add(ax.AttrInfo(
'http://openid.net/schema/contact/phone/business',
alias='ngpvanphone',
required=False))
fetch_request.add(ax.AttrInfo(
'http://openid.net/schema/namePerson/first',
alias='ngpvanfirstname',
required=False))
fetch_request.add(ax.AttrInfo(
'http://openid.net/schema/namePerson/last',
alias='ngpvanlastname',
required=False))
request.addExtension(fetch_request)

return request
193 changes: 193 additions & 0 deletions social/tests/backends/test_ngpvan.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
"""Tests for NGP VAN ActionID Backend"""
import datetime

from httpretty import HTTPretty

from social.p3 import urlencode
from social.tests.backends.open_id import OpenIdTest


JANRAIN_NONCE = datetime.datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ')


class NGPVANActionIDOpenIDTest(OpenIdTest):
"""Test the NGP VAN ActionID OpenID 1.1 Backend"""
backend_path = 'social.backends.ngpvan.ActionIDOpenID'
expected_username = 'testuser@user.local'
discovery_body = ' '.join(
[
'<?xml version="1.0" encoding="UTF-8"?>',
'<xrds:XRDS',
'xmlns:xrds="xri://$xrds"',
'xmlns:openid="http://openid.net/xmlns/1.0"',
'xmlns="xri://$xrd*($v*2.0)">',
'<XRD>',
'<Service priority="10">',
'<Type>http://specs.openid.net/auth/2.0/signon</Type>',
'<Type>http://openid.net/extensions/sreg/1.1</Type>',
'<Type>http://axschema.org/contact/email</Type>',
'<URI>https://accounts.ngpvan.com/OpenId/Provider</URI>',
'</Service>',
'<Service priority="20">',
'<Type>http://openid.net/signon/1.0</Type>',
'<Type>http://openid.net/extensions/sreg/1.1</Type>',
'<Type>http://axschema.org/contact/email</Type>',
'<URI>https://accounts.ngpvan.com/OpenId/Provider</URI>',
'</Service>',
'</XRD>',
'</xrds:XRDS>',
])
server_response = urlencode({
'openid.claimed_id': 'https://accounts.ngpvan.com/user/abcd123',
'openid.identity': 'https://accounts.ngpvan.com/user/abcd123',
'openid.sig': 'Midw8F/rCDwW7vMz3y+vK6rjz6s=',
'openid.signed': 'claimed_id,identity,assoc_handle,op_endpoint,return_'
'to,response_nonce,ns.alias3,alias3.mode,alias3.type.'
'alias1,alias3.value.alias1,alias3.type.alias2,alias3'
'.value.alias2,alias3.type.alias3,alias3.value.alias3'
',alias3.type.alias4,alias3.value.alias4,alias3.type.'
'alias5,alias3.value.alias5,alias3.type.alias6,alias3'
'.value.alias6,alias3.type.alias7,alias3.value.alias7'
',alias3.type.alias8,alias3.value.alias8,ns.sreg,sreg'
'.fullname',
'openid.assoc_handle': '{635790678917902781}{GdSyFA==}{20}',
'openid.op_endpoint': 'https://accounts.ngpvan.com/OpenId/Provider',
'openid.return_to': 'http://myapp.com/complete/actionid-openid/',
'openid.response_nonce': JANRAIN_NONCE + 'MMgBGEre',
'openid.mode': 'id_res',
'openid.ns': 'http://specs.openid.net/auth/2.0',
'openid.ns.alias3': 'http://openid.net/srv/ax/1.0',
'openid.alias3.mode': 'fetch_response',
'openid.alias3.type.alias1': 'http://openid.net/schema/contact/phone/b'
'usiness',
'openid.alias3.value.alias1': '+12015555555',
'openid.alias3.type.alias2': 'http://openid.net/schema/contact/interne'
't/email',
'openid.alias3.value.alias2': 'testuser@user.local',
'openid.alias3.type.alias3': 'http://openid.net/schema/namePerson/firs'
't',
'openid.alias3.value.alias3': 'John',
'openid.alias3.type.alias4': 'http://openid.net/schema/namePerson/las'
't',
'openid.alias3.value.alias4': 'Smith',
'openid.alias3.type.alias5': 'http://axschema.org/namePerson/first',
'openid.alias3.value.alias5': 'John',
'openid.alias3.type.alias6': 'http://axschema.org/namePerson/last',
'openid.alias3.value.alias6': 'Smith',
'openid.alias3.type.alias7': 'http://axschema.org/namePerson',
'openid.alias3.value.alias7': 'John Smith',
'openid.alias3.type.alias8': 'http://openid.net/schema/namePerson',
'openid.alias3.value.alias8': 'John Smith',
'openid.ns.sreg': 'http://openid.net/extensions/sreg/1.1',
'openid.sreg.fullname': 'John Smith',
})

def setUp(self):
"""Setup the test"""
super(NGPVANActionIDOpenIDTest, self).setUp()

# Mock out the NGP VAN endpoints
HTTPretty.register_uri(
HTTPretty.POST,
'https://accounts.ngpvan.com/Home/Xrds',
status=200,
body=self.discovery_body)
HTTPretty.register_uri(
HTTPretty.GET,
'https://accounts.ngpvan.com/user/abcd123',
status=200,
body=self.discovery_body)
HTTPretty.register_uri(
HTTPretty.GET,
'https://accounts.ngpvan.com/OpenId/Provider',
status=200,
body=self.discovery_body)

def test_login(self):
"""Test the login flow using python-social-auth's built in test"""
self.do_login()

def test_partial_pipeline(self):
"""Test the partial flow using python-social-auth's built in test"""
self.do_partial_pipeline()

def test_get_ax_attributes(self):
"""Test that the AX attributes that NGP VAN responds with are present"""
records = self.backend.get_ax_attributes()

self.assertEqual(
records,
[
('http://openid.net/schema/contact/internet/email', 'email'),
('http://openid.net/schema/contact/phone/business', 'phone'),
('http://openid.net/schema/namePerson/first', 'first_name'),
('http://openid.net/schema/namePerson/last', 'last_name'),
('http://openid.net/schema/namePerson', 'fullname'),
]
)

def test_setup_request(self):
"""Test the setup_request functionality in the NGP VAN backend"""
# We can grab the requested attributes by grabbing the HTML of the
# OpenID auth form and pulling out the hidden fields
_, inputs = self.get_form_data(self.backend.auth_html())

# Confirm that the only required attribute is email
self.assertEqual(inputs['openid.ax.required'], 'ngpvanemail')

# Confirm that the 3 optional attributes are requested "if available"
self.assertIn('ngpvanphone', inputs['openid.ax.if_available'])
self.assertIn('ngpvanfirstname', inputs['openid.ax.if_available'])
self.assertIn('ngpvanlastname', inputs['openid.ax.if_available'])

# Verify the individual attribute properties
self.assertEqual(
inputs['openid.ax.type.ngpvanemail'],
'http://openid.net/schema/contact/internet/email')
self.assertEqual(
inputs['openid.ax.type.ngpvanfirstname'],
'http://openid.net/schema/namePerson/first')
self.assertEqual(
inputs['openid.ax.type.ngpvanlastname'],
'http://openid.net/schema/namePerson/last')
self.assertEqual(
inputs['openid.ax.type.ngpvanphone'],
'http://openid.net/schema/contact/phone/business')

def test_user_data(self):
"""Ensure that the correct user data is being passed to create_user"""
self.strategy.set_settings({
'USER_FIELDS': [
'email',
'first_name',
'last_name',
'username',
'phone',
'fullname'
]
})
user = self.do_start()

self.assertEqual(user.username, u'testuser@user.local')
self.assertEqual(user.email, u'testuser@user.local')
self.assertEqual(user.extra_user_fields['phone'], u'+12015555555')
self.assertEqual(user.extra_user_fields['first_name'], u'John')
self.assertEqual(user.extra_user_fields['last_name'], u'Smith')
self.assertEqual(user.extra_user_fields['fullname'], u'John Smith')

def test_extra_data_phone(self):
"""Confirm that you can get a phone number via the relevant setting"""
self.strategy.set_settings({
'SOCIAL_AUTH_ACTIONID_OPENID_AX_EXTRA_DATA': [
('http://openid.net/schema/contact/phone/business', 'phone')
]
})
user = self.do_start()
self.assertEqual(user.social_user.extra_data['phone'], u'+12015555555')

def test_association_uid(self):
"""Test that the correct association uid is stored in the database"""
user = self.do_start()
self.assertEqual(
user.social_user.uid,
'https://accounts.ngpvan.com/user/abcd123')
7 changes: 4 additions & 3 deletions social/tests/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,15 @@ class User(BaseModel):
cache = {}
_is_active = True

def __init__(self, username, email=None):
def __init__(self, username, email=None, **extra_user_fields):
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added this here to better mimic what Django allows during the create_user step and thus better test USER_FIELDS

self.id = User.next_id()
self.username = username
self.email = email
self.password = None
self.slug = None
self.social = []
self.extra_data = {}
self.extra_user_fields = extra_user_fields
self.save()

def is_active(self):
Expand Down Expand Up @@ -100,8 +101,8 @@ def user_exists(cls, username):
return User.cache.get(username) is not None

@classmethod
def create_user(cls, username, email=None):
return User(username=username, email=email)
def create_user(cls, username, email=None, **extra_user_fields):
return User(username=username, email=email, **extra_user_fields)

@classmethod
def get_user(cls, pk):
Expand Down