add refresh token
This commit is contained in:
parent
96af8e564b
commit
d11b0c65e0
@ -17,18 +17,15 @@ export class App {
|
|||||||
constructor(private authService: AuthService, private router: Router) {}
|
constructor(private authService: AuthService, private router: Router) {}
|
||||||
|
|
||||||
logout(): void {
|
logout(): void {
|
||||||
// Make a best-effort to log out on the server.
|
// With the interceptor fixed, this is now the correct and robust way.
|
||||||
// The client-side logout will run regardless of whether this call
|
// The error from a failed server logout will propagate here.
|
||||||
// succeeds or fails.
|
|
||||||
this.authService.serverSideLogout().subscribe({
|
this.authService.serverSideLogout().subscribe({
|
||||||
// The server call can succeed or fail, we don't care about the result,
|
|
||||||
// we just want to ensure the client is logged out.
|
|
||||||
next: () => {
|
next: () => {
|
||||||
console.info("server logged out")
|
console.log('Server-side logout successful.');
|
||||||
this.authService.clientSideLogout();
|
this.authService.clientSideLogout();
|
||||||
},
|
},
|
||||||
error: () => {
|
error: (err) => {
|
||||||
console.error("server failed to log out")
|
console.error('Server-side logout failed, logging out client-side anyway.', err);
|
||||||
this.authService.clientSideLogout();
|
this.authService.clientSideLogout();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { Observable, of } from 'rxjs';
|
import { Observable, of, throwError } from 'rxjs';
|
||||||
import { tap } from 'rxjs/operators';
|
import { tap } from 'rxjs/operators';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
|
|
||||||
@ -40,7 +40,9 @@ export class AuthService {
|
|||||||
refreshToken(): Observable<any> {
|
refreshToken(): Observable<any> {
|
||||||
const refreshToken = this.getRefreshToken();
|
const refreshToken = this.getRefreshToken();
|
||||||
if (!refreshToken) {
|
if (!refreshToken) {
|
||||||
return of(null);
|
// If no refresh token is present, logout and return an error.
|
||||||
|
this.clientSideLogout();
|
||||||
|
return throwError(() => new Error('No refresh token available'));
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.http.post<{ accessToken: string; refreshToken: string }>(`${this.apiUrl}/refresh`, {}, {
|
return this.http.post<{ accessToken: string; refreshToken: string }>(`${this.apiUrl}/refresh`, {}, {
|
||||||
|
|||||||
@ -7,12 +7,13 @@ import {
|
|||||||
HttpErrorResponse,
|
HttpErrorResponse,
|
||||||
} from '@angular/common/http';
|
} from '@angular/common/http';
|
||||||
import { Observable, throwError, BehaviorSubject } from 'rxjs';
|
import { Observable, throwError, BehaviorSubject } from 'rxjs';
|
||||||
import { catchError, switchMap, filter, take } from 'rxjs/operators';
|
import { catchError, switchMap, filter, take, finalize } from 'rxjs/operators'; // Import finalize
|
||||||
import { AuthService } from './auth.service';
|
import { AuthService } from './auth.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class JwtInterceptor implements HttpInterceptor {
|
export class JwtInterceptor implements HttpInterceptor {
|
||||||
private isRefreshing = false;
|
private isRefreshing = false;
|
||||||
|
// Initialize refreshTokenSubject with null
|
||||||
private refreshTokenSubject: BehaviorSubject<any> = new BehaviorSubject<any>(null);
|
private refreshTokenSubject: BehaviorSubject<any> = new BehaviorSubject<any>(null);
|
||||||
|
|
||||||
constructor(private authService: AuthService) {}
|
constructor(private authService: AuthService) {}
|
||||||
@ -21,8 +22,11 @@ export class JwtInterceptor implements HttpInterceptor {
|
|||||||
request: HttpRequest<any>,
|
request: HttpRequest<any>,
|
||||||
next: HttpHandler
|
next: HttpHandler
|
||||||
): Observable<HttpEvent<any>> {
|
): Observable<HttpEvent<any>> {
|
||||||
const accessToken = this.authService.getAccessToken();
|
if (request.url.includes('/auth/refresh')) {
|
||||||
|
return next.handle(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
const accessToken = this.authService.getAccessToken();
|
||||||
if (accessToken) {
|
if (accessToken) {
|
||||||
request = this.addToken(request, accessToken);
|
request = this.addToken(request, accessToken);
|
||||||
}
|
}
|
||||||
@ -31,41 +35,39 @@ export class JwtInterceptor implements HttpInterceptor {
|
|||||||
catchError((error) => {
|
catchError((error) => {
|
||||||
if (error instanceof HttpErrorResponse && error.status === 401) {
|
if (error instanceof HttpErrorResponse && error.status === 401) {
|
||||||
return this.handle401Error(request, next);
|
return this.handle401Error(request, next);
|
||||||
} else {
|
|
||||||
return throwError(() => error);
|
|
||||||
}
|
}
|
||||||
|
return throwError(() => error);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private handle401Error(request: HttpRequest<any>, next: HttpHandler) {
|
private handle401Error(request: HttpRequest<any>, next: HttpHandler): Observable<any> {
|
||||||
if (!this.isRefreshing) {
|
if (!this.isRefreshing) {
|
||||||
this.isRefreshing = true;
|
this.isRefreshing = true;
|
||||||
this.refreshTokenSubject = new BehaviorSubject<any>(null);
|
// Reset the refreshTokenSubject to null so that subsequent requests will wait
|
||||||
|
this.refreshTokenSubject.next(null);
|
||||||
|
|
||||||
return this.authService.refreshToken().pipe(
|
return this.authService.refreshToken().pipe(
|
||||||
switchMap((token: any) => {
|
switchMap((token: any) => {
|
||||||
this.isRefreshing = false;
|
|
||||||
this.refreshTokenSubject.next(token.accessToken);
|
this.refreshTokenSubject.next(token.accessToken);
|
||||||
return next.handle(this.addToken(request, token.accessToken));
|
return next.handle(this.addToken(request, token.accessToken));
|
||||||
}),
|
}),
|
||||||
catchError((err) => {
|
catchError((err) => {
|
||||||
this.isRefreshing = false;
|
// If refresh fails, logout the user
|
||||||
this.refreshTokenSubject.error(err);
|
this.authService.clientSideLogout();
|
||||||
|
|
||||||
// The interceptor's job is done. It failed to refresh.
|
|
||||||
// It should NOT handle logout. It should just propagate the error.
|
|
||||||
// The calling service/component will be responsible for the user-facing action.
|
|
||||||
return throwError(() => err);
|
return throwError(() => err);
|
||||||
|
}),
|
||||||
|
finalize(() => {
|
||||||
|
// When the refresh attempt completes, set isRefreshing to false
|
||||||
|
this.isRefreshing = false;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
// If a refresh is already in progress, wait for it to complete
|
||||||
return this.refreshTokenSubject.pipe(
|
return this.refreshTokenSubject.pipe(
|
||||||
filter((token) => token != null),
|
filter(token => token != null),
|
||||||
take(1),
|
take(1),
|
||||||
switchMap((jwt) => {
|
switchMap(jwt => next.handle(this.addToken(request, jwt)))
|
||||||
return next.handle(this.addToken(request, jwt));
|
|
||||||
})
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,16 +5,20 @@ import { ConfigService } from '@nestjs/config';
|
|||||||
import { Role } from './role.enum';
|
import { Role } from './role.enum';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class JwtStrategy extends PassportStrategy(Strategy) {
|
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
|
||||||
constructor(configService: ConfigService) {
|
constructor(private configService: ConfigService) {
|
||||||
super({
|
super({
|
||||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||||
ignoreExpiration: false,
|
ignoreExpiration: false,
|
||||||
|
// This is the critical fix: ensure the strategy uses the ConfigService
|
||||||
|
// to get the secret at runtime.
|
||||||
secretOrKey: configService.get<string>('JWT_SECRET') as string,
|
secretOrKey: configService.get<string>('JWT_SECRET') as string,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
validate(payload: { sub: string; username: string; roles: Role[] }) {
|
// The payload is already validated by passport-jwt at this point,
|
||||||
|
// so we can trust its contents.
|
||||||
|
validate(payload: { sub: number; username: string; roles: Role[] }) {
|
||||||
return {
|
return {
|
||||||
userId: payload.sub,
|
userId: payload.sub,
|
||||||
username: payload.username,
|
username: payload.username,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user