Skip to content

Commit 34bb7d3

Browse files
Add OIDC examples ✨
1 parent 0576849 commit 34bb7d3

17 files changed

+377
-0
lines changed

oidc/README.md

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# OIDC Examples
2+
Working examples of OpenID Connect integrations in BlackSheep applications.
3+
4+
---
5+
6+
This repository contains examples of OpenID Connect integration with three
7+
identity providers:
8+
9+
- [Auth0](https://auth0.com)
10+
- [Okta](https://www.okta.com)
11+
- [Azure Active Directory](https://azure.microsoft.com/en-us/products/active-directory)
12+
13+
## Basic examples (id_token only)
14+
15+
The files whose names start with "basic_" are configured to obtain only
16+
an `id_token`. This is useful when the same application that integrates with
17+
OpenID Connect does not need an access token for another API and all it needs
18+
is to _identify_ users. The values in these files describe real applications
19+
configured in tenants owned by the owner of this repository.
20+
21+
App registrations for these examples only need to be configured with authentication
22+
URL `http://localhost:5000/authorization-callback` and enable issuing `id_token`s.
23+
24+
## Running the examples
25+
26+
1. create a Python virtual environment, and activate it
27+
2. install dependencies `pip install -r requirements.txt`
28+
3. run one of the basic examples `python basic_auth0.py`
29+
4. navigate to [http://localhost:5000](http://localhost:5000)
30+
5. click on the sign-in link, follow the instructions to sign-up / sign-in
31+
32+
:warning: these examples only work with `localhost`!!!
33+
34+
### Azure Active Directory
35+
36+
This example can be tested using any Microsoft account, signing-in in the
37+
demo app inside the Azure tenant Neoteroi.
38+
39+
![AAD demo](./docs/aad-demo.png)
40+
41+
After sign-in, the application shows user claims.
42+
43+
![AAD demo claims](./docs/aad-demo-claims.png)
44+
45+
### Auth0
46+
47+
This example can be tested by anyone, since the application in Auth0 enable
48+
creating an account in the Neoteroi account in Auth0.
49+
50+
![Auth0 demo](./docs/auth0-demo.png)
51+
52+
![Auth0 demo with Google](./docs/auth0-demo-with-google.png)
53+
54+
### Okta
55+
56+
This example illustrates navigating to a sign-in page, but cannot be tested
57+
fully because the Okta account is not configured to enable sign-up.
58+
59+
![Okta demo](./docs/okta-demo.png)
60+
61+
## Examples with scopes (id_token and access_token)
62+
63+
The files whose names start with "scopes_" are configured to obtain, together
64+
with an `id_token`, an `access_token` for an API. These require secrets which,
65+
of course, are not exposed in this repository.
66+
67+
App registrations for these examples are slightly more complex, since they
68+
require configuring an API with scopes. Describing these details is beyond the
69+
scope of this repository. For more information on this subject, refer to the
70+
documentation of the identity providers (for example, for [Auth0 here](https://auth0.com/docs/get-started/apis/api-settings))

oidc/basic_aad.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import uvicorn
2+
from blacksheep.server.application import Application
3+
from blacksheep.server.authentication.oidc import (
4+
OpenIDSettings,
5+
use_openid_connect,
6+
CookiesTokensStore,
7+
)
8+
9+
from common.routes import register_routes
10+
11+
app = Application(show_error_details=True)
12+
13+
14+
# basic AAD integration that handles only an id_token
15+
use_openid_connect(
16+
app,
17+
OpenIDSettings(
18+
authority="https://login.microsoftonline.com/b62b317a-19c2-40c0-8650-2d9672324ac4/v2.0/",
19+
client_id="499adb65-5e26-459e-bc35-b3e1b5f71a9d",
20+
),
21+
tokens_store=CookiesTokensStore(),
22+
)
23+
24+
register_routes(app)
25+
26+
27+
if __name__ == "__main__":
28+
uvicorn.run(app, host="127.0.0.1", port=5000, log_level="debug")

oidc/basic_auth0.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import uvicorn
2+
from blacksheep.server.application import Application
3+
from blacksheep.server.authentication.oidc import (
4+
OpenIDSettings,
5+
use_openid_connect,
6+
CookiesTokensStore,
7+
)
8+
9+
from common.routes import register_routes
10+
11+
app = Application(show_error_details=True)
12+
13+
14+
# basic Auth0 integration that handles only an id_token
15+
use_openid_connect(
16+
app,
17+
OpenIDSettings(
18+
authority="https://neoteroi.eu.auth0.com",
19+
client_id="OOGPl4dgG7qKsm2IOWq72QhXV4wsLhbQ",
20+
callback_path="/signin-oidc",
21+
),
22+
tokens_store=CookiesTokensStore(),
23+
)
24+
25+
register_routes(app)
26+
27+
28+
if __name__ == "__main__":
29+
uvicorn.run(app, host="127.0.0.1", port=5000, log_level="debug")

oidc/basic_okta.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import uvicorn
2+
from blacksheep.server.application import Application
3+
from blacksheep.server.authentication.oidc import (
4+
OpenIDSettings,
5+
use_openid_connect,
6+
CookiesTokensStore,
7+
)
8+
9+
from common.routes import register_routes
10+
11+
app = Application(show_error_details=True)
12+
13+
14+
# basic Okta integration that handles only an id_token
15+
use_openid_connect(
16+
app,
17+
OpenIDSettings(
18+
authority="https://dev-34685660.okta.com",
19+
client_id="0oa2gy88qiVyuOClI5d7",
20+
callback_path="/authorization-code/callback",
21+
),
22+
tokens_store=CookiesTokensStore(),
23+
)
24+
25+
register_routes(app)
26+
27+
28+
if __name__ == "__main__":
29+
uvicorn.run(app, host="127.0.0.1", port=5000, log_level="debug")

oidc/common/__init__.py

Whitespace-only changes.

oidc/common/logs.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import logging
2+
3+
from blacksheep.baseapp import get_logger
4+
from blacksheep.server.authentication.oidc import get_logger as get_oidc_logger
5+
6+
logging.basicConfig(level=logging.DEBUG, format="%(message)s")
7+
8+
for logger in {get_logger(), get_oidc_logger()}:
9+
logger.setLevel(logging.DEBUG)
10+
logger.addHandler(logging.StreamHandler())

oidc/common/routes.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import jwt
2+
from textwrap import dedent
3+
4+
from blacksheep.messages import Request
5+
from blacksheep.server.application import Application
6+
from blacksheep.server.authorization import allow_anonymous
7+
from blacksheep.server.responses import html, redirect
8+
from essentials.json import dumps
9+
from guardpost.authentication import Identity
10+
11+
12+
def _render_access_token(user: Identity) -> str:
13+
if not user.access_token:
14+
return ""
15+
16+
# parse without validating the access token
17+
# (the id_token was validated upon sign-in!)
18+
claims = jwt.decode(user.access_token, options={"verify_signature": False})
19+
20+
return dedent(
21+
f"""
22+
<h3>You also have an access token for an API</h3>
23+
<p>These are the claims, from your <strong>access_token</strong>:</p>
24+
<pre>{dumps(claims, indent=4)}</pre>
25+
"""
26+
)
27+
28+
29+
def register_routes(app: Application) -> None:
30+
@allow_anonymous()
31+
@app.route("/sign-in-error")
32+
async def error_handler(request: Request, error: str):
33+
if error == "access_denied":
34+
# the user declined consents to the app
35+
return html("<h1>OK, but you won't be able to use our wonderful app.</h1>")
36+
return html(f"<h1>Oh, no! {error}</h1>")
37+
38+
@app.route("/")
39+
async def home(request: Request, user: Identity):
40+
host = request.get_first_header(b"Host")
41+
if b"localhost" not in host:
42+
return redirect("http://localhost:5000/")
43+
44+
if user.is_authenticated():
45+
id_claims = dumps(user.claims, indent=4)
46+
47+
return html(
48+
dedent(
49+
f"""
50+
<!DOCTYPE html>
51+
<html>
52+
<head>
53+
<style>
54+
pre {{
55+
border: 1px dotted darkred;
56+
padding: 1rem;
57+
}}
58+
</style>
59+
</head>
60+
<body>
61+
<h1>Welcome!</h1>
62+
<p>These are your claims, from your <strong>id_token</strong>:</p>
63+
<pre>{id_claims}</pre>
64+
{_render_access_token(user)}
65+
</body>
66+
</html>
67+
"""
68+
)
69+
)
70+
71+
return html(
72+
dedent(
73+
f"""
74+
<!DOCTYPE html>
75+
<html>
76+
<head>
77+
</head>
78+
<body>
79+
<h1>You are not authenticated!</h1>
80+
<a href='/sign-in'>Sign in here.</a><br/>
81+
</body>
82+
</html>
83+
"""
84+
)
85+
)

oidc/common/secrets.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import os
2+
from dataclasses import dataclass
3+
4+
5+
@dataclass
6+
class Secrets:
7+
auth0_client_secret: str
8+
okta_client_secret: str
9+
aad_client_secret: str
10+
11+
@classmethod
12+
def from_env(cls):
13+
return cls(
14+
auth0_client_secret=os.environ["AUTH0_CLIENT_SECRET"],
15+
okta_client_secret=os.environ["OKTA_CLIENT_SECRET"],
16+
aad_client_secret=os.environ["AAD_CLIENT_SECRET"],
17+
)

oidc/docs/aad-demo-claims.png

98.5 KB
Loading

oidc/docs/aad-demo.png

97.7 KB
Loading

0 commit comments

Comments
 (0)