Skip to content

Commit

Permalink
Merge pull request omab#8 from kawaguchi-ks/develop/dogwood/cherry-pi…
Browse files Browse the repository at this point in the history
…ck-cypress

cherry-pick from cypress omab#5 omab#7
  • Loading branch information
kawaguchi-ks committed Jul 25, 2016
2 parents 45560de + 8dde932 commit 993238c
Show file tree
Hide file tree
Showing 7 changed files with 185 additions and 8 deletions.
110 changes: 110 additions & 0 deletions social/backends/daccount.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
"""
Docomo d-account, OpenID Connect
"""
from calendar import timegm
import datetime
from jwt import InvalidTokenError, decode as jwt_decode
from requests.auth import HTTPBasicAuth

from .open_id import OpenIdConnectAuth, OpenIdConnectAssociation
from ..exceptions import AuthTokenError


class DAccountOpenIdConnect(OpenIdConnectAuth):
name = 'd-account'
AUTHORIZATION_URL = 'https://id.smt.docomo.ne.jp/cgi8/oidc/authorize'
ACCESS_TOKEN_URL = 'https://conf.uw.docomo.ne.jp/token'
USERINFO_URL = 'https://conf.uw.docomo.ne.jp/userinfo'
ACCESS_TOKEN_METHOD = 'POST'
DEFAULT_SCOPE = ['openid', 'profile1']
RESPONSE_TYPE = 'code'
REDIRECT_STATE = False
ID_TOKEN_ISSUER = 'https://conf.uw.docomo.ne.jp/'
ID_KEY = 'sub'

def auth_complete_params(self, state=None):
# client_id and client_secret will set in the HTTP Header
return {
'grant_type': 'authorization_code',
'code': self.data.get('code', ''),
'redirect_uri': self.get_redirect_uri(state),
}

def get_and_store_nonce(self, url, state):
"""Inherit in order to change the length of the nonce"""
# Create a nonce
nonce = self.strategy.random_string(60)
# Store the nonce
association = OpenIdConnectAssociation(nonce, assoc_type=state)
self.strategy.storage.association.store(url, association)
return nonce

def get_nonce(self, nonce):
"""Inherit because we shold use AUTHORIZATION_URL, not ACCESS_TOKEN_URL"""
try:
return self.strategy.storage.association.get(
server_url=self.AUTHORIZATION_URL,
handle=nonce
)[0]
except IndexError:
pass

def validate_and_return_id_token(self, id_token):
"""
Validates the id_token according to the steps at
http://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation.
"""
client_id, client_secret = self.get_key_and_secret()
try:
# Decode the JWT and raise an error if the secret is invalid or
# the response has expired.
# TODO not verify until d-account fix signature
id_token = jwt_decode(id_token, client_secret, audience=client_id,
issuer=self.ID_TOKEN_ISSUER,
verify=False)
except InvalidTokenError as err:
raise AuthTokenError(self, err)

# Verify the token was issued in the last 10 minutes
utc_timestamp = timegm(datetime.datetime.utcnow().utctimetuple())
if id_token['iat'] < (utc_timestamp - 600):
raise AuthTokenError(self, 'Incorrect id_token: iat')

# Validate the nonce to ensure the request was not modified
nonce = id_token.get('nonce')
if not nonce:
raise AuthTokenError(self, 'Incorrect id_token: nonce')

nonce_obj = self.get_nonce(nonce)
if nonce_obj:
self.remove_nonce(nonce_obj.id)
else:
raise AuthTokenError(self, 'Incorrect id_token: nonce')
return id_token

def request_access_token(self, *args, **kwargs):
kwargs['auth'] = HTTPBasicAuth(*self.get_key_and_secret())
return super(DAccountOpenIdConnect, self).request_access_token(*args, **kwargs)

def user_data(self, access_token, *args, **kwargs):
"""Loads user data from service."""
return self.get_json(
self.USERINFO_URL,
headers={'Authorization': 'Bearer {0}'.format(access_token)}
)

def get_user_details(self, response):
"""Return user details from d-account API account"""
email = response.get('email', '')
fullname, first_name, last_name = self.get_user_names(
response.get('name', ''),
response.get('given_name', ''),
response.get('family_name', '')
)
return {
'username': email.split('@', 1)[0],
'email': email,
'fullname': fullname,
'first_name': first_name,
'last_name': last_name,
}
49 changes: 49 additions & 0 deletions social/tests/backends/test_daccount.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import json
import unittest

from ..models import TestAssociation
from .oauth import OAuth2Test
from .open_id import OpenIdConnectTestMixin


class DAccountOpenIdConnectTest(OpenIdConnectTestMixin, OAuth2Test):
backend_path = 'social.backends.daccount.DAccountOpenIdConnect'
expected_username = 'foo'
user_data_url = 'https://conf.uw.docomo.ne.jp/userinfo'
user_data_body = json.dumps({
'email': 'foo@example.com',
})

def access_token_body(self, request, _url, headers):
"""
Get the nonce from the request parameters, add it to the id_token, and
return the complete response.
"""
# get nonce generated at authorization
nonce = filter(
lambda x: x.server_url == self.backend.authorization_url(),
TestAssociation.cache.values()
)[0].handle
body = self.prepare_access_token_body(nonce=nonce)
return 200, headers, body

@unittest.skip('Not verified until daccount fix signature')
def test_invalid_secret(self):
pass

@unittest.skip('Not verified until daccount fix signature')
def test_expired_signature(self):
pass

@unittest.skip('Not verified until daccount fix signature')
def test_invalid_issuer(self):
pass

@unittest.skip('Not verified until daccount fix signature')
def test_invalid_audience(self):
pass

def test_login(self):
user = self.do_login()
user_data = json.loads(self.user_data_body)
self.assertEqual(user_data['email'], user.email)
2 changes: 2 additions & 0 deletions social/tests/backends/test_google.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import datetime
import json
import unittest

from httpretty import HTTPretty

Expand Down Expand Up @@ -165,6 +166,7 @@ def test_with_anonymous_key_and_secret(self):
JANRAIN_NONCE = datetime.datetime.now().strftime('%Y-%m-%dT%H:%M:%SZ')


@unittest.skip("Unsupported")
class GoogleOpenIdTest(OpenIdTest):
backend_path = 'social.backends.google.GoogleOpenId'
expected_username = 'FooBar'
Expand Down
2 changes: 2 additions & 0 deletions social/tests/backends/test_livejournal.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import datetime
import unittest

from httpretty import HTTPretty

Expand All @@ -11,6 +12,7 @@
JANRAIN_NONCE = datetime.datetime.now().strftime('%Y-%m-%dT%H:%M:%SZ')


@unittest.skip("Unsupported")
class LiveJournalOpenIdTest(OpenIdTest):
backend_path = 'social.backends.livejournal.LiveJournalOpenId'
expected_username = 'foobar'
Expand Down
16 changes: 16 additions & 0 deletions social/tests/test_pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,22 @@ def test_random_username(self):
})
self.do_login()

def test_no_username(self):
"""
Tests for backend that does not return email.
There is a case in which email can not be acquired(e.g Facebook).
"""
_user_data_body = json.loads(self.user_data_body)
del _user_data_body['email']
del _user_data_body['login']

self.strategy.set_settings({
'SOCIAL_AUTH_CLEAN_USERNAMES': False,
'SOCIAL_AUTH_SLUGIFY_USERNAMES': True
})
# Verify that exception does not occur
self.do_login(after_complete_checks=False, user_data_body=json.dumps(_user_data_body))


class RepeatedUsernameTest(BaseActionTest):
def test_random_username(self):
Expand Down
11 changes: 3 additions & 8 deletions social/tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,14 +80,9 @@ def is_active(self):

class SlugifyTest(unittest.TestCase):
def test_slugify_formats(self):
if PY3:
self.assertEqual(slugify('FooBar'), 'foobar')
self.assertEqual(slugify('Foo Bar'), 'foo-bar')
self.assertEqual(slugify('Foo (Bar)'), 'foo-bar')
else:
self.assertEqual(slugify('FooBar'.decode('utf-8')), 'foobar')
self.assertEqual(slugify('Foo Bar'.decode('utf-8')), 'foo-bar')
self.assertEqual(slugify('Foo (Bar)'.decode('utf-8')), 'foo-bar')
self.assertEqual(slugify('FooBar'), 'foobar')
self.assertEqual(slugify('Foo Bar'), 'foo-bar')
self.assertEqual(slugify('Foo (Bar)'), 'foo-bar')


class BuildAbsoluteURITest(unittest.TestCase):
Expand Down
3 changes: 3 additions & 0 deletions social/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,9 @@ def slugify(value):
"""Converts to lowercase, removes non-word characters (alphanumerics
and underscores) and converts spaces to hyphens. Also strips leading
and trailing whitespace."""
# convert str of Python2 to unicode
if six.PY2 and not isinstance(value, six.text_type):
value = six.text_type(value)
value = unicodedata.normalize('NFKD', value) \
.encode('ascii', 'ignore') \
.decode('ascii')
Expand Down

0 comments on commit 993238c

Please sign in to comment.