diff --git a/social/backends/open_id.py b/social/backends/open_id.py index 3ab525257..91cd39e80 100644 --- a/social/backends/open_id.py +++ b/social/backends/open_id.py @@ -2,18 +2,16 @@ from calendar import timegm from jwt import InvalidTokenError, decode as jwt_decode - from openid.consumer.consumer import Consumer, SUCCESS, CANCEL, FAILURE from openid.consumer.discover import DiscoveryFailure from openid.extensions import sreg, ax, pape -from social.utils import url_add_parameters -from social.exceptions import AuthException, AuthFailed, AuthCanceled, \ - AuthUnknownError, AuthMissingParameter, \ - AuthTokenError from social.backends.base import BaseAuth from social.backends.oauth import BaseOAuth2 - +from social.exceptions import ( + AuthException, AuthFailed, AuthCanceled, AuthUnknownError, AuthMissingParameter, AuthTokenError +) +from social.utils import url_add_parameters # OpenID configuration OLD_AX_ATTRS = [ @@ -278,6 +276,7 @@ class OpenIdConnectAuth(BaseOAuth2): Currently only the code response type is supported. """ ID_TOKEN_ISSUER = None + ID_TOKEN_MAX_AGE = 600 DEFAULT_SCOPE = ['openid'] EXTRA_DATA = ['id_token', 'refresh_token', ('sub', 'id')] # Set after access_token is retrieved @@ -325,19 +324,35 @@ def validate_and_return_id_token(self, id_token): http://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation. """ client_id, _client_secret = self.get_key_and_secret() - decryption_key = self.setting('ID_TOKEN_DECRYPTION_KEY') + + decode_kwargs = { + 'algorithms': ['HS256'], + 'audience': client_id, + 'issuer': self.ID_TOKEN_ISSUER, + 'key': self.setting('ID_TOKEN_DECRYPTION_KEY'), + 'options': { + 'verify_signature': True, + 'verify_exp': True, + 'verify_iat': True, + 'verify_aud': True, + 'verify_iss': True, + 'require_exp': True, + 'require_iat': True, + }, + } + decode_kwargs.update(self.setting('ID_TOKEN_JWT_DECODE_KWARGS', {})) + try: # Decode the JWT and raise an error if the secret is invalid or # the response has expired. - id_token = jwt_decode(id_token, decryption_key, audience=client_id, - issuer=self.ID_TOKEN_ISSUER, - algorithms=['HS256']) + id_token = jwt_decode(id_token, **decode_kwargs) except InvalidTokenError as err: raise AuthTokenError(self, err) - # Verify the token was issued in the last 10 minutes + # Verify the token was issued within a specified amount of time + iat_leeway = self.setting('ID_TOKEN_MAX_AGE', self.ID_TOKEN_MAX_AGE) utc_timestamp = timegm(datetime.datetime.utcnow().utctimetuple()) - if id_token['iat'] < (utc_timestamp - 600): + if id_token['iat'] < (utc_timestamp - iat_leeway): raise AuthTokenError(self, 'Incorrect id_token: iat') # Validate the nonce to ensure the request was not modified diff --git a/social/tests/backends/open_id.py b/social/tests/backends/open_id.py index 22e6d45e3..c46a550d1 100644 --- a/social/tests/backends/open_id.py +++ b/social/tests/backends/open_id.py @@ -1,13 +1,11 @@ # -*- coding: utf-8 -*- -from calendar import timegm - -import sys -import json import datetime +import json +import sys +from calendar import timegm -import requests import jwt - +import requests from openid import oidutil @@ -127,14 +125,15 @@ class OpenIdConnectTestMixin(object): client_key = 'a-key' client_secret = 'a-secret-key' issuer = None # id_token issuer + id_token_max_age = 600 # seconds def extra_settings(self): settings = super(OpenIdConnectTestMixin, self).extra_settings() settings.update({ 'SOCIAL_AUTH_{0}_KEY'.format(self.name): self.client_key, 'SOCIAL_AUTH_{0}_SECRET'.format(self.name): self.client_secret, - 'SOCIAL_AUTH_{0}_ID_TOKEN_DECRYPTION_KEY'.format(self.name): - self.client_secret + 'SOCIAL_AUTH_{0}_ID_TOKEN_DECRYPTION_KEY'.format(self.name): self.client_secret, + 'SOCIAL_AUTH_{0}_ID_TOKEN_MAX_AGE'.format(self.name): self.id_token_max_age, }) return settings @@ -197,11 +196,11 @@ def prepare_access_token_body(self, client_key=None, client_secret=None, algorithm='HS256').decode('utf-8') return json.dumps(body) - def authtoken_raised(self, expected_message, **access_token_kwargs): + def authtoken_raised(self, expected_message_regexp, **access_token_kwargs): self.access_token_body = self.prepare_access_token_body( **access_token_kwargs ) - with self.assertRaisesRegexp(AuthTokenError, expected_message): + with self.assertRaisesRegexp(AuthTokenError, expected_message_regexp): self.do_login() def test_invalid_secret(self): @@ -225,10 +224,10 @@ def test_invalid_audience(self): client_key='someone-else') def test_invalid_issue_time(self): - expiration_datetime = datetime.datetime.utcnow() - \ - datetime.timedelta(hours=1) + issue_datetime = datetime.datetime.utcnow() - \ + datetime.timedelta(seconds=self.id_token_max_age + 1) self.authtoken_raised('Token error: Incorrect id_token: iat', - issue_datetime=expiration_datetime) + issue_datetime=issue_datetime) def test_invalid_nonce(self): self.authtoken_raised(