Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 .bandit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,4 +82,4 @@
tests:

# (optional) list skipped test IDs here, eg '[B101, B406]':
skips: [B101]
skips: [B101, B104]
2 changes: 1 addition & 1 deletion .flake8
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
[flake8]
ignore = W291, W293, E501, E731
ignore = W291, W293, E501, E731, W503
54 changes: 54 additions & 0 deletions .github/workflows/publish.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
name: Publish

on:
release:
types: [created]

jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.9]

steps:
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v1
with:
python-version: ${{ matrix.python-version }}

- name: Install Task
uses: arduino/setup-task@v1

- name: Install Poetry
uses: snok/install-poetry@v1
with:
virtualenvs-create: true
virtualenvs-in-project: true

- name: Load cached venv
id: cached-poetry-dependencies
uses: actions/cache@v2
with:
path: .venv
key: venv-${{ runner.os }}-${{ hashFiles('**/poetry.lock') }}

- name: Install dependencies
if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true'
run: poetry install --no-interaction --no-root

- name: Run CI
run: task ci

- name: Build wheels
run: |
poetry version $(git tag --points-at HEAD)
poetry build

- name: Upload
env:
USERNAME: __token__
PASSWORD: ${{ secrets.PYPI_TOKEN }}
run: |
poetry publish --username=$USERNAME --password=$PASSWORD
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,4 @@ repos:
rev: "1.7.0"
hooks:
- id: bandit
entry: bandit -c .bandit.yml
entry: bandit -c .bandit.yml
12 changes: 12 additions & 0 deletions .readthedocs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
version: 2

sphinx:
configuration: docs/conf.py

python:
version: 3.8
install:
- method: pip
path: .
extra_requirements:
- docs
110 changes: 67 additions & 43 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,71 +18,95 @@

---

:warning: **See [this issue](https://github.com/HarryMWinters/fastapi-oidc/issues/1) for
simple role-your-own example of checking OIDC tokens.**
**Documentation**: <a href="https://fastapi-oidc.readthedocs.io/" target="_blank">https://fastapi-oidc.readthedocs.io/</a>

Verify and decrypt 3rd party OIDC ID tokens to protect your
[fastapi](https://github.com/tiangolo/fastapi) endpoints.
**Source Code**: <a href="https://github.com/HarryMWinters/fastapi-oidc" target="_blank">https://github.com/HarryMWinters/fastapi-oidc</a>

**Documentation:** [ReadTheDocs](https://fastapi-oidc.readthedocs.io/en/latest/)
---

Verify and decrypt 3rd party OpenID Connect tokens to protect your
[FastAPI](https://github.com/tiangolo/fastapi) endpoints.

Easily used with authentication services such as:

- [Keycloak](https://www.keycloak.org/) (open source)
- [SuperTokens](https://supertokens.io/) (open source)
- [Auth0](https://auth0.com/)
- [Okta](https://www.okta.com/products/authentication/)

FastAPI's generated interactive documentation supports the grant flows:

```python3
GrantType.AUTHORIZATION_CODE
GrantType.IMPLICIT
GrantType.PASSWORD
GrantType.CLIENT_CREDENTIALS
```

**Source code:** [Github](https://github.com/HarryMWinters/fastapi-oidc)
![example documentation](example-docs.png)

## Installation

`pip install fastapi-oidc`
```
poetry add fastapi-oidc
```

Or, for the old-timers:

```
pip install fastapi-oidc
```

## Usage

### Verify ID Tokens Issued by Third Party
See [this example](tree/master/example) for how to use
`docker-compose` to set up authentication with `fastapi-third-party-auth` +
[Keycloak](https://www.keycloak.org/).

This is great if you just want to use something like Okta or google to handle
your auth. All you need to do is verify the token and then you can extract user ID info
from it.
### Standard usage

```python3
from fastapi import Depends
from fastapi import FastAPI

# Set up our OIDC
from fastapi_oidc import IDToken
from fastapi_oidc import get_auth

OIDC_config = {
"client_id": "0oa1e3pv9opbyq2Gm4x7",
# Audience can be omitted in which case the aud value defaults to client_id
"audience": "https://yourapi.url.com/api",
"base_authorization_server_uri": "https://dev-126594.okta.com",
"issuer": "dev-126594.okta.com",
"signature_cache_ttl": 3600,
}

authenticate_user: Callable = get_auth(**OIDC_config)

app = FastAPI()
from fastapi import Security
from fastapi import status

from fastapi_oidc import Auth
from fastapi_oidc import GrantType
from fastapi_oidc import KeycloakIDToken

auth = Auth(
openid_connect_url="http://localhost:8080/auth/realms/my-realm/.well-known/openid-configuration",
issuer="http://localhost:8080/auth/realms/my-realm", # optional, verification only
client_id="my-client", # optional, verification only
scopes=["email"], # optional, verification only
grant_types=[GrantType.IMPLICIT], # optional, docs only
idtoken_model=KeycloakIDToken, # optional, verification only
)

app = FastAPI(
title="Example",
version="dev",
dependencies=[Depends(auth)],
)

@app.get("/protected")
def protected(id_token: IDToken = Depends(authenticate_user)):
return {"Hello": "World", "user_email": id_token.email}
def protected(id_token: KeycloakIDToken = Security(auth.required)):
return dict(message=f"You are {id_token.email}")
```

#### Using your own tokens
### Optional: Custom token validation

The IDToken class will accept any number of extra field but if you want to craft your
own token class and validation that's accounted for too.
The IDToken class will accept any number of extra fields but you can also
validate fields in the token like this:

```python3
class CustomIDToken(fastapi_oidc.IDToken):
class MyAuthenticatedUser(IDToken):
custom_field: str
custom_default: float = 3.14


authenticate_user: Callable = get_auth(**OIDC_config, token_type=CustomIDToken)

app = FastAPI()


@app.get("/protected")
def protected(id_token: CustomIDToken = Depends(authenticate_user)):
return {"Hello": "World", "user_email": id_token.custom_default}
auth = Auth(
...,
idtoken_model=MyAuthenticatedUser,
)
```
20 changes: 14 additions & 6 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,16 @@
# import os
# import sys
# sys.path.insert(0, os.path.abspath('.'))

from typing import List

# -- Project information -----------------------------------------------------

project = "fastapi-oidc"
copyright = "2020, Harry M. Winters"
author = "Harry M. Winters"
copyright = "2020, Harry M. Winters, Richard Löwenström"
author = "Harry M. Winters, Richard Löwenström"

# The full version, including alpha/beta/rc tags
release = "0.5.0"
release = "0.0.0"


# -- General configuration ---------------------------------------------------
Expand All @@ -46,9 +46,17 @@
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = "alabaster"
html_theme = "sphinx_rtd_theme"

# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ["_static"]
html_static_path: List[str] = []

autodoc_default_options = {
"members": True,
"member-order": "bysource",
"special-members": "__init__",
"undoc-members": True,
"exclude-members": "__weakref__",
}
78 changes: 45 additions & 33 deletions docs/index.rst
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
Welcome to fastapi-oidc's documentation!
========================================

Verify ID Tokens Issued by Third Party
Verify and decrypt 3rd party OpenID Connect tokens to protect your
`FastAPI <https://github.com/tiangolo/fastapi>`_ endpoints.

This is great if you just want to use something like Okta or google to handle
your auth. All you need to do is verify the token and then you can extract
user ID info from it.
Easily used with authenticators such as:

- `Keycloak <https://www.keycloak.org/>`_ (open source)
- `SuperTokens <https://supertokens.io/>`_ (open source)
- `Auth0 <https://auth0.com/>`_
- `Okta <https://www.okta.com/products/authentication/>`_


FastAPI's generated interactive documentation supports the grant types
``authorization_code``, ``implicit``, ``password`` and ``client_credentials``.

.. toctree::
:maxdepth: 2
Expand All @@ -23,13 +31,13 @@ Installation

.. code-block:: bash

pip install fastapi-oidc
poetry add fastapi-oidc

Or, if you you're feeling hip...
Or, for the old-timers:

.. code-block:: bash

poetry add fastapi-oidc
pip install fastapi-oidc

Example
-------
Expand All @@ -40,44 +48,48 @@ Basic configuration for verifying OIDC tokens.

from fastapi import Depends
from fastapi import FastAPI

# Set up our OIDC
from fastapi_oidc import IDToken
from fastapi_oidc import get_auth

OIDC_config = {
"client_id": "0oa1e3pv9opbyq2Gm4x7",
"base_authorization_server_uri": "https://dev-126594.okta.com",
"issuer": "dev-126594.okta.com",
"signature_cache_ttl": 3600,
}

authenticate_user: Callable = get_auth(**OIDC_config)

app = FastAPI()
from fastapi import Security
from fastapi import status

from fastapi_oidc import Auth
from fastapi_oidc import GrantType
from fastapi_oidc import KeycloakIDToken

auth = Auth(
openid_connect_url="http://localhost:8080/auth/realms/my-realm/.well-known/openid-configuration",
issuer="http://localhost:8080/auth/realms/my-realm", # optional, verification only
client_id="my-client", # optional, verification only
scopes=["email"], # optional, verification only
grant_types=[GrantType.IMPLICIT], # optional, docs only
idtoken_model=KeycloakIDToken, # optional, verification only
)

app = FastAPI(
title="Example",
version="dev",
dependencies=[Depends(auth)],
)

@app.get("/protected")
def protected(id_token: IDToken = Depends(authenticate_user)):
return {"Hello": "World", "user_email": id_token.email}
def protected(id_token: KeycloakIDToken = Security(auth.required)):
return dict(message=f"You are {id_token.email}")


API Reference
=============

Auth
----

.. automodule:: fastapi_oidc.auth
:members:


Discovery
---------

.. automodule:: fastapi_oidc.discovery
Grant Types
-----------
.. automodule:: fastapi_oidc.grant_types
:members:
:undoc-members:

Types
------------
.. automodule:: fastapi_oidc.types
IDToken Types
-------------
.. automodule:: fastapi_oidc.idtoken_types
:members:
Binary file added example-docs.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions example/.python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.8.8
Loading