Skip to content

Commit

Permalink
Merge pull request #24 from jamesholcombe/dev
Browse files Browse the repository at this point in the history
Dev
  • Loading branch information
jamesholcombe committed Jul 6, 2023
2 parents 40dafb6 + 9831db6 commit f9f4fbe
Show file tree
Hide file tree
Showing 5 changed files with 79 additions and 100 deletions.
10 changes: 5 additions & 5 deletions dash_auth_external/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ def __init__(
external_auth_url: str,
external_token_url: str,
client_id: str,
client_secret: str = None,
with_pkce=True,
app_url: str = "http://127.0.0.1:8050",
redirect_suffix: str = "/redirect",
Expand All @@ -75,6 +76,7 @@ def __init__(
_secret_key: str = None,
auth_request_headers: dict = None,
token_request_headers: dict = None,
token_body_params: dict = None,
scope: str = None,
_server_name: str = __name__,
):
Expand All @@ -90,10 +92,10 @@ def __init__(
auth_suffix (str, optional): The route that will trigger the initial redirect to the external OAuth provider. Defaults to "/".
home_suffix (str, optional): The route your dash application will sit, relative to your url. Defaults to "/home".
_flask_server (Flask, optional): Flask server to use if additional config required. Defaults to None.
_token_field_name (str, optional): The key for the token returned in JSON from the token endpoint. Defaults to "access_token".
_secret_key (str, optional): Secret key for flask app, normally generated at runtime. Defaults to None.
auth_request_headers (dict, optional): Additional params to send to the authorization endpoint. Defaults to None.
auth_request_headers (dict, optional): Additional headers to send to the authorization endpoint. Defaults to None.
token_request_headers (dict, optional): Additional headers to send to the access token endpoint. Defaults to None.
token_body_params (dict, optional): Additional body params to send to the access token endpoint. Defaults to None.
scope (str, optional): Header required by most Oauth2 Providers. Defaults to None.
_server_name (str, optional): The name of the Flask Server. Defaults to __name__, ignored if _flask_server is not None.
Expand All @@ -108,7 +110,6 @@ def __init__(
token_request_headers = {}

if _flask_server is None:

app = Flask(
_server_name, instance_relative_config=False, static_folder="./assets"
)
Expand Down Expand Up @@ -136,11 +137,11 @@ def __init__(
app,
external_token_url=external_token_url,
client_id=client_id,
client_secret=client_secret,
redirect_uri=redirect_uri,
redirect_suffix=redirect_suffix,
_home_suffix=home_suffix,
token_request_headers=token_request_headers,
_token_field_name=_token_field_name,
with_pkce=with_pkce,
)

Expand All @@ -156,7 +157,6 @@ def __init__(


def refresh_token(url: str, token_data: OAuth2Token, headers: dict) -> OAuth2Token:

body = {
"grant_type": "refresh_token",
"refresh_token": token_data.refresh_token,
Expand Down
17 changes: 9 additions & 8 deletions dash_auth_external/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,9 @@ def get_auth_code():
return app


def build_token_body(url: str, redirect_uri: str, client_id: str, with_pkce: bool):
def build_token_body(
url: str, redirect_uri: str, client_id: str, with_pkce: bool, client_secret: str
):
query = urllib.parse.urlparse(url).query
redirect_params = urllib.parse.parse_qs(query)
code = redirect_params["code"][0]
Expand All @@ -80,9 +82,11 @@ def build_token_body(url: str, redirect_uri: str, client_id: str, with_pkce: boo
)

if with_pkce:

body["code_verifier"] = session["cv"]

if client_secret:
body["client_secret"] = client_secret

return body


Expand All @@ -93,7 +97,7 @@ def make_access_token_route(
_home_suffix: str,
redirect_uri: str,
client_id: str,
_token_field_name: str,
client_secret: str,
with_pkce: bool,
token_request_headers: dict,
):
Expand All @@ -105,6 +109,7 @@ def get_token_route():
redirect_uri=redirect_uri,
with_pkce=with_pkce,
client_id=client_id,
client_secret=client_secret,
)

response_data = token_request(
Expand All @@ -124,9 +129,5 @@ def get_token_route():

def token_request(url: str, body: dict, headers: dict):
r = requests.post(url, data=body, headers=headers)

if r.status_code != 200:
raise requests.RequestException(
f"{r.status_code} {r.reason}:The request to the access token endpoint failed."
)
r.raise_for_status()
return r.json()
4 changes: 2 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@

setup(
name=NAME,
version="1.0.1",
description="Integrate Dash with 3rd Parties and external providers",
version="1.1.0",
description=long_description,
python_requires=">=3.7",
author_email="jholcombe@hotmail.co.uk",
url="https://github.com/jamesholcombe/dash-auth-external",
Expand Down
147 changes: 62 additions & 85 deletions tests/test_app.py
Original file line number Diff line number Diff line change
@@ -1,114 +1,91 @@
"""Module for integation tests, testing full OAuth2 flow, excluding the get_token method
"""

import pytest
from dash_auth_external import DashAuthExternal
from unittest.mock import Mock
from dash_auth_external.config import FLASK_SESSION_TOKEN_KEY
from .test_config import EXERNAL_TOKEN_URL, EXTERNAL_AUTH_URL, CLIENT_ID
from pytest_mock import mocker
from .test_config import (
EXERNAL_TOKEN_URL,
EXTERNAL_AUTH_URL,
CLIENT_ID,
CLIENT_SECRET,
)
from requests_oauthlib import OAuth2Session


def test_pkce_true(mocker):
@pytest.mark.parametrize(
"with_pkce, with_client_secret",
[(True, True), (True, False), (False, True), (False, False)],
)
def test_flow(with_pkce, with_client_secret, mocker):

auth = DashAuthExternal(
EXTERNAL_AUTH_URL, EXERNAL_TOKEN_URL, CLIENT_ID, with_pkce=True
EXTERNAL_AUTH_URL,
EXERNAL_TOKEN_URL,
CLIENT_ID,
with_pkce=with_pkce,
client_secret=CLIENT_SECRET if with_client_secret else None,
)
app = auth.server
redirect_uri = "http://127.0.0.1:8050" + auth.redirect_suffix

redirect_uri = "http://localhost:8050"
session_mock = Mock(
OAuth2Session(CLIENT_ID, redirect_uri=redirect_uri, scope=auth.scope)
OAuth2Session(
CLIENT_ID,
redirect_uri=redirect_uri,
scope=auth.scope,
)
)

app = auth.server
session_mock.authorization_url.return_value = ("https://example.com", "state")

mocker.patch("dash_auth_external.routes.OAuth2Session", return_value=session_mock)

mocker.patch(
"dash_auth_external.routes.make_code_challenge",
return_value=("code_challenge", "code_verifier"),
token_request_mock = mocker.patch(
"dash_auth_external.routes.token_request",
return_value={
"access_token": "access_token",
"refresh_token": "refresh_token",
"token_type": "Bearer",
"expires_in": "3599",
},
)

# mocking the two helper functions called within the view function for the redirect to home suffix.

with app.test_client() as client:

response = client.get(auth.auth_suffix)
assert response.status_code == 302

# assert that the authorization_url method was called with the correct arguments
session_mock.authorization_url.assert_called_with(
EXTERNAL_AUTH_URL,
code_challenge="code_challenge",
code_challenge_method="S256",
)

# user logs in and is redirected to the redirect_uri, with a code and state

# mocking the token request in the token route
expected_token_request_body = {
"grant_type": "authorization_code",
"code": "code",
"redirect_uri": redirect_uri,
"client_id": CLIENT_ID,
"state": "state",
}
if with_pkce:
expected_token_request_body["code_verifier"] = "code_verifier"
if with_client_secret:
expected_token_request_body["client_secret"] = CLIENT_SECRET

if with_pkce:
mocker.patch(
"dash_auth_external.routes.token_request",
return_value={
"access_token": "access_token",
"refresh_token": "refresh_token",
"token_type": "Bearer",
"expires_in": "3599",
},
"dash_auth_external.routes.make_code_challenge",
return_value=("code_challenge", "code_verifier"),
)

# now we call the token route with the code and state returned from the authorization_url method
response = client.get(
auth.redirect_suffix, query_string={"code": "code", "state": "state"}
)
assert response.status_code == 302
with client.session_transaction() as session:
token_data = session[FLASK_SESSION_TOKEN_KEY]
assert token_data["access_token"] == "access_token"
assert token_data["refresh_token"] == "refresh_token"
assert token_data["token_type"] == "Bearer"
assert token_data["expires_in"] == "3599"


def test_pkce_false(mocker):
auth = DashAuthExternal(
EXTERNAL_AUTH_URL, EXERNAL_TOKEN_URL, CLIENT_ID, with_pkce=False
)
app = auth.server

redirect_uri = "http://localhost:8050"
session_mock = Mock(
OAuth2Session(CLIENT_ID, redirect_uri=redirect_uri, scope=auth.scope)
)

session_mock.authorization_url.return_value = ("https://example.com", "state")

mocker.patch("dash_auth_external.routes.OAuth2Session", return_value=session_mock)

with app.test_client() as client:

response = client.get(auth.auth_suffix)
assert response.status_code == 302

# assert that the authorization_url method was called with the correct arguments
session_mock.authorization_url.assert_called_with(EXTERNAL_AUTH_URL)

# user logs in and is redirected to the redirect_uri, with a code and state
if with_pkce:
session_mock.authorization_url.assert_called_with(
EXTERNAL_AUTH_URL,
code_challenge="code_challenge",
code_challenge_method="S256",
)
else:
session_mock.authorization_url.assert_called_with(EXTERNAL_AUTH_URL)

# mocking the token request in the token route
mocker.patch(
"dash_auth_external.routes.token_request",
return_value={
"access_token": "access_token",
"refresh_token": "refresh_token",
"token_type": "Bearer",
"expires_in": "3599",
},
)

# now we call the token route with the code and state returned from the authorization_url method
response = client.get(
auth.redirect_suffix, query_string={"code": "code", "state": "state"}
)
token_request_mock.assert_called_with(
url=EXERNAL_TOKEN_URL,
body=expected_token_request_body,
headers={},
)

assert response.status_code == 302
with client.session_transaction() as session:
token_data = session[FLASK_SESSION_TOKEN_KEY]
Expand Down
1 change: 1 addition & 0 deletions tests/test_config.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
CLIENT_ID = "FAKE_ID"
EXERNAL_TOKEN_URL = "TOKEN_URL"
EXTERNAL_AUTH_URL = "AUTH_URL"
CLIENT_SECRET = "FAKE_SECRET"

0 comments on commit f9f4fbe

Please sign in to comment.