Skip to content

Commit

Permalink
Add NGP VAN ActionID support
Browse files Browse the repository at this point in the history
  • Loading branch information
nickcatal committed Jan 2, 2016
1 parent 15d75da commit d8d7119
Show file tree
Hide file tree
Showing 6 changed files with 295 additions and 0 deletions.
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')

0 comments on commit d8d7119

Please sign in to comment.