API - add missing tests on Authorization Code Grant (code challenge)
This commit is contained in:
parent
40a5fcccf3
commit
6b497bd72f
@ -106,7 +106,10 @@ class ApiTestCaseMixin(OAuth2Mixin, RandomMixin):
|
|||||||
oauth_client: OAuth2Client,
|
oauth_client: OAuth2Client,
|
||||||
auth_token: str,
|
auth_token: str,
|
||||||
scope: Optional[str] = None,
|
scope: Optional[str] = None,
|
||||||
|
code_challenge: Optional[Dict] = None,
|
||||||
) -> Union[List[str], str]:
|
) -> Union[List[str], str]:
|
||||||
|
if code_challenge is None:
|
||||||
|
code_challenge = {}
|
||||||
response = client.post(
|
response = client.post(
|
||||||
'/api/oauth/authorize',
|
'/api/oauth/authorize',
|
||||||
data={
|
data={
|
||||||
@ -114,6 +117,7 @@ class ApiTestCaseMixin(OAuth2Mixin, RandomMixin):
|
|||||||
'confirm': True,
|
'confirm': True,
|
||||||
'response_type': 'code',
|
'response_type': 'code',
|
||||||
'scope': 'read' if not scope else scope,
|
'scope': 'read' if not scope else scope,
|
||||||
|
**code_challenge,
|
||||||
},
|
},
|
||||||
headers=dict(
|
headers=dict(
|
||||||
Authorization=f'Bearer {auth_token}',
|
Authorization=f'Bearer {auth_token}',
|
||||||
@ -234,11 +238,14 @@ class ApiTestCaseMixin(OAuth2Mixin, RandomMixin):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def assert_invalid_request(response: TestResponse) -> Dict:
|
def assert_invalid_request(
|
||||||
|
response: TestResponse, error_description: Optional[str] = None
|
||||||
|
) -> Dict:
|
||||||
return assert_oauth_errored_response(
|
return assert_oauth_errored_response(
|
||||||
response,
|
response,
|
||||||
400,
|
400,
|
||||||
error='invalid_request',
|
error='invalid_request',
|
||||||
|
error_description=error_description,
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import json
|
import json
|
||||||
from typing import Dict, List, Tuple, Union
|
from typing import Dict, List, Optional, Tuple, Union
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from authlib.common.security import generate_token
|
||||||
|
from authlib.oauth2.rfc7636 import create_s256_code_challenge
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
from werkzeug.test import TestResponse
|
from werkzeug.test import TestResponse
|
||||||
|
|
||||||
@ -367,15 +369,82 @@ class TestOAuthClientAuthorization(ApiTestCaseMixin):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestOAuthClientAuthorizationWithCodeChallenge(ApiTestCaseMixin):
|
||||||
|
route = '/api/oauth/authorize'
|
||||||
|
|
||||||
|
def test_it_returns_error_when_code_challenge_method_is_invalid(
|
||||||
|
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)
|
||||||
|
code_verifier = generate_token(48)
|
||||||
|
code_challenge = create_s256_code_challenge(code_verifier)
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
self.route,
|
||||||
|
data={
|
||||||
|
'client_id': oauth_client.client_id,
|
||||||
|
'response_type': 'code',
|
||||||
|
'confirm': 'true',
|
||||||
|
'code_challenge': code_challenge,
|
||||||
|
'code_challenge_method': self.random_string(),
|
||||||
|
},
|
||||||
|
headers=dict(
|
||||||
|
Authorization=f'Bearer {auth_token}',
|
||||||
|
content_type='multipart/form-data',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assert_400(response, 'Unsupported "code_challenge_method"')
|
||||||
|
|
||||||
|
def test_it_creates_authorization_code(
|
||||||
|
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)
|
||||||
|
code_verifier = generate_token(48)
|
||||||
|
code_challenge = create_s256_code_challenge(code_verifier)
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
self.route,
|
||||||
|
data={
|
||||||
|
'client_id': oauth_client.client_id,
|
||||||
|
'response_type': 'code',
|
||||||
|
'confirm': 'true',
|
||||||
|
'code_challenge': code_challenge,
|
||||||
|
'code_challenge_method': 'S256',
|
||||||
|
},
|
||||||
|
headers=dict(
|
||||||
|
Authorization=f'Bearer {auth_token}',
|
||||||
|
content_type='multipart/form-data',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
code = OAuth2AuthorizationCode.query.filter_by(
|
||||||
|
client_id=oauth_client.client_id
|
||||||
|
).first()
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = json.loads(response.data.decode())
|
||||||
|
assert data['redirect_url'] == (
|
||||||
|
f'{oauth_client.get_default_redirect_uri()}?code={code.code}'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class OAuthIssueTokenTestCase(ApiTestCaseMixin):
|
class OAuthIssueTokenTestCase(ApiTestCaseMixin):
|
||||||
def create_authorized_oauth_client(
|
def create_authorized_oauth_client(
|
||||||
self, app: Flask, user_1: User
|
self, app: Flask, user_1: User, code_challenge: Optional[Dict] = None
|
||||||
) -> Tuple[OAuth2Client, Union[List[str], str]]:
|
) -> Tuple[OAuth2Client, Union[List[str], str]]:
|
||||||
client, auth_token = self.get_test_client_and_auth_token(
|
client, auth_token = self.get_test_client_and_auth_token(
|
||||||
app, user_1.email
|
app, user_1.email
|
||||||
)
|
)
|
||||||
oauth_client = self.create_oauth_client(user_1)
|
oauth_client = self.create_oauth_client(user_1)
|
||||||
code = self.authorize_client(client, oauth_client, auth_token)
|
code = self.authorize_client(
|
||||||
|
client, oauth_client, auth_token, code_challenge=code_challenge
|
||||||
|
)
|
||||||
return oauth_client, code
|
return oauth_client, code
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -497,6 +566,64 @@ class TestOAuthIssueAccessToken(OAuthIssueTokenTestCase):
|
|||||||
self.assert_token_is_returned(response)
|
self.assert_token_is_returned(response)
|
||||||
|
|
||||||
|
|
||||||
|
class TestOAuthIssueAccessTokenWithCodeChallenge(OAuthIssueTokenTestCase):
|
||||||
|
route = '/api/oauth/token'
|
||||||
|
|
||||||
|
def test_it_raises_error_when_code_verifier_is_invalid(
|
||||||
|
self, app: Flask, user_1: User
|
||||||
|
) -> None:
|
||||||
|
code_verifier = generate_token(48)
|
||||||
|
code_challenge = create_s256_code_challenge(code_verifier)
|
||||||
|
oauth_client, code = self.create_authorized_oauth_client(
|
||||||
|
app,
|
||||||
|
user_1,
|
||||||
|
{
|
||||||
|
'code_challenge': code_challenge,
|
||||||
|
'code_challenge_method': 'S256',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
client = app.test_client()
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
self.route,
|
||||||
|
data={
|
||||||
|
'client_id': oauth_client.client_id,
|
||||||
|
'client_secret': oauth_client.client_secret,
|
||||||
|
'grant_type': 'authorization_code',
|
||||||
|
'code': code,
|
||||||
|
'code_verifier': self.random_string(),
|
||||||
|
},
|
||||||
|
headers=dict(content_type='multipart/form-data'),
|
||||||
|
)
|
||||||
|
self.assert_invalid_request(response, 'Invalid "code_verifier"')
|
||||||
|
|
||||||
|
def test_it_returns_access_token(self, app: Flask, user_1: User) -> None:
|
||||||
|
code_verifier = generate_token(48)
|
||||||
|
oauth_client, code = self.create_authorized_oauth_client(
|
||||||
|
app,
|
||||||
|
user_1,
|
||||||
|
code_challenge={
|
||||||
|
'code_challenge': create_s256_code_challenge(code_verifier),
|
||||||
|
'code_challenge_method': 'S256',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
client = app.test_client()
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
self.route,
|
||||||
|
data={
|
||||||
|
'client_id': oauth_client.client_id,
|
||||||
|
'client_secret': oauth_client.client_secret,
|
||||||
|
'grant_type': 'authorization_code',
|
||||||
|
'code': code,
|
||||||
|
'code_verifier': code_verifier,
|
||||||
|
},
|
||||||
|
headers=dict(content_type='multipart/form-data'),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assert_token_is_returned(response)
|
||||||
|
|
||||||
|
|
||||||
class TestOAuthIssueRefreshToken(OAuthIssueTokenTestCase):
|
class TestOAuthIssueRefreshToken(OAuthIssueTokenTestCase):
|
||||||
route = '/api/oauth/token'
|
route = '/api/oauth/token'
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user