API - revoke all token for a given client
This commit is contained in:
parent
1f26b69cba
commit
e01248d0d1
@ -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()
|
||||||
|
@ -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]:
|
||||||
|
@ -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,
|
||||||
|
@ -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()
|
||||||
|
Loading…
Reference in New Issue
Block a user