Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[MCC-961880] Add MAuthASGIMiddleware #34

Merged
merged 38 commits into from
Sep 8, 2022
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
72ea924
add fastapi authenticator [skip ci]
ejinotti-mdsol Aug 25, 2022
a3cb337
use similar pattern to flask authenticator [skip ci]
ejinotti-mdsol Aug 25, 2022
8e19c2f
add tests
ejinotti-mdsol Aug 26, 2022
6cf5ab8
update readme
ejinotti-mdsol Aug 26, 2022
b570bdf
add some formatting
ejinotti-mdsol Aug 26, 2022
8971405
update contributing and changelog
ejinotti-mdsol Aug 26, 2022
e7b1c9c
readme fix [skip ci]
ejinotti-mdsol Aug 26, 2022
de20288
use async kw
ejinotti-mdsol Aug 26, 2022
151fc4a
remove python 3.6, add 3.10
ejinotti-mdsol Aug 26, 2022
60db262
update changelog
ejinotti-mdsol Aug 26, 2022
de00c7f
upgrade pytest for 3.10 compat
ejinotti-mdsol Aug 26, 2022
ee79eda
some dependency hell fixes
ejinotti-mdsol Aug 26, 2022
0066550
first pass at generic asgi middleware [skip ci]
ejinotti-mdsol Sep 1, 2022
1fa62d0
missed an await [skip ci]
ejinotti-mdsol Sep 1, 2022
a3e95dd
send response instead of raising [skip ci]
ejinotti-mdsol Sep 1, 2022
15dcdd6
better decoding, some initial tests [skip ci]
ejinotti-mdsol Sep 1, 2022
fdaa43a
update contrib [skip ci]
ejinotti-mdsol Sep 1, 2022
aeb4ba2
reorganize tests [skip ci]
ejinotti-mdsol Sep 1, 2022
1041277
rest of tests
ejinotti-mdsol Sep 2, 2022
e41271f
update readme
ejinotti-mdsol Sep 2, 2022
028f38f
update changelog
ejinotti-mdsol Sep 2, 2022
d401ba1
try 3.10.6 tiny version
ejinotti-mdsol Sep 2, 2022
5ae7dc6
try jammy
ejinotti-mdsol Sep 2, 2022
d7c5801
try older poetry version
ejinotti-mdsol Sep 2, 2022
ad3617e
add comment to travis file [skip ci]
ejinotti-mdsol Sep 2, 2022
2c96be3
poetry 1.1.15 in travis
ejinotti-mdsol Sep 2, 2022
768904d
go with same context from other clients
ejinotti-mdsol Sep 2, 2022
905f455
update readme example [skip ci]
ejinotti-mdsol Sep 2, 2022
739f897
add exempt option
ejinotti-mdsol Sep 2, 2022
5501fa4
forgot to decode query string
ejinotti-mdsol Sep 2, 2022
a630160
only decode query string if present
ejinotti-mdsol Sep 2, 2022
61f7889
style nit
ejinotti-mdsol Sep 2, 2022
78ccfe2
wrap in list to match ruby response
ejinotti-mdsol Sep 2, 2022
6bc5a4c
epic hack to allow downstream receiving
ejinotti-mdsol Sep 2, 2022
15f04b7
keep body as binary
ejinotti-mdsol Sep 6, 2022
0e9c0b2
ignore non-http request types
ejinotti-mdsol Sep 6, 2022
c1f9bc7
make copy of exempt arg and raise type error if not a set
ejinotti-mdsol Sep 7, 2022
f74cce4
use optional type hint
ejinotti-mdsol Sep 7, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ language: python
cache: pip

python:
- 3.6
- 3.6.2
ejinotti-mdsol marked this conversation as resolved.
Show resolved Hide resolved
- 3.7.13 # specify micro version to avoid having EnvCommandError
- 3.8
- 3.9
ejinotti-mdsol marked this conversation as resolved.
Show resolved Hide resolved
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# 1.3.0
- Add `FastAPIAuthenticator` to authenticate requests in FastAPI applications.

# 1.2.3
- Ignore `boto3` import error (`ModuleNotFoundError`).

Expand Down
13 changes: 4 additions & 9 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,23 +16,18 @@ To setup your environment:
```
1. Install Pyenv versions for the Tox Suite
```bash
pyenv install 3.5.8
ejinotti-mdsol marked this conversation as resolved.
Show resolved Hide resolved
pyenv install 3.6.10
pyenv install 3.7.7
pyenv install 3.8.2
pyenv install pypy3.6-7.3.1
```
1. Install Poetry
```bash
curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python
pip install poetry
ejinotti-mdsol marked this conversation as resolved.
Show resolved Hide resolved
```
1. Install Tox
1. Install Dependencies
```bash
pip install tox
```
1. Setup the local project versions (one for each env in the `envlist`)
```bash
pyenv local 3.5.8 3.6.10 3.7.7 3.8.2 pypy3.6-7.1.1
poetry install -v
ejinotti-mdsol marked this conversation as resolved.
Show resolved Hide resolved
```


Expand All @@ -54,5 +49,5 @@ to init the submodule.

## Unit Tests

1. Make any changes, update the tests and then run tests with `tox`
1. Make any changes, update the tests and then run tests with `poetry run tox`
1. Coverage report can be viewed using `open htmlcov/index.html`
33 changes: 24 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,22 +114,16 @@ app_uuid = authenticator.get_app_uuid()

#### Flask applications

You will need to create an application instance and initialize it with FlaskAuthenticator:
You will need to create an application instance and initialize it with `FlaskAuthenticator`.
To specify routes that need to be authenticated use the `requires_authentication` decorator.

```python
from flask import Flask
from mauth_client.flask_authenticator import FlaskAuthenticator
from mauth_client.flask_authenticator import FlaskAuthenticator, requires_authentication

app = Flask("Some Sample App")
authenticator = FlaskAuthenticator()
authenticator.init_app(app)
```

To specify routes that need to be authenticated use the `requires_authentication` decorator:

```python
from flask import Flask
from mauth_client.flask_authenticator import requires_authentication

@app.route("/some/private/route", methods=["GET"])
@requires_authentication
Expand All @@ -141,6 +135,27 @@ def app_status():
return "OK"
```

#### FastAPI applications

You will need to create an application instance and initialize it with `FastAPIAuthenticator`.
To specify routes that need to be authenticated use the `requires_authentication` dependency.

```python
from fastapi import FastAPI, Depends
from mauth_client.fastapi_authenticator import FastAPIAuthenticator, requires_authentication

app = FastAPI()
authenticator = FastAPIAuthenticator()
authenticator.init_app(app)

@app.get("/some/private/route", dependencies=[Depends(requires_authentication)])
async def private_route():
return {"msg": "top secret"}

@app.get("/app_status")
async def app_status():
return {"msg": "OK"}
```

## Contributing

Expand Down
1 change: 1 addition & 0 deletions mauth_client/fastapi_authenticator/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .fastapi_authenticator import FastAPIAuthenticator, requires_authentication
75 changes: 75 additions & 0 deletions mauth_client/fastapi_authenticator/fastapi_authenticator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import logging

from fastapi import FastAPI, HTTPException, Request
from mauth_client.authenticator import LocalAuthenticator, RemoteAuthenticator
from mauth_client.config import Config
from mauth_client.signable import RequestSignable
from mauth_client.signed import Signed
from typing import Tuple, Union

logger = logging.getLogger("fastapi_mauth")
MAuthAuthenticator = LocalAuthenticator if Config.MAUTH_MODE == "local" else RemoteAuthenticator
ejinotti-mdsol marked this conversation as resolved.
Show resolved Hide resolved
state_key = 'fastapi_mauth.client'


class MAuthAuthenticationError(HTTPException):
pass


class FastAPIAuthenticator:
"""
The MAuth Authenticator instance
"""

def __init__(self, app: FastAPI = None) -> None:
self._app = app
self._authenticator = None
if app:
self.init_app(app)

def init_app(self, app: FastAPI) -> None:
"""
Attach authenticator to FastAPI app instance
"""
self._app = app
self._authenticator = self
setattr(app.state, state_key, self)

self._authenticator = self._get_authenticator()

def _get_authenticator(self) -> Union[LocalAuthenticator, RemoteAuthenticator]:
# Validate the client settings (APP_UUID, PRIVATE_KEY)
if not all([Config.APP_UUID, Config.PRIVATE_KEY]):
raise TypeError("FastAPIAuthenticator requires APP_UUID and PRIVATE_KEY")
# Validate the mauth settings (MAUTH_BASE_URL, MAUTH_API_VERSION)
if not all([Config.MAUTH_URL, Config.MAUTH_API_VERSION]):
raise TypeError("FastAPIAuthenticator requires MAUTH_URL and MAUTH_API_VERSION")
# Validate MAUTH_MODE
if Config.MAUTH_MODE not in ("local", "remote"):
raise TypeError("FastAPIAuthenticator MAUTH_MODE must be one of local or remote")

return LocalAuthenticator if Config.MAUTH_MODE == "local" else RemoteAuthenticator

async def authenticate(self, request: Request) -> Tuple[bool, int, str]:
"""
Authenticate given FastAPI request
"""
body = await request.body()
signable = RequestSignable(
method=request.method,
url=request.url.path,
body=body,
)
return self._authenticator(
signable, Signed.from_headers(request.headers), logger
).is_authentic()


async def requires_authentication(request: Request) -> None:
"""
FastAPI Dependency function for routes requiring MAuth authentication
"""
authenticator = getattr(request.app.state, state_key)
is_authentic, status, msg = await authenticator.authenticate(request)
if not is_authentic:
raise MAuthAuthenticationError(status_code=status, detail=msg)
Loading