From d9e2599e3bb4607861958429565bafb08d9e1177 Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Tue, 6 Feb 2024 16:02:40 +0800 Subject: [PATCH] Add GitHub OAuth2 (#3) --- README.md | 5 ++- src/fastapi_oauth20/clients/github.py | 46 +++++++++++++++++++++++++++ src/fastapi_oauth20/oauth20.py | 22 +++++++++++-- 3 files changed, 67 insertions(+), 6 deletions(-) create mode 100644 src/fastapi_oauth20/clients/github.py diff --git a/README.md b/README.md index 3637a7e..0fae69c 100644 --- a/README.md +++ b/README.md @@ -6,12 +6,11 @@ 我们的目标是集成多个 CN 第三方客户端,敬请期待 -感谢 [httpx_oauth](https://github.com/frankie567/httpx-oauth) 鼎力相助 - #### TODO: 如果我们能够很容易获取测试客户端,对接将会很快发生 +- [ ] library tests - [x] Google - [ ] 微信 - [ ] QQ @@ -21,7 +20,7 @@ - [ ] 微博 - [ ] 百度 - [x] Gitee -- [ ] Github +- [x] Github - [ ] 开源中国 - [ ] 阿里云 - [ ] TestHome diff --git a/src/fastapi_oauth20/clients/github.py b/src/fastapi_oauth20/clients/github.py new file mode 100644 index 0000000..843aabb --- /dev/null +++ b/src/fastapi_oauth20/clients/github.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import httpx + +from fastapi_oauth20.oauth20 import OAuth20Base + +AUTHORIZE_ENDPOINT = 'https://github.com/login/oauth/authorize' +ACCESS_TOKEN_ENDPOINT = 'https://github.com/login/oauth/access_token' +DEFAULT_SCOPES = ['user user:email'] +PROFILE_ENDPOINT = 'https://api.github.com/user' +EMAILS_ENDPOINT = 'https://api.github.com/user/emails' + + +class GitHubOAuth20(OAuth20Base): + def __init__(self, client_id: str, client_secret: str): + super().__init__( + client_id=client_id, + client_secret=client_secret, + authorize_endpoint=AUTHORIZE_ENDPOINT, + access_token_endpoint=ACCESS_TOKEN_ENDPOINT, + refresh_token_endpoint=None, + revoke_token_endpoint=None, + oauth_callback_route_name='github', + default_scopes=DEFAULT_SCOPES, + ) + + async def get_userinfo(self, access_token: str) -> dict: + """Get user info from GitHub""" + headers = {'Authorization': f'Bearer {access_token}'} + async with httpx.AsyncClient(headers=headers) as client: + response = await client.get(PROFILE_ENDPOINT) + await self.raise_httpx_oauth20_errors(response) + + res = response.json() + + email = res.get('email') + if email is None: + response = await client.get(EMAILS_ENDPOINT) + await self.raise_httpx_oauth20_errors(response) + + emails = response.json() + + email = next((email['email'] for email in emails if email.get('primary')), emails[0]['email']) + res['email'] = email + + return res diff --git a/src/fastapi_oauth20/oauth20.py b/src/fastapi_oauth20/oauth20.py index ef48604..29a6331 100644 --- a/src/fastapi_oauth20/oauth20.py +++ b/src/fastapi_oauth20/oauth20.py @@ -30,6 +30,10 @@ def __init__( self.oauth_callback_route_name = oauth_callback_route_name self.default_scopes = default_scopes + self.request_headers = { + 'Accept': 'application/json', + } + async def get_authorization_url( self, redirect_uri: str, @@ -86,7 +90,11 @@ async def get_access_token(self, code: str, redirect_uri: str, code_verifier: st if code_verifier: data.update({'code_verifier': code_verifier}) async with httpx.AsyncClient() as client: - response = await client.post(self.access_token_endpoint, data=data) + response = await client.post( + self.access_token_endpoint, + data=data, + headers=self.request_headers, + ) await self.raise_httpx_oauth20_errors(response) res = response.json() @@ -104,7 +112,11 @@ async def refresh_token(self, refresh_token: str) -> dict: 'grant_type': 'refresh_token', } async with httpx.AsyncClient() as client: - response = await client.post(self.refresh_token_endpoint, data=data) + response = await client.post( + self.refresh_token_endpoint, + data=data, + headers=self.request_headers, + ) await self.raise_httpx_oauth20_errors(response) res = response.json() @@ -122,7 +134,11 @@ async def revoke_token(self, token: str, token_type_hint: str | None = None) -> if token_type_hint is not None: data.update({'token_type_hint': token_type_hint}) - response = await client.post(self.revoke_token_endpoint, data=data) + response = await client.post( + self.revoke_token_endpoint, + data=data, + headers=self.request_headers, + ) await self.raise_httpx_oauth20_errors(response)