Skip to content

Commit 3f8c800

Browse files
committed
feat: add Litestar framework integration with SuperTokens and setup environment
1 parent 1e499b1 commit 3f8c800

File tree

6 files changed

+1165
-908
lines changed

6 files changed

+1165
-908
lines changed
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
![SuperTokens banner](https://raw.githubusercontent.com/supertokens/supertokens-logo/master/images/Artboard%20%E2%80%93%2027%402x.png)
2+
3+
# FastAPI example
4+
5+
This is an example to show how to use supertokens-python library with FastAPI framework and ThirdpartyEmailpassword recipe.
6+
7+
This server code can be used to implement social auth using:
8+
- Google
9+
- Google Workspaces
10+
- Github
11+
- Apple
12+
- Discord
13+
14+
## Installation
15+
16+
Before installing, use the script to create a virtual environment and to install all the required packages.
17+
```bash
18+
source create_env.sh
19+
```
20+
21+
## Start web app
22+
23+
```bash
24+
uvicorn main:app --reload --host=0.0.0.0 --port=3001
25+
```
26+
27+
## Author
28+
29+
Created with :heart: by the folks at supertokens.com.
30+
31+
## License
32+
33+
This project is licensed under the Apache 2.0 license.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
#!/bin/bash
2+
3+
pip install virtualenv
4+
#Create a virtualenv
5+
virtualenv litestar_example
6+
# shellcheck disable=SC2164
7+
source litestar_example/bin/activate
8+
9+
pip install -r requirements.txt
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
import os
2+
from typing import Any
3+
4+
import uvicorn # type: ignore
5+
from dotenv import load_dotenv
6+
from litestar import Litestar, MediaType, Response, get
7+
from litestar.config.cors import CORSConfig
8+
from litestar.di import Provide
9+
from supertokens_python import (
10+
InputAppInfo,
11+
SupertokensConfig,
12+
get_all_cors_headers,
13+
init,
14+
)
15+
from supertokens_python.framework.litestar.litestar_middleware import LitestarMiddleware
16+
from supertokens_python.recipe import (
17+
dashboard,
18+
emailverification,
19+
session,
20+
thirdparty,
21+
usermetadata,
22+
)
23+
from supertokens_python.recipe.session import SessionContainer
24+
from supertokens_python.recipe.session.framework.litestar import verify_session
25+
26+
load_dotenv()
27+
28+
29+
def get_api_port():
30+
return 3001
31+
32+
33+
def get_website_port():
34+
return "3000"
35+
36+
37+
def get_website_domain():
38+
return "http://localhost:" + get_website_port()
39+
40+
41+
init(
42+
supertokens_config=SupertokensConfig(connection_uri="https://try.supertokens.io"),
43+
app_info=InputAppInfo(
44+
app_name="Supertokens",
45+
api_domain="http://localhost:" + str(get_api_port()),
46+
website_domain=get_website_domain(),
47+
api_base_path="/auth",
48+
),
49+
framework="litestar",
50+
recipe_list=[
51+
session.init(),
52+
dashboard.init(),
53+
emailverification.init("REQUIRED"),
54+
usermetadata.init(),
55+
thirdparty.init(
56+
sign_in_and_up_feature=thirdparty.SignInAndUpFeature(
57+
providers=[
58+
thirdparty.ProviderInput(
59+
config=thirdparty.ProviderConfig(
60+
third_party_id="google",
61+
clients=[
62+
thirdparty.ProviderClientConfig(
63+
client_id=os.environ["GOOGLE_CLIENT_ID"],
64+
client_secret=os.environ["GOOGLE_CLIENT_SECRET"],
65+
client_type="web",
66+
),
67+
thirdparty.ProviderClientConfig(
68+
client_id=os.environ["GOOGLE_CLIENT_ID_MOBILE"],
69+
client_secret=os.environ[
70+
"GOOGLE_CLIENT_SECRET_MOBILE"
71+
],
72+
client_type="mobile",
73+
),
74+
],
75+
),
76+
),
77+
thirdparty.ProviderInput(
78+
config=thirdparty.ProviderConfig(
79+
third_party_id="github",
80+
clients=[
81+
thirdparty.ProviderClientConfig(
82+
client_id=os.environ["GITHUB_CLIENT_ID"],
83+
client_secret=os.environ["GITHUB_CLIENT_SECRET"],
84+
client_type="web",
85+
),
86+
thirdparty.ProviderClientConfig(
87+
client_id=os.environ["GITHUB_CLIENT_ID_MOBILE"],
88+
client_secret=os.environ[
89+
"GITHUB_CLIENT_SECRET_MOBILE"
90+
],
91+
client_type="mobile",
92+
),
93+
],
94+
)
95+
),
96+
thirdparty.ProviderInput(
97+
config=thirdparty.ProviderConfig(
98+
third_party_id="apple",
99+
clients=[
100+
thirdparty.ProviderClientConfig(
101+
client_id=os.environ["APPLE_CLIENT_ID"],
102+
client_type="web",
103+
additional_config={
104+
"keyId": os.environ["APPLE_KEY_ID"],
105+
"teamId": os.environ["APPLE_TEAM_ID"],
106+
"privateKey": os.environ["APPLE_PRIVATE_KEY"],
107+
},
108+
),
109+
thirdparty.ProviderClientConfig(
110+
client_id=os.environ["APPLE_CLIENT_ID_MOBILE"],
111+
client_type="mobile",
112+
additional_config={
113+
"keyId": os.environ["APPLE_KEY_ID"],
114+
"teamId": os.environ["APPLE_TEAM_ID"],
115+
"privateKey": os.environ["APPLE_PRIVATE_KEY"],
116+
},
117+
),
118+
],
119+
)
120+
),
121+
thirdparty.ProviderInput(
122+
config=thirdparty.ProviderConfig(
123+
third_party_id="google-workspaces",
124+
clients=[
125+
thirdparty.ProviderClientConfig(
126+
client_id=os.environ["GOOGLE_WORKSPACES_CLIENT_ID"],
127+
client_secret=os.environ[
128+
"GOOGLE_WORKSPACES_CLIENT_SECRET"
129+
],
130+
),
131+
],
132+
)
133+
),
134+
thirdparty.ProviderInput(
135+
config=thirdparty.ProviderConfig(
136+
third_party_id="discord",
137+
clients=[
138+
thirdparty.ProviderClientConfig(
139+
client_id=os.environ["DISCORD_CLIENT_ID"],
140+
client_secret=os.environ["DISCORD_CLIENT_SECRET"],
141+
),
142+
],
143+
)
144+
),
145+
]
146+
)
147+
),
148+
],
149+
telemetry=False,
150+
)
151+
152+
153+
@get(
154+
"/sessioninfo",
155+
dependencies={"session": Provide(verify_session())},
156+
)
157+
async def get_session_info(session: SessionContainer) -> Response[Any]:
158+
return Response(
159+
{
160+
"sessionHandle": session.get_handle(),
161+
"userId": session.get_user_id(),
162+
"accessTokenPayload": session.get_access_token_payload(),
163+
}
164+
)
165+
166+
167+
def f_405(_, __: Exception):
168+
return Response("", status_code=404, media_type=MediaType.TEXT)
169+
170+
171+
cors = CORSConfig(
172+
allow_origins=[get_website_domain()],
173+
allow_credentials=True,
174+
allow_methods=["GET", "PUT", "POST", "DELETE", "OPTIONS", "PATCH"],
175+
allow_headers=["Content-Type"] + get_all_cors_headers(),
176+
)
177+
178+
app = Litestar(
179+
route_handlers=[
180+
get_session_info,
181+
],
182+
middleware=[
183+
LitestarMiddleware(),
184+
],
185+
cors_config=cors,
186+
exception_handlers={
187+
Exception: f_405,
188+
},
189+
)
190+
191+
if __name__ == "__main__":
192+
uvicorn.run(app, host="0.0.0.0", port=get_api_port()) # type: ignore
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
litestar
2+
uvicorn
3+
python-dotenv
4+
supertokens-python

supertokens_python/framework/litestar/litestar_middleware.py

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22

33
from litestar import Request, Response
44
from litestar.datastructures import MutableScopeHeaders
5-
from litestar.middleware.base import AbstractMiddleware
6-
from litestar.types import Message, Receive, Scope, Send
5+
from litestar.enums import ScopeType
6+
from litestar.middleware import ASGIMiddleware
7+
from litestar.types import ASGIApp, Message, Receive, Scope, Send
78
from supertokens_python import Supertokens
89
from supertokens_python.exceptions import SuperTokensError
910
from supertokens_python.framework.litestar.litestar_request import LitestarRequest
@@ -13,12 +14,17 @@
1314
from supertokens_python.utils import default_user_context
1415

1516

16-
class LitestarMiddleware(AbstractMiddleware):
17-
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
17+
class LitestarMiddleware(ASGIMiddleware):
18+
scopes = (ScopeType.HTTP, ScopeType.ASGI)
19+
20+
async def handle(
21+
self, scope: Scope, receive: Receive, send: Send, next_app: ASGIApp
22+
) -> None:
1823
if scope["type"] != "http":
19-
await self.app(scope, receive, send)
24+
await next_app(scope, receive, send)
2025
return
2126

27+
# Initialize SuperTokens and request/response objects
2228
st = Supertokens.get_instance()
2329
request = Request[Any, Any, Any](scope, receive=receive, send=send)
2430
custom_request = LitestarRequest(request)
@@ -27,6 +33,7 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
2733
try:
2834
response = LitestarResponse(Response[Any](content=None))
2935
result = await st.middleware(custom_request, response, user_context)
36+
3037
if result is not None:
3138
# SuperTokens handled the request
3239
if hasattr(request.state, "supertokens") and isinstance(
@@ -35,9 +42,9 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
3542
manage_session_post_response(
3643
request.state.supertokens, result, user_context
3744
)
38-
# Add cookies using MutableScopeHeaders
45+
# Convert response to ASGI and add cookies to headers
3946
asgi_response = await result.response.to_asgi_response(
40-
app=None, request=request
47+
app=next_app, request=request
4148
)
4249

4350
async def modified_send(message: Message):
@@ -49,9 +56,8 @@ async def modified_send(message: Message):
4956
await send(message)
5057

5158
await asgi_response(scope, receive, modified_send)
52-
return
5359
else:
54-
# SuperTokens didn’t handle the request; wrap the send function
60+
# SuperTokens didn’t handle the request; pass to next app with wrapped send
5561
async def send_wrapper(message: Message):
5662
if message["type"] == "http.response.start":
5763
if hasattr(request.state, "supertokens") and isinstance(
@@ -71,18 +77,17 @@ async def send_wrapper(message: Message):
7177
mutable_headers.add("set-cookie", cookie_value)
7278
await send(message)
7379

74-
await self.app(scope, receive, send_wrapper)
75-
return
80+
await next_app(scope, receive, send_wrapper)
7681

7782
except SuperTokensError as e:
83+
# Handle SuperTokens errors
7884
response = LitestarResponse(Response[Any](content=None))
7985
result = await st.handle_supertokens_error(
8086
custom_request, e, response, user_context
8187
)
8288
if isinstance(result, LitestarResponse):
83-
# Add cookies using MutableScopeHeaders
8489
asgi_response = await result.response.to_asgi_response(
85-
app=None, request=request
90+
app=next_app, request=request
8691
)
8792

8893
async def modified_send(message: Message):
@@ -94,5 +99,5 @@ async def modified_send(message: Message):
9499
await send(message)
95100

96101
await asgi_response(scope, receive, modified_send)
97-
return
98-
raise Exception("Should never come here")
102+
else:
103+
raise Exception("Unexpected error handling in SuperTokens middleware")

0 commit comments

Comments
 (0)