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'))
Пример #2
0
    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'))
Пример #4
0
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'))