Skip to content

Commit

Permalink
Merge pull request #5 from jamesholcombe/dev
Browse files Browse the repository at this point in the history
Dev
  • Loading branch information
jamesholcombe authored Oct 19, 2022
2 parents c9e3628 + ea93055 commit 45cceb2
Show file tree
Hide file tree
Showing 9 changed files with 87 additions and 141 deletions.
43 changes: 21 additions & 22 deletions .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,36 +5,35 @@ name: Python package

on:
push:
branches: [ main ]
branches: [main]
pull_request:
branches: [ main ]
branches: [main]

jobs:
build:

runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.8", "3.9", "3.10"]

steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install flake8 pytest
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
- name: Lint with flake8
run: |
# stop the build if there are Python syntax errors or undefined names
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- name: Test with pytest
run: |
pytest
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install flake8 pytest pytest-mock
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
- name: Lint with flake8
run: |
# stop the build if there are Python syntax errors or undefined names
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- name: Test with pytest
run: |
pytest
36 changes: 17 additions & 19 deletions .github/workflows/python-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,28 +8,26 @@

name: Upload Python Package

on:
-release
on: -release

jobs:
deploy:

runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: '3.x'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install build
- name: Build package
run: python -m build
- name: Publish package
uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29
with:
user: __token__
password: ${{ secrets.PYPI_API_TOKEN }}
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: "3.x"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install build
- name: Build package
run: python -m build
- name: Publish package
uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29
with:
user: __token__
password: ${{ secrets.PYPI_API_TOKEN }}
47 changes: 19 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
# dash-auth-external

Integrate your dashboards with 3rd party APIs and external OAuth providers.
Integrate your dashboards with 3rd party APIs and external OAuth providers.

## Overview

Do you want to build a Plotly Dash app which pulls user data from external APIs such as Google, Spotify, Slack etc?

**Dash-auth-external** provides a simple interface to authenticate users through OAuth2 code flow. Allowing developers to serve user tailored content.
**Dash-auth-external** provides a simple interface to authenticate users through OAuth2 code flow. Allowing developers to serve user tailored content.

## Installation

**Dash-auth-external** is distributed via [PyPi](https://pypi.org/project/dash-auth-external/)

```
pip install dash-auth-external
```

## Simple Usage

```python
#using spotify as an example
AUTH_URL = "https://accounts.spotify.com/authorize"
Expand All @@ -24,17 +27,22 @@ CLIENT_ID = "YOUR_CLIENT_ID"
# creating the instance of our auth class
auth = DashAuthExternal(AUTH_URL, TOKEN_URL, CLIENT_ID)
```

We then pass the flask server from this object to dash on init.

```python
app = Dash(__name__, server= auth.server)
```
That's it! You can now define your layout and callbacks as usual.

That's it! You can now define your layout and callbacks as usual.

> To obtain your access token, call the get_token method of your Auth object.
> **NOTE** This can **ONLY** be done in the context of a dash callback.
```python
app.layout = html.Div(
[
html.Div(id="example-output"),
html.Div(id="example-output"),
dcc.Input(id="example-input")
])

Expand All @@ -48,38 +56,21 @@ def example_callback(value):
) ##The token can only be retrieved in the context of a dash callback
return token
```

## Troubleshooting

If you hit 400 responses (bad request) from either endpoint, there are a number of things that might need configuration.

Make sure you have checked the following

- **Register your redirect URI** with OAuth provider!

*The library uses a default redirect URI of http://127.0.0.1:8050/redirect*.
Make sure you have checked the following

- Check whether your OAuth provider requires a client secret as well as a client id.
- **Register your redirect URI** with OAuth provider!

*This can be passed as a keyword argument to the main class.*
_The library uses a default redirect URI of http://127.0.0.1:8050/redirect_.

- Check the **key field** for the **token** in the JSON response returned by the token endpoint by your OAuth provider.

*The default is "access_token" but different OAuth providers will use a different key for this.*


## Feature Roadmap

- [x] OAuth2 support
- [ ] OAuth1 support
- [x] Full test coverage
- [x] Support for PKCE/ non-PKCE








_The default is "access_token" but different OAuth providers may use a different key for this._

## Contributing

Contributions, issues, and ideas are all more than welcome
13 changes: 2 additions & 11 deletions dash_auth_external/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,6 @@ def get_token(self) -> str:
)
return token

# except RuntimeError:
# raise ValueError(
# "This method must be called in a callback as it makes use of the flask request context."
# )

def __init__(
self,
external_auth_url: str,
Expand All @@ -45,7 +40,6 @@ def __init__(
auth_suffix: str = "/",
home_suffix="/home",
_token_field_name: str = "access_token",
client_secret: str = None,
_secret_key: str = None,
auth_request_headers: dict = None,
token_request_headers: dict = None,
Expand All @@ -63,9 +57,8 @@ 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".
_token_field_name (str, optional): The key for the token returned in JSON from the token endpoint. Defaults to "access_token".
client_secret (str, optional): Client secret if enforced by Oauth2 provider. Defaults to None.
_secret_key (str, optional): Secret key for flask app, normally generated at runtime. Defaults to None.
auth_request_headers (dict, optional): Additional headers to send to the authorization endpoint. Defaults to None.
auth_request_params (dict, optional): Additional params 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.
scope (str, optional): Header required by most Oauth2 Providers. Defaults to None.
Expand All @@ -85,18 +78,16 @@ def __init__(
app=app,
external_auth_url=external_auth_url,
client_id=client_id,
client_secret=client_secret,
auth_suffix=auth_suffix,
redirect_uri=redirect_uri,
with_pkce=with_pkce,
scope=scope,
auth_request_headers=auth_request_headers,
auth_request_params=auth_request_headers,
)
app = make_access_token_route(
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,
Expand Down
52 changes: 19 additions & 33 deletions dash_auth_external/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@


def make_code_challenge(length: int = 40):
code_verifier = base64.urlsafe_b64encode(os.urandom(length)).decode("utf-8")
code_verifier = base64.urlsafe_b64encode(
os.urandom(length)).decode("utf-8")
code_verifier = re.sub("[^a-zA-Z0-9]+", "", code_verifier)
code_challenge = hashlib.sha256(code_verifier.encode("utf-8")).digest()
code_challenge = base64.urlsafe_b64encode(code_challenge).decode("utf-8")
Expand All @@ -27,39 +28,34 @@ def make_auth_route(
auth_suffix: str,
redirect_uri: str,
with_pkce: bool = True,
client_secret: str = None,
scope: str = None,
auth_request_headers: dict = None,
auth_request_params: dict = None,
):
@app.route(auth_suffix)
def get_auth_code():
"""
Redirect the user/resource owner to the OAuth provider
using an URL with a few key OAuth parameters.
"""
# making code verifier and challenge for PKCE

if with_pkce:
code_challenge, code_verifier = make_code_challenge()
session["cv"] = code_verifier

# TODO implement this myself
oauth_session = OAuth2Session(
client_id,
redirect_uri=redirect_uri,
scope=scope,
)

if with_pkce:

code_challenge, code_verifier = make_code_challenge()
session["cv"] = code_verifier
authorization_url, state = oauth_session.authorization_url(
external_auth_url,
code_challenge=code_challenge,
code_challenge_method="S256",
**auth_request_params,
)
else:
authorization_url, state = oauth_session.authorization_url(
external_auth_url,
**auth_request_params,
)

resp = redirect(authorization_url)
Expand All @@ -69,33 +65,24 @@ def get_auth_code():


def build_token_body(
url, redirect_uri: str, client_id: str, with_pkce: bool, client_secret: str
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]
state = redirect_params["state"][0]
body = dict(
grant_type="authorization_code",
code=code,
redirect_uri=redirect_uri,
client_id=client_id,
state=state,
)

if with_pkce:
code_verifier = session["cv"]
body = dict(
grant_type="authorization_code",
code=code,
redirect_uri=redirect_uri,
code_verifier=code_verifier,
client_id=client_id,
state=state,
client_secret=client_secret,
)
else:
body = dict(
grant_type="authorization_code",
code=code,
redirect_uri=redirect_uri,
client_id=client_id,
state=state,
client_secret=client_secret,
)

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

return body


Expand All @@ -108,7 +95,6 @@ def make_access_token_route(
client_id: str,
_token_field_name: str,
with_pkce: bool = True,
client_secret: str = None,
token_request_headers: dict = None,
):
@app.route(redirect_suffix, methods=["GET", "POST"])
Expand All @@ -119,7 +105,7 @@ def get_token():
redirect_uri=redirect_uri,
with_pkce=with_pkce,
client_id=client_id,
client_secret=client_secret,

)

response_data = get_token_response_data(
Expand Down
2 changes: 2 additions & 0 deletions dev-requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pytest
pytest-mock
26 changes: 3 additions & 23 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,23 +1,3 @@
Brotli==1.0.9
certifi==2021.10.8
charset-normalizer==2.0.9
click==8.0.3
dash==2.0.0
dash-auth-external==0.1
dash-core-components==2.0.0
dash-html-components==2.0.0
dash-table==5.0.0
Flask==2.0.2
Flask-Compress==1.10.1
idna==3.3
itsdangerous==2.0.1
Jinja2==3.0.3
MarkupSafe==2.0.1
oauthlib==3.1.1
plotly==5.5.0
requests==2.27.0
requests-oauthlib==1.3.0
six==1.16.0
tenacity==8.0.1
urllib3==1.26.7
Werkzeug==2.0.2
dash >= 2.0.0
requests >= 1.0.0
requests-oauthlib >= 0.3.0
Loading

0 comments on commit 45cceb2

Please sign in to comment.