🔒 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 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 ..config import settings
from ..db.core import init_user_data from ..db.core import init_user_data
@ -16,21 +17,34 @@ router = APIRouter(prefix="/api/auth", tags=["auth"])
async def auth_params() -> AuthParams: async def auth_params() -> AuthParams:
data = {"oidc": None, "register_enabled": settings.REGISTER_ENABLE} data = {"oidc": None, "register_enabled": settings.REGISTER_ENABLE}
response = JSONResponse(content=data)
if settings.OIDC_CLIENT_ID and settings.OIDC_CLIENT_SECRET: if settings.OIDC_CLIENT_ID and settings.OIDC_CLIENT_SECRET:
oidc_config = await get_oidc_config() oidc_config = await get_oidc_config()
auth_endpoint = oidc_config.get("authorization_endpoint") auth_endpoint = oidc_config.get("authorization_endpoint")
data["oidc"] = ( uri, state = get_oidc_client().create_authorization_url(auth_endpoint)
f"{auth_endpoint}?client_id={settings.OIDC_CLIENT_ID}&redirect_uri={settings.OIDC_REDIRECT_URI}&response_type=code&scope=openid+profile" 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) @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): if not (settings.OIDC_CLIENT_ID or settings.OIDC_CLIENT_SECRET):
raise HTTPException(status_code=400, detail="Partial OIDC config") 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() oidc_config = await get_oidc_config()
token_endpoint = oidc_config.get("token_endpoint") token_endpoint = oidc_config.get("token_endpoint")
try: try:

View File

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