API - revoke all token for a given client

This commit is contained in:
Sam 2022-06-12 17:15:18 +02:00
parent 1f26b69cba
commit e01248d0d1
4 changed files with 140 additions and 19 deletions

View File

@ -93,3 +93,15 @@ class OAuth2Token(BaseModel, OAuth2TokenMixin):
return False return False
expires_at = self.issued_at + self.expires_in * 2 expires_at = self.issued_at + self.expires_in * 2
return expires_at >= time.time() return expires_at >= time.time()
@classmethod
def revoke_client_tokens(cls, client_id: str) -> None:
sql = """
UPDATE oauth2_token
SET access_token_revoked_at = %(revoked_at)s
WHERE client_id = %(client_id)s;
"""
db.engine.execute(
sql, {'client_id': client_id, 'revoked_at': int(time.time())}
)
db.session.commit()

View File

@ -5,7 +5,7 @@ from flask import Blueprint, Response, request
from urllib3.util import parse_url from urllib3.util import parse_url
from fittrackee import db from fittrackee import db
from fittrackee.oauth2.models import OAuth2Client from fittrackee.oauth2.models import OAuth2Client, OAuth2Token
from fittrackee.oauth2.server import require_auth from fittrackee.oauth2.server import require_auth
from fittrackee.responses import ( from fittrackee.responses import (
HttpResponse, HttpResponse,
@ -152,6 +152,20 @@ def delete_client(
return {'status': 'no content'}, 204 return {'status': 'no content'}, 204
@oauth_blueprint.route('/oauth/apps/<int:client_id>/revoke', methods=['POST'])
@require_auth()
def revoke_client_tokens(
auth_user: User, client_id: int
) -> Union[Dict, HttpResponse]:
client = OAuth2Client.query.filter_by(id=client_id).first()
if not client:
return NotFoundErrorResponse('OAuth client not found')
OAuth2Token.revoke_client_tokens(client.client_id)
return {'status': 'success'}
@oauth_blueprint.route('/oauth/authorize', methods=['POST']) @oauth_blueprint.route('/oauth/authorize', methods=['POST'])
@require_auth() @require_auth()
def authorize(auth_user: User) -> Union[HttpResponse, Dict]: def authorize(auth_user: User) -> Union[HttpResponse, Dict]:

View File

@ -1,4 +1,5 @@
import json import json
import time
from random import randint from random import randint
from typing import Any, Dict, List, Optional, Tuple, Union from typing import Any, Dict, List, Optional, Tuple, Union
from urllib.parse import parse_qs from urllib.parse import parse_qs
@ -10,7 +11,7 @@ from werkzeug.test import TestResponse
from fittrackee import db from fittrackee import db
from fittrackee.oauth2.client import create_oauth_client from fittrackee.oauth2.client import create_oauth_client
from fittrackee.oauth2.models import OAuth2Client from fittrackee.oauth2.models import OAuth2Client, OAuth2Token
from fittrackee.users.models import User from fittrackee.users.models import User
from .custom_asserts import ( from .custom_asserts import (
@ -42,7 +43,45 @@ class RandomMixin:
return randint(min_val, max_val) return randint(min_val, max_val)
class ApiTestCaseMixin(RandomMixin): class OAuth2Mixin(RandomMixin):
@staticmethod
def create_oauth_client(
user: User,
metadata: Optional[Dict] = None,
scope: Optional[str] = None,
) -> OAuth2Client:
client_metadata = (
TEST_OAUTH_CLIENT_METADATA if metadata is None else metadata
)
if scope is not None:
client_metadata['scope'] = scope
oauth_client = create_oauth_client(client_metadata, user)
db.session.add(oauth_client)
db.session.commit()
return oauth_client
def create_oauth2_token(
self,
oauth_client: OAuth2Client,
issued_at: Optional[int] = None,
access_token_revoked_at: Optional[int] = 0,
expires_in: Optional[int] = 1000,
) -> OAuth2Token:
issued_at = issued_at if issued_at else int(time.time())
token = OAuth2Token(
client_id=oauth_client.client_id,
access_token=self.random_string(),
refresh_token=self.random_string(),
issued_at=issued_at,
access_token_revoked_at=access_token_revoked_at,
expires_in=expires_in,
)
db.session.add(token)
db.session.commit()
return token
class ApiTestCaseMixin(OAuth2Mixin, RandomMixin):
@staticmethod @staticmethod
def get_test_client_and_auth_token( def get_test_client_and_auth_token(
app: Flask, user_email: str app: Flask, user_email: str
@ -61,22 +100,6 @@ class ApiTestCaseMixin(RandomMixin):
auth_token = json.loads(resp_login.data.decode())['auth_token'] auth_token = json.loads(resp_login.data.decode())['auth_token']
return client, auth_token return client, auth_token
@staticmethod
def create_oauth_client(
user: User,
metadata: Optional[Dict] = None,
scope: Optional[str] = None,
) -> OAuth2Client:
client_metadata = (
TEST_OAUTH_CLIENT_METADATA if metadata is None else metadata
)
if scope is not None:
client_metadata['scope'] = scope
oauth_client = create_oauth_client(client_metadata, user)
db.session.add(oauth_client)
db.session.commit()
return oauth_client
@staticmethod @staticmethod
def authorize_client( def authorize_client(
client: FlaskClient, client: FlaskClient,

View File

@ -992,3 +992,75 @@ class TestOAuthDeleteClient(ApiTestCaseMixin):
self.assert_404_with_message(response, 'OAuth client not found') self.assert_404_with_message(response, 'OAuth client not found')
client = OAuth2Client.query.filter_by(id=client_id).first() client = OAuth2Client.query.filter_by(id=client_id).first()
assert client is not None assert client is not None
class TestOAuthRevokeClientToken(ApiTestCaseMixin):
route = '/api/oauth/apps/{client_id}/revoke'
def test_it_returns_error_when_not_authenticated(
self, app: Flask, user_1: User
) -> None:
client = app.test_client()
response = client.post(
self.route.format(client_id=self.random_int()),
content_type='application/json',
)
self.assert_401(response)
def test_it_returns_error_when_client_not_found(
self, app: Flask, user_1: User
) -> None:
client, auth_token = self.get_test_client_and_auth_token(
app, user_1.email
)
response = client.post(
self.route.format(client_id=self.random_int()),
content_type='application/json',
headers=dict(Authorization=f'Bearer {auth_token}'),
)
self.assert_404_with_message(response, 'OAuth client not found')
def test_it_revokes_all_client_tokens(
self, app: Flask, user_1: User
) -> None:
client, auth_token = self.get_test_client_and_auth_token(
app, user_1.email
)
oauth_client = self.create_oauth_client(user_1)
tokens = [self.create_oauth2_token(oauth_client) for _ in range(3)]
response = client.post(
self.route.format(client_id=oauth_client.id),
content_type='application/json',
headers=dict(Authorization=f'Bearer {auth_token}'),
)
assert response.status_code == 200
data = json.loads(response.data.decode())
assert data['status'] == 'success'
for token in tokens:
assert token.is_revoked()
def test_it_does_not_revoke_another_client_token(
self, app: Flask, user_1: User
) -> None:
client, auth_token = self.get_test_client_and_auth_token(
app, user_1.email
)
oauth_client = self.create_oauth_client(user_1)
client_id = oauth_client.id
another_client = self.create_oauth_client(user_1)
another_client_token = self.create_oauth2_token(another_client)
response = client.post(
self.route.format(client_id=client_id),
content_type='application/json',
headers=dict(Authorization=f'Bearer {auth_token}'),
)
assert response.status_code == 200
assert not another_client_token.is_revoked()