diff --git a/backend/app/api/auth.py b/backend/app/api/auth.py index 9681457..adb4869 100644 --- a/backend/app/api/auth.py +++ b/backend/app/api/auth.py @@ -1,43 +1,60 @@ -from fastapi import APIRouter, Depends, HTTPException, Response, status +from fastapi import APIRouter, Depends, HTTPException, Request, Response, status +from fastapi.responses import RedirectResponse from sqlalchemy.orm import Session +from authlib.integrations.starlette_client import OAuthError from app import models, schemas -from app.auth import create_access_token, get_current_user, hash_password, verify_password -from app.database import get_db +from app.auth import create_access_token, get_current_user, oauth +from app.database import get_db, settings router = APIRouter() - -@router.post("/register", response_model=schemas.UserOut, status_code=201) -def register(body: schemas.UserCreate, db: Session = Depends(get_db)) -> models.User: - if db.query(models.User).filter(models.User.username == body.username).first(): - raise HTTPException(status_code=400, detail="Username already registered") - if db.query(models.User).filter(models.User.email == body.email).first(): - raise HTTPException(status_code=400, detail="Email already registered") - user = models.User( - username=body.username, - email=body.email, - hashed_password=hash_password(body.password), - ) - db.add(user) - db.commit() - db.refresh(user) - return user +FIREWALL_ADMINS_GROUP = "firewall admins" -@router.post("/login") -def login(body: schemas.LoginRequest, response: Response, db: Session = Depends(get_db)) -> dict: - user = db.query(models.User).filter(models.User.username == body.username).first() - if not user or not verify_password(body.password, user.hashed_password): - raise HTTPException(status_code=401, detail="Invalid credentials") - token = create_access_token(user.id) +@router.get("/oidc/login") +async def oidc_login(request: Request) -> RedirectResponse: + return await oauth.keycloak.authorize_redirect(request, settings.keycloak_redirect_uri) + + +@router.get("/oidc/callback") +async def oidc_callback(request: Request, db: Session = Depends(get_db)) -> RedirectResponse: + try: + token = await oauth.keycloak.authorize_access_token(request) + except OAuthError as e: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) + + userinfo = token.get("userinfo") or {} + groups = userinfo.get("groups", []) + if FIREWALL_ADMINS_GROUP not in groups: + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not in firewall admins group") + + sub = userinfo["sub"] + email = userinfo.get("email", "") + username = userinfo.get("preferred_username", sub) + + user = db.query(models.User).filter(models.User.keycloak_sub == sub).first() + if not user: + user = models.User( + keycloak_sub=sub, + email=email, + username=username, + hashed_password=None, + is_active=True, + ) + db.add(user) + db.commit() + db.refresh(user) + + access_token = create_access_token(user.id) + response = RedirectResponse(url="/", status_code=status.HTTP_302_FOUND) response.set_cookie( key="access_token", - value=token, + value=access_token, httponly=True, samesite="lax", max_age=3600, ) - return {"message": "Logged in"} + return response @router.post("/logout") diff --git a/backend/app/auth.py b/backend/app/auth.py index 32ea150..464566a 100644 --- a/backend/app/auth.py +++ b/backend/app/auth.py @@ -1,21 +1,23 @@ from datetime import datetime, timedelta, timezone from typing import Optional from jose import JWTError, jwt -from passlib.context import CryptContext +from authlib.integrations.starlette_client import OAuth from fastapi import Cookie, HTTPException, status, Depends from sqlalchemy.orm import Session from app.database import get_db, settings from app import models -pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") - - -def hash_password(password: str) -> str: - return pwd_context.hash(password) - - -def verify_password(plain: str, hashed: str) -> bool: - return pwd_context.verify(plain, hashed) +oauth = OAuth() +oauth.register( + name="keycloak", + client_id=settings.keycloak_client_id, + client_secret=settings.keycloak_client_secret, + server_metadata_url=( + f"{settings.keycloak_url}/realms/{settings.keycloak_realm}" + "/.well-known/openid-configuration" + ), + client_kwargs={"scope": "openid email profile"}, +) def create_access_token(user_id: int) -> str: diff --git a/backend/app/main.py b/backend/app/main.py index c1e9678..2e05851 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -1,9 +1,12 @@ from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware +from starlette.middleware.sessions import SessionMiddleware from app.api import auth, configs, zones, interfaces, policies, rules, masq +from app.database import settings app = FastAPI(title="Shorefront", version="0.1.0") +app.add_middleware(SessionMiddleware, secret_key=settings.jwt_secret_key) app.add_middleware( CORSMiddleware, allow_origins=["http://localhost:5173", "http://localhost:80"],