mirror of
https://github.com/bellingcat/auto-archiver-api.git
synced 2026-06-12 13:38:33 +03:00
implements firebase ID tokens
This commit is contained in:
@@ -27,6 +27,8 @@ class Settings(BaseSettings):
|
||||
ALLOWED_ORIGINS: Annotated[Set[str], Len(min_length=1)]
|
||||
CHROME_APP_IDS: Annotated[Set[Annotated[str, Len(min_length=10)]], Len(min_length=1)]
|
||||
BLOCKED_EMAILS: Annotated[Set[str], Len(min_length=0)] = set()
|
||||
# if not provided only OAUTH access_tokens are allowed
|
||||
FIREBASE_SERVICE_ACCOUNT_JSON: str = ""
|
||||
|
||||
# redis
|
||||
REDIS_PASSWORD: str = ""
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from unittest import mock
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from fastapi import HTTPException
|
||||
@@ -98,6 +99,37 @@ async def test_authenticate_user():
|
||||
assert mock_get.call_count == 9
|
||||
|
||||
|
||||
@mock.patch("app.web.security.FIREBASE_OAUTH_ENABLED", True)
|
||||
@mock.patch("app.web.security.firebase_admin.initialize_app")
|
||||
@pytest.mark.asyncio
|
||||
async def test_authenticate_user_with_id_token(m_init):
|
||||
from firebase_admin import exceptions
|
||||
from app.web.security import authenticate_user
|
||||
|
||||
with pytest.raises(ValueError) as e:
|
||||
authenticate_user("test")
|
||||
assert "The default Firebase app does not exist." in str(e.value)
|
||||
|
||||
with patch("app.web.security.auth.verify_id_token") as mock_verify:
|
||||
# missing email
|
||||
mock_verify.return_value = {"email": None}
|
||||
assert authenticate_user("fake_token") == (False, "email not found in token")
|
||||
assert mock_verify.call_count == 1
|
||||
|
||||
# blocked email
|
||||
mock_verify.return_value = {"email": "blocked@example.com", }
|
||||
assert authenticate_user("fake_token") == (False, "email 'blocked@example.com' not allowed")
|
||||
assert mock_verify.call_count == 2
|
||||
|
||||
# valid email
|
||||
mock_verify.return_value = {"email": "rick@example.com"}
|
||||
assert authenticate_user("fake_token") == (True, "rick@example.com")
|
||||
assert mock_verify.call_count == 3
|
||||
|
||||
mock_verify.side_effect = exceptions.FirebaseError(2, "mocked error")
|
||||
assert authenticate_user("fake_token") == (False, "invalid token")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_authenticate_user_exception():
|
||||
from app.web.security import authenticate_user
|
||||
|
||||
@@ -4,6 +4,9 @@ from fastapi import HTTPException, status, Depends
|
||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
import firebase_admin
|
||||
from firebase_admin import credentials, auth, exceptions
|
||||
|
||||
from app.web.config import ALLOW_ANY_EMAIL
|
||||
from app.shared.settings import get_settings
|
||||
from app.shared.db.database import get_db_dependency
|
||||
@@ -12,6 +15,11 @@ from app.web.db.user_state import UserState
|
||||
settings = get_settings()
|
||||
bearer_security = HTTPBearer()
|
||||
|
||||
FIREBASE_OAUTH_ENABLED = settings.FIREBASE_SERVICE_ACCOUNT_JSON != ""
|
||||
if FIREBASE_OAUTH_ENABLED:
|
||||
logger.debug("Firebase OAUTH enabled, initializing...")
|
||||
firebase_admin.initialize_app(credentials.Certificate(settings.FIREBASE_SERVICE_ACCOUNT_JSON))
|
||||
|
||||
|
||||
def secure_compare(token, api_key):
|
||||
return secrets.compare_digest(token.encode("utf8"), api_key.encode("utf8"))
|
||||
@@ -59,6 +67,19 @@ async def get_user_auth(credentials: HTTPAuthorizationCredentials = Depends(bear
|
||||
|
||||
|
||||
def authenticate_user(access_token):
|
||||
if FIREBASE_OAUTH_ENABLED:
|
||||
try:
|
||||
j = auth.verify_id_token(access_token)
|
||||
email = j.get('email', None)
|
||||
logger.debug(f"Successfully verified the ID token for {email}")
|
||||
if email is None:
|
||||
return False, "email not found in token"
|
||||
if email in settings.BLOCKED_EMAILS:
|
||||
return False, f"email '{email}' not allowed"
|
||||
return True, email
|
||||
except exceptions.FirebaseError as e:
|
||||
logger.warning(f"Error verifying ID token: {str(e)}")
|
||||
|
||||
# https://cloud.google.com/docs/authentication/token-types#access
|
||||
if type(access_token) != str or len(access_token) < 10: return False, "invalid access_token"
|
||||
r = requests.get("https://oauth2.googleapis.com/tokeninfo", {"access_token": access_token})
|
||||
@@ -79,5 +100,5 @@ def authenticate_user(access_token):
|
||||
return False, "exception occurred"
|
||||
|
||||
|
||||
def get_user_state(email:str=Depends(get_user_auth), db:Session=Depends(get_db_dependency)):
|
||||
return UserState(db, email)
|
||||
def get_user_state(email: str = Depends(get_user_auth), db: Session = Depends(get_db_dependency)):
|
||||
return UserState(db, email)
|
||||
|
||||
Reference in New Issue
Block a user