def test_refresh_token(self): """ A request to the Token Endpoint can also use a Refresh Token by using the grant_type value refresh_token, as described in Section 6 of OAuth 2.0 [RFC6749]. """ SIGKEYS = self._get_keys() # Retrieve refresh token code = self._create_code() post_data = self._auth_code_post_data(code=code.code) real_now = timezone.now with patch('oidc_provider.lib.utils.token.timezone.now') as now: now.return_value = real_now() response = self._post_request(post_data) response_dic1 = json.loads(response.content.decode('utf-8')) id_token1 = JWS().verify_compact(response_dic1['id_token'].encode('utf-8'), SIGKEYS) # Use refresh token to obtain new token post_data = self._refresh_token_post_data(response_dic1['refresh_token']) with patch('oidc_provider.lib.utils.token.timezone.now') as now: now.return_value = real_now() + timedelta(minutes=10) response = self._post_request(post_data) response_dic2 = json.loads(response.content.decode('utf-8')) id_token2 = JWS().verify_compact(response_dic2['id_token'].encode('utf-8'), SIGKEYS) self.assertNotEqual(response_dic1['id_token'], response_dic2['id_token']) self.assertNotEqual(response_dic1['access_token'], response_dic2['access_token']) self.assertNotEqual(response_dic1['refresh_token'], response_dic2['refresh_token']) # http://openid.net/specs/openid-connect-core-1_0.html#rfc.section.12.2 self.assertEqual(id_token1['iss'], id_token2['iss']) self.assertEqual(id_token1['sub'], id_token2['sub']) self.assertNotEqual(id_token1['iat'], id_token2['iat']) self.assertEqual(id_token1['aud'], id_token2['aud']) self.assertEqual(id_token1['auth_time'], id_token2['auth_time']) self.assertEqual(id_token1.get('azp'), id_token2.get('azp')) # Refresh token can't be reused post_data = self._refresh_token_post_data(response_dic1['refresh_token']) response = self._post_request(post_data) self.assertIn('invalid_grant', response.content.decode('utf-8')) # Old access token is invalidated self.assertEqual(self._get_userinfo(response_dic1['access_token']).status_code, 401) self.assertEqual(self._get_userinfo(response_dic2['access_token']).status_code, 200) # Empty refresh token is invalid post_data = self._refresh_token_post_data('') response = self._post_request(post_data) self.assertIn('invalid_grant', response.content.decode('utf-8')) # No refresh token is invalid post_data = self._refresh_token_post_data('') del post_data['refresh_token'] response = self._post_request(post_data) self.assertIn('invalid_grant', response.content.decode('utf-8'))
def test_refresh_token(self): """ A request to the Token Endpoint can also use a Refresh Token by using the grant_type value refresh_token, as described in Section 6 of OAuth 2.0 [RFC6749]. """ SIGKEYS = self._get_keys() # Retrieve refresh token code = self._create_code() post_data = self._auth_code_post_data(code=code.code) real_now = timezone.now with patch('oidc_provider.lib.utils.token.timezone.now') as now: now.return_value = real_now() response = self._post_request(post_data) response_dic1 = json.loads(response.content.decode('utf-8')) id_token1 = JWS().verify_compact( response_dic1['id_token'].encode('utf-8'), SIGKEYS) # Use refresh token to obtain new token post_data = self._refresh_token_post_data( response_dic1['refresh_token']) with patch('oidc_provider.lib.utils.token.timezone.now') as now: now.return_value = real_now() + timedelta(minutes=10) response = self._post_request(post_data) response_dic2 = json.loads(response.content.decode('utf-8')) id_token2 = JWS().verify_compact( response_dic2['id_token'].encode('utf-8'), SIGKEYS) self.assertNotEqual(response_dic1['id_token'], response_dic2['id_token']) self.assertNotEqual(response_dic1['access_token'], response_dic2['access_token']) self.assertNotEqual(response_dic1['refresh_token'], response_dic2['refresh_token']) # http://openid.net/specs/openid-connect-core-1_0.html#rfc.section.12.2 self.assertEqual(id_token1['iss'], id_token2['iss']) self.assertEqual(id_token1['sub'], id_token2['sub']) self.assertNotEqual(id_token1['iat'], id_token2['iat']) self.assertEqual(id_token1['aud'], id_token2['aud']) self.assertEqual(id_token1['auth_time'], id_token2['auth_time']) self.assertEqual(id_token1.get('azp'), id_token2.get('azp')) # Refresh token can't be reused post_data = self._refresh_token_post_data( response_dic1['refresh_token']) response = self._post_request(post_data) self.assertIn('invalid_grant', response.content.decode('utf-8')) # Old access token is invalidated self.assertEqual( self._get_userinfo(response_dic1['access_token']).status_code, 401) self.assertEqual( self._get_userinfo(response_dic2['access_token']).status_code, 200) # Empty refresh token is invalid post_data = self._refresh_token_post_data('') response = self._post_request(post_data) self.assertIn('invalid_grant', response.content.decode('utf-8')) # No refresh token is invalid post_data = self._refresh_token_post_data('') del post_data['refresh_token'] response = self._post_request(post_data) self.assertIn('invalid_grant', response.content.decode('utf-8'))
def do_refresh_token_check(self, scope=None): SIGKEYS = self._get_keys() # Retrieve refresh token code = self._create_code() self.assertEqual(code.scope, TokenTestCase.SCOPE_LIST) post_data = self._auth_code_post_data(code=code.code) start_time = time.time() with patch('oidc_provider.lib.utils.token.time.time') as time_func: time_func.return_value = start_time response = self._post_request(post_data) response_dic1 = json.loads(response.content.decode('utf-8')) id_token1 = JWS().verify_compact( response_dic1['id_token'].encode('utf-8'), SIGKEYS) # Use refresh token to obtain new token post_data = self._refresh_token_post_data( response_dic1['refresh_token'], scope) with patch('oidc_provider.lib.utils.token.time.time') as time_func: time_func.return_value = start_time + 600 response = self._post_request(post_data) response_dic2 = json.loads(response.content.decode('utf-8')) if scope and set(scope) - set(code.scope): # too broad scope self.assertEqual(response.status_code, 400) # Bad Request self.assertIn('error', response_dic2) self.assertEqual(response_dic2['error'], 'invalid_scope') return # No more checks id_token2 = JWS().verify_compact( response_dic2['id_token'].encode('utf-8'), SIGKEYS) if scope and 'email' not in scope: # narrowed scope The auth # The auth code request had email in scope, so it should be # in the first id token self.assertIn('email', id_token1) # but the refresh request had no email in scope self.assertNotIn('email', id_token2, 'email was not requested') self.assertNotEqual(response_dic1['id_token'], response_dic2['id_token']) self.assertNotEqual(response_dic1['access_token'], response_dic2['access_token']) self.assertNotEqual(response_dic1['refresh_token'], response_dic2['refresh_token']) # http://openid.net/specs/openid-connect-core-1_0.html#rfc.section.12.2 self.assertEqual(id_token1['iss'], id_token2['iss']) self.assertEqual(id_token1['sub'], id_token2['sub']) self.assertNotEqual(id_token1['iat'], id_token2['iat']) self.assertEqual(id_token1['iat'], int(start_time)) self.assertEqual(id_token2['iat'], int(start_time + 600)) self.assertEqual(id_token1['aud'], id_token2['aud']) self.assertEqual(id_token1['auth_time'], id_token2['auth_time']) self.assertEqual(id_token1.get('azp'), id_token2.get('azp')) # Refresh token can't be reused post_data = self._refresh_token_post_data( response_dic1['refresh_token']) response = self._post_request(post_data) self.assertIn('invalid_grant', response.content.decode('utf-8')) # Old access token is invalidated self.assertEqual( self._get_userinfo(response_dic1['access_token']).status_code, 401) self.assertEqual( self._get_userinfo(response_dic2['access_token']).status_code, 200) # Empty refresh token is invalid post_data = self._refresh_token_post_data('') response = self._post_request(post_data) self.assertIn('invalid_grant', response.content.decode('utf-8')) # No refresh token is invalid post_data = self._refresh_token_post_data('') del post_data['refresh_token'] response = self._post_request(post_data) self.assertIn('invalid_grant', response.content.decode('utf-8'))
class TokenSignalHandler(SignalHandler): def __init__(self, keys: List, leeway: int, cache: BaseCache) -> None: self._keys = keys self._leeway = leeway self._cache = cache self._request_jwt_claims = dict(iss='public', aud='example-app') self._hash_algs = dict(S256='sha256', S384='sha384', S512='sha512') def request_started_handler(self, sender, **extra): token = get_token_from_request(request) if not token: raise HttpException("No EX-JWT authorization token provided", 401) try: self._request_jwt_claims = JWS().verify_compact(token, self._keys) except JWKESTException: raise HttpException("Invalid token", 401) errors = list() now = int(datetime.utcnow().timestamp()) if self._request_jwt_claims.get('iss') != "valid-client": errors.append("missing or invalid issuer") if self._request_jwt_claims.get('aud') != "api-server": errors.append("missing or invalid audience") if not 'jti' in self._request_jwt_claims: errors.append("missing token ID") elif not self._cache.add(self._request_jwt_claims['jti'], 1, 3600): errors.append("duplicate token ID") if 'nbf' not in self._request_jwt_claims: errors.append("missing not before") elif not isinstance(self._request_jwt_claims['nbf'], int): errors.append("invalid not before type") elif self._request_jwt_claims['nbf'] + self._leeway < now: errors.append("invalid not before") if 'exp' not in self._request_jwt_claims: errors.append("missing expires") elif not isinstance(self._request_jwt_claims['exp'], int): errors.append("invalid expires type") elif self._request_jwt_claims['exp'] - self._leeway > now: errors.append("invalid expires") if 'request' not in self._request_jwt_claims: errors.append("request claim missing") if 'path' not in self._request_jwt_claims['request']: errors.append("request[path] claim missing") elif self._request_jwt_claims['request']['path'] != request.path: errors.append("invalid request[path] claim") if 'method' not in self._request_jwt_claims['request']: errors.append("request[method] claim missing") elif self._request_jwt_claims['request']['method'] != request.method: errors.append("invalid request[method] claim") if request.content_length is not None and request.content_length > 0: if 'body_hash_alg' not in self._request_jwt_claims['request']: errors.append("request[body_hash_alg] claim missing") elif self._request_jwt_claims['request'][ 'body_hash_alg'] not in self._hash_algs: errors.append( "request[body_hash_alg] must be one of: {}".format( ", ".join(self._hash_algs.keys()))) elif 'body_hash' not in self._request_jwt_claims['request']: errors.append("request[body_hash_alg] claim missing") hasher = hashlib.new(self._hash_algs[ self._request_jwt_claims['request']['body_hash_alg']]) hasher.update(request.data) actual = hasher.hexdigest() if actual != self._request_jwt_claims['request']['body_hash']: errors.append("invalid body hash") if len(errors) > 0: raise HttpException("Invalid token: {}".format(", ".join(errors)), 401) def request_finished_handler(self, sender, response, **extra): if response.status_code < 300: now = int(datetime.utcnow().timestamp()) claims = { 'jti': self._request_jwt_claims['jti'], 'iat': now, 'nbf': now, 'exp': now, 'iss': self._request_jwt_claims['aud'], 'aud': self._request_jwt_claims['iss'], 'response': { 'status_code': response.status_code, 'body_hash_alg': 'S512', 'body_hash': sha512(response.data).hexdigest() } } jws = JWS(json.dumps(claims), alg="HS256") signed_content = jws.sign_compact(keys=self._keys) response.headers['X-JWT'] = signed_content
def do_refresh_token_check(self, scope=None): SIGKEYS = self._get_keys() # Retrieve refresh token code = self._create_code() self.assertEqual(code.scope, ['openid', 'email']) post_data = self._auth_code_post_data(code=code.code) start_time = time.time() with patch('oidc_provider.lib.utils.token.time.time') as time_func: time_func.return_value = start_time response = self._post_request(post_data) response_dic1 = json.loads(response.content.decode('utf-8')) id_token1 = JWS().verify_compact(response_dic1['id_token'].encode('utf-8'), SIGKEYS) # Use refresh token to obtain new token post_data = self._refresh_token_post_data( response_dic1['refresh_token'], scope) with patch('oidc_provider.lib.utils.token.time.time') as time_func: time_func.return_value = start_time + 600 response = self._post_request(post_data) response_dic2 = json.loads(response.content.decode('utf-8')) if scope and set(scope) - set(code.scope): # too broad scope self.assertEqual(response.status_code, 400) # Bad Request self.assertIn('error', response_dic2) self.assertEqual(response_dic2['error'], 'invalid_scope') return # No more checks id_token2 = JWS().verify_compact(response_dic2['id_token'].encode('utf-8'), SIGKEYS) if scope and 'email' not in scope: # narrowed scope The auth # The auth code request had email in scope, so it should be # in the first id token self.assertIn('email', id_token1) # but the refresh request had no email in scope self.assertNotIn('email', id_token2, 'email was not requested') self.assertNotEqual(response_dic1['id_token'], response_dic2['id_token']) self.assertNotEqual(response_dic1['access_token'], response_dic2['access_token']) self.assertNotEqual(response_dic1['refresh_token'], response_dic2['refresh_token']) # http://openid.net/specs/openid-connect-core-1_0.html#rfc.section.12.2 self.assertEqual(id_token1['iss'], id_token2['iss']) self.assertEqual(id_token1['sub'], id_token2['sub']) self.assertNotEqual(id_token1['iat'], id_token2['iat']) self.assertEqual(id_token1['iat'], int(start_time)) self.assertEqual(id_token2['iat'], int(start_time + 600)) self.assertEqual(id_token1['aud'], id_token2['aud']) self.assertEqual(id_token1['auth_time'], id_token2['auth_time']) self.assertEqual(id_token1.get('azp'), id_token2.get('azp')) # Refresh token can't be reused post_data = self._refresh_token_post_data(response_dic1['refresh_token']) response = self._post_request(post_data) self.assertIn('invalid_grant', response.content.decode('utf-8')) # Old access token is invalidated self.assertEqual(self._get_userinfo(response_dic1['access_token']).status_code, 401) self.assertEqual(self._get_userinfo(response_dic2['access_token']).status_code, 200) # Empty refresh token is invalid post_data = self._refresh_token_post_data('') response = self._post_request(post_data) self.assertIn('invalid_grant', response.content.decode('utf-8')) # No refresh token is invalid post_data = self._refresh_token_post_data('') del post_data['refresh_token'] response = self._post_request(post_data) self.assertIn('invalid_grant', response.content.decode('utf-8'))