Skip to content

Commit 078f2cb

Browse files
committed
feat(auth): supports Github OAuth 2.0
1 parent 0305c22 commit 078f2cb

File tree

7 files changed

+132
-3
lines changed

7 files changed

+132
-3
lines changed

fastapi/app/auth/github/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
GITHUB_USERINFO_URL = 'https://api.github.com/user'

fastapi/app/auth/github/libs.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
from datetime import datetime
2+
from typing import Any, Optional
3+
4+
import requests
5+
from fastapi import Response
6+
from fastapi_users.authentication import AuthenticationBackend
7+
from fastapi_users.authentication.strategy import Strategy
8+
9+
from ..exceptions import BadCredentialException
10+
from ..libs import bearer_transport, get_jwt_strategy
11+
from ..models import User
12+
from .constants import GITHUB_USERINFO_URL
13+
14+
15+
class GithubAuthBackend(AuthenticationBackend):
16+
async def login(self, strategy: Strategy, user: User, response: Response) -> Any:
17+
strategy_response = await super().login(strategy, user, response)
18+
token = self.get_google_access_token(user)
19+
profile = get_profile(token)
20+
user.first_name = profile.get('first_name')
21+
user.last_name = profile.get('last_name')
22+
user.picture = profile.get('avatar_url')
23+
user.last_login_at = datetime.now()
24+
await user.save()
25+
return strategy_response
26+
27+
def get_google_access_token(self, user: User) -> Optional[str]:
28+
for account in user.oauth_accounts:
29+
if account.oauth_name == 'github':
30+
return account.access_token
31+
return None
32+
33+
34+
def get_profile(access_token: str) -> dict:
35+
headers = dict(Authorization=f'Bearer {access_token}')
36+
response = requests.get(GITHUB_USERINFO_URL, headers=headers)
37+
if not response.ok:
38+
raise BadCredentialException(
39+
'Failed to get user information from Github.')
40+
profile = dict(response.json())
41+
name = profile.get('name').split()
42+
try:
43+
profile.update({
44+
"first_name": name[0],
45+
"last_name": name[1],
46+
})
47+
except:
48+
pass
49+
return profile
50+
51+
52+
auth_backend_github = GithubAuthBackend(
53+
name="jwt-github",
54+
transport=bearer_transport,
55+
get_strategy=get_jwt_strategy,
56+
)

fastapi/app/auth/github/routes.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
2+
from app.configs import Configs
3+
from httpx_oauth.clients.github import GitHubOAuth2
4+
5+
from ..libs import fastapi_users
6+
from .libs import auth_backend_github
7+
8+
github_oauth_client = GitHubOAuth2(
9+
client_id=Configs.GITHUB_CLIENT_ID,
10+
client_secret=Configs.GITHUB_CLIENT_SECRET,
11+
)
12+
13+
github_oauth_router = fastapi_users.get_oauth_router(
14+
oauth_client=github_oauth_client,
15+
backend=auth_backend_github,
16+
state_secret=Configs.SECRET_KEY,
17+
redirect_url=Configs.GITHUB_CALLBACK_URL,
18+
associate_by_email=True,
19+
)

fastapi/app/auth/routes.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from fastapi import APIRouter, Depends
22

33
from .google.routes import google_oauth_router
4+
from .github.routes import github_oauth_router
45
from .libs import auth_backend, current_active_user, fastapi_users
56
from .models import User, UserCreate, UserRead, UserUpdate
67

@@ -20,6 +21,7 @@
2021
(get_reset_password_router, dict(prefix="/auth", tags=["auth"])),
2122
(get_verify_router, dict(prefix="/auth", tags=["auth"])),
2223
(google_oauth_router, dict(prefix="/auth/google", tags=["auth"])),
24+
(github_oauth_router, dict(prefix="/auth/github", tags=["auth"])),
2325
(get_users_router, dict(prefix="/users", tags=["users"])),
2426
]
2527

fastapi/app/configs.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ class Settings(BaseSettings):
3131
GOOGLE_CLIENT_SECRET: str = None
3232
GOOGLE_CALLBACK_URL: str = None
3333

34+
GITHUB_CLIENT_ID: str = None
35+
GITHUB_CLIENT_SECRET: str = None
36+
GITHUB_CALLBACK_URL: str = None
37+
3438
class Config:
3539
env_file = get_env_file()
3640

react-app/src/App.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const App = () => {
2121
<Route path="/login">
2222
<Route index element={<Auth.Login />} />
2323
<Route path="google" element={<Auth.Redirects.Google />} />
24+
<Route path="github" element={<Auth.Redirects.Github />} />
2425
</Route>
2526
<Route path="/logout" element={<Auth.Logout />} />
2627
<Route path="/my" element={<MyPage />} />

react-app/src/pages/Auth.jsx

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { useEffect, useState } from 'react';
44
import { useLocation, useNavigate } from 'react-router-dom';
55

66
import { FcGoogle } from 'react-icons/fc';
7+
import { FaGithub } from 'react-icons/fa';
78
import { OAuth } from 'components/Buttons';
89
import { customAxios } from 'libs/customAxios';
910
import qs from 'qs';
@@ -54,19 +55,24 @@ export const Login = () => {
5455
});
5556
};
5657

57-
const onClickGoogleButton = (evt) => {
58-
evt.preventDefault();
58+
const handleOAuth = (link) => {
5959
customAxios()
60-
.get('/auth/google/authorize')
60+
.get(link)
6161
.then((response) => {
6262
const { authorization_url } = response.data;
6363
window.location.href = authorization_url;
6464
})
6565
.catch((e) => console.log('log', e));
6666
};
6767

68+
const onClickGoogleButton = (evt) => {
69+
evt.preventDefault();
70+
handleOAuth('/auth/google/authorize');
71+
};
72+
6873
const onClickGitHubButton = (evt) => {
6974
evt.preventDefault();
75+
handleOAuth('/auth/github/authorize');
7076
};
7177

7278
const onClickKakaoButton = (evt) => {
@@ -176,10 +182,50 @@ const CallbackGoogle = () => {
176182
);
177183
};
178184

185+
const CallbackGithub = () => {
186+
const location = useLocation();
187+
const navigate = useNavigate();
188+
const { login } = useAuth();
189+
190+
useEffect(() => {
191+
customAxios()
192+
.get('/auth/github/callback' + location.search)
193+
.then(({ data }) => {
194+
console.log('Recieved data', data);
195+
login({
196+
token: data.access_token,
197+
});
198+
})
199+
.catch(({ response }) => {
200+
console.error(response);
201+
navigate('/login', {
202+
replace: true,
203+
state: {
204+
from: location,
205+
message: 'Failed to authroize with Github',
206+
},
207+
});
208+
});
209+
}, [location]);
210+
211+
return (
212+
<LoginContainer>
213+
<Card>
214+
<Box sx={{ width: '100%', textAlign: 'center', fontSize: '5em', marginBottom: '10px' }}>
215+
<FaGithub />
216+
<LinearProgress />
217+
</Box>
218+
<Typography>Waiting for GitHub Sign-in to complete...</Typography>
219+
</Card>
220+
</LoginContainer>
221+
);
222+
};
223+
179224
export default {
180225
Login,
181226
Logout,
182227
Redirects: {
183228
Google: CallbackGoogle,
229+
Github: CallbackGithub,
184230
},
185231
};

0 commit comments

Comments
 (0)