From 18fed113f9152d099fcc10fa576c6c41a99b9c79 Mon Sep 17 00:00:00 2001 From: Krrish Ghimire Date: Wed, 1 Jul 2026 10:25:37 +0545 Subject: [PATCH] fix jwt handler to return proper error codes --- .../java/com/indie/config/SecurityConfig.java | 9 +++ .../app/core/interceptors/jwt.interceptor.ts | 79 +++++++++++++++++-- 2 files changed, 83 insertions(+), 5 deletions(-) diff --git a/backend/src/main/java/com/indie/config/SecurityConfig.java b/backend/src/main/java/com/indie/config/SecurityConfig.java index 887f7cd..96c3b6f 100644 --- a/backend/src/main/java/com/indie/config/SecurityConfig.java +++ b/backend/src/main/java/com/indie/config/SecurityConfig.java @@ -64,6 +64,12 @@ public class SecurityConfig { .requestMatchers("/swagger-ui/**", "/api-docs/**").permitAll() .requestMatchers(HttpMethod.GET, "/api/sites/**").authenticated() .anyRequest().authenticated() + ) + .exceptionHandling(ex -> ex + .authenticationEntryPoint((request, response, authException) -> + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized")) + .accessDeniedHandler((request, response, accessDeniedException) -> + response.sendError(HttpServletResponse.SC_FORBIDDEN, "Forbidden")) ); if (isOAuth2Configured()) { @@ -128,6 +134,9 @@ public class SecurityConfig { } catch (Exception e) { org.springframework.security.core.context.SecurityContextHolder .clearContext(); + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, + "Invalid or expired token"); + return; } } diff --git a/frontend/src/app/core/interceptors/jwt.interceptor.ts b/frontend/src/app/core/interceptors/jwt.interceptor.ts index 3623828..0d8d073 100644 --- a/frontend/src/app/core/interceptors/jwt.interceptor.ts +++ b/frontend/src/app/core/interceptors/jwt.interceptor.ts @@ -1,11 +1,21 @@ -import { Injectable } from '@angular/core'; -import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent } from '@angular/common/http'; -import { Observable } from 'rxjs'; +import { Injectable, inject } from '@angular/core'; +import { + HttpInterceptor, + HttpRequest, + HttpHandler, + HttpEvent, + HttpErrorResponse, +} from '@angular/common/http'; +import { BehaviorSubject, Observable, catchError, filter, switchMap, take, throwError } from 'rxjs'; import { AuthService } from '../services/auth.service'; +import { Router } from '@angular/router'; @Injectable() export class JwtInterceptor implements HttpInterceptor { - constructor(private authService: AuthService) {} + private authService = inject(AuthService); + private router = inject(Router); + private isRefreshing = false; + private refreshTokenSubject = new BehaviorSubject(null); intercept(req: HttpRequest, next: HttpHandler): Observable> { const token = this.authService.getAccessToken(); @@ -14,6 +24,65 @@ export class JwtInterceptor implements HttpInterceptor { setHeaders: { Authorization: `Bearer ${token}` }, }); } - return next.handle(req); + + return next.handle(req).pipe( + catchError((error) => { + if ( + error instanceof HttpErrorResponse && + error.status === 401 && + !req.url.includes('/api/auth/refresh') + ) { + return this.handle401Error(req, next); + } + return throwError(() => error); + }), + ); + } + + private handle401Error( + req: HttpRequest, + next: HttpHandler, + ): Observable> { + if (!this.isRefreshing) { + this.isRefreshing = true; + this.refreshTokenSubject.next(null); + + const refreshToken = this.authService.getRefreshToken(); + if (!refreshToken) { + this.authService.clearSessionForLogout(); + this.router.navigate(['/login']); + return throwError(() => new Error('No refresh token available')); + } + + return this.authService.refreshToken(refreshToken).pipe( + switchMap((res) => { + this.isRefreshing = false; + this.refreshTokenSubject.next(res.accessToken); + return next.handle( + req.clone({ + setHeaders: { Authorization: `Bearer ${res.accessToken}` }, + }), + ); + }), + catchError((err) => { + this.isRefreshing = false; + this.authService.clearSessionForLogout(); + this.router.navigate(['/login']); + return throwError(() => err); + }), + ); + } else { + return this.refreshTokenSubject.pipe( + filter((token): token is string => token !== null), + take(1), + switchMap((token) => + next.handle( + req.clone({ + setHeaders: { Authorization: `Bearer ${token}` }, + }), + ), + ), + ); + } } }