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) {}
|
||||
|
||||
logout(): void {
|
||||
// Make a best-effort to log out on the server.
|
||||
// The client-side logout will run regardless of whether this call
|
||||
// succeeds or fails.
|
||||
// With the interceptor fixed, this is now the correct and robust way.
|
||||
// The error from a failed server logout will propagate here.
|
||||
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: () => {
|
||||
console.info("server logged out")
|
||||
console.log('Server-side logout successful.');
|
||||
this.authService.clientSideLogout();
|
||||
},
|
||||
error: () => {
|
||||
console.error("server failed to log out")
|
||||
error: (err) => {
|
||||
console.error('Server-side logout failed, logging out client-side anyway.', err);
|
||||
this.authService.clientSideLogout();
|
||||
},
|
||||
});
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { Observable, of, throwError } from 'rxjs';
|
||||
import { tap } from 'rxjs/operators';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
@ -40,7 +40,9 @@ export class AuthService {
|
||||
refreshToken(): Observable<any> {
|
||||
const refreshToken = this.getRefreshToken();
|
||||
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`, {}, {
|
||||
|
||||
@ -7,12 +7,13 @@ import {
|
||||
HttpErrorResponse,
|
||||
} from '@angular/common/http';
|
||||
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';
|
||||
|
||||
@Injectable()
|
||||
export class JwtInterceptor implements HttpInterceptor {
|
||||
private isRefreshing = false;
|
||||
// Initialize refreshTokenSubject with null
|
||||
private refreshTokenSubject: BehaviorSubject<any> = new BehaviorSubject<any>(null);
|
||||
|
||||
constructor(private authService: AuthService) {}
|
||||
@ -21,8 +22,11 @@ export class JwtInterceptor implements HttpInterceptor {
|
||||
request: HttpRequest<any>,
|
||||
next: HttpHandler
|
||||
): 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) {
|
||||
request = this.addToken(request, accessToken);
|
||||
}
|
||||
@ -31,41 +35,39 @@ export class JwtInterceptor implements HttpInterceptor {
|
||||
catchError((error) => {
|
||||
if (error instanceof HttpErrorResponse && error.status === 401) {
|
||||
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) {
|
||||
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(
|
||||
switchMap((token: any) => {
|
||||
this.isRefreshing = false;
|
||||
this.refreshTokenSubject.next(token.accessToken);
|
||||
return next.handle(this.addToken(request, token.accessToken));
|
||||
}),
|
||||
catchError((err) => {
|
||||
this.isRefreshing = false;
|
||||
this.refreshTokenSubject.error(err);
|
||||
|
||||
// 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.
|
||||
// If refresh fails, logout the user
|
||||
this.authService.clientSideLogout();
|
||||
return throwError(() => err);
|
||||
}),
|
||||
finalize(() => {
|
||||
// When the refresh attempt completes, set isRefreshing to false
|
||||
this.isRefreshing = false;
|
||||
})
|
||||
);
|
||||
} else {
|
||||
// If a refresh is already in progress, wait for it to complete
|
||||
return this.refreshTokenSubject.pipe(
|
||||
filter((token) => token != null),
|
||||
filter(token => token != null),
|
||||
take(1),
|
||||
switchMap((jwt) => {
|
||||
return next.handle(this.addToken(request, jwt));
|
||||
})
|
||||
switchMap(jwt => next.handle(this.addToken(request, jwt)))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,16 +5,20 @@ import { ConfigService } from '@nestjs/config';
|
||||
import { Role } from './role.enum';
|
||||
|
||||
@Injectable()
|
||||
export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||
constructor(configService: ConfigService) {
|
||||
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
|
||||
constructor(private configService: ConfigService) {
|
||||
super({
|
||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
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 {
|
||||
userId: payload.sub,
|
||||
username: payload.username,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user