1+ import secrets
12from typing import Annotated
23
34from 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
78from app .core .auth import (
89 GitHubOAuthDeps ,
910 GoogleOAuthDeps ,
1011 PasswordOAuthDeps ,
1112 UserAuthDeps ,
1213)
14+ from app .core .configs import configs , oauth_endpoints
1315from app .core .container import Container
1416from app .core .router import CoreAPIRouter
17+ from app .exceptions .auth import GitHubOAuthFailed
1518from 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 ,
0 commit comments