-
Notifications
You must be signed in to change notification settings - Fork 3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
API supabase #593
API supabase #593
Changes from 21 commits
072d591
e6d6d9f
75817ff
8c60a66
3b222f0
6d950a4
380a0ed
ca26ad6
0a0a931
09f5d49
e35ebc6
693a2ea
375dd17
a2f7c2d
190fcb7
4444b5d
37f83ad
5f1106b
789173a
239e3f5
223c0e9
03fcdc4
34af069
f94e0bb
15cc81e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -37,6 +37,7 @@ env: | |
FIREBASE_API_KEY: ${{ secrets.FIREBASE_API_KEY }} | ||
SENDGRID_API_KEY: fake_api_key_for_sendgrid_test | ||
SYSTEM_EMAIL: [email protected] | ||
AUTH_SERVICE: FIREBASE | ||
|
||
# A workflow run is made up of one or more jobs that can run sequentially or in parallel | ||
jobs: | ||
|
Large diffs are not rendered by default.
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
from fastapi import Depends, HTTPException, status | ||
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer | ||
from sqlalchemy.orm import Session | ||
|
||
from app import persistence | ||
from app.auth.auth_module import AuthModule, get_auth_module | ||
|
||
from ..database import get_db | ||
from ..models import Account | ||
|
||
|
||
def get_current_user( | ||
token: HTTPAuthorizationCredentials = Depends( | ||
HTTPBearer(scheme_name=None, description=None, auto_error=False) | ||
), | ||
auth_module: AuthModule = Depends(get_auth_module), | ||
db: Session = Depends(get_db), | ||
) -> Account: | ||
uid, email = auth_module.check_and_get_user_info(token) | ||
user = persistence.get_account_by_uid(db, uid) | ||
if not user: | ||
raise HTTPException( | ||
status_code=status.HTTP_404_NOT_FOUND, | ||
detail="No such user", | ||
) | ||
if user.disabled: | ||
raise HTTPException( | ||
status_code=status.HTTP_400_BAD_REQUEST, | ||
detail="Inactive user", | ||
) | ||
return user |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
from fastapi import HTTPException, status | ||
|
||
from ..schemas import Token | ||
|
||
|
||
def get_auth_module(): | ||
return AuthModule(None) | ||
|
||
|
||
class AuthModule: | ||
def __init__(self): | ||
pass | ||
|
||
def login_for_access_token(self, username, password) -> Token: | ||
return Token(access_token="", token_type="bearer", refresh_token="") | ||
|
||
def refresh_access_token(self, refresh_token) -> Token: | ||
return Token(access_token="", token_type="bearer", refresh_token="") | ||
|
||
def check_and_get_user_info(self, token): | ||
pass | ||
|
||
def check_token(self, token): | ||
if token is None: | ||
raise HTTPException( | ||
status_code=status.HTTP_401_UNAUTHORIZED, | ||
detail="Could not validate credentials", | ||
headers={"WWW-Authenticate": "Bearer"}, | ||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,152 @@ | ||
import json | ||
import os | ||
|
||
import requests | ||
from fastapi import HTTPException, status | ||
from firebase_admin import auth, credentials, initialize_app | ||
|
||
from app.auth.auth_module import AuthModule | ||
|
||
from ..schemas import Token | ||
|
||
|
||
class FirebaseAuthModule(AuthModule): | ||
def __init__(self): | ||
super().__init__() | ||
self.cred = credentials.Certificate(os.environ["FIREBASE_CRED"]) | ||
initialize_app(self.cred) | ||
|
||
def login_for_access_token(self, username, password) -> Token: | ||
payload = { | ||
"email": username, | ||
"password": password.get_secret_value(), | ||
"returnSecureToken": True, | ||
} | ||
|
||
api_key = os.getenv("FIREBASE_API_KEY", "") | ||
# https://github.com/firebase/firebase-admin-python/blob/master/firebase_admin/_auth_utils.py | ||
id_toolkit = "identitytoolkit.googleapis.com/v1" | ||
sign_in_url_without_scheme = f"{id_toolkit}/accounts:signInWithPassword?key={api_key}" | ||
emulator_host = os.getenv("FIREBASE_AUTH_EMULATOR_HOST", "") | ||
if emulator_host != "": | ||
sign_in_url = f"http://{emulator_host}/{sign_in_url_without_scheme}" | ||
else: | ||
sign_in_url = f"https://{sign_in_url_without_scheme}" | ||
|
||
try: | ||
resp = requests.post( | ||
sign_in_url, | ||
json.dumps(payload), | ||
headers={"Content-Type": "application/json"}, | ||
timeout=30, | ||
) | ||
except requests.exceptions.Timeout as firebase_timeout: | ||
raise HTTPException( | ||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, | ||
detail="Could not validate credentials", | ||
headers={"WWW-Authenticate": "Bearer"}, | ||
) from firebase_timeout | ||
|
||
data = resp.json() | ||
if not resp.ok: | ||
error_message = data["error"]["message"] | ||
raise HTTPException( | ||
status_code=status.HTTP_401_UNAUTHORIZED, | ||
detail=error_message if error_message else "Could not validate credentials", | ||
headers={"WWW-Authenticate": "Bearer"}, | ||
) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ここではHTTPExceptionでなく、Exceptionを発出し、router関数の中 ( |
||
return Token( | ||
access_token=data["idToken"], token_type="bearer", refresh_token=data["refreshToken"] | ||
) | ||
|
||
def refresh_access_token(self, refresh_token) -> Token: | ||
payload = { | ||
"grant_type": "refresh_token", | ||
"refresh_token": refresh_token, | ||
} | ||
|
||
# see https://firebase.google.com/docs/reference/rest/auth#section-refresh-token | ||
api_key = os.environ["FIREBASE_API_KEY"] | ||
refresh_path = f"securetoken.googleapis.com/v1/token?key={api_key}" | ||
emulator_host = os.getenv("FIREBASE_AUTH_EMULATOR_HOST", "") | ||
if emulator_host != "": | ||
refresh_token_url = f"http://{emulator_host}/{refresh_path}" | ||
else: | ||
refresh_token_url = f"https://{refresh_path}" | ||
|
||
try: | ||
resp = requests.post( | ||
refresh_token_url, | ||
json.dumps(payload), | ||
headers={"Content-Type": "application/json"}, | ||
timeout=30, | ||
) | ||
except requests.exceptions.Timeout as firebase_timeout: | ||
raise HTTPException( | ||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, | ||
detail="Could not refresh token", | ||
headers={"WWW-Authenticate": "Bearer"}, | ||
) from firebase_timeout | ||
|
||
data: dict = resp.json() | ||
if not resp.ok: | ||
error_message: str = data["error"]["message"] | ||
raise HTTPException( | ||
status_code=status.HTTP_401_UNAUTHORIZED, | ||
detail=error_message if error_message else "Could not refresh token", | ||
headers={"WWW-Authenticate": "Bearer"}, | ||
) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. こちらも同様に、HTTPException をここで発出するのを避けて欲しいです |
||
return Token( | ||
access_token=data["id_token"], | ||
token_type=data["token_type"], | ||
refresh_token=data["refresh_token"], | ||
) | ||
|
||
def check_and_get_user_info(self, token): | ||
super().check_token(token) | ||
try: | ||
decoded_token = auth.verify_id_token(token.credentials, check_revoked=True) | ||
except auth.ExpiredIdTokenError as error: | ||
raise HTTPException( | ||
status_code=status.HTTP_401_UNAUTHORIZED, | ||
detail="Token has expired", | ||
) from error | ||
except auth.RevokedIdTokenError as error: | ||
raise HTTPException( | ||
status_code=status.HTTP_401_UNAUTHORIZED, | ||
detail="Token has revoked", | ||
) from error | ||
except auth.CertificateFetchError as error: | ||
raise HTTPException( | ||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE, | ||
detail="Failed to obtain required credentials", | ||
) from error | ||
except auth.UserDisabledError as error: | ||
raise HTTPException( | ||
status_code=status.HTTP_401_UNAUTHORIZED, | ||
detail="Disabled user", | ||
) from error | ||
except (auth.InvalidIdTokenError, ValueError) as error: | ||
raise HTTPException( | ||
status_code=status.HTTP_401_UNAUTHORIZED, | ||
detail="Could not validate credentials", | ||
headers={"WWW-Authenticate": "Bearer"}, | ||
) from error | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. HTTPExceptionについて同様 |
||
user_info = auth.get_user(decoded_token["uid"]) | ||
|
||
# check email verified if not using firebase emulator | ||
emulator_host = os.getenv("FIREBASE_AUTH_EMULATOR_HOST", "") | ||
if emulator_host == "" and user_info.email_verified is False: | ||
raise HTTPException( | ||
status_code=status.HTTP_401_UNAUTHORIZED, | ||
detail="Email is not verified. Try logging in on UI and verify email.", | ||
) | ||
|
||
email = user_info.email | ||
# if user_info.email is empty, get email from auth provider data | ||
if email is None: | ||
if len(user_info.provider_data) > 0: | ||
email = user_info.provider_data[0].email | ||
|
||
return user_info.uid, email |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
import os | ||
|
||
from supabase import create_client | ||
|
||
from app.auth.auth_module import AuthModule | ||
|
||
from ..schemas import Token | ||
|
||
|
||
class SupabaseAuthModule(AuthModule): | ||
def __init__(self): | ||
super().__init__() | ||
|
||
url = os.getenv("SUPABASE_URL") | ||
if url is None: | ||
raise Exception(f"Unsupported SUPABASE_URL: {url}") | ||
key = os.getenv("SUPABASE_ANON_KEY") | ||
if key is None: | ||
raise Exception(f"Unsupported SUPABASE_ANON_KEY: {key}") | ||
|
||
self.supabase = create_client(url, key) | ||
|
||
def login_for_access_token(self, username, password) -> Token: | ||
payload = { | ||
"email": username, | ||
"password": password.get_secret_value(), | ||
} | ||
user_data = self.supabase.auth.sign_in_with_password(payload) | ||
session = user_data.model_dump().get("session") | ||
return Token( | ||
access_token=session.get("access_token"), | ||
token_type="bearer", | ||
refresh_token=session.get("refresh_token"), | ||
) | ||
|
||
def refresh_access_token(self, refresh_token) -> Token: | ||
return Token(access_token="", token_type="bearer", refresh_token="") | ||
|
||
def check_and_get_user_info(self, token): | ||
super().check_token(token) | ||
user_data = self.supabase.auth.get_user(token.credentials) | ||
user = user_data.model_dump().get("user") | ||
return user.get("id"), user.get("email") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
router以外で、HTTPExceptionはできれば避けたいが、現状HTTPBearerからトークンを取得して認証するので、HTTP専用の関数となっている。一旦このままでも問題なさそう