🔒 Enforce OIDC state key

This commit is contained in:
itskovacs 2025-07-31 19:30:25 +02:00
parent 9d03d3ddb9
commit c7686d9c84
4 changed files with 33 additions and 14 deletions

View File

@ -1 +1 @@
__version__ = "1.8.1"
__version__ = "1.8.2"

View File

@ -1,5 +1,6 @@
import jwt
from fastapi import APIRouter, Body, HTTPException
from fastapi import APIRouter, Body, Cookie, HTTPException
from fastapi.responses import JSONResponse
from ..config import settings
from ..db.core import init_user_data
@ -16,21 +17,34 @@ router = APIRouter(prefix="/api/auth", tags=["auth"])
async def auth_params() -> AuthParams:
data = {"oidc": None, "register_enabled": settings.REGISTER_ENABLE}
response = JSONResponse(content=data)
if settings.OIDC_CLIENT_ID and settings.OIDC_CLIENT_SECRET:
oidc_config = await get_oidc_config()
auth_endpoint = oidc_config.get("authorization_endpoint")
data["oidc"] = (
f"{auth_endpoint}?client_id={settings.OIDC_CLIENT_ID}&redirect_uri={settings.OIDC_REDIRECT_URI}&response_type=code&scope=openid+profile"
uri, state = get_oidc_client().create_authorization_url(auth_endpoint)
data["oidc"] = uri
response = JSONResponse(content=data)
response.set_cookie(
"oidc_state", value=state, httponly=True, secure=True, samesite="Lax", max_age=60
)
return data
return response
@router.post("/oidc/login", response_model=Token)
async def oidc_login(session: SessionDep, code: str = Body(..., embed=True)) -> Token:
async def oidc_login(
session: SessionDep,
code: str = Body(..., embed=True),
state: str = Body(..., embed=True),
oidc_state: str = Cookie(None),
) -> Token:
if not (settings.OIDC_CLIENT_ID or settings.OIDC_CLIENT_SECRET):
raise HTTPException(status_code=400, detail="Partial OIDC config")
if not oidc_state or state != oidc_state:
raise HTTPException(status_code=400, detail="OIDC login failed, invalid state")
oidc_config = await get_oidc_config()
token_endpoint = oidc_config.get("token_endpoint")
try:

View File

@ -47,9 +47,11 @@ export class AuthComponent {
private fb: FormBuilder,
) {
this.route.queryParams.subscribe((params) => {
if (!Object.keys(params).length) return;
const code = params["code"];
if (code) {
this.authService.oidcLogin(code).subscribe({
const state = params["state"];
if (code && state) {
this.authService.oidcLogin(code, state).subscribe({
next: (data) => {
if (!data.access_token) {
this.error = "Authentication failed";
@ -61,9 +63,12 @@ export class AuthComponent {
}
});
this.authService.authParams().subscribe({
next: (params) => (this.authParams = params),
});
// Timeout to handle race condition
setTimeout(() => {
this.authService.authParams().subscribe({
next: (params) => (this.authParams = params),
});
}, 100);
this.redirectURL =
this.route.snapshot.queryParams["redirectURL"] || "/home";
@ -97,7 +102,7 @@ export class AuthComponent {
authenticate(): void {
this.error = "";
if (this.authParams?.oidc) {
window.location.replace(encodeURI(this.authParams.oidc));
window.location.replace(this.authParams.oidc);
}
this.authService.login(this.authForm.value).subscribe({

View File

@ -109,9 +109,9 @@ export class AuthService {
);
}
oidcLogin(code: string): Observable<Token> {
oidcLogin(code: string, state: string): Observable<Token> {
return this.httpClient
.post<Token>(this.apiBaseUrl + "/auth/oidc/login", { code })
.post<Token>(this.apiBaseUrl + "/auth/oidc/login", { code, state })
.pipe(
tap((data: any) => {
if (data.access_token && data.refresh_token) {