@nauth-toolkit/client-angular 0.1.53 → 0.1.55
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/README.md +4 -177
- package/ng-package.json +12 -0
- package/package.json +22 -18
- package/src/lib/auth.guard.ts +86 -0
- package/src/lib/auth.interceptor.ts +194 -0
- package/src/lib/social-redirect-callback.guard.ts +87 -0
- package/src/ngmodule/auth.interceptor.class.ts +124 -0
- package/src/ngmodule/auth.service.ts +865 -0
- package/src/ngmodule/http-adapter.ts +79 -0
- package/src/ngmodule/nauth.module.ts +59 -0
- package/src/ngmodule/tokens.ts +7 -0
- package/src/package.json +11 -0
- package/src/public-api.ts +21 -0
- package/src/standalone/ng-package.json +7 -0
- package/src/standalone/package.json +8 -0
- package/src/standalone/public-api.ts +12 -0
- package/standalone/auth.guard.ts +86 -0
- package/standalone/auth.interceptor.ts +194 -0
- package/standalone/auth.service.ts +865 -0
- package/standalone/http-adapter.ts +79 -0
- package/standalone/ng-package.json +7 -0
- package/standalone/package.json +8 -0
- package/standalone/public-api.ts +18 -0
- package/standalone/social-redirect-callback.guard.ts +87 -0
- package/standalone/tokens.ts +7 -0
- package/tsconfig.json +10 -0
- package/tsconfig.lib.json +28 -0
- package/tsconfig.lib.prod.json +10 -0
- package/fesm2022/nauth-toolkit-client-angular.mjs +0 -1211
- package/fesm2022/nauth-toolkit-client-angular.mjs.map +0 -1
- package/types/nauth-toolkit-client-angular.d.ts +0 -747
package/README.md
CHANGED
|
@@ -1,182 +1,9 @@
|
|
|
1
1
|
# @nauth-toolkit/client-angular
|
|
2
2
|
|
|
3
|
-
Angular adapter for nauth-toolkit client SDK
|
|
3
|
+
Angular adapter for nauth-toolkit client SDK
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Preview Release Notice
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
- **authInterceptor** - HTTP interceptor for auth headers, CSRF, and token refresh
|
|
9
|
-
- **authGuard** - Route guard for protected routes
|
|
10
|
-
- **socialRedirectCallbackGuard** - OAuth callback handler
|
|
11
|
-
- **NAuthModule** - NgModule for non-standalone apps (Angular < 17)
|
|
12
|
-
- **AngularHttpAdapter** - Uses Angular's HttpClient for all requests
|
|
13
|
-
|
|
14
|
-
## Installation
|
|
15
|
-
|
|
16
|
-
```bash
|
|
17
|
-
npm install @nauth-toolkit/client-angular @nauth-toolkit/client
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
Or with Yarn:
|
|
21
|
-
|
|
22
|
-
```bash
|
|
23
|
-
yarn add @nauth-toolkit/client-angular @nauth-toolkit/client
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
## Quick Start
|
|
27
|
-
|
|
28
|
-
### Standalone Components (Angular 17+)
|
|
29
|
-
|
|
30
|
-
```typescript
|
|
31
|
-
// app.config.ts
|
|
32
|
-
import { ApplicationConfig, inject } from '@angular/core';
|
|
33
|
-
import { Router, provideRouter } from '@angular/router';
|
|
34
|
-
import { provideHttpClient, withInterceptors } from '@angular/common/http';
|
|
35
|
-
import { NAUTH_CLIENT_CONFIG, authInterceptor } from '@nauth-toolkit/client-angular';
|
|
36
|
-
|
|
37
|
-
export const appConfig: ApplicationConfig = {
|
|
38
|
-
providers: [
|
|
39
|
-
provideRouter(routes),
|
|
40
|
-
{
|
|
41
|
-
provide: NAUTH_CLIENT_CONFIG,
|
|
42
|
-
useFactory: () => {
|
|
43
|
-
const router = inject(Router);
|
|
44
|
-
return {
|
|
45
|
-
baseUrl: 'https://api.example.com/auth',
|
|
46
|
-
tokenDelivery: 'cookies',
|
|
47
|
-
onSessionExpired: () => router.navigate(['/login']),
|
|
48
|
-
};
|
|
49
|
-
},
|
|
50
|
-
},
|
|
51
|
-
provideHttpClient(withInterceptors([authInterceptor])),
|
|
52
|
-
],
|
|
53
|
-
};
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
### NgModule (Angular < 17)
|
|
57
|
-
|
|
58
|
-
```typescript
|
|
59
|
-
// app.module.ts
|
|
60
|
-
import { NgModule } from '@angular/core';
|
|
61
|
-
import { BrowserModule } from '@angular/platform-browser';
|
|
62
|
-
import { Router } from '@angular/router';
|
|
63
|
-
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
|
|
64
|
-
import { NAuthModule, AuthInterceptor, NAUTH_CLIENT_CONFIG } from '@nauth-toolkit/client-angular';
|
|
65
|
-
|
|
66
|
-
@NgModule({
|
|
67
|
-
imports: [BrowserModule, HttpClientModule, NAuthModule],
|
|
68
|
-
providers: [
|
|
69
|
-
{
|
|
70
|
-
provide: NAUTH_CLIENT_CONFIG,
|
|
71
|
-
useFactory: (router: Router) => ({
|
|
72
|
-
baseUrl: 'https://api.example.com/auth',
|
|
73
|
-
tokenDelivery: 'cookies',
|
|
74
|
-
onSessionExpired: () => router.navigate(['/login']),
|
|
75
|
-
}),
|
|
76
|
-
deps: [Router],
|
|
77
|
-
},
|
|
78
|
-
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
|
|
79
|
-
],
|
|
80
|
-
bootstrap: [AppComponent],
|
|
81
|
-
})
|
|
82
|
-
export class AppModule {}
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
## Usage
|
|
86
|
-
|
|
87
|
-
### Authentication
|
|
88
|
-
|
|
89
|
-
```typescript
|
|
90
|
-
import { Component } from '@angular/core';
|
|
91
|
-
import { Router } from '@angular/router';
|
|
92
|
-
import { AuthService } from '@nauth-toolkit/client-angular';
|
|
93
|
-
|
|
94
|
-
@Component({
|
|
95
|
-
selector: 'app-login',
|
|
96
|
-
template: `
|
|
97
|
-
<form (ngSubmit)="login()">
|
|
98
|
-
<input [(ngModel)]="email" placeholder="Email" />
|
|
99
|
-
<input [(ngModel)]="password" type="password" />
|
|
100
|
-
<button type="submit">Login</button>
|
|
101
|
-
</form>
|
|
102
|
-
`,
|
|
103
|
-
})
|
|
104
|
-
export class LoginComponent {
|
|
105
|
-
email = '';
|
|
106
|
-
password = '';
|
|
107
|
-
|
|
108
|
-
constructor(
|
|
109
|
-
private auth: AuthService,
|
|
110
|
-
private router: Router,
|
|
111
|
-
) {}
|
|
112
|
-
|
|
113
|
-
async login() {
|
|
114
|
-
const response = await this.auth.login(this.email, this.password);
|
|
115
|
-
if (response.challengeName) {
|
|
116
|
-
this.router.navigate(['/verify', response.challengeName.toLowerCase()]);
|
|
117
|
-
} else {
|
|
118
|
-
this.router.navigate(['/dashboard']);
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
### Route Protection
|
|
125
|
-
|
|
126
|
-
```typescript
|
|
127
|
-
// app.routes.ts
|
|
128
|
-
import { Routes } from '@angular/router';
|
|
129
|
-
import { authGuard } from '@nauth-toolkit/client-angular';
|
|
130
|
-
|
|
131
|
-
export const routes: Routes = [
|
|
132
|
-
{ path: 'login', component: LoginComponent },
|
|
133
|
-
{
|
|
134
|
-
path: 'dashboard',
|
|
135
|
-
component: DashboardComponent,
|
|
136
|
-
canActivate: [authGuard()],
|
|
137
|
-
},
|
|
138
|
-
];
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
### Reactive State
|
|
142
|
-
|
|
143
|
-
```typescript
|
|
144
|
-
import { Component } from '@angular/core';
|
|
145
|
-
import { AuthService } from '@nauth-toolkit/client-angular';
|
|
146
|
-
import { AsyncPipe } from '@angular/common';
|
|
147
|
-
|
|
148
|
-
@Component({
|
|
149
|
-
selector: 'app-profile',
|
|
150
|
-
template: `
|
|
151
|
-
@if (currentUser$ | async; as user) {
|
|
152
|
-
<h1>Welcome, {{ user.email }}</h1>
|
|
153
|
-
}
|
|
154
|
-
`,
|
|
155
|
-
imports: [AsyncPipe],
|
|
156
|
-
})
|
|
157
|
-
export class ProfileComponent {
|
|
158
|
-
currentUser$ = this.auth.currentUser$;
|
|
159
|
-
|
|
160
|
-
constructor(private auth: AuthService) {}
|
|
161
|
-
}
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
## Compatibility
|
|
165
|
-
|
|
166
|
-
- **Angular**: 14.0.0+
|
|
167
|
-
- **RxJS**: 7.x or 8.x
|
|
168
|
-
- **Node.js**: 22.0.0+
|
|
169
|
-
|
|
170
|
-
## Documentation
|
|
171
|
-
|
|
172
|
-
Full documentation available at [https://nauth-toolkit.com/docs](https://nauth-toolkit.com/docs)
|
|
173
|
-
|
|
174
|
-
- [Angular Integration Guide](https://nauth-toolkit.com/docs/frontend-sdk/angular/overview)
|
|
175
|
-
- [API Reference](https://nauth-toolkit.com/docs/frontend-sdk/api/overview)
|
|
176
|
-
- [Challenge Handling](https://nauth-toolkit.com/docs/frontend-sdk/guides/challenge-handling)
|
|
177
|
-
- [MFA Setup](https://nauth-toolkit.com/docs/frontend-sdk/guides/mfa-setup)
|
|
178
|
-
|
|
179
|
-
## License
|
|
180
|
-
|
|
181
|
-
UNLICENSED
|
|
7
|
+
**This is a preview release for internal testing. Do not use in production yet.**
|
|
182
8
|
|
|
9
|
+
This package is part of nauth-toolkit and is currently in early access/preview. Features and APIs may change between releases. For production use, please wait for the stable v1.0 release.
|
package/ng-package.json
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "node_modules/ng-packagr/ng-package.schema.json",
|
|
3
|
+
"dest": "./dist",
|
|
4
|
+
"lib": {
|
|
5
|
+
"entryFile": "src/public-api.ts",
|
|
6
|
+
"flatModuleFile": "nauth-toolkit-client-angular"
|
|
7
|
+
},
|
|
8
|
+
"allowedNonPeerDependencies": [
|
|
9
|
+
"@nauth-toolkit/client"
|
|
10
|
+
]
|
|
11
|
+
}
|
|
12
|
+
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nauth-toolkit/client-angular",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.55",
|
|
4
4
|
"description": "Angular adapter for nauth-toolkit client SDK",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"nauth",
|
|
@@ -22,27 +22,31 @@
|
|
|
22
22
|
"tag": "latest"
|
|
23
23
|
},
|
|
24
24
|
"peerDependencies": {
|
|
25
|
-
"@angular/common": ">=
|
|
26
|
-
"@angular/core": ">=
|
|
27
|
-
"@nauth-toolkit/client": "^0.1.
|
|
25
|
+
"@angular/common": ">=17.0.0",
|
|
26
|
+
"@angular/core": ">=17.0.0",
|
|
27
|
+
"@nauth-toolkit/client": "^0.1.55",
|
|
28
28
|
"rxjs": "^7.0.0 || ^8.0.0"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"tslib": "^2.3.0"
|
|
32
32
|
},
|
|
33
|
-
"
|
|
34
|
-
"
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@angular/common": "^17.3.12",
|
|
35
|
+
"@angular/compiler": "^17.3.12",
|
|
36
|
+
"@angular/compiler-cli": "^17.3.12",
|
|
37
|
+
"@angular/core": "^17.3.12",
|
|
38
|
+
"@angular/platform-browser": "^17.3.12",
|
|
39
|
+
"@angular/platform-browser-dynamic": "^17.3.12",
|
|
40
|
+
"@angular/router": "^17.3.12",
|
|
41
|
+
"ng-packagr": "^17.0.0",
|
|
42
|
+
"rxjs": "~7.8.0",
|
|
43
|
+
"typescript": "~5.3.3"
|
|
35
44
|
},
|
|
36
|
-
"
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
"./package.json": {
|
|
40
|
-
"default": "./package.json"
|
|
41
|
-
},
|
|
42
|
-
".": {
|
|
43
|
-
"types": "./types/nauth-toolkit-client-angular.d.ts",
|
|
44
|
-
"default": "./fesm2022/nauth-toolkit-client-angular.mjs"
|
|
45
|
-
}
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "ng-packagr -p ng-package.json",
|
|
47
|
+
"clean": "rm -rf dist"
|
|
46
48
|
},
|
|
47
|
-
"
|
|
48
|
-
|
|
49
|
+
"engines": {
|
|
50
|
+
"node": ">=22.0.0"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { inject } from '@angular/core';
|
|
2
|
+
import { CanActivateFn, Router, UrlTree } from '@angular/router';
|
|
3
|
+
import { AuthService } from '../ngmodule/auth.service';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Functional route guard for authentication (Angular 17+).
|
|
7
|
+
*
|
|
8
|
+
* Protects routes by checking if user is authenticated.
|
|
9
|
+
* Redirects to login page if not authenticated.
|
|
10
|
+
*
|
|
11
|
+
* @param redirectTo - Path to redirect to if not authenticated (default: '/login')
|
|
12
|
+
* @returns CanActivateFn guard function
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```typescript
|
|
16
|
+
* // In route configuration
|
|
17
|
+
* const routes: Routes = [
|
|
18
|
+
* {
|
|
19
|
+
* path: 'home',
|
|
20
|
+
* component: HomeComponent,
|
|
21
|
+
* canActivate: [authGuard()]
|
|
22
|
+
* },
|
|
23
|
+
* {
|
|
24
|
+
* path: 'admin',
|
|
25
|
+
* component: AdminComponent,
|
|
26
|
+
* canActivate: [authGuard('/admin/login')]
|
|
27
|
+
* }
|
|
28
|
+
* ];
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export function authGuard(redirectTo = '/login'): CanActivateFn {
|
|
32
|
+
return (): boolean | UrlTree => {
|
|
33
|
+
const auth = inject(AuthService);
|
|
34
|
+
const router = inject(Router);
|
|
35
|
+
|
|
36
|
+
if (auth.isAuthenticated()) {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return router.createUrlTree([redirectTo]);
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Class-based authentication guard for NgModule compatibility.
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```typescript
|
|
49
|
+
* // In route configuration (NgModule)
|
|
50
|
+
* const routes: Routes = [
|
|
51
|
+
* {
|
|
52
|
+
* path: 'home',
|
|
53
|
+
* component: HomeComponent,
|
|
54
|
+
* canActivate: [AuthGuard]
|
|
55
|
+
* }
|
|
56
|
+
* ];
|
|
57
|
+
*
|
|
58
|
+
* // In module providers
|
|
59
|
+
* @NgModule({
|
|
60
|
+
* providers: [AuthGuard]
|
|
61
|
+
* })
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
export class AuthGuard {
|
|
65
|
+
/**
|
|
66
|
+
* @param auth - Authentication service
|
|
67
|
+
* @param router - Angular router
|
|
68
|
+
*/
|
|
69
|
+
constructor(
|
|
70
|
+
private auth: AuthService,
|
|
71
|
+
private router: Router,
|
|
72
|
+
) {}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Check if route can be activated.
|
|
76
|
+
*
|
|
77
|
+
* @returns True if authenticated, otherwise redirects to login
|
|
78
|
+
*/
|
|
79
|
+
canActivate(): boolean | UrlTree {
|
|
80
|
+
if (this.auth.isAuthenticated()) {
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return this.router.createUrlTree(['/login']);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { inject, PLATFORM_ID } from '@angular/core';
|
|
2
|
+
import { isPlatformBrowser } from '@angular/common';
|
|
3
|
+
import { HttpHandlerFn, HttpInterceptorFn, HttpRequest, HttpClient, HttpErrorResponse } from '@angular/common/http';
|
|
4
|
+
import { Router } from '@angular/router';
|
|
5
|
+
import { catchError, switchMap, throwError, filter, take, BehaviorSubject, from } from 'rxjs';
|
|
6
|
+
import { NAUTH_CLIENT_CONFIG } from '../ngmodule/tokens';
|
|
7
|
+
import { AuthService } from '../ngmodule/auth.service';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Refresh state management.
|
|
11
|
+
* BehaviorSubject pattern is the industry-standard for token refresh.
|
|
12
|
+
*/
|
|
13
|
+
let isRefreshing = false;
|
|
14
|
+
const refreshTokenSubject = new BehaviorSubject<string | null>(null);
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Track retried requests to prevent infinite loops.
|
|
18
|
+
*/
|
|
19
|
+
const retriedRequests = new WeakSet<HttpRequest<unknown>>();
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Get CSRF token from cookie.
|
|
23
|
+
*/
|
|
24
|
+
function getCsrfToken(cookieName: string): string | null {
|
|
25
|
+
if (typeof document === 'undefined') return null;
|
|
26
|
+
const match = document.cookie.match(new RegExp(`(^| )${cookieName}=([^;]+)`));
|
|
27
|
+
return match ? decodeURIComponent(match[2]) : null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Angular HTTP interceptor for nauth-toolkit.
|
|
32
|
+
*
|
|
33
|
+
* Handles:
|
|
34
|
+
* - Cookies mode: withCredentials + CSRF tokens + refresh via POST
|
|
35
|
+
* - JSON mode: refresh via SDK, retry with new token
|
|
36
|
+
*/
|
|
37
|
+
export const authInterceptor: HttpInterceptorFn = (req: HttpRequest<unknown>, next: HttpHandlerFn) => {
|
|
38
|
+
const config = inject(NAUTH_CLIENT_CONFIG);
|
|
39
|
+
const http = inject(HttpClient);
|
|
40
|
+
const authService = inject(AuthService);
|
|
41
|
+
const platformId = inject(PLATFORM_ID);
|
|
42
|
+
const router = inject(Router);
|
|
43
|
+
const isBrowser = isPlatformBrowser(platformId);
|
|
44
|
+
|
|
45
|
+
if (!isBrowser) {
|
|
46
|
+
return next(req);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const tokenDelivery = config.tokenDelivery;
|
|
50
|
+
const baseUrl = config.baseUrl;
|
|
51
|
+
const endpoints = config.endpoints ?? {};
|
|
52
|
+
const refreshPath = endpoints.refresh ?? '/refresh';
|
|
53
|
+
const loginPath = endpoints.login ?? '/login';
|
|
54
|
+
const signupPath = endpoints.signup ?? '/signup';
|
|
55
|
+
const socialExchangePath = endpoints.socialExchange ?? '/social/exchange';
|
|
56
|
+
const refreshUrl = `${baseUrl}${refreshPath}`;
|
|
57
|
+
|
|
58
|
+
const isAuthApiRequest = req.url.includes(baseUrl);
|
|
59
|
+
const isRefreshEndpoint = req.url.includes(refreshPath);
|
|
60
|
+
const isPublicEndpoint =
|
|
61
|
+
req.url.includes(loginPath) || req.url.includes(signupPath) || req.url.includes(socialExchangePath);
|
|
62
|
+
|
|
63
|
+
// Build request with credentials (cookies mode only)
|
|
64
|
+
let authReq = req;
|
|
65
|
+
if (tokenDelivery === 'cookies') {
|
|
66
|
+
authReq = authReq.clone({ withCredentials: true });
|
|
67
|
+
|
|
68
|
+
if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(req.method)) {
|
|
69
|
+
const csrfCookieName = config.csrf?.cookieName ?? 'nauth_csrf_token';
|
|
70
|
+
const csrfHeaderName = config.csrf?.headerName ?? 'x-csrf-token';
|
|
71
|
+
const csrfToken = getCsrfToken(csrfCookieName);
|
|
72
|
+
if (csrfToken) {
|
|
73
|
+
authReq = authReq.clone({ setHeaders: { [csrfHeaderName]: csrfToken } });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return next(authReq).pipe(
|
|
79
|
+
catchError((error: unknown) => {
|
|
80
|
+
const shouldHandle =
|
|
81
|
+
error instanceof HttpErrorResponse &&
|
|
82
|
+
error.status === 401 &&
|
|
83
|
+
isAuthApiRequest &&
|
|
84
|
+
!isRefreshEndpoint &&
|
|
85
|
+
!isPublicEndpoint &&
|
|
86
|
+
!retriedRequests.has(req);
|
|
87
|
+
|
|
88
|
+
if (!shouldHandle) {
|
|
89
|
+
return throwError(() => error);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (config.debug) {
|
|
93
|
+
console.warn('[nauth-interceptor] 401 detected:', req.url);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!isRefreshing) {
|
|
97
|
+
isRefreshing = true;
|
|
98
|
+
refreshTokenSubject.next(null);
|
|
99
|
+
|
|
100
|
+
if (config.debug) {
|
|
101
|
+
console.warn('[nauth-interceptor] Starting refresh...');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Refresh based on mode
|
|
105
|
+
const refresh$ =
|
|
106
|
+
tokenDelivery === 'cookies'
|
|
107
|
+
? http.post<{ accessToken?: string }>(refreshUrl, {}, { withCredentials: true })
|
|
108
|
+
: from(authService.refresh());
|
|
109
|
+
|
|
110
|
+
return refresh$.pipe(
|
|
111
|
+
switchMap((response) => {
|
|
112
|
+
if (config.debug) {
|
|
113
|
+
console.warn('[nauth-interceptor] Refresh successful');
|
|
114
|
+
}
|
|
115
|
+
isRefreshing = false;
|
|
116
|
+
|
|
117
|
+
// Get new token (JSON mode) or signal success (cookies mode)
|
|
118
|
+
const newToken = 'accessToken' in response ? response.accessToken : 'success';
|
|
119
|
+
refreshTokenSubject.next(newToken ?? 'success');
|
|
120
|
+
|
|
121
|
+
// Build retry request
|
|
122
|
+
const retryReq = buildRetryRequest(authReq, tokenDelivery, newToken);
|
|
123
|
+
retriedRequests.add(retryReq);
|
|
124
|
+
|
|
125
|
+
if (config.debug) {
|
|
126
|
+
console.warn('[nauth-interceptor] Retrying:', req.url);
|
|
127
|
+
}
|
|
128
|
+
return next(retryReq);
|
|
129
|
+
}),
|
|
130
|
+
catchError((err) => {
|
|
131
|
+
if (config.debug) {
|
|
132
|
+
console.error('[nauth-interceptor] Refresh failed:', err);
|
|
133
|
+
}
|
|
134
|
+
isRefreshing = false;
|
|
135
|
+
refreshTokenSubject.next(null);
|
|
136
|
+
|
|
137
|
+
// Handle session expiration - redirect to configured URL
|
|
138
|
+
if (config.redirects?.sessionExpired) {
|
|
139
|
+
router.navigateByUrl(config.redirects.sessionExpired).catch((navError) => {
|
|
140
|
+
if (config.debug) {
|
|
141
|
+
console.error('[nauth-interceptor] Navigation failed:', navError);
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return throwError(() => err);
|
|
147
|
+
}),
|
|
148
|
+
);
|
|
149
|
+
} else {
|
|
150
|
+
// Wait for ongoing refresh
|
|
151
|
+
if (config.debug) {
|
|
152
|
+
console.warn('[nauth-interceptor] Waiting for refresh...');
|
|
153
|
+
}
|
|
154
|
+
return refreshTokenSubject.pipe(
|
|
155
|
+
filter((token): token is string => token !== null),
|
|
156
|
+
take(1),
|
|
157
|
+
switchMap((token) => {
|
|
158
|
+
if (config.debug) {
|
|
159
|
+
console.warn('[nauth-interceptor] Refresh done, retrying:', req.url);
|
|
160
|
+
}
|
|
161
|
+
const retryReq = buildRetryRequest(authReq, tokenDelivery, token);
|
|
162
|
+
retriedRequests.add(retryReq);
|
|
163
|
+
return next(retryReq);
|
|
164
|
+
}),
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
}),
|
|
168
|
+
);
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Build retry request with appropriate auth.
|
|
173
|
+
*/
|
|
174
|
+
function buildRetryRequest(
|
|
175
|
+
originalReq: HttpRequest<unknown>,
|
|
176
|
+
tokenDelivery: string,
|
|
177
|
+
newToken?: string,
|
|
178
|
+
): HttpRequest<unknown> {
|
|
179
|
+
if (tokenDelivery === 'json' && newToken && newToken !== 'success') {
|
|
180
|
+
return originalReq.clone({
|
|
181
|
+
setHeaders: { Authorization: `Bearer ${newToken}` },
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
return originalReq.clone();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Class-based interceptor for NgModule compatibility.
|
|
189
|
+
*/
|
|
190
|
+
export class AuthInterceptor {
|
|
191
|
+
intercept(req: HttpRequest<unknown>, next: HttpHandlerFn) {
|
|
192
|
+
return authInterceptor(req, next);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { inject, PLATFORM_ID } from '@angular/core';
|
|
2
|
+
import { isPlatformBrowser } from '@angular/common';
|
|
3
|
+
import { type CanActivateFn } from '@angular/router';
|
|
4
|
+
import { AuthService } from '../ngmodule/auth.service';
|
|
5
|
+
import { NAUTH_CLIENT_CONFIG } from '../ngmodule/tokens';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Social redirect callback route guard.
|
|
9
|
+
*
|
|
10
|
+
* This guard supports the redirect-first social flow where the backend redirects
|
|
11
|
+
* back to the frontend with:
|
|
12
|
+
* - `appState` (always optional)
|
|
13
|
+
* - `exchangeToken` (present for json/hybrid flows, and for cookie flows that return a challenge)
|
|
14
|
+
* - `error` / `error_description` (provider errors)
|
|
15
|
+
*
|
|
16
|
+
* Behavior:
|
|
17
|
+
* - If `exchangeToken` exists: exchanges it via backend and redirects to success or challenge routes.
|
|
18
|
+
* - If no `exchangeToken`: treat as cookie-success path and redirect to success route.
|
|
19
|
+
* - If `error` exists: redirects to oauthError route.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```typescript
|
|
23
|
+
* import { socialRedirectCallbackGuard } from '@nauth-toolkit/client/angular';
|
|
24
|
+
*
|
|
25
|
+
* export const routes: Routes = [
|
|
26
|
+
* { path: 'auth/callback', canActivate: [socialRedirectCallbackGuard], component: CallbackComponent },
|
|
27
|
+
* ];
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export const socialRedirectCallbackGuard: CanActivateFn = async (): Promise<boolean> => {
|
|
31
|
+
const auth = inject(AuthService);
|
|
32
|
+
const config = inject(NAUTH_CLIENT_CONFIG);
|
|
33
|
+
const platformId = inject(PLATFORM_ID);
|
|
34
|
+
const isBrowser = isPlatformBrowser(platformId);
|
|
35
|
+
|
|
36
|
+
if (!isBrowser) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const params = new URLSearchParams(window.location.search);
|
|
41
|
+
const error = params.get('error');
|
|
42
|
+
const exchangeToken = params.get('exchangeToken');
|
|
43
|
+
|
|
44
|
+
// Provider error: redirect to oauthError
|
|
45
|
+
if (error) {
|
|
46
|
+
const errorUrl = config.redirects?.oauthError || '/login';
|
|
47
|
+
window.location.replace(errorUrl);
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// No exchangeToken: cookie success path; redirect to success.
|
|
52
|
+
//
|
|
53
|
+
// Note: we do not "activate" the callback route to avoid consumers needing to render a page.
|
|
54
|
+
if (!exchangeToken) {
|
|
55
|
+
// ============================================================================
|
|
56
|
+
// Cookies mode: hydrate user state before redirecting
|
|
57
|
+
// ============================================================================
|
|
58
|
+
// WHY: In cookie delivery, the OAuth callback completes via browser redirects, so the frontend
|
|
59
|
+
// does not receive a JSON AuthResponse to populate the SDK's cached `currentUser`.
|
|
60
|
+
//
|
|
61
|
+
// Without this, sync guards (`authGuard`) can immediately redirect to /login because
|
|
62
|
+
// `currentUser` is still null even though cookies were set successfully.
|
|
63
|
+
try {
|
|
64
|
+
await auth.getProfile();
|
|
65
|
+
} catch {
|
|
66
|
+
const errorUrl = config.redirects?.oauthError || '/login';
|
|
67
|
+
window.location.replace(errorUrl);
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
const successUrl = config.redirects?.success || '/';
|
|
71
|
+
window.location.replace(successUrl);
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Exchange token and route accordingly
|
|
76
|
+
const response = await auth.exchangeSocialRedirect(exchangeToken);
|
|
77
|
+
if (response.challengeName) {
|
|
78
|
+
const challengeBase = config.redirects?.challengeBase || '/auth/challenge';
|
|
79
|
+
const challengeRoute = response.challengeName.toLowerCase().replace(/_/g, '-');
|
|
80
|
+
window.location.replace(`${challengeBase}/${challengeRoute}`);
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const successUrl = config.redirects?.success || '/';
|
|
85
|
+
window.location.replace(successUrl);
|
|
86
|
+
return false;
|
|
87
|
+
};
|