diff --git a/README.rst b/README.rst index 03df9cd74..3117b7d29 100644 --- a/README.rst +++ b/README.rst @@ -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 @@ -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 diff --git a/docs/backends/index.rst b/docs/backends/index.rst index 799ca6238..c54ce049f 100644 --- a/docs/backends/index.rst +++ b/docs/backends/index.rst @@ -98,6 +98,7 @@ Social backends moves naszaklasa nationbuilder + ngpvan_actionid odnoklassnikiru openstreetmap orbi diff --git a/docs/backends/ngpvan_actionid.rst b/docs/backends/ngpvan_actionid.rst new file mode 100644 index 000000000..cc980a70d --- /dev/null +++ b/docs/backends/ngpvan_actionid.rst @@ -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 diff --git a/docs/intro.rst b/docs/intro.rst index b138974dc..191d7eb44 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -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 @@ -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 diff --git a/social/backends/ngpvan.py b/social/backends/ngpvan.py new file mode 100644 index 000000000..42a34c81e --- /dev/null +++ b/social/backends/ngpvan.py @@ -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 diff --git a/social/tests/backends/test_ngpvan.py b/social/tests/backends/test_ngpvan.py new file mode 100644 index 000000000..1189839a1 --- /dev/null +++ b/social/tests/backends/test_ngpvan.py @@ -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( + [ + '', + '', + '', + '', + 'http://specs.openid.net/auth/2.0/signon', + 'http://openid.net/extensions/sreg/1.1', + 'http://axschema.org/contact/email', + 'https://accounts.ngpvan.com/OpenId/Provider', + '', + '', + 'http://openid.net/signon/1.0', + 'http://openid.net/extensions/sreg/1.1', + 'http://axschema.org/contact/email', + 'https://accounts.ngpvan.com/OpenId/Provider', + '', + '', + '', + ]) + 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')