Skip to content

Commit f55a958

Browse files
committed
🔨 update: oauth for svelte
1 parent ced7e47 commit f55a958

File tree

7 files changed

+150
-16
lines changed

7 files changed

+150
-16
lines changed

app/api/v1/endpoints/auth.py

Lines changed: 126 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
1+
import secrets
12
from typing import Annotated
23

34
from dependency_injector.wiring import Provide, inject
4-
from fastapi import Depends, Form, status
5-
from fastapi.responses import ORJSONResponse
5+
from fastapi import Depends, Form, Request, status
6+
from fastapi.responses import ORJSONResponse, RedirectResponse
67

78
from app.core.auth import (
89
GitHubOAuthDeps,
910
GoogleOAuthDeps,
1011
PasswordOAuthDeps,
1112
UserAuthDeps,
1213
)
14+
from app.core.configs import configs, oauth_endpoints
1315
from app.core.container import Container
1416
from app.core.router import CoreAPIRouter
17+
from app.exceptions.auth import GitHubOAuthFailed
1518
from app.schemas.auth import (
1619
GitHubOAuthRequest,
1720
GoogleOAuthRequest,
@@ -44,7 +47,7 @@ async def refresh(
4447

4548

4649
@router.post(
47-
"/register/password",
50+
"/password/register",
4851
response_model=UserResponse,
4952
response_class=ORJSONResponse,
5053
status_code=status.HTTP_201_CREATED,
@@ -61,7 +64,7 @@ async def register_password(
6164

6265

6366
@router.post(
64-
"/token/password",
67+
"/password/token",
6568
response_model=JwtToken,
6669
response_class=ORJSONResponse,
6770
status_code=status.HTTP_200_OK,
@@ -78,8 +81,69 @@ async def token_password(
7881
return await service.token_password(schema=request)
7982

8083

84+
@router.get(
85+
"/google/login",
86+
response_model=None,
87+
response_class=RedirectResponse,
88+
status_code=status.HTTP_307_TEMPORARY_REDIRECT,
89+
summary="",
90+
description="",
91+
)
92+
async def log_in_google():
93+
state = secrets.token_urlsafe(nbytes=configs.TOKEN_URLSAFE_NBYTES)
94+
response = RedirectResponse(
95+
url="https://accounts.google.com/o/oauth2/v2/auth?"
96+
f"client_id={configs.GOOGLE_OAUTH_CLIENT_ID}"
97+
"&response_type=code"
98+
f"&redirect_uri={configs.BACKEND_URL}{oauth_endpoints.GOOGLE_CALLBACK}"
99+
f"&state={state}"
100+
"&scope=email profile",
101+
status_code=status.HTTP_307_TEMPORARY_REDIRECT,
102+
)
103+
response.set_cookie(key="oauth_google_state", value=state, httponly=True)
104+
return response
105+
106+
107+
@router.get(
108+
"/google/callback",
109+
response_model=JwtToken,
110+
response_class=RedirectResponse,
111+
status_code=status.HTTP_302_FOUND,
112+
summary="Obtain an access token via Google OAuth",
113+
description="- Authenticate using Google OAuth and receive an access token.<br/>\n"
114+
"- The client must provide an authorization code obtained from Google.",
115+
)
116+
@inject
117+
async def callback_google(
118+
code: str,
119+
state: str,
120+
request: Request,
121+
service: AuthService = Depends(Provide[Container.auth_service]),
122+
):
123+
# NOTE: /?code=***&state=***
124+
# /?state=***&code=***&scope=***&authuser=***&prompt=***
125+
126+
if state != request.cookies.get("oauth_google_state"):
127+
raise GitHubOAuthFailed
128+
jwt_token = await service.token_google(
129+
GoogleOAuthRequest(
130+
grant_type="authorization_code",
131+
code=code,
132+
redirect_uri=f"{configs.BACKEND_URL}{oauth_endpoints.GOOGLE_CALLBACK}",
133+
)
134+
)
135+
return RedirectResponse(
136+
url=f"{configs.FRONTEND_URL}/login"
137+
f"?access_token={jwt_token.access_token}"
138+
f"&refresh_token={jwt_token.refresh_token}"
139+
f"&token_type={jwt_token.token_type}"
140+
f"&expires_in={jwt_token.expires_in}",
141+
status_code=status.HTTP_302_FOUND,
142+
)
143+
144+
81145
@router.post(
82-
"/token/google",
146+
"/google/token",
83147
response_model=JwtToken,
84148
response_class=ORJSONResponse,
85149
status_code=status.HTTP_200_OK,
@@ -95,8 +159,64 @@ async def token_google(
95159
return await service.token_google(request)
96160

97161

162+
@router.get(
163+
"/github/login",
164+
response_model=None,
165+
response_class=RedirectResponse,
166+
status_code=status.HTTP_307_TEMPORARY_REDIRECT,
167+
summary="",
168+
description="",
169+
)
170+
async def log_in_github():
171+
state = secrets.token_urlsafe(nbytes=configs.TOKEN_URLSAFE_NBYTES)
172+
response = RedirectResponse(
173+
url="https://github.com/login/oauth/authorize?"
174+
f"client_id={configs.GITHUB_OAUTH_CLIENT_ID}"
175+
"&response_type=code"
176+
f"&redirect_uri={configs.BACKEND_URL}{oauth_endpoints.GITHUB_CALLBACK}"
177+
f"&state={state}",
178+
status_code=status.HTTP_307_TEMPORARY_REDIRECT,
179+
)
180+
response.set_cookie(key="oauth_github_state", value=state, httponly=True)
181+
return response
182+
183+
184+
@router.get(
185+
"/github/callback",
186+
response_model=JwtToken,
187+
response_class=RedirectResponse,
188+
status_code=status.HTTP_302_FOUND,
189+
summary="Obtain an access token via GitHub OAuth",
190+
description="- Authenticate using GitHub OAuth and receive an access token.<br/>\n"
191+
"- The client must provide an authorization code obtained from GitHub.",
192+
)
193+
@inject
194+
async def callback_github(
195+
code: str,
196+
state: str,
197+
request: Request,
198+
service: AuthService = Depends(Provide[Container.auth_service]),
199+
):
200+
# NOTE: /?code=***&state=***
201+
if state != request.cookies.get("oauth_github_state"):
202+
raise GitHubOAuthFailed
203+
jwt_token = await service.token_github(
204+
GitHubOAuthRequest(
205+
grant_type="authorization_code", code=code, redirect_uri=configs.BACKEND_URL
206+
)
207+
)
208+
return RedirectResponse(
209+
url=f"{configs.FRONTEND_URL}/login"
210+
f"?access_token={jwt_token.access_token}"
211+
f"&refresh_token={jwt_token.refresh_token}"
212+
f"&token_type={jwt_token.token_type}"
213+
f"&expires_in={jwt_token.expires_in}",
214+
status_code=status.HTTP_302_FOUND,
215+
)
216+
217+
98218
@router.post(
99-
"/token/github",
219+
"/github/token",
100220
response_model=JwtToken,
101221
response_class=ORJSONResponse,
102222
status_code=status.HTTP_200_OK,

app/core/auth.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,13 +67,13 @@ async def get_admin_user(user: Annotated[UserOut, UserAuthDeps]) -> UserOut:
6767

6868
PasswordOAuthDeps = Depends(
6969
OAuth2PasswordBearer(
70-
tokenUrl=oauth_endpoints.PASSWORD, scheme_name="Password OAuth"
70+
tokenUrl=oauth_endpoints.PASSWORD_TOKEN, scheme_name="Password OAuth"
7171
)
7272
)
7373
GoogleOAuthDeps = Depends(
7474
OAuth2AuthorizationCodeBearer(
7575
authorizationUrl=f"https://accounts.google.com/o/oauth2/v2/auth?client_id={configs.GOOGLE_OAUTH_CLIENT_ID}",
76-
tokenUrl=oauth_endpoints.GOOGLE,
76+
tokenUrl=oauth_endpoints.GOOGLE_TOKEN,
7777
refreshUrl=None,
7878
scheme_name="Google OAuth",
7979
scopes={
@@ -85,7 +85,7 @@ async def get_admin_user(user: Annotated[UserOut, UserAuthDeps]) -> UserOut:
8585
GitHubOAuthDeps = Depends(
8686
OAuth2AuthorizationCodeBearer(
8787
authorizationUrl=f"https://github.com/login/oauth/authorize?client_id={configs.GITHUB_OAUTH_CLIENT_ID}",
88-
tokenUrl=oauth_endpoints.GITHUB,
88+
tokenUrl=oauth_endpoints.GITHUB_TOKEN,
8989
# NOTE: https://github.com/Zerohertz/fastapi-cookbook/issues/33#issuecomment-2627994536
9090
refreshUrl=None,
9191
scheme_name="GitHub OAuth",

app/core/configs.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ class Configs(BaseSettings):
1919
PROJECT_NAME: str
2020
DESCRIPTION: str
2121
VERSION: str
22+
BACKEND_URL: str
23+
FRONTEND_URL: str
2224
PREFIX: str
2325
TZ: str = "Asia/Seoul"
2426

@@ -47,6 +49,8 @@ def allow_origins(cls, value: str) -> List[str]:
4749
JWT_SECRET_KEY: str
4850
JWT_ALGORITHM: str
4951

52+
TOKEN_URLSAFE_NBYTES: int = 128
53+
5054
ADMIN_NAME: str
5155
ADMIN_EMAIL: str
5256
ADMIN_PASSWORD: str
@@ -85,9 +89,12 @@ def DATABASE_URI(self) -> str:
8589

8690

8791
class OAuthEndpoints(BaseSettings):
88-
PASSWORD: str = f"{configs.PREFIX}/v1/auth/token/password"
89-
GOOGLE: str = f"{configs.PREFIX}/v1/auth/token/google"
90-
GITHUB: str = f"{configs.PREFIX}/v1/auth/token/github"
92+
PASSWORD_TOKEN: str = f"{configs.PREFIX}/v1/auth/password/token"
93+
GOOGLE_TOKEN: str = f"{configs.PREFIX}/v1/auth/google/token"
94+
GITHUB_TOKEN: str = f"{configs.PREFIX}/v1/auth/github/token"
95+
96+
GOOGLE_CALLBACK: str = f"{configs.PREFIX}/v1/auth/google/callback"
97+
GITHUB_CALLBACK: str = f"{configs.PREFIX}/v1/auth/github/callback"
9198

9299

93100
oauth_endpoints = OAuthEndpoints()

app/exceptions/handlers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ async def global_exception_handler(request: Request, exc: Exception) -> ORJSONRe
2121
async def core_exception_handler(
2222
request: Request, exc: CoreException # pylint: disable=unused-argument
2323
) -> ORJSONResponse:
24-
logger.error(exc)
24+
logger.exception(exc)
2525
return ORJSONResponse(
2626
content=APIResponse.error(status=exc.status, message=repr(exc)).model_dump(
2727
mode="json"

app/tests/api/v1/test_auth.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ def __init__(
3737

3838
def register(self) -> None:
3939
response = self.client.post(
40-
f"{configs.PREFIX}/v1/auth/register/password",
40+
f"{configs.PREFIX}/v1/auth/password/register",
4141
headers=self.headers,
4242
data=self.request.model_dump(),
4343
)
@@ -50,7 +50,7 @@ def register(self) -> None:
5050
def log_in(self) -> str:
5151
request = PasswordOAuthRequest.model_validate(self.request.model_dump())
5252
response = self.client.post(
53-
f"{configs.PREFIX}/v1/auth/token/password",
53+
f"{configs.PREFIX}/v1/auth/password/token",
5454
headers=self.headers,
5555
data=request.model_dump(),
5656
)
@@ -93,7 +93,7 @@ def test_register_and_log_in(sync_client: TestClient):
9393
mock_user, access_token = register_and_log_in(sync_client)
9494
mock_user.get_me(access_token)
9595
response = sync_client.post(
96-
f"{configs.PREFIX}/v1/auth/register/password",
96+
f"{configs.PREFIX}/v1/auth/password/register",
9797
headers=mock_user.headers,
9898
data=mock_user.request.model_dump(),
9999
)

envs/test.env

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ PORT="8000"
22

33
PROJECT_NAME="Zerohertz's FastAPI Cookbook (test)"
44
VERSION="v0.1.4"
5+
BACKEND_URL=""
6+
FRONTEND_URL=""
57
PREFIX="/api"
68
TZ="Asia/Seoul"
79

@@ -21,6 +23,7 @@ GITHUB_OAUTH_CLIENT_ID=""
2123
GITHUB_OAUTH_CLIENT_SECRET=""
2224
JWT_SECRET_KEY="fastapi"
2325
JWT_ALGORITHM="HS256"
26+
TOKEN_URLSAFE_NBYTES=128
2427
ADMIN_NAME="PyTest"
2528
ADMIN_EMAIL="[email protected]"
2629
ADMIN_PASSWORD="PyTest98!@"

k8s/postgresql/configmap.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ data:
77
PORT="8000"
88
PROJECT_NAME="Zerohertz's FastAPI Cookbook (dev)"
99
VERSION="v0.1.4"
10+
BACKEND_URL="https://dev.zerohertz.xyz"
11+
FRONTEND_URL="http://localhost:5173"
1012
PREFIX="/api"
1113
TZ="Asia/Seoul"
1214
DB_ECHO=true
@@ -16,6 +18,8 @@ data:
1618
PORT="8000"
1719
PROJECT_NAME="Zerohertz's FastAPI Cookbook (prod)"
1820
VERSION="v0.1.4"
21+
BACKEND_URL="https://api.zerohertz.xyz"
22+
FRONTEND_URL="https://zerohertz.vercel.app"
1923
PREFIX=""
2024
TZ="Asia/Seoul"
2125
DB_ECHO=false

0 commit comments

Comments
 (0)