@nauth-toolkit/client-angular 0.1.63 → 0.1.65
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/esm2022/lib/auth-interceptor.shared.mjs +136 -0
- package/esm2022/lib/auth.guard.mjs +17 -10
- package/esm2022/lib/auth.interceptor.mjs +4 -263
- package/esm2022/ngmodule/auth.interceptor.class.mjs +10 -63
- package/esm2022/ngmodule/auth.service.mjs +17 -1
- package/fesm2022/nauth-toolkit-client-angular.mjs +162 -319
- package/fesm2022/nauth-toolkit-client-angular.mjs.map +1 -1
- package/lib/auth-interceptor.shared.d.ts +13 -0
- package/lib/auth.guard.d.ts +3 -0
- package/ngmodule/auth.service.d.ts +14 -0
- package/package.json +2 -2
|
@@ -2,13 +2,12 @@ import { NAuthErrorCode, NAuthClientError, NAuthClient } from '@nauth-toolkit/cl
|
|
|
2
2
|
export * from '@nauth-toolkit/client';
|
|
3
3
|
import * as i0 from '@angular/core';
|
|
4
4
|
import { InjectionToken, Injectable, Inject, inject, Optional, NgModule, PLATFORM_ID } from '@angular/core';
|
|
5
|
-
import { firstValueFrom, BehaviorSubject, Subject,
|
|
5
|
+
import { firstValueFrom, BehaviorSubject, Subject, from, map, of, switchMap, catchError, throwError, filter as filter$1, take } from 'rxjs';
|
|
6
6
|
import { filter } from 'rxjs/operators';
|
|
7
7
|
import * as i1 from '@angular/common/http';
|
|
8
8
|
import { HttpErrorResponse, HTTP_INTERCEPTORS, HttpClientModule, HttpClient } from '@angular/common/http';
|
|
9
9
|
import * as i3 from '@angular/router';
|
|
10
10
|
import { Router } from '@angular/router';
|
|
11
|
-
import { __decorate, __param } from 'tslib';
|
|
12
11
|
import { isPlatformBrowser } from '@angular/common';
|
|
13
12
|
|
|
14
13
|
/**
|
|
@@ -605,6 +604,22 @@ class AuthService {
|
|
|
605
604
|
await this.client.clearStoredChallenge();
|
|
606
605
|
this.challengeSubject.next(null);
|
|
607
606
|
}
|
|
607
|
+
/**
|
|
608
|
+
* Get current access token (JSON mode only).
|
|
609
|
+
*
|
|
610
|
+
* This is primarily useful for consumers using Angular `HttpClient` directly
|
|
611
|
+
* (outside of the SDK methods) and relying on an interceptor to attach Bearer tokens.
|
|
612
|
+
*
|
|
613
|
+
* @returns Access token, or null if not available
|
|
614
|
+
*
|
|
615
|
+
* @example
|
|
616
|
+
* ```typescript
|
|
617
|
+
* const token = await this.auth.getAccessToken();
|
|
618
|
+
* ```
|
|
619
|
+
*/
|
|
620
|
+
async getAccessToken() {
|
|
621
|
+
return await this.client.getAccessToken();
|
|
622
|
+
}
|
|
608
623
|
// ============================================================================
|
|
609
624
|
// Social Authentication
|
|
610
625
|
// ============================================================================
|
|
@@ -947,20 +962,139 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
947
962
|
}] }, { type: AngularHttpAdapter }] });
|
|
948
963
|
|
|
949
964
|
/**
|
|
950
|
-
*
|
|
965
|
+
* Shared interceptor logic for both:
|
|
966
|
+
* - Functional interceptor (Angular 17+ standalone)
|
|
967
|
+
* - Class-based interceptor (NgModule apps)
|
|
968
|
+
*
|
|
969
|
+
* WHY:
|
|
970
|
+
* - Keep one implementation for cookies + json mode behavior.
|
|
971
|
+
* - Avoid divergence between standalone and NgModule integrations.
|
|
951
972
|
*/
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
973
|
+
// ============================================================================
|
|
974
|
+
// Refresh state management (module-level)
|
|
975
|
+
// ============================================================================
|
|
976
|
+
let isRefreshing = false;
|
|
977
|
+
const refreshTokenSubject = new BehaviorSubject(null);
|
|
978
|
+
const retriedRequests = new WeakSet();
|
|
955
979
|
/**
|
|
956
980
|
* Get CSRF token from cookie.
|
|
957
981
|
*/
|
|
958
|
-
function getCsrfToken
|
|
982
|
+
function getCsrfToken(cookieName) {
|
|
959
983
|
if (typeof document === 'undefined')
|
|
960
984
|
return null;
|
|
961
985
|
const match = document.cookie.match(new RegExp(`(^| )${cookieName}=([^;]+)`));
|
|
962
986
|
return match ? decodeURIComponent(match[2]) : null;
|
|
963
987
|
}
|
|
988
|
+
/**
|
|
989
|
+
* Build retry request with appropriate auth.
|
|
990
|
+
*
|
|
991
|
+
* In cookies mode: Browser automatically sends updated httpOnly cookies (access/refresh tokens).
|
|
992
|
+
* We must re-read CSRF token after refresh to avoid stale headers.
|
|
993
|
+
*
|
|
994
|
+
* In JSON mode: Clones the request and adds the new Bearer token.
|
|
995
|
+
*/
|
|
996
|
+
function buildRetryRequest(originalReq, tokenDelivery, newToken, csrfConfig) {
|
|
997
|
+
if (tokenDelivery === 'json' && newToken && newToken !== 'success') {
|
|
998
|
+
return originalReq.clone({ setHeaders: { Authorization: `Bearer ${newToken}` } });
|
|
999
|
+
}
|
|
1000
|
+
if (tokenDelivery === 'cookies' && ['POST', 'PUT', 'PATCH', 'DELETE'].includes(originalReq.method)) {
|
|
1001
|
+
const csrfCookieName = csrfConfig?.cookieName ?? 'nauth_csrf_token';
|
|
1002
|
+
const csrfHeaderName = csrfConfig?.headerName ?? 'x-csrf-token';
|
|
1003
|
+
const freshCsrfToken = getCsrfToken(csrfCookieName);
|
|
1004
|
+
if (freshCsrfToken) {
|
|
1005
|
+
return originalReq.clone({ setHeaders: { [csrfHeaderName]: freshCsrfToken } });
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
return originalReq;
|
|
1009
|
+
}
|
|
1010
|
+
function createNAuthAuthHttpInterceptor(params) {
|
|
1011
|
+
const { config, http, authService, router, next, req } = params;
|
|
1012
|
+
const tokenDelivery = config.tokenDelivery;
|
|
1013
|
+
const baseUrl = config.baseUrl;
|
|
1014
|
+
const endpoints = config.endpoints ?? {};
|
|
1015
|
+
const refreshPath = endpoints.refresh ?? '/refresh';
|
|
1016
|
+
const loginPath = endpoints.login ?? '/login';
|
|
1017
|
+
const signupPath = endpoints.signup ?? '/signup';
|
|
1018
|
+
const socialExchangePath = endpoints.socialExchange ?? '/social/exchange';
|
|
1019
|
+
const refreshUrl = `${baseUrl}${refreshPath}`;
|
|
1020
|
+
const isAuthApiRequest = req.url.includes(baseUrl);
|
|
1021
|
+
const isRefreshEndpoint = req.url.includes(refreshPath);
|
|
1022
|
+
const isPublicEndpoint = req.url.includes(loginPath) || req.url.includes(signupPath) || req.url.includes(socialExchangePath);
|
|
1023
|
+
// ============================================================================
|
|
1024
|
+
// Build request for cookies mode (withCredentials + CSRF)
|
|
1025
|
+
// ============================================================================
|
|
1026
|
+
let authReq = req;
|
|
1027
|
+
if (tokenDelivery === 'cookies') {
|
|
1028
|
+
authReq = authReq.clone({ withCredentials: true });
|
|
1029
|
+
if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(req.method)) {
|
|
1030
|
+
const csrfCookieName = config.csrf?.cookieName ?? 'nauth_csrf_token';
|
|
1031
|
+
const csrfHeaderName = config.csrf?.headerName ?? 'x-csrf-token';
|
|
1032
|
+
const csrfToken = getCsrfToken(csrfCookieName);
|
|
1033
|
+
if (csrfToken) {
|
|
1034
|
+
authReq = authReq.clone({ setHeaders: { [csrfHeaderName]: csrfToken } });
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
// ============================================================================
|
|
1039
|
+
// JSON mode: attach Authorization header for HttpClient calls
|
|
1040
|
+
// ============================================================================
|
|
1041
|
+
const attachJsonAuth$ = tokenDelivery === 'json' &&
|
|
1042
|
+
isAuthApiRequest &&
|
|
1043
|
+
!isRefreshEndpoint &&
|
|
1044
|
+
!isPublicEndpoint &&
|
|
1045
|
+
!authReq.headers.has('Authorization')
|
|
1046
|
+
? from(authService.getAccessToken()).pipe(map((token) => {
|
|
1047
|
+
if (!token)
|
|
1048
|
+
return authReq;
|
|
1049
|
+
return authReq.clone({ setHeaders: { Authorization: `Bearer ${token}` } });
|
|
1050
|
+
}))
|
|
1051
|
+
: of(authReq);
|
|
1052
|
+
return attachJsonAuth$.pipe(switchMap((requestWithAuth) => next(requestWithAuth).pipe(catchError((error) => {
|
|
1053
|
+
const shouldHandle = error instanceof HttpErrorResponse &&
|
|
1054
|
+
error.status === 401 &&
|
|
1055
|
+
isAuthApiRequest &&
|
|
1056
|
+
!isRefreshEndpoint &&
|
|
1057
|
+
!isPublicEndpoint &&
|
|
1058
|
+
!retriedRequests.has(req);
|
|
1059
|
+
if (!shouldHandle) {
|
|
1060
|
+
return throwError(() => error);
|
|
1061
|
+
}
|
|
1062
|
+
retriedRequests.add(req);
|
|
1063
|
+
if (!isRefreshing) {
|
|
1064
|
+
isRefreshing = true;
|
|
1065
|
+
refreshTokenSubject.next(null);
|
|
1066
|
+
// Refresh based on mode
|
|
1067
|
+
const refresh$ = tokenDelivery === 'cookies'
|
|
1068
|
+
? http.post(refreshUrl, {}, { withCredentials: true })
|
|
1069
|
+
: from(authService.refresh());
|
|
1070
|
+
return refresh$.pipe(switchMap((response) => {
|
|
1071
|
+
isRefreshing = false;
|
|
1072
|
+
const newToken = 'accessToken' in response ? response.accessToken : 'success';
|
|
1073
|
+
refreshTokenSubject.next(newToken ?? 'success');
|
|
1074
|
+
const retryReq = buildRetryRequest(requestWithAuth, tokenDelivery, newToken, config.csrf);
|
|
1075
|
+
return next(retryReq).pipe(catchError((retryErr) => {
|
|
1076
|
+
return throwError(() => retryErr);
|
|
1077
|
+
}));
|
|
1078
|
+
}), catchError((err) => {
|
|
1079
|
+
isRefreshing = false;
|
|
1080
|
+
refreshTokenSubject.next(null);
|
|
1081
|
+
// Refresh failed -> redirect if configured
|
|
1082
|
+
if (config.redirects?.sessionExpired) {
|
|
1083
|
+
router.navigateByUrl(config.redirects.sessionExpired).catch(() => { });
|
|
1084
|
+
}
|
|
1085
|
+
return throwError(() => err);
|
|
1086
|
+
}));
|
|
1087
|
+
}
|
|
1088
|
+
// Wait for ongoing refresh
|
|
1089
|
+
return refreshTokenSubject.pipe(filter$1((token) => token !== null), take(1), switchMap((token) => {
|
|
1090
|
+
const retryReq = buildRetryRequest(requestWithAuth, tokenDelivery, token, config.csrf);
|
|
1091
|
+
return next(retryReq).pipe(catchError((retryErr) => {
|
|
1092
|
+
return throwError(() => retryErr);
|
|
1093
|
+
}));
|
|
1094
|
+
}));
|
|
1095
|
+
}))));
|
|
1096
|
+
}
|
|
1097
|
+
|
|
964
1098
|
/**
|
|
965
1099
|
* Class-based HTTP interceptor for NgModule apps (Angular < 17).
|
|
966
1100
|
*
|
|
@@ -991,52 +1125,14 @@ class AuthInterceptorClass {
|
|
|
991
1125
|
this.router = router;
|
|
992
1126
|
}
|
|
993
1127
|
intercept(req, next) {
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(req.method)) {
|
|
1003
|
-
const csrfToken = getCsrfToken$1(this.config.csrf?.cookieName || 'XSRF-TOKEN');
|
|
1004
|
-
if (csrfToken) {
|
|
1005
|
-
clonedReq = clonedReq.clone({
|
|
1006
|
-
setHeaders: { [this.config.csrf?.headerName || 'X-XSRF-TOKEN']: csrfToken },
|
|
1007
|
-
});
|
|
1008
|
-
}
|
|
1009
|
-
}
|
|
1010
|
-
return next.handle(clonedReq).pipe(catchError((error) => {
|
|
1011
|
-
if (error.status === 401 && !retriedRequests$1.has(req)) {
|
|
1012
|
-
retriedRequests$1.add(req);
|
|
1013
|
-
if (!isRefreshing$1) {
|
|
1014
|
-
isRefreshing$1 = true;
|
|
1015
|
-
refreshTokenSubject$1.next(null);
|
|
1016
|
-
return from(this.http
|
|
1017
|
-
.post(`${baseUrl}/refresh`, {}, { withCredentials: true })
|
|
1018
|
-
.toPromise()).pipe(switchMap(() => {
|
|
1019
|
-
isRefreshing$1 = false;
|
|
1020
|
-
refreshTokenSubject$1.next('refreshed');
|
|
1021
|
-
return next.handle(clonedReq);
|
|
1022
|
-
}), catchError((refreshError) => {
|
|
1023
|
-
isRefreshing$1 = false;
|
|
1024
|
-
this.authService.logout();
|
|
1025
|
-
this.router.navigate([this.config.redirects?.sessionExpired || '/login']);
|
|
1026
|
-
return throwError(() => refreshError);
|
|
1027
|
-
}));
|
|
1028
|
-
}
|
|
1029
|
-
else {
|
|
1030
|
-
return refreshTokenSubject$1.pipe(filter$1((token) => token !== null), take(1), switchMap(() => next.handle(clonedReq)));
|
|
1031
|
-
}
|
|
1032
|
-
}
|
|
1033
|
-
return throwError(() => error);
|
|
1034
|
-
}));
|
|
1035
|
-
}
|
|
1036
|
-
// ============================================================================
|
|
1037
|
-
// JSON MODE: Delegate to SDK for token handling
|
|
1038
|
-
// ============================================================================
|
|
1039
|
-
return next.handle(req);
|
|
1128
|
+
return createNAuthAuthHttpInterceptor({
|
|
1129
|
+
config: this.config,
|
|
1130
|
+
http: this.http,
|
|
1131
|
+
authService: this.authService,
|
|
1132
|
+
router: this.router,
|
|
1133
|
+
req,
|
|
1134
|
+
next: (r) => next.handle(r),
|
|
1135
|
+
});
|
|
1040
1136
|
}
|
|
1041
1137
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: AuthInterceptorClass, deps: [{ token: NAUTH_CLIENT_CONFIG }, { token: i1.HttpClient }, { token: AuthService }, { token: i3.Router }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1042
1138
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: AuthInterceptorClass });
|
|
@@ -1127,7 +1223,7 @@ function authGuard(redirectTo) {
|
|
|
1127
1223
|
* export class FeatureModule {}
|
|
1128
1224
|
* ```
|
|
1129
1225
|
*/
|
|
1130
|
-
|
|
1226
|
+
class AuthGuard {
|
|
1131
1227
|
auth;
|
|
1132
1228
|
router;
|
|
1133
1229
|
config;
|
|
@@ -1154,11 +1250,17 @@ let AuthGuard = class AuthGuard {
|
|
|
1154
1250
|
const redirectPath = this.config?.redirects?.sessionExpired ?? '/login';
|
|
1155
1251
|
return this.router.createUrlTree([redirectPath]);
|
|
1156
1252
|
}
|
|
1157
|
-
};
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1253
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: AuthGuard, deps: [{ token: AuthService }, { token: i3.Router }, { token: NAUTH_CLIENT_CONFIG, optional: true }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1254
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: AuthGuard });
|
|
1255
|
+
}
|
|
1256
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: AuthGuard, decorators: [{
|
|
1257
|
+
type: Injectable
|
|
1258
|
+
}], ctorParameters: () => [{ type: AuthService }, { type: i3.Router }, { type: undefined, decorators: [{
|
|
1259
|
+
type: Optional
|
|
1260
|
+
}, {
|
|
1261
|
+
type: Inject,
|
|
1262
|
+
args: [NAUTH_CLIENT_CONFIG]
|
|
1263
|
+
}] }] });
|
|
1162
1264
|
|
|
1163
1265
|
/**
|
|
1164
1266
|
* NgModule for nauth-toolkit Angular integration.
|
|
@@ -1220,25 +1322,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
1220
1322
|
}]
|
|
1221
1323
|
}] });
|
|
1222
1324
|
|
|
1223
|
-
/**
|
|
1224
|
-
* Refresh state management.
|
|
1225
|
-
* BehaviorSubject pattern is the industry-standard for token refresh.
|
|
1226
|
-
*/
|
|
1227
|
-
let isRefreshing = false;
|
|
1228
|
-
const refreshTokenSubject = new BehaviorSubject(null);
|
|
1229
|
-
/**
|
|
1230
|
-
* Track retried requests to prevent infinite loops.
|
|
1231
|
-
*/
|
|
1232
|
-
const retriedRequests = new WeakSet();
|
|
1233
|
-
/**
|
|
1234
|
-
* Get CSRF token from cookie.
|
|
1235
|
-
*/
|
|
1236
|
-
function getCsrfToken(cookieName) {
|
|
1237
|
-
if (typeof document === 'undefined')
|
|
1238
|
-
return null;
|
|
1239
|
-
const match = document.cookie.match(new RegExp(`(^| )${cookieName}=([^;]+)`));
|
|
1240
|
-
return match ? decodeURIComponent(match[2]) : null;
|
|
1241
|
-
}
|
|
1242
1325
|
/**
|
|
1243
1326
|
* Angular HTTP interceptor for nauth-toolkit.
|
|
1244
1327
|
*
|
|
@@ -1256,248 +1339,8 @@ const authInterceptor = (req, next) => {
|
|
|
1256
1339
|
if (!isBrowser) {
|
|
1257
1340
|
return next(req);
|
|
1258
1341
|
}
|
|
1259
|
-
|
|
1260
|
-
if (req.url.includes('/profile') && req.method === 'PUT') {
|
|
1261
|
-
fetch('http://127.0.0.1:7242/ingest/97f9fe53-6a8b-43e2-ae9b-4b2d0f725816', {
|
|
1262
|
-
method: 'POST',
|
|
1263
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1264
|
-
body: JSON.stringify({
|
|
1265
|
-
location: 'auth.interceptor.ts:entry',
|
|
1266
|
-
message: 'Original request entry',
|
|
1267
|
-
data: { reqBody: req.body, reqBodyType: typeof req.body, reqMethod: req.method, reqUrl: req.url },
|
|
1268
|
-
timestamp: Date.now(),
|
|
1269
|
-
sessionId: 'debug-session',
|
|
1270
|
-
hypothesisId: 'A',
|
|
1271
|
-
}),
|
|
1272
|
-
}).catch(() => { });
|
|
1273
|
-
}
|
|
1274
|
-
// #endregion
|
|
1275
|
-
const tokenDelivery = config.tokenDelivery;
|
|
1276
|
-
const baseUrl = config.baseUrl;
|
|
1277
|
-
const endpoints = config.endpoints ?? {};
|
|
1278
|
-
const refreshPath = endpoints.refresh ?? '/refresh';
|
|
1279
|
-
const loginPath = endpoints.login ?? '/login';
|
|
1280
|
-
const signupPath = endpoints.signup ?? '/signup';
|
|
1281
|
-
const socialExchangePath = endpoints.socialExchange ?? '/social/exchange';
|
|
1282
|
-
const refreshUrl = `${baseUrl}${refreshPath}`;
|
|
1283
|
-
const isAuthApiRequest = req.url.includes(baseUrl);
|
|
1284
|
-
const isRefreshEndpoint = req.url.includes(refreshPath);
|
|
1285
|
-
const isPublicEndpoint = req.url.includes(loginPath) || req.url.includes(signupPath) || req.url.includes(socialExchangePath);
|
|
1286
|
-
// Build request with credentials (cookies mode only)
|
|
1287
|
-
let authReq = req;
|
|
1288
|
-
if (tokenDelivery === 'cookies') {
|
|
1289
|
-
authReq = authReq.clone({ withCredentials: true });
|
|
1290
|
-
if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(req.method)) {
|
|
1291
|
-
const csrfCookieName = config.csrf?.cookieName ?? 'nauth_csrf_token';
|
|
1292
|
-
const csrfHeaderName = config.csrf?.headerName ?? 'x-csrf-token';
|
|
1293
|
-
const csrfToken = getCsrfToken(csrfCookieName);
|
|
1294
|
-
if (csrfToken) {
|
|
1295
|
-
authReq = authReq.clone({ setHeaders: { [csrfHeaderName]: csrfToken } });
|
|
1296
|
-
}
|
|
1297
|
-
}
|
|
1298
|
-
}
|
|
1299
|
-
return next(authReq).pipe(catchError((error) => {
|
|
1300
|
-
const shouldHandle = error instanceof HttpErrorResponse &&
|
|
1301
|
-
error.status === 401 &&
|
|
1302
|
-
isAuthApiRequest &&
|
|
1303
|
-
!isRefreshEndpoint &&
|
|
1304
|
-
!isPublicEndpoint &&
|
|
1305
|
-
!retriedRequests.has(req);
|
|
1306
|
-
if (!shouldHandle) {
|
|
1307
|
-
return throwError(() => error);
|
|
1308
|
-
}
|
|
1309
|
-
// Mark original request as retried to prevent infinite loops
|
|
1310
|
-
retriedRequests.add(req);
|
|
1311
|
-
if (config.debug) {
|
|
1312
|
-
console.warn('[nauth-interceptor] 401 detected:', req.url);
|
|
1313
|
-
}
|
|
1314
|
-
if (!isRefreshing) {
|
|
1315
|
-
isRefreshing = true;
|
|
1316
|
-
refreshTokenSubject.next(null);
|
|
1317
|
-
if (config.debug) {
|
|
1318
|
-
console.warn('[nauth-interceptor] Starting refresh...');
|
|
1319
|
-
}
|
|
1320
|
-
// Refresh based on mode
|
|
1321
|
-
const refresh$ = tokenDelivery === 'cookies'
|
|
1322
|
-
? http.post(refreshUrl, {}, { withCredentials: true })
|
|
1323
|
-
: from(authService.refresh());
|
|
1324
|
-
return refresh$.pipe(switchMap((response) => {
|
|
1325
|
-
if (config.debug) {
|
|
1326
|
-
console.warn('[nauth-interceptor] Refresh successful');
|
|
1327
|
-
}
|
|
1328
|
-
isRefreshing = false;
|
|
1329
|
-
// Get new token (JSON mode) or signal success (cookies mode)
|
|
1330
|
-
const newToken = 'accessToken' in response ? response.accessToken : 'success';
|
|
1331
|
-
refreshTokenSubject.next(newToken ?? 'success');
|
|
1332
|
-
// #region agent log
|
|
1333
|
-
fetch('http://127.0.0.1:7242/ingest/97f9fe53-6a8b-43e2-ae9b-4b2d0f725816', {
|
|
1334
|
-
method: 'POST',
|
|
1335
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1336
|
-
body: JSON.stringify({
|
|
1337
|
-
location: 'auth.interceptor.ts:125',
|
|
1338
|
-
message: 'Before buildRetryRequest',
|
|
1339
|
-
data: {
|
|
1340
|
-
authReqBody: authReq.body,
|
|
1341
|
-
authReqMethod: authReq.method,
|
|
1342
|
-
authReqUrl: authReq.url,
|
|
1343
|
-
authReqBodyType: typeof authReq.body,
|
|
1344
|
-
},
|
|
1345
|
-
timestamp: Date.now(),
|
|
1346
|
-
sessionId: 'debug-session',
|
|
1347
|
-
hypothesisId: 'A',
|
|
1348
|
-
}),
|
|
1349
|
-
}).catch(() => { });
|
|
1350
|
-
// #endregion
|
|
1351
|
-
// Build retry request with fresh CSRF token (re-read from cookie after refresh)
|
|
1352
|
-
const retryReq = buildRetryRequest(authReq, tokenDelivery, newToken, config.csrf);
|
|
1353
|
-
// #region agent log
|
|
1354
|
-
fetch('http://127.0.0.1:7242/ingest/97f9fe53-6a8b-43e2-ae9b-4b2d0f725816', {
|
|
1355
|
-
method: 'POST',
|
|
1356
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1357
|
-
body: JSON.stringify({
|
|
1358
|
-
location: 'auth.interceptor.ts:130',
|
|
1359
|
-
message: 'After buildRetryRequest',
|
|
1360
|
-
data: {
|
|
1361
|
-
retryReqBody: retryReq.body,
|
|
1362
|
-
retryReqMethod: retryReq.method,
|
|
1363
|
-
retryReqUrl: retryReq.url,
|
|
1364
|
-
retryReqBodyType: typeof retryReq.body,
|
|
1365
|
-
headersKeys: retryReq.headers.keys(),
|
|
1366
|
-
},
|
|
1367
|
-
timestamp: Date.now(),
|
|
1368
|
-
sessionId: 'debug-session',
|
|
1369
|
-
hypothesisId: 'B',
|
|
1370
|
-
}),
|
|
1371
|
-
}).catch(() => { });
|
|
1372
|
-
// #endregion
|
|
1373
|
-
if (config.debug) {
|
|
1374
|
-
console.warn('[nauth-interceptor] Retrying:', req.url);
|
|
1375
|
-
}
|
|
1376
|
-
// Retry the request with fresh token/CSRF
|
|
1377
|
-
// IMPORTANT: Errors from the retry (e.g., 400 validation) should NOT trigger
|
|
1378
|
-
// session expiration redirect. Only the refresh failure should redirect.
|
|
1379
|
-
return next(retryReq).pipe(catchError((retryErr) => {
|
|
1380
|
-
// Retry failed (could be 400, 403, 500, etc.)
|
|
1381
|
-
// Just propagate the error - don't redirect to login
|
|
1382
|
-
if (config.debug) {
|
|
1383
|
-
console.warn('[nauth-interceptor] Retry request failed:', retryErr);
|
|
1384
|
-
}
|
|
1385
|
-
return throwError(() => retryErr);
|
|
1386
|
-
}));
|
|
1387
|
-
}), catchError((err) => {
|
|
1388
|
-
// This only catches REFRESH failures, not retry failures
|
|
1389
|
-
if (config.debug) {
|
|
1390
|
-
console.error('[nauth-interceptor] Refresh failed:', err);
|
|
1391
|
-
}
|
|
1392
|
-
isRefreshing = false;
|
|
1393
|
-
refreshTokenSubject.next(null);
|
|
1394
|
-
// Handle session expiration - redirect to configured URL
|
|
1395
|
-
// Only redirect if refresh itself failed (not if retry failed)
|
|
1396
|
-
if (config.redirects?.sessionExpired) {
|
|
1397
|
-
router.navigateByUrl(config.redirects.sessionExpired).catch((navError) => {
|
|
1398
|
-
if (config.debug) {
|
|
1399
|
-
console.error('[nauth-interceptor] Navigation failed:', navError);
|
|
1400
|
-
}
|
|
1401
|
-
});
|
|
1402
|
-
}
|
|
1403
|
-
return throwError(() => err);
|
|
1404
|
-
}));
|
|
1405
|
-
}
|
|
1406
|
-
else {
|
|
1407
|
-
// Wait for ongoing refresh
|
|
1408
|
-
if (config.debug) {
|
|
1409
|
-
console.warn('[nauth-interceptor] Waiting for refresh...');
|
|
1410
|
-
}
|
|
1411
|
-
return refreshTokenSubject.pipe(filter$1((token) => token !== null), take(1), switchMap((token) => {
|
|
1412
|
-
if (config.debug) {
|
|
1413
|
-
console.warn('[nauth-interceptor] Refresh done, retrying:', req.url);
|
|
1414
|
-
}
|
|
1415
|
-
const retryReq = buildRetryRequest(authReq, tokenDelivery, token, config.csrf);
|
|
1416
|
-
// Retry the request - errors here should propagate normally
|
|
1417
|
-
// without triggering session expiration redirect
|
|
1418
|
-
return next(retryReq).pipe(catchError((retryErr) => {
|
|
1419
|
-
if (config.debug) {
|
|
1420
|
-
console.warn('[nauth-interceptor] Retry request failed:', retryErr);
|
|
1421
|
-
}
|
|
1422
|
-
return throwError(() => retryErr);
|
|
1423
|
-
}));
|
|
1424
|
-
}));
|
|
1425
|
-
}
|
|
1426
|
-
}));
|
|
1342
|
+
return createNAuthAuthHttpInterceptor({ config, http, authService, router, next, req });
|
|
1427
1343
|
};
|
|
1428
|
-
/**
|
|
1429
|
-
* Build retry request with appropriate auth.
|
|
1430
|
-
*
|
|
1431
|
-
* CRITICAL FIX: In cookies mode, after refresh the server may send updated cookies.
|
|
1432
|
-
* We MUST re-read the CSRF token from the cookie before retrying to ensure we have
|
|
1433
|
-
* the current CSRF token that matches what the server expects.
|
|
1434
|
-
*
|
|
1435
|
-
* In JSON mode: Clones the request and adds the new Bearer token.
|
|
1436
|
-
*
|
|
1437
|
-
* @param originalReq - The base request (already has withCredentials if cookies mode)
|
|
1438
|
-
* @param tokenDelivery - 'cookies' or 'json'
|
|
1439
|
-
* @param newToken - The new access token (JSON mode only)
|
|
1440
|
-
* @param csrfConfig - CSRF configuration to re-read token from cookie
|
|
1441
|
-
* @returns The request ready for retry with fresh auth
|
|
1442
|
-
*/
|
|
1443
|
-
function buildRetryRequest(originalReq, tokenDelivery, newToken, csrfConfig) {
|
|
1444
|
-
if (tokenDelivery === 'json' && newToken && newToken !== 'success') {
|
|
1445
|
-
return originalReq.clone({
|
|
1446
|
-
setHeaders: { Authorization: `Bearer ${newToken}` },
|
|
1447
|
-
});
|
|
1448
|
-
}
|
|
1449
|
-
// Cookies mode: Browser automatically sends updated httpOnly cookies (access/refresh tokens).
|
|
1450
|
-
// However, CSRF token must match the cookie value at the moment of retry.
|
|
1451
|
-
// We ALWAYS re-read from document.cookie here (using defaults when csrfConfig
|
|
1452
|
-
// is not provided) to avoid stale header values after refresh or across tabs.
|
|
1453
|
-
if (tokenDelivery === 'cookies' && ['POST', 'PUT', 'PATCH', 'DELETE'].includes(originalReq.method)) {
|
|
1454
|
-
const csrfCookieName = csrfConfig?.cookieName ?? 'nauth_csrf_token';
|
|
1455
|
-
const csrfHeaderName = csrfConfig?.headerName ?? 'x-csrf-token';
|
|
1456
|
-
const freshCsrfToken = getCsrfToken(csrfCookieName);
|
|
1457
|
-
// #region agent log
|
|
1458
|
-
fetch('http://127.0.0.1:7242/ingest/97f9fe53-6a8b-43e2-ae9b-4b2d0f725816', {
|
|
1459
|
-
method: 'POST',
|
|
1460
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1461
|
-
body: JSON.stringify({
|
|
1462
|
-
location: 'auth.interceptor.ts:buildRetryRequest',
|
|
1463
|
-
message: 'Inside buildRetryRequest cookies branch',
|
|
1464
|
-
data: {
|
|
1465
|
-
originalReqBody: originalReq.body,
|
|
1466
|
-
originalReqBodyType: typeof originalReq.body,
|
|
1467
|
-
freshCsrfToken: freshCsrfToken?.substring(0, 8),
|
|
1468
|
-
method: originalReq.method,
|
|
1469
|
-
},
|
|
1470
|
-
timestamp: Date.now(),
|
|
1471
|
-
sessionId: 'debug-session',
|
|
1472
|
-
hypothesisId: 'C',
|
|
1473
|
-
}),
|
|
1474
|
-
}).catch(() => { });
|
|
1475
|
-
// #endregion
|
|
1476
|
-
if (freshCsrfToken) {
|
|
1477
|
-
// Clone with fresh CSRF token in header
|
|
1478
|
-
const cloned = originalReq.clone({
|
|
1479
|
-
setHeaders: { [csrfHeaderName]: freshCsrfToken },
|
|
1480
|
-
});
|
|
1481
|
-
// #region agent log
|
|
1482
|
-
fetch('http://127.0.0.1:7242/ingest/97f9fe53-6a8b-43e2-ae9b-4b2d0f725816', {
|
|
1483
|
-
method: 'POST',
|
|
1484
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1485
|
-
body: JSON.stringify({
|
|
1486
|
-
location: 'auth.interceptor.ts:buildRetryRequest:afterClone',
|
|
1487
|
-
message: 'After clone with setHeaders',
|
|
1488
|
-
data: { clonedBody: cloned.body, clonedBodyType: typeof cloned.body, originalBody: originalReq.body },
|
|
1489
|
-
timestamp: Date.now(),
|
|
1490
|
-
sessionId: 'debug-session',
|
|
1491
|
-
hypothesisId: 'D',
|
|
1492
|
-
}),
|
|
1493
|
-
}).catch(() => { });
|
|
1494
|
-
// #endregion
|
|
1495
|
-
return cloned;
|
|
1496
|
-
}
|
|
1497
|
-
}
|
|
1498
|
-
// No changes needed (GET request or no CSRF token available)
|
|
1499
|
-
return originalReq;
|
|
1500
|
-
}
|
|
1501
1344
|
/**
|
|
1502
1345
|
* Class-based interceptor for NgModule compatibility.
|
|
1503
1346
|
*/
|