-
Notifications
You must be signed in to change notification settings - Fork 1
Firebase auth branch #11
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
Open
emma-x1
wants to merge
22
commits into
main
Choose a base branch
from
firebase-auth-branch
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
22 commits
Select commit
Hold shift + click to select a range
bf152e5
initialized firebase
emma-x1 26d7eb9
add dependencies
mmiqball c3e2868
add pdyantic user models
mmiqball 06456a2
add Firebase authentication initialization
mmiqball f6975c1
implement user creation endpoint
mmiqball ce34aa5
add user creation unit test
mmiqball 751de85
separate user service initialization in user routes
mmiqball 494a7b1
simplify firebase initialization
mmiqball 663260d
load env before code executes
mmiqball de58f13
construct firebase service account key path from pwd
mmiqball 36edc43
add google SSO signup functionality
mmiqball 6a2851b
minor pr comments fixes
mmiqball 43228b6
remove unusued test, add todo comment
mmiqball 1c2482b
lint
mmiqball 72a0680
middlwares
emma-x1 70c1e17
fix incorrect import path
mmiqball da41302
rough drafts for auth service, router, schemas (to be fixed)
mmiqball e8c6675
added middlewares
emma-x1 28b1853
middlwares
emma-x1 7d57ec4
added middlewares
emma-x1 7d762ff
worked on auth middleware
emma-x1 e9022ec
Merge branch 'firebase-auth-branch' of https://github.com/uwblueprint…
emma-x1 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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 hidden or 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,25 @@ | ||
| from fastapi import FASTAPI, HTTPException, Depends | ||
| from firebase_admin import auth | ||
| import firebase_unit | ||
| from pydantic import BaseModel | ||
|
|
||
| app = FastAPI() | ||
| class User(BaseModel): | ||
| email: str | ||
| password: str | ||
|
|
||
| @app.post("/signup") | ||
| async def sign_up(user: User): | ||
| try: | ||
| new_user = auth.create_user(email=user.email, password=user.password) | ||
| return {"message": "User created successfully", "user_id": new_user.uid} | ||
| except Exception as e: | ||
| raise HTTPException(status_code=400, detail=str(e)) | ||
|
|
||
| @app.post("/login") | ||
| async def login(user: User): | ||
| try: | ||
| custom_token = auth.create_custom_token(user.email) | ||
| return {"token": custom_token} | ||
| except Exception as e: | ||
| raise HTTPException(status_code=400, detail=str(e)) |
This file contains hidden or 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 hidden or 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,61 @@ | ||
| import os | ||
| import firebase | ||
|
|
||
| from firebase import credentials, auth | ||
| from fastapi import FastAPI, Request, HTTPException, status, Depends, HTTPException, Security | ||
| from fastapi.responses import JSONResponse | ||
| from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials | ||
| from typing import List | ||
|
|
||
| app = FastAPI() | ||
|
|
||
| #use is_authorized_by_role to get info about user - NOT DONE | ||
| #input is user (get info when accessing endpoint?) | ||
| #output is user info found using token - token, role, etc. | ||
| class get_user_info(is_authorized_by_role): | ||
| def __init__(self, roles: List[str]): | ||
| #self.roles = roles | ||
|
|
||
| def __call__(self, user: dict = Depends(verify_firebase_token)): | ||
| #role = user.get("role") | ||
| #if role not in self.roles: | ||
| # raise HTTPException( | ||
| # status_code=403, | ||
| # detail=f"Access denied: role '{role}' not authorized" | ||
| # ) | ||
| #return user | ||
|
|
||
| #middleware function | ||
| #input allowed roles, endpoint | ||
| #output: allowing/disallowing access to endpoint | ||
| def role_based_access_control(allowed_roles: List[str]): | ||
| def middleware_decorator(endpoint: Callable): | ||
| async def wrapper(request: Request, *args, **kwargs): | ||
| roles = getattr(request.state, "user_roles", []) | ||
| if not allowed_roles: | ||
| return await endpoint(request, *args, **kwargs) | ||
| if not any(role in allowed_roles for role in roles): | ||
| raise HTTPException( | ||
| status_code=403, | ||
| detail="Access denied: user cannot access this endpoint" | ||
| ) | ||
| return await endpoint(request, *args, **kwargs) | ||
| return wrapper | ||
| return middleware_decorator | ||
|
|
||
| #add middleware to request state | ||
| #basically applies middleware function to all requests | ||
| @app.middleware("http") | ||
| async def add_user_roles(request: Request, call_next): | ||
| #before request | ||
| request.state.user_roles = ["user"] #get roles from is_authorized_by_roles | ||
| #run request | ||
| response = await call_next(request) | ||
| #after request | ||
| return response | ||
|
|
||
| #example: applying middleware to endpoint | ||
| @app.post("/admin_only") | ||
| @role_based_access_control(allowed_roles=["admin"]) | ||
| async def admin_endpoint(request: Request): | ||
| return {"message": "This is an admin endpoint"} | ||
This file contains hidden or 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,20 @@ | ||
| from fastapi import APIRouter, Depends, HTTPException | ||
| from sqlalchemy.orm import Session | ||
| from ..schemas.auth import AuthResponse, LoginRequest | ||
| from ..services.implementations.auth_service import AuthService | ||
| from ..services.implementations.user_service import UserService | ||
| from ..utilities.db_utils import get_db | ||
| import logging | ||
|
|
||
| router = APIRouter(prefix="/auth", tags=["auth"]) | ||
|
|
||
| def get_auth_service(db: Session = Depends(get_db)): | ||
| logger = logging.getLogger(__name__) | ||
| return AuthService(logger=logger, user_service=UserService(db)) | ||
|
|
||
| @router.post("/login", response_model=AuthResponse) | ||
| async def login( | ||
| credentials: LoginRequest, | ||
| auth_service: AuthService = Depends(get_auth_service) | ||
| ): | ||
| return auth_service.generate_token(credentials.email, credentials.password) |
This file contains hidden or 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,31 @@ | ||
| from fastapi import APIRouter, Depends, HTTPException | ||
| from sqlalchemy.orm import Session | ||
|
|
||
| from app.schemas.user import UserCreateRequest, UserCreateResponse | ||
| from app.services.implementations.user_service import UserService | ||
| from app.utilities.db_utils import get_db | ||
|
|
||
| router = APIRouter( | ||
| prefix="/users", | ||
| tags=["users"], | ||
| ) | ||
|
|
||
| # TODO: | ||
| # send email verification via auth_service | ||
| # allow signup methods other than email (like sign up w Google)?? | ||
|
|
||
|
|
||
| def get_user_service(db: Session = Depends(get_db)): | ||
| return UserService(db) | ||
|
|
||
|
|
||
| @router.post("/", response_model=UserCreateResponse) | ||
| async def create_user( | ||
| user: UserCreateRequest, user_service: UserService = Depends(get_user_service) | ||
| ): | ||
| try: | ||
| return await user_service.create_user(user) | ||
| except HTTPException as http_ex: | ||
| raise http_ex | ||
| except Exception as e: | ||
| raise HTTPException(status_code=500, detail=str(e)) |
This file contains hidden or 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,23 @@ | ||
| from pydantic import BaseModel, ConfigDict | ||
| from .user import UserCreateResponse | ||
|
|
||
| class LoginRequest(BaseModel): | ||
| email: str | ||
| password: str | ||
|
|
||
| class Token(BaseModel): | ||
| """ | ||
| For authentication tokens from Firebase | ||
| Access tokens are short-lived and used to access resources | ||
| Refresh tokens are long-lived and used to obtain new access tokens | ||
| """ | ||
| access_token: str | ||
| refresh_token: str | ||
|
|
||
| model_config = ConfigDict(from_attributes=True) | ||
|
|
||
| class AuthResponse(Token): | ||
| """Authentication response containing tokens and user info""" | ||
| user: UserCreateResponse | ||
|
|
||
| model_config = ConfigDict(from_attributes=True) |
This file contains hidden or 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,89 @@ | ||
| """ | ||
| Pydantic schemas for user-related data validation and serialization. | ||
| Handles user CRUD and response models for the API. | ||
| """ | ||
|
|
||
| from enum import Enum | ||
| from typing import Optional | ||
| from uuid import UUID | ||
|
|
||
| from pydantic import BaseModel, ConfigDict, EmailStr, Field, field_validator | ||
|
|
||
| # TODO: | ||
| # confirm complexity rules for fields (such as password) | ||
|
|
||
|
|
||
| class SignUpMethod(str, Enum): | ||
| """Authentication methods supported for user signup""" | ||
|
|
||
| PASSWORD = "PASSWORD" | ||
| GOOGLE = "GOOGLE" | ||
|
|
||
|
|
||
| class UserRole(str, Enum): | ||
| """ | ||
| Enum for possible user roles. | ||
| """ | ||
|
|
||
| PARTICIPANT = "participant" | ||
| VOLUNTEER = "volunteer" | ||
| ADMIN = "admin" | ||
|
|
||
| @classmethod | ||
| def to_role_id(cls, role: "UserRole") -> int: | ||
| role_map = {cls.PARTICIPANT: 1, cls.VOLUNTEER: 2, cls.ADMIN: 3} | ||
| return role_map[role] | ||
|
|
||
|
|
||
| class UserBase(BaseModel): | ||
| """ | ||
| Base schema for user model with common attributes shared across schemas. | ||
| """ | ||
|
|
||
| first_name: str = Field(..., min_length=1, max_length=50) | ||
| last_name: str = Field(..., min_length=1, max_length=50) | ||
| email: EmailStr | ||
| role: UserRole | ||
|
|
||
|
|
||
| class UserCreateRequest(UserBase): | ||
| """ | ||
| Request schema for user creation with conditional password validation | ||
| """ | ||
|
|
||
| password: Optional[str] = Field(None, min_length=8) | ||
| auth_id: Optional[str] = Field(None) | ||
| signup_method: SignUpMethod = Field(default=SignUpMethod.PASSWORD) | ||
|
|
||
| @field_validator("password") | ||
| def validate_password(cls, password: Optional[str], info): | ||
| signup_method = info.data.get("signup_method") | ||
|
|
||
| if signup_method == SignUpMethod.PASSWORD and not password: | ||
| raise ValueError("Password is required for password signup") | ||
|
|
||
| if password: | ||
| if not any(char.isdigit() for char in password): | ||
| raise ValueError("Password must contain at least one digit") | ||
| if not any(char.isupper() for char in password): | ||
| raise ValueError("Password must contain at least one uppercase letter") | ||
| if not any(char.islower() for char in password): | ||
| raise ValueError("Password must contain at least one lowercase letter") | ||
|
|
||
| return password | ||
|
|
||
|
|
||
| class UserCreateResponse(BaseModel): | ||
| """ | ||
| Response schema for user creation, maps directly from ORM User object. | ||
| """ | ||
|
|
||
| id: UUID | ||
| first_name: str | ||
| last_name: str | ||
| email: EmailStr | ||
| role_id: int | ||
| auth_id: str | ||
|
|
||
| # from_attributes enables automatic mapping from SQLAlchemy model to Pydantic model | ||
| model_config = ConfigDict(from_attributes=True) |
This file contains hidden or 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
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
it's much much safer to get a user's roles from firebase or the db ourselves using the methods in the auth service, instead of sending it in the request
imagine someone malicious adding in roles to their requests that they don't have access to, which would let them access other roles that they haven't been assigned to in the db