-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
OAuth2.0 with github and 42intra (#80)
* OAuth2.0 with github and 42intra
- Loading branch information
Showing
11 changed files
with
345 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
#To receive a github client ID + secret, you need to log in to github and then register your app here: github.com/settings/applications/new | ||
GITHUB_CLIENT_ID=xxxx | ||
GITHUB_CLIENT_SECRET=xxx | ||
#To receive a 42 api client ID + secret, you need to log in to 42intra and then register your app here: profile.intra.42.fr/oauth/applications/new | ||
FT_API_CLIENT_ID=xxx | ||
FT_API_CLIENT_SECRET=xxx |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,194 @@ | ||
from abc import ABC, abstractmethod | ||
from datetime import timedelta | ||
from hashlib import sha256 | ||
|
||
import requests | ||
from django.http import JsonResponse | ||
from django.utils import timezone | ||
from django.views import View | ||
|
||
from user.models import PendingOAuth, User | ||
from user_management import settings | ||
from user_management.JWTManager import JWTManager | ||
from user_management.utils import (download_image_from_url, | ||
generate_random_string) | ||
|
||
|
||
class OAuthFactory: | ||
@staticmethod | ||
def create_oauth_handler(auth_service): | ||
if auth_service == 'github': | ||
return GitHubOAuth() | ||
elif auth_service == '42api': | ||
return FtApiOAuth() | ||
else: | ||
return None | ||
|
||
|
||
class BaseOAuth(View, ABC): | ||
@staticmethod | ||
def get(request, auth_service): | ||
oauth_handler = OAuthFactory.create_oauth_handler(auth_service) | ||
if oauth_handler: | ||
return oauth_handler.handle_auth(request) | ||
else: | ||
return JsonResponse(data={'errors': ['Unknown auth service']}, status=400) | ||
|
||
@staticmethod | ||
def create_pending_oauth(): | ||
state = generate_random_string(settings.OAUTH_STATE_MAX_LENGTH) | ||
hashed_state = sha256(str(state).encode('utf-8')).hexdigest() | ||
PendingOAuth.objects.create(hashed_state=hashed_state) | ||
return state | ||
|
||
@abstractmethod | ||
def handle_auth(self, request): | ||
pass | ||
|
||
|
||
class GitHubOAuth(BaseOAuth): | ||
def handle_auth(self, request): | ||
state = self.create_pending_oauth() | ||
authorization_url = self.get_github_authorization_url(state) | ||
return JsonResponse(data={'redirection_url': authorization_url}, status=200) | ||
|
||
@staticmethod | ||
def get_github_authorization_url(state): | ||
return ( | ||
f"{settings.GITHUB_AUTHORIZE_URL}" | ||
f"?client_id={settings.GITHUB_CLIENT_ID}" | ||
f"&redirect_uri={settings.GITHUB_REDIRECT_URI}" | ||
f"&state={state}" | ||
f"&scope=user:email" | ||
) | ||
|
||
|
||
class FtApiOAuth(BaseOAuth): | ||
def handle_auth(self, request): | ||
state = self.create_pending_oauth() | ||
authorization_url = self.get_ft_api_authorization_url(state) | ||
return JsonResponse(data={'redirection_url': authorization_url}, status=200) | ||
|
||
@staticmethod | ||
def get_ft_api_authorization_url(state): | ||
return ( | ||
f"{settings.FT_API_AUTHORIZE_URL}" | ||
f"?client_id={settings.FT_API_CLIENT_ID}" | ||
f"&redirect_uri={settings.FT_API_REDIRECT_URI}" | ||
f"&response_type=code" | ||
f"&state={state}" | ||
) | ||
|
||
|
||
class OAuthCallback(View): | ||
access_token_url = None | ||
client_id = None | ||
client_secret = None | ||
redirect_uri = None | ||
|
||
def set_params(self, auth_service): | ||
if auth_service == 'github': | ||
self.access_token_url = settings.GITHUB_ACCESS_TOKEN_URL | ||
self.client_id = settings.GITHUB_CLIENT_ID | ||
self.client_secret = settings.GITHUB_CLIENT_SECRET | ||
self.redirect_uri = settings.GITHUB_REDIRECT_URI | ||
elif auth_service == '42api': | ||
self.access_token_url = settings.FT_API_ACCESS_TOKEN_URL | ||
self.client_id = settings.FT_API_CLIENT_ID | ||
self.client_secret = settings.FT_API_CLIENT_SECRET | ||
self.redirect_uri = settings.FT_API_REDIRECT_URI | ||
|
||
def get(self, request, auth_service): | ||
code = request.GET.get('code') | ||
state = request.GET.get('state') | ||
self.set_params(auth_service) | ||
self.check_and_update_state(state) | ||
access_token = self.get_access_token(code) | ||
if not access_token: | ||
return JsonResponse(data={'errors': ['Failed to retrieve access token']}, status=400) | ||
|
||
login, avatar_url, email = self.get_user_infos(access_token, auth_service) | ||
user = self.create_or_get_user(login, email, avatar_url) | ||
if not user: | ||
return JsonResponse(data={'errors': ['Failed to create or get user']}, status=400) | ||
|
||
success, refresh_token, errors = JWTManager('refresh').generate_token(user.id) | ||
if not success: | ||
return JsonResponse(data={'errors': errors}, status=400) | ||
|
||
return JsonResponse(data={'refresh_token': refresh_token}, status=201) | ||
|
||
@staticmethod | ||
def create_or_get_user(login, email, avatar_url): | ||
user = User.objects.filter(email=email).first() | ||
|
||
if user is None: | ||
user = User.objects.create(username=login, email=email, password=None) | ||
if not download_image_from_url(avatar_url, user): | ||
return None | ||
user.save() | ||
|
||
return user | ||
|
||
@staticmethod | ||
def get_user_infos(access_token, auth_service): | ||
if auth_service == 'github': | ||
github_user_profile_url = settings.GITHUB_USER_PROFILE_URL | ||
headers = { | ||
'Accept': 'application/vnd.github+json', | ||
'Authorization': f'Bearer {access_token}', | ||
'X-GitHub-Api-Version': '2022-11-28' | ||
} | ||
response = requests.get(github_user_profile_url, headers=headers) | ||
if response.status_code != 200: | ||
return None | ||
user_profile = response.json() | ||
login = user_profile['login'] | ||
avatar_url = user_profile['avatar_url'] | ||
email_url = settings.GITHUB_USER_PROFILE_URL + '/emails' | ||
response = requests.get(email_url, headers=headers) | ||
email = response.json()[0]['email'] | ||
return login, avatar_url, email | ||
if auth_service == '42api': | ||
ft_api_user_profile_url = settings.FT_API_USER_PROFILE_URL | ||
headers = { | ||
'Accept': 'application/json', | ||
'Authorization': f'Bearer {access_token}', | ||
} | ||
response = requests.get(ft_api_user_profile_url, headers=headers) | ||
if response.status_code != 200: | ||
return None | ||
user_profile = response.json() | ||
login = user_profile['login'] | ||
avatar_url = user_profile['image']['link'] | ||
email = user_profile['email'] | ||
return login, avatar_url, email | ||
|
||
@staticmethod | ||
def check_and_update_state(state): | ||
hashed_state = sha256(str(state).encode('utf-8')).hexdigest() | ||
pending_oauth = PendingOAuth.objects.filter(hashed_state=hashed_state).first() | ||
if pending_oauth is None: | ||
return JsonResponse(data={'errors': ['Invalid state']}, status=400) | ||
pending_oauth.delete() | ||
PendingOAuth.objects.filter(created_at__lte=timezone.now() - timedelta(minutes=5)).delete() | ||
|
||
def get_access_token(self, code): | ||
payload = { | ||
'client_id': self.client_id, | ||
'client_secret': self.client_secret, | ||
'code': code, | ||
'redirect_uri': self.redirect_uri, | ||
'grant_type': 'authorization_code', | ||
'scope': 'public' | ||
} | ||
headers = { | ||
'Accept': 'application/json' | ||
} | ||
|
||
response = requests.post(self.access_token_url, data=payload, headers=headers) | ||
if response.status_code != 200: | ||
return None | ||
access_token = response.json()['access_token'] | ||
|
||
return access_token |
Empty file.
File renamed without changes.
Oops, something went wrong.