Skip to content

Commit 392eccb

Browse files
committed
refactor!: remove client_id and more transparent name openid_connect_url
base_authorization_server_uri is never used on its own and openid_connect_url is already used by fastapi and says more about what is going on BREAKING CHANGE
1 parent 52f08af commit 392eccb

File tree

6 files changed

+65
-79
lines changed

6 files changed

+65
-79
lines changed

README.md

Lines changed: 35 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -18,70 +18,76 @@
1818

1919
---
2020

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

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

27-
**Documentation:** [ReadTheDocs](https://fastapi-oidc.readthedocs.io/en/latest/)
25+
---
26+
27+
Verify and decrypt 3rd party OpenID Connect tokens to protect your
28+
[FastAPI](https://github.com/tiangolo/fastapi) endpoints.
2829

29-
**Source code:** [Github](https://github.com/HarryMWinters/fastapi-oidc)
30+
Easily used with authenticators such as:
31+
- [Keycloak](https://www.keycloak.org/) (open source)
32+
- [SuperTokens](https://supertokens.io/) (open source)
33+
- [Auth0](https://auth0.com/)
34+
- [Okta](https://www.okta.com/products/authentication/)
35+
36+
FastAPI's generated interactive documentation supports the grant flows
37+
`authorization_code`, `implicit`, `password` and `client_credentials`.
3038

3139
## Installation
3240

33-
`pip install fastapi-oidc`
41+
```
42+
poetry add fastapi-oidc
43+
```
3444

35-
## Usage
45+
Or, for the old-timers:
3646

37-
### Verify ID Tokens Issued by Third Party
47+
```
48+
pip install fastapi-oidc
49+
```
3850

39-
This is great if you just want to use something like Okta or google to handle
40-
your auth. All you need to do is verify the token and then you can extract user ID info
41-
from it.
51+
## Usage
4252

4353
```python3
4454
from fastapi import Depends
4555
from fastapi import FastAPI
4656

47-
# Set up our OIDC
4857
from fastapi_oidc import IDToken
4958
from fastapi_oidc import get_auth
5059

5160

61+
app = FastAPI()
62+
5263
authenticate_user = get_auth(
53-
client_id": "0oa1e3pv9opbyq2Gm4x7",
54-
"base_authorization_server_uri": "https://dev-126594.okta.com",
55-
"issuer": "dev-126594.okta.com",
56-
# Audience can be omitted in which case it defaults to client_id
57-
"audience": "https://yourapi.url.com/api", # optional, verification only
58-
"signature_cache_ttl": 3600, # optional
64+
openid_connect_url="https://dev-123456.okta.com/.well-known/openid-configuration",
65+
issuer="dev-126594.okta.com", # optional, verification only
66+
audience="https://yourapi.url.com/api", # optional, verification only
67+
signature_cache_ttl=3600, # optional
5968
)
6069

61-
app = FastAPI()
62-
6370
@app.get("/protected")
6471
def protected(id_token: IDToken = Depends(authenticate_user)):
6572
return {"Hello": "World", "user_email": id_token.email}
6673
```
6774

68-
#### Using your own tokens
75+
### Optional: Custom token validation
6976

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

7380
```python3
74-
class CustomIDToken(fastapi_oidc.IDToken):
81+
class MyAuthenticatedUser(IDToken):
7582
custom_field: str
7683
custom_default: float = 3.14
7784

7885

79-
authenticate_user = get_auth(**OIDC_config)
80-
8186
app = FastAPI()
8287

88+
authenticate_user = get_auth(...)
8389

8490
@app.get("/protected")
85-
def protected(id_token: CustomIDToken = Depends(authenticate_user)):
86-
return {"Hello": "World", "user_email": id_token.custom_default}
91+
def protected(user: MyAuthenticatedUser = Depends(authenticate_user)):
92+
return {"Hello": "World", "custom_field": user.custom_field}
8793
```

docs/index.rst

Lines changed: 10 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,13 @@ Installation
2323

2424
.. code-block:: bash
2525
26-
pip install fastapi-oidc
26+
poetry add fastapi-oidc
2727
28-
Or, if you you're feeling hip...
28+
Or, for the old-timers:
2929

3030
.. code-block:: bash
3131
32-
poetry add fastapi-oidc
32+
pip install fastapi-oidc
3333
3434
Example
3535
-------
@@ -41,21 +41,19 @@ Basic configuration for verifying OIDC tokens.
4141
from fastapi import Depends
4242
from fastapi import FastAPI
4343
44-
# Set up our OIDC
4544
from fastapi_oidc import IDToken
4645
from fastapi_oidc import get_auth
4746
48-
OIDC_config = {
49-
"client_id": "0oa1e3pv9opbyq2Gm4x7",
50-
"base_authorization_server_uri": "https://dev-126594.okta.com",
51-
"issuer": "dev-126594.okta.com",
52-
"signature_cache_ttl": 3600,
53-
}
54-
55-
authenticate_user: Callable = get_auth(**OIDC_config)
5647
5748
app = FastAPI()
5849
50+
authenticate_user = get_auth(
51+
openid_connect_url="https://dev-123456.okta.com/.well-known/openid-configuration",
52+
issuer="dev-126594.okta.com", # optional, verification only
53+
audience="https://yourapi.url.com/api", # optional, verification only
54+
signature_cache_ttl=3600, # optional
55+
)
56+
5957
@app.get("/protected")
6058
def protected(id_token: IDToken = Depends(authenticate_user)):
6159
return {"Hello": "World", "user_email": id_token.email}
@@ -70,13 +68,6 @@ Auth
7068
.. automodule:: fastapi_oidc.auth
7169
:members:
7270

73-
74-
Discovery
75-
---------
76-
77-
.. automodule:: fastapi_oidc.discovery
78-
:members:
79-
8071
Types
8172
------------
8273
.. automodule:: fastapi_oidc.types

fastapi_oidc/auth.py

Lines changed: 14 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,8 @@ def test_auth(authenticated_user: AuthenticatedUser = Depends(authenticate_user)
3232

3333

3434
def get_auth(
35-
client_id: str,
36-
base_authorization_server_uri: str,
37-
issuer: str,
35+
openid_connect_url: str,
36+
issuer: Optional[str] = None,
3837
audience: Optional[str] = None,
3938
signature_cache_ttl: int = 3600,
4039
) -> Callable[[str], Dict]:
@@ -44,19 +43,13 @@ def get_auth(
4443
server code. The function it returns should be used to check user credentials.
4544
4645
Args:
47-
client_id (str): This string is provided when you register with your resource
48-
server.
49-
base_authorization_server_uri(URL): Everything before /.wellknow in your auth
50-
server URL. I.E. https://dev-123456.okta.com
46+
openid_connect_url (URL): URL to the "well known" openid connect config
47+
e.g. https://dev-123456.okta.com/.well-known/openid-configuration
5148
issuer (URL): Same as base_authorization. This is used to generating OpenAPI3.0
5249
docs which is broken (in OpenAPI/FastAPI) right now.
50+
audience (str): (Optional) The audience string configured by your auth server.
5351
signature_cache_ttl (int): How many seconds your app should cache the
5452
authorization server's public signatures.
55-
audience (str): (Optional) The audience string configured by your auth server.
56-
If not set defaults to client_id
57-
token_type (IDToken or subclass): (Optional) An optional class to be returned by
58-
the authenticate_user function.
59-
6053
6154
Returns:
6255
func: authenticate_user(auth_header: str) -> Dict
@@ -65,9 +58,7 @@ def get_auth(
6558
Nothing intentional
6659
"""
6760

68-
oauth2_scheme = OpenIdConnect(
69-
openIdConnectUrl=f"{base_authorization_server_uri}/.well-known/openid-configuration"
70-
)
61+
oauth2_scheme = OpenIdConnect(openIdConnectUrl=openid_connect_url)
7162

7263
discover = discovery.configure(cache_ttl=signature_cache_ttl)
7364

@@ -87,7 +78,7 @@ def authenticate_user(auth_header: str = Depends(oauth2_scheme)) -> Dict:
8778
HTTPException(status_code=401, detail=f"Unauthorized: {err}")
8879
"""
8980
id_token = auth_header.split(" ")[-1]
90-
OIDC_discoveries = discover.auth_server(base_url=base_authorization_server_uri)
81+
OIDC_discoveries = discover.auth_server(openid_connect_url=openid_connect_url)
9182
key = discover.public_keys(OIDC_discoveries)
9283
algorithms = discover.signing_algos(OIDC_discoveries)
9384

@@ -96,10 +87,14 @@ def authenticate_user(auth_header: str = Depends(oauth2_scheme)) -> Dict:
9687
id_token,
9788
key,
9889
algorithms,
99-
audience=audience if audience else client_id,
90+
audience=audience,
10091
issuer=issuer,
101-
# Disabled at_hash check since we aren't using the access token
102-
options={"verify_at_hash": False},
92+
options={
93+
# Disabled at_hash check since we aren't using the access token
94+
"verify_at_hash": False,
95+
"verify_iss": issuer is not None,
96+
"verify_aud": audience is not None,
97+
},
10398
)
10499

105100
except (ExpiredSignatureError, JWTError, JWTClaimsError) as err:

fastapi_oidc/discovery.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,9 @@ def get_signing_algos(OIDC_spec: Dict):
2222
return algos
2323

2424
@cached(TTLCache(1, cache_ttl))
25-
def discover_auth_server(*_, base_url: str) -> Dict:
26-
discovery_url = f"{base_url}/.well-known/openid-configuration"
27-
r = requests.get(discovery_url)
28-
# If the auth server is failing we can't verify tokens.
29-
# Soooo panic I guess?
25+
def discover_auth_server(*_, openid_connect_url: str) -> Dict:
26+
r = requests.get(openid_connect_url)
27+
# Raise if the auth server is failing since we can't verify tokens
3028
r.raise_for_status()
3129
configuration = r.json()
3230
return configuration

tests/conftest.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,8 @@ def public_key(key):
5757
@pytest.fixture
5858
def config_w_aud():
5959
return {
60-
"client_id": "CongenitalOptimist",
6160
"audience": "NeverAgain",
62-
"base_authorization_server_uri": "WhatAreTheCivilianApplications?",
61+
"openid_connect_url": "WhatAreTheCivilianApplications?",
6362
"issuer": "PokeItWithAStick",
6463
"signature_cache_ttl": 6e3,
6564
}
@@ -68,8 +67,7 @@ def config_w_aud():
6867
@pytest.fixture
6968
def no_audience_config():
7069
return {
71-
"client_id": "CongenitalOptimist",
72-
"base_authorization_server_uri": "WhatAreTheCivilianApplications?",
70+
"openid_connect_url": "WhatAreTheCivilianApplications?",
7371
"issuer": "PokeItWithAStick",
7472
"signature_cache_ttl": 6e3,
7573
}
@@ -107,13 +105,12 @@ def token_with_audience(private_key, config_w_aud, test_email) -> str:
107105
@pytest.fixture
108106
def token_without_audience(private_key, no_audience_config, test_email) -> str:
109107
# Make a token where audience is client_id
110-
client_id: str = str(no_audience_config["client_id"])
111108
issuer: str = str(no_audience_config["issuer"])
112109
now = int(time.time())
113110

114111
return jwt.encode(
115112
{
116-
"aud": client_id,
113+
"aud": "NoAudience",
117114
"iss": issuer,
118115
"email": test_email,
119116
"name": "SweetAndFullOfGrace",

tests/test_auth.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@ def test__authenticate_user_no_aud(
3939
id_token = IDToken(**authenticate_user(auth_header=f"Bearer {token}"))
4040

4141
assert id_token.email == test_email # nosec
42-
assert id_token.aud == no_audience_config["client_id"]
4342

4443

4544
def test__authenticate_user_returns_custom_tokens(

0 commit comments

Comments
 (0)