add initial oidc flow

This commit is contained in:
Markbeep
2025-03-12 22:13:59 +01:00
parent 89553295a2
commit 82b4203b83
7 changed files with 73 additions and 8 deletions
+1
View File
@@ -1,3 +1,4 @@
ABR_APP__CONFIG_DIR=config # Path to the config directory. Default: /config
ABR_APP__DEBUG=true # Default: false
ABR_APP__OPENAPI_ENABLED=true # Default: false
ABR_APP__PUBLIC_HOST=http://localhost:8000
+1 -1
View File
@@ -1,8 +1,8 @@
# Install daisyui
FROM node:23-alpine3.20
WORKDIR /app
# Install daisyui
COPY package.json package.json
COPY package-lock.json package-lock.json
RUN npm install
+2 -1
View File
@@ -166,7 +166,8 @@ class ABRAuth:
return user
# TODO
async def _get_oidc_auth(self, request: Request, session: Session) -> User: ...
async def _get_oidc_auth(self, request: Request, session: Session) -> User:
raise RequiresLoginException()
async def _get_none_auth(self, session: Session) -> User:
"""Treats every request as being root by returning the first admin user"""
+11 -1
View File
@@ -8,11 +8,20 @@ class DBSettings(BaseModel):
"""Relative path to the sqlite database given the config directory. If absolute, it ignores the config dir location."""
class OIDCSettings(BaseModel):
client_id: str = ""
client_secret: str = ""
scope: str = "openid"
endpoint: str = ""
username_claim: str = "sub"
class ApplicationSettings(BaseModel):
debug: bool = False
openapi_enabled: bool = False
config_dir: str = "/config"
port: int = 8000
public_host: str = ""
class Settings(BaseSettings):
@@ -20,11 +29,12 @@ class Settings(BaseSettings):
env_prefix="ABR_",
env_nested_delimiter="__",
nested_model_default_partial_update=True,
env_file=".env.local",
env_file=(".env.local", ".env"),
)
db: DBSettings = DBSettings()
app: ApplicationSettings = ApplicationSettings()
oidc: OIDCSettings = OIDCSettings()
def get_sqlite_path(self):
if self.db.sqlite_path.startswith("/"):
+39 -1
View File
@@ -1,6 +1,8 @@
from typing import Annotated
from typing import Annotated, Optional
from aiohttp import ClientSession
from fastapi import APIRouter, Depends, Form, Request, Response, status
from fastapi.responses import RedirectResponse
from fastapi.security import OAuth2PasswordRequestForm
from sqlmodel import Session
@@ -9,6 +11,8 @@ from app.internal.auth.login import (
authenticate_user,
get_authenticated_user,
)
from app.internal.env_settings import Settings
from app.util.connection import get_connection
from app.util.db import get_session
from app.util.templates import templates
@@ -44,3 +48,37 @@ def login_access_token(
return Response(
status_code=status.HTTP_200_OK, headers={"HX-Redirect": redirect_uri}
)
@router.get("/oidc")
async def login_oidc(
request: Request,
client_session: Annotated[ClientSession, Depends(get_connection)],
code: str,
state: Optional[str] = None,
):
endpoint = Settings().oidc.endpoint.rstrip("/")
client_id = Settings().oidc.client_id
client_secret = Settings().oidc.client_secret
username_claim = Settings().oidc.username_claim
data = {
"grant_type": "authorization_code",
"code": code,
"client_id": client_id,
"client_secret": client_secret,
"redirect_uri": f"{Settings().app.public_host}/auth/oidc", # TODO: is this even required?
}
# TODO: get endpoint from .well-known
async with client_session.post(
endpoint + "/token/",
data=data,
headers={"Content-Type": "application/x-www-form-urlencoded"},
) as response:
body = await response.json()
print(body)
# TODO: validate the token and extract username and group claims
access_token = body["access_token"]
id_token = body["id_token"]
# return RedirectResponse(state or "/")
+18 -1
View File
@@ -1,4 +1,5 @@
from typing import Annotated, Optional
from urllib.parse import urlencode
from fastapi import APIRouter, Depends, HTTPException, Request
from fastapi.responses import RedirectResponse
@@ -6,6 +7,7 @@ from sqlmodel import Session
from app.internal.auth.config import LoginTypeEnum, auth_config
from app.internal.auth.login import RequiresLoginException, get_authenticated_user
from app.internal.env_settings import Settings
from app.util.db import get_session
from app.util.templates import templates
@@ -20,7 +22,7 @@ async def login(
redirect_uri: str = "/",
):
login_type = auth_config.get(session, "login_type")
if login_type != LoginTypeEnum.forms:
if login_type in [LoginTypeEnum.basic, LoginTypeEnum.none]:
return RedirectResponse(redirect_uri)
try:
@@ -30,6 +32,21 @@ async def login(
except (HTTPException, RequiresLoginException):
pass
if login_type == LoginTypeEnum.oidc:
host = Settings().app.public_host.rstrip("/")
client_id = Settings().oidc.client_id
scope = Settings().oidc.scope
endpoint = Settings().oidc.endpoint.rstrip("/")
params = {
"response_type": "code",
"client_id": client_id,
"redirect_uri": f"{host}/auth/oidc",
"scope": scope,
"state": redirect_uri,
}
# TODO: get endpoint from .well-known
return RedirectResponse(f"{endpoint}/authorize/?" + urlencode(params))
return templates.TemplateResponse(
"login.html",
{
+1 -3
View File
@@ -2,7 +2,7 @@ import json
import uuid
from typing import Annotated, Any, Optional, cast
from aiohttp import ClientResponseError, ClientSession
from aiohttp import ClientResponseError
from fastapi import APIRouter, Depends, Form, HTTPException, Request, Response
from sqlmodel import Session, select
@@ -19,7 +19,6 @@ from app.internal.notifications import send_notification
from app.internal.prowlarr.indexer_categories import indexer_categories
from app.internal.prowlarr.prowlarr import flush_prowlarr_cache, prowlarr_config
from app.internal.ranking.quality import IndexerFlag, QualityRange, quality_config
from app.util.connection import get_connection
from app.util.db import get_session
from app.util.templates import template_response
from app.util.time import Minute
@@ -609,7 +608,6 @@ async def execute_notification(
DetailedUser, Depends(get_authenticated_user(GroupEnum.admin))
],
session: Annotated[Session, Depends(get_session)],
client_session: Annotated[ClientSession, Depends(get_connection)],
):
notification = session.exec(
select(Notification).where(Notification.id == notification_id)