From b7570e334f398f6f4f6ed2cc14021cda6c48b6dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Trkan?= Date: Fri, 10 Oct 2025 15:51:18 +0200 Subject: [PATCH 1/8] feat(auth): add support for OAuth and MojeID --- ..._10_10_1405-7af8f296d089_add_user_oauth.py | 48 +++++++++++++++++++ 7project/backend/app/app.py | 30 ++++++++---- 7project/backend/app/models/user.py | 11 ++++- 7project/backend/app/oauth/__init__.py | 0 7project/backend/app/oauth/moje_id.py | 48 +++++++++++++++++++ 7project/backend/app/services/db.py | 6 ++- 7project/backend/app/services/user_service.py | 6 +++ 7project/backend/requirements.txt | 4 ++ 8 files changed, 140 insertions(+), 13 deletions(-) create mode 100644 7project/backend/alembic/versions/2025_10_10_1405-7af8f296d089_add_user_oauth.py create mode 100644 7project/backend/app/oauth/__init__.py create mode 100644 7project/backend/app/oauth/moje_id.py diff --git a/7project/backend/alembic/versions/2025_10_10_1405-7af8f296d089_add_user_oauth.py b/7project/backend/alembic/versions/2025_10_10_1405-7af8f296d089_add_user_oauth.py new file mode 100644 index 0000000..fe71142 --- /dev/null +++ b/7project/backend/alembic/versions/2025_10_10_1405-7af8f296d089_add_user_oauth.py @@ -0,0 +1,48 @@ +"""add user oauth + +Revision ID: 7af8f296d089 +Revises: 390041bd839e +Create Date: 2025-10-10 14:05:00.153376 + +""" +from typing import Sequence, Union + +import fastapi_users_db_sqlalchemy +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '7af8f296d089' +down_revision: Union[str, Sequence[str], None] = '390041bd839e' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('oauth_account', + sa.Column('id', fastapi_users_db_sqlalchemy.generics.GUID(), nullable=False), + sa.Column('user_id', fastapi_users_db_sqlalchemy.generics.GUID(), nullable=False), + sa.Column('oauth_name', sa.String(length=100), nullable=False), + sa.Column('access_token', sa.String(length=1024), nullable=False), + sa.Column('expires_at', sa.Integer(), nullable=True), + sa.Column('refresh_token', sa.String(length=1024), nullable=True), + sa.Column('account_id', sa.String(length=320), nullable=False), + sa.Column('account_email', sa.String(length=320), nullable=False), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], ondelete='cascade'), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_oauth_account_account_id'), 'oauth_account', ['account_id'], unique=False) + op.create_index(op.f('ix_oauth_account_oauth_name'), 'oauth_account', ['oauth_name'], unique=False) + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index(op.f('ix_oauth_account_oauth_name'), table_name='oauth_account') + op.drop_index(op.f('ix_oauth_account_account_id'), table_name='oauth_account') + op.drop_table('oauth_account') + # ### end Alembic commands ### diff --git a/7project/backend/app/app.py b/7project/backend/app/app.py index 6349483..56ef23c 100644 --- a/7project/backend/app/app.py +++ b/7project/backend/app/app.py @@ -1,15 +1,16 @@ from fastapi import Depends, FastAPI from fastapi.middleware.cors import CORSMiddleware +import app.services.user_service from app.models.user import User from app.schemas.user import UserCreate, UserRead, UserUpdate from app.services.user_service import auth_backend, current_active_verified_user, fastapi_users -app = FastAPI() +fastApi = FastAPI() # CORS for frontend dev server -app.add_middleware( +fastApi.add_middleware( CORSMiddleware, allow_origins=[ "http://localhost:5173", @@ -20,37 +21,48 @@ app.add_middleware( allow_headers=["*"], ) -app.include_router( +fastApi.include_router( fastapi_users.get_auth_router(auth_backend), prefix="/auth/jwt", tags=["auth"] ) -app.include_router( +fastApi.include_router( fastapi_users.get_register_router(UserRead, UserCreate), prefix="/auth", tags=["auth"], ) -app.include_router( +fastApi.include_router( fastapi_users.get_reset_password_router(), prefix="/auth", tags=["auth"], ) -app.include_router( +fastApi.include_router( fastapi_users.get_verify_router(UserRead), prefix="/auth", tags=["auth"], ) -app.include_router( +fastApi.include_router( fastapi_users.get_users_router(UserRead, UserUpdate), prefix="/users", tags=["users"], ) +fastApi.include_router( + fastapi_users.get_oauth_router( + app.services.user_service.mojeid_oauth_service, + auth_backend, + "SECRET", + associate_by_email=True + ), + prefix="/auth/mojeid", + tags=["auth"], +) + # Liveness/root endpoint -@app.get("/", include_in_schema=False) +@fastApi.get("/", include_in_schema=False) async def root(): return {"status": "ok"} -@app.get("/authenticated-route") +@fastApi.get("/authenticated-route") async def authenticated_route(user: User = Depends(current_active_verified_user)): return {"message": f"Hello {user.email}!"} diff --git a/7project/backend/app/models/user.py b/7project/backend/app/models/user.py index 836dddf..6a1865c 100644 --- a/7project/backend/app/models/user.py +++ b/7project/backend/app/models/user.py @@ -1,13 +1,20 @@ +from typing import List + from sqlalchemy import Column, String from sqlalchemy.orm import relationship -from fastapi_users.db import SQLAlchemyBaseUserTableUUID +from fastapi_users.db import SQLAlchemyBaseUserTableUUID, SQLAlchemyBaseOAuthAccountTableUUID from app.core.base import Base +class OAuthAccount(SQLAlchemyBaseOAuthAccountTableUUID, Base): + pass + + class User(SQLAlchemyBaseUserTableUUID, Base): first_name = Column(String(length=100), nullable=True) last_name = Column(String(length=100), nullable=True) + oauth_accounts = relationship("OAuthAccount", lazy="joined") # Relationship transactions = relationship("Transaction", back_populates="user") - categories = relationship("Category", back_populates="user") \ No newline at end of file + categories = relationship("Category", back_populates="user") diff --git a/7project/backend/app/oauth/__init__.py b/7project/backend/app/oauth/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/7project/backend/app/oauth/moje_id.py b/7project/backend/app/oauth/moje_id.py new file mode 100644 index 0000000..0c118e3 --- /dev/null +++ b/7project/backend/app/oauth/moje_id.py @@ -0,0 +1,48 @@ +import json +from typing import Optional, Literal + +from httpx_oauth.clients.openid import OpenID +from httpx_oauth.oauth2 import OAuth2Token, GetAccessTokenError, T + + +# claims=%7B%22id_token%22%3A%7B%22birthdate%22%3A%7B%22essential%22%3Atrue%7D%2C%22name%22%3A%7B%22essential%22%3Atrue%7D%2C%22given_name%22%3A%7B%22essential%22%3Atrue%7D%2C%22family_name%22%3A%7B%22essential%22%3Atrue%7D%2C%22email%22%3A%7B%22essential%22%3Atrue%7D%2C%22address%22%3A%7B%22essential%22%3Afalse%7D%2C%22mojeid_valid%22%3A%7B%22essential%22%3Atrue%7D%7D%7D +class MojeIDOAuth(OpenID): + def __init__(self, client_id: str, client_secret: str): + super().__init__( + client_id, + client_secret, + "https://mojeid.regtest.nic.cz/.well-known/openid-configuration/", + "MojeID", + base_scopes=["openid", "email", "profile"], + ) + + async def get_authorization_url( + self, + redirect_uri: str, + state: Optional[str] = None, + scope: Optional[list[str]] = None, + code_challenge: Optional[str] = None, + code_challenge_method: Optional[Literal["plain", "S256"]] = None, + extras_params: Optional[T] = None, + ) -> str: + required_fields = { + 'id_token': { + 'name': {'essential': True}, + 'given_name': {'essential': True}, + 'family_name': {'essential': True}, + 'email': {'essential': True}, + 'mojeid_valid': {'essential': True}, + }} + + if extras_params is None: + extras_params = {} + extras_params["claims"] = json.dumps(required_fields) + + return await super().get_authorization_url( + redirect_uri, + state, + scope, + code_challenge, + code_challenge_method, + extras_params, + ) diff --git a/7project/backend/app/services/db.py b/7project/backend/app/services/db.py index 99929b0..606af8d 100644 --- a/7project/backend/app/services/db.py +++ b/7project/backend/app/services/db.py @@ -4,11 +4,13 @@ from sqlalchemy.ext.asyncio import AsyncSession from fastapi_users.db import SQLAlchemyUserDatabase from ..core.db import async_session_maker -from ..models.user import User +from ..models.user import User, OAuthAccount + async def get_async_session() -> AsyncGenerator[AsyncSession, None]: async with async_session_maker() as session: yield session + async def get_user_db(session: AsyncSession = Depends(get_async_session)): - yield SQLAlchemyUserDatabase(session, User) + yield SQLAlchemyUserDatabase(session, User, OAuthAccount) diff --git a/7project/backend/app/services/user_service.py b/7project/backend/app/services/user_service.py index 1d3857e..dcaf0d9 100644 --- a/7project/backend/app/services/user_service.py +++ b/7project/backend/app/services/user_service.py @@ -12,6 +12,7 @@ from fastapi_users.authentication.strategy.jwt import JWTStrategy from fastapi_users.db import SQLAlchemyUserDatabase from app.models.user import User +from app.oauth.moje_id import MojeIDOAuth from app.services.db import get_user_db from app.core.queue import enqueue_email @@ -19,6 +20,11 @@ SECRET = os.getenv("SECRET", "CHANGE_ME_SECRET") FRONTEND_URL = os.getenv("FRONTEND_URL", "http://localhost:5173") BACKEND_URL = os.getenv("BACKEND_URL", "http://localhost:8000") +mojeid_oauth_service = MojeIDOAuth( + os.getenv("MOJEID_CLIENT_ID", "CHANGE_ME_CLIENT_ID"), + os.getenv("MOJEID_CLIENT_SECRET", "CHANGE_ME_CLIENT_SECRET"), +) + class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]): reset_password_token_secret = SECRET verification_token_secret = SECRET diff --git a/7project/backend/requirements.txt b/7project/backend/requirements.txt index 1f8e9bf..6d41c19 100644 --- a/7project/backend/requirements.txt +++ b/7project/backend/requirements.txt @@ -11,6 +11,7 @@ asyncmy==0.2.9 bcrypt==4.3.0 billiard==4.2.2 celery==5.5.3 +certifi==2025.10.5 cffi==2.0.0 click==8.1.8 click-didyoumean==0.3.1 @@ -25,7 +26,10 @@ fastapi-users==14.0.1 fastapi-users-db-sqlalchemy==7.0.0 greenlet==3.2.4 h11==0.16.0 +httpcore==1.0.9 httptools==0.6.4 +httpx==0.28.1 +httpx-oauth==0.16.1 idna==3.10 kombu==5.5.4 makefun==1.16.0 From df0f2584ae4522448f16f6c3d1a1acd5b92370a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Trkan?= Date: Fri, 10 Oct 2025 15:58:40 +0200 Subject: [PATCH 2/8] feat(auth): add support for OAuth and MojeID --- 7project/backend/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/7project/backend/Dockerfile b/7project/backend/Dockerfile index b517161..c958447 100644 --- a/7project/backend/Dockerfile +++ b/7project/backend/Dockerfile @@ -5,4 +5,4 @@ COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . EXPOSE 8000 -CMD alembic upgrade head && uvicorn app.app:app --host 0.0.0.0 --port 8000 +CMD alembic upgrade head && uvicorn app.app:fastApi --host 0.0.0.0 --port 8000 From 32764ab1b08bf231983c2ac4d9d34ed6f662f93b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Trkan?= Date: Sat, 11 Oct 2025 20:34:36 +0200 Subject: [PATCH 3/8] feat(auth): allow updating custom fields from oauth, update MojeID --- 7project/backend/app/app.py | 4 +- 7project/backend/app/oauth/moje_id.py | 13 ++++-- 7project/backend/app/services/user_service.py | 41 ++++++++++++++++--- 3 files changed, 47 insertions(+), 11 deletions(-) diff --git a/7project/backend/app/app.py b/7project/backend/app/app.py index 56ef23c..c9e5ded 100644 --- a/7project/backend/app/app.py +++ b/7project/backend/app/app.py @@ -47,10 +47,10 @@ fastApi.include_router( fastApi.include_router( fastapi_users.get_oauth_router( - app.services.user_service.mojeid_oauth_service, + app.services.user_service.get_oauth_provider("MojeID"), auth_backend, "SECRET", - associate_by_email=True + associate_by_email=True, ), prefix="/auth/mojeid", tags=["auth"], diff --git a/7project/backend/app/oauth/moje_id.py b/7project/backend/app/oauth/moje_id.py index 0c118e3..b199f63 100644 --- a/7project/backend/app/oauth/moje_id.py +++ b/7project/backend/app/oauth/moje_id.py @@ -1,11 +1,10 @@ import json -from typing import Optional, Literal +from typing import Optional, Literal, Any from httpx_oauth.clients.openid import OpenID -from httpx_oauth.oauth2 import OAuth2Token, GetAccessTokenError, T +from httpx_oauth.oauth2 import T -# claims=%7B%22id_token%22%3A%7B%22birthdate%22%3A%7B%22essential%22%3Atrue%7D%2C%22name%22%3A%7B%22essential%22%3Atrue%7D%2C%22given_name%22%3A%7B%22essential%22%3Atrue%7D%2C%22family_name%22%3A%7B%22essential%22%3Atrue%7D%2C%22email%22%3A%7B%22essential%22%3Atrue%7D%2C%22address%22%3A%7B%22essential%22%3Afalse%7D%2C%22mojeid_valid%22%3A%7B%22essential%22%3Atrue%7D%7D%7D class MojeIDOAuth(OpenID): def __init__(self, client_id: str, client_secret: str): super().__init__( @@ -16,6 +15,14 @@ class MojeIDOAuth(OpenID): base_scopes=["openid", "email", "profile"], ) + async def get_user_info(self, token: str) -> Optional[Any]: + info = await self.get_profile(token) + + return { + "first_name": info.get("given_name"), + "last_name": info.get("family_name"), + } + async def get_authorization_url( self, redirect_uri: str, diff --git a/7project/backend/app/services/user_service.py b/7project/backend/app/services/user_service.py index dcaf0d9..feee712 100644 --- a/7project/backend/app/services/user_service.py +++ b/7project/backend/app/services/user_service.py @@ -3,7 +3,7 @@ import uuid from typing import Optional from fastapi import Depends, Request -from fastapi_users import BaseUserManager, FastAPIUsers, UUIDIDMixin +from fastapi_users import BaseUserManager, FastAPIUsers, UUIDIDMixin, models from fastapi_users.authentication import ( AuthenticationBackend, BearerTransport, @@ -20,15 +20,41 @@ SECRET = os.getenv("SECRET", "CHANGE_ME_SECRET") FRONTEND_URL = os.getenv("FRONTEND_URL", "http://localhost:5173") BACKEND_URL = os.getenv("BACKEND_URL", "http://localhost:8000") -mojeid_oauth_service = MojeIDOAuth( - os.getenv("MOJEID_CLIENT_ID", "CHANGE_ME_CLIENT_ID"), - os.getenv("MOJEID_CLIENT_SECRET", "CHANGE_ME_CLIENT_SECRET"), -) +providers = { + "MojeID": MojeIDOAuth( + os.getenv("MOJEID_CLIENT_ID", "CHANGE_ME_CLIENT_ID"), + os.getenv("MOJEID_CLIENT_SECRET", "CHANGE_ME_CLIENT_SECRET"), + ) +} + + +def get_oauth_provider(name: str) -> Optional[MojeIDOAuth]: + if name not in providers: + return None + return providers[name] + class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]): reset_password_token_secret = SECRET verification_token_secret = SECRET + async def oauth_callback(self: "BaseUserManager[models.UOAP, models.ID]", oauth_name: str, access_token: str, + account_id: str, account_email: str, expires_at: Optional[int] = None, + refresh_token: Optional[str] = None, request: Optional[Request] = None, *, + associate_by_email: bool = False, is_verified_by_default: bool = False) -> models.UOAP: + + user = await super().oauth_callback(oauth_name, access_token, account_id, account_email, expires_at, + refresh_token, request, associate_by_email=associate_by_email, + is_verified_by_default=is_verified_by_default) + + # set additional user info from the OAuth provider + provider = get_oauth_provider(oauth_name) + if provider is not None and hasattr(provider, "get_user_info"): + update_dict = await provider.get_user_info(access_token) + await self.user_db.update(user, update_dict) + + return user + async def on_after_register(self, user: User, request: Optional[Request] = None): await self.request_verify(user, request) @@ -58,14 +84,18 @@ class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]): print("[Email Fallback] Subject:", subject) print("[Email Fallback] Body:\n", body) + async def get_user_manager(user_db: SQLAlchemyUserDatabase = Depends(get_user_db)): yield UserManager(user_db) + bearer_transport = BearerTransport(tokenUrl="auth/jwt/login") + def get_jwt_strategy() -> JWTStrategy: return JWTStrategy(secret=SECRET, lifetime_seconds=3600) + auth_backend = AuthenticationBackend( name="jwt", transport=bearer_transport, @@ -76,4 +106,3 @@ fastapi_users = FastAPIUsers[User, uuid.UUID](get_user_manager, [auth_backend]) current_active_user = fastapi_users.current_user(active=True) current_active_verified_user = fastapi_users.current_user(active=True, verified=True) - From a91aea805f5e4ddbeefa6e73b8e3833fec4a716e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Trkan?= Date: Sat, 11 Oct 2025 21:16:53 +0200 Subject: [PATCH 4/8] feat(auth): add BankID OAuth provider --- ...1_2107-5ab2e654c96e_change_token_lenght.py | 38 ++++++++++++++ 7project/backend/app/app.py | 11 +++++ 7project/backend/app/models/user.py | 7 ++- 7project/backend/app/oauth/bank_id.py | 49 +++++++++++++++++++ 7project/backend/app/services/user_service.py | 6 +++ 5 files changed, 107 insertions(+), 4 deletions(-) create mode 100644 7project/backend/alembic/versions/2025_10_11_2107-5ab2e654c96e_change_token_lenght.py create mode 100644 7project/backend/app/oauth/bank_id.py diff --git a/7project/backend/alembic/versions/2025_10_11_2107-5ab2e654c96e_change_token_lenght.py b/7project/backend/alembic/versions/2025_10_11_2107-5ab2e654c96e_change_token_lenght.py new file mode 100644 index 0000000..e3b8e3a --- /dev/null +++ b/7project/backend/alembic/versions/2025_10_11_2107-5ab2e654c96e_change_token_lenght.py @@ -0,0 +1,38 @@ +"""change token lenght + +Revision ID: 5ab2e654c96e +Revises: 7af8f296d089 +Create Date: 2025-10-11 21:07:41.930470 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision: str = '5ab2e654c96e' +down_revision: Union[str, Sequence[str], None] = '7af8f296d089' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('oauth_account', 'access_token', + existing_type=mysql.VARCHAR(length=1024), + type_=sa.String(length=4096), + existing_nullable=False) + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('oauth_account', 'access_token', + existing_type=sa.String(length=4096), + type_=mysql.VARCHAR(length=1024), + existing_nullable=False) + # ### end Alembic commands ### diff --git a/7project/backend/app/app.py b/7project/backend/app/app.py index c9e5ded..ea05748 100644 --- a/7project/backend/app/app.py +++ b/7project/backend/app/app.py @@ -56,6 +56,17 @@ fastApi.include_router( tags=["auth"], ) +fastApi.include_router( + fastapi_users.get_oauth_router( + app.services.user_service.get_oauth_provider("BankID"), + auth_backend, + "SECRET", + associate_by_email=True, + ), + prefix="/auth/bankid", + tags=["auth"], +) + # Liveness/root endpoint @fastApi.get("/", include_in_schema=False) diff --git a/7project/backend/app/models/user.py b/7project/backend/app/models/user.py index 6a1865c..3a47034 100644 --- a/7project/backend/app/models/user.py +++ b/7project/backend/app/models/user.py @@ -1,13 +1,12 @@ -from typing import List - from sqlalchemy import Column, String -from sqlalchemy.orm import relationship +from sqlalchemy.orm import relationship, mapped_column, Mapped from fastapi_users.db import SQLAlchemyBaseUserTableUUID, SQLAlchemyBaseOAuthAccountTableUUID from app.core.base import Base class OAuthAccount(SQLAlchemyBaseOAuthAccountTableUUID, Base): - pass + # BankID token is longer than default + access_token: Mapped[str] = mapped_column(String(length=4096), nullable=False) class User(SQLAlchemyBaseUserTableUUID, Base): diff --git a/7project/backend/app/oauth/bank_id.py b/7project/backend/app/oauth/bank_id.py new file mode 100644 index 0000000..a522f3a --- /dev/null +++ b/7project/backend/app/oauth/bank_id.py @@ -0,0 +1,49 @@ +import secrets +from typing import Optional, Literal + +from httpx_oauth.clients.openid import OpenID +from httpx_oauth.oauth2 import T + + +class BankID(OpenID): + def __init__(self, client_id: str, client_secret: str): + super().__init__( + client_id, + client_secret, + "https://oidc.sandbox.bankid.cz/.well-known/openid-configuration", + "BankID", + base_scopes=["openid", "profile.email", "profile.name"], + ) + + async def get_user_info(self, token: str) -> dict: + info = await self.get_profile(token) + + return { + "first_name": info.get("given_name"), + "last_name": info.get("family_name"), + } + + async def get_authorization_url( + self, + redirect_uri: str, + state: Optional[str] = None, + scope: Optional[list[str]] = None, + code_challenge: Optional[str] = None, + code_challenge_method: Optional[Literal["plain", "S256"]] = None, + extras_params: Optional[T] = None, + ) -> str: + if extras_params is None: + extras_params = {} + + # BankID requires random nonce parameter for security + # https://developer.bankid.cz/docs/security_sep + extras_params["nonce"] = secrets.token_urlsafe() + + return await super().get_authorization_url( + redirect_uri, + state, + scope, + code_challenge, + code_challenge_method, + extras_params, + ) diff --git a/7project/backend/app/services/user_service.py b/7project/backend/app/services/user_service.py index feee712..ab40760 100644 --- a/7project/backend/app/services/user_service.py +++ b/7project/backend/app/services/user_service.py @@ -12,11 +12,13 @@ from fastapi_users.authentication.strategy.jwt import JWTStrategy from fastapi_users.db import SQLAlchemyUserDatabase from app.models.user import User +from app.oauth.bank_id import BankID from app.oauth.moje_id import MojeIDOAuth from app.services.db import get_user_db from app.core.queue import enqueue_email SECRET = os.getenv("SECRET", "CHANGE_ME_SECRET") + FRONTEND_URL = os.getenv("FRONTEND_URL", "http://localhost:5173") BACKEND_URL = os.getenv("BACKEND_URL", "http://localhost:8000") @@ -24,6 +26,10 @@ providers = { "MojeID": MojeIDOAuth( os.getenv("MOJEID_CLIENT_ID", "CHANGE_ME_CLIENT_ID"), os.getenv("MOJEID_CLIENT_SECRET", "CHANGE_ME_CLIENT_SECRET"), + ), + "BankID": BankID( + os.getenv("BANKID_CLIENT_ID", "CHANGE_ME_CLIENT_ID"), + os.getenv("BANKID_CLIENT_SECRET", "CHANGE_ME_CLIENT_SECRET"), ) } From 7a67b125339aad85c3d45487ee67493624c326ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Trkan?= Date: Sat, 11 Oct 2025 21:32:03 +0200 Subject: [PATCH 5/8] Update 7project/backend/alembic/versions/2025_10_11_2107-5ab2e654c96e_change_token_lenght.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../2025_10_11_2107-5ab2e654c96e_change_token_lenght.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/7project/backend/alembic/versions/2025_10_11_2107-5ab2e654c96e_change_token_lenght.py b/7project/backend/alembic/versions/2025_10_11_2107-5ab2e654c96e_change_token_lenght.py index e3b8e3a..d4cebb0 100644 --- a/7project/backend/alembic/versions/2025_10_11_2107-5ab2e654c96e_change_token_lenght.py +++ b/7project/backend/alembic/versions/2025_10_11_2107-5ab2e654c96e_change_token_lenght.py @@ -1,4 +1,4 @@ -"""change token lenght +"""change token length Revision ID: 5ab2e654c96e Revises: 7af8f296d089 From 0cf06b7bd914ad1d99e18d972e1c92248655ce56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Trkan?= Date: Sat, 11 Oct 2025 21:37:49 +0200 Subject: [PATCH 6/8] feat(auth): add CustomOpenID class to force get_user_info implementation --- 7project/backend/app/oauth/bank_id.py | 5 +++-- 7project/backend/app/oauth/custom_openid.py | 6 ++++++ 7project/backend/app/oauth/moje_id.py | 5 +++-- 7project/backend/app/services/user_service.py | 6 ++++-- 4 files changed, 16 insertions(+), 6 deletions(-) create mode 100644 7project/backend/app/oauth/custom_openid.py diff --git a/7project/backend/app/oauth/bank_id.py b/7project/backend/app/oauth/bank_id.py index a522f3a..fa2e55f 100644 --- a/7project/backend/app/oauth/bank_id.py +++ b/7project/backend/app/oauth/bank_id.py @@ -1,11 +1,12 @@ import secrets from typing import Optional, Literal -from httpx_oauth.clients.openid import OpenID from httpx_oauth.oauth2 import T +from app.oauth.custom_openid import CustomOpenID -class BankID(OpenID): + +class BankID(CustomOpenID): def __init__(self, client_id: str, client_secret: str): super().__init__( client_id, diff --git a/7project/backend/app/oauth/custom_openid.py b/7project/backend/app/oauth/custom_openid.py new file mode 100644 index 0000000..226682d --- /dev/null +++ b/7project/backend/app/oauth/custom_openid.py @@ -0,0 +1,6 @@ +from httpx_oauth.clients.openid import OpenID + + +class CustomOpenID(OpenID): + async def get_user_info(self, token: str) -> dict: + raise NotImplementedError() \ No newline at end of file diff --git a/7project/backend/app/oauth/moje_id.py b/7project/backend/app/oauth/moje_id.py index b199f63..7e1cd45 100644 --- a/7project/backend/app/oauth/moje_id.py +++ b/7project/backend/app/oauth/moje_id.py @@ -1,11 +1,12 @@ import json from typing import Optional, Literal, Any -from httpx_oauth.clients.openid import OpenID from httpx_oauth.oauth2 import T +from app.oauth.custom_openid import CustomOpenID -class MojeIDOAuth(OpenID): + +class MojeIDOAuth(CustomOpenID): def __init__(self, client_id: str, client_secret: str): super().__init__( client_id, diff --git a/7project/backend/app/services/user_service.py b/7project/backend/app/services/user_service.py index ab40760..afee558 100644 --- a/7project/backend/app/services/user_service.py +++ b/7project/backend/app/services/user_service.py @@ -10,9 +10,11 @@ from fastapi_users.authentication import ( ) from fastapi_users.authentication.strategy.jwt import JWTStrategy from fastapi_users.db import SQLAlchemyUserDatabase +from httpx_oauth.oauth2 import BaseOAuth2 from app.models.user import User from app.oauth.bank_id import BankID +from app.oauth.custom_openid import CustomOpenID from app.oauth.moje_id import MojeIDOAuth from app.services.db import get_user_db from app.core.queue import enqueue_email @@ -34,7 +36,7 @@ providers = { } -def get_oauth_provider(name: str) -> Optional[MojeIDOAuth]: +def get_oauth_provider(name: str) -> Optional[BaseOAuth2]: if name not in providers: return None return providers[name] @@ -55,7 +57,7 @@ class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]): # set additional user info from the OAuth provider provider = get_oauth_provider(oauth_name) - if provider is not None and hasattr(provider, "get_user_info"): + if provider is not None and isinstance(provider, CustomOpenID): update_dict = await provider.get_user_info(access_token) await self.user_db.update(user, update_dict) From b213f22a150733e660f1153f45d009249f4b8d98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Trkan?= Date: Sat, 11 Oct 2025 22:22:36 +0200 Subject: [PATCH 7/8] feat(auth): refactor --- 7project/backend/app/app.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/7project/backend/app/app.py b/7project/backend/app/app.py index ea05748..23fd0b7 100644 --- a/7project/backend/app/app.py +++ b/7project/backend/app/app.py @@ -5,7 +5,7 @@ import app.services.user_service from app.models.user import User from app.schemas.user import UserCreate, UserRead, UserUpdate -from app.services.user_service import auth_backend, current_active_verified_user, fastapi_users +from app.services.user_service import auth_backend, current_active_verified_user, fastapi_users, get_oauth_provider fastApi = FastAPI() @@ -47,7 +47,7 @@ fastApi.include_router( fastApi.include_router( fastapi_users.get_oauth_router( - app.services.user_service.get_oauth_provider("MojeID"), + get_oauth_provider("MojeID"), auth_backend, "SECRET", associate_by_email=True, @@ -58,7 +58,7 @@ fastApi.include_router( fastApi.include_router( fastapi_users.get_oauth_router( - app.services.user_service.get_oauth_provider("BankID"), + get_oauth_provider("BankID"), auth_backend, "SECRET", associate_by_email=True, From 95c8bf1e92baba48cab85219b93f52735f0f4299 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Trkan?= Date: Sat, 11 Oct 2025 22:25:04 +0200 Subject: [PATCH 8/8] Update 7project/backend/app/app.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- 7project/backend/app/app.py | 1 - 1 file changed, 1 deletion(-) diff --git a/7project/backend/app/app.py b/7project/backend/app/app.py index 23fd0b7..423ab2c 100644 --- a/7project/backend/app/app.py +++ b/7project/backend/app/app.py @@ -1,7 +1,6 @@ from fastapi import Depends, FastAPI from fastapi.middleware.cors import CORSMiddleware -import app.services.user_service from app.models.user import User from app.schemas.user import UserCreate, UserRead, UserUpdate