@sd-angular/core 19.0.35 → 19.0.38
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 +2 -4
- package/components/index.d.ts +1 -0
- package/components/query-builder/src/query-builder.model.d.ts +1 -1
- package/components/table/src/components/desktop-cell/desktop-cell.component.d.ts +1 -1
- package/components/table/src/table.component.d.ts +1 -1
- package/components/timeline/index.d.ts +2 -0
- package/components/{history/models/history.model.d.ts → timeline/models/timeline.model.d.ts} +1 -1
- package/components/timeline/src/timeline.component.d.ts +7 -0
- package/configurations/src/sd-core.configuration.d.ts +0 -1
- package/fesm2022/sd-angular-core-components-form-generic.mjs.map +1 -1
- package/fesm2022/sd-angular-core-components-query-bar.mjs +8 -8
- package/fesm2022/sd-angular-core-components-query-bar.mjs.map +1 -1
- package/fesm2022/sd-angular-core-components-query-builder.mjs +2 -2
- package/fesm2022/sd-angular-core-components-query-builder.mjs.map +1 -1
- package/fesm2022/sd-angular-core-components-tab-router.mjs +11 -5
- package/fesm2022/sd-angular-core-components-tab-router.mjs.map +1 -1
- package/fesm2022/sd-angular-core-components-table.mjs +110 -39
- package/fesm2022/sd-angular-core-components-table.mjs.map +1 -1
- package/fesm2022/sd-angular-core-components-timeline.mjs +49 -0
- package/fesm2022/sd-angular-core-components-timeline.mjs.map +1 -0
- package/fesm2022/sd-angular-core-components-upload-file.mjs.map +1 -1
- package/fesm2022/sd-angular-core-components.mjs +1 -0
- package/fesm2022/sd-angular-core-components.mjs.map +1 -1
- package/fesm2022/sd-angular-core-configurations.mjs.map +1 -1
- package/fesm2022/sd-angular-core-forms-autocomplete.mjs +1 -1
- package/fesm2022/sd-angular-core-forms-autocomplete.mjs.map +1 -1
- package/fesm2022/sd-angular-core-forms-date-range.mjs +1 -1
- package/fesm2022/sd-angular-core-forms-date-range.mjs.map +1 -1
- package/fesm2022/sd-angular-core-forms-date.mjs +1 -1
- package/fesm2022/sd-angular-core-forms-date.mjs.map +1 -1
- package/fesm2022/sd-angular-core-forms-datetime.mjs +1 -1
- package/fesm2022/sd-angular-core-forms-datetime.mjs.map +1 -1
- package/fesm2022/sd-angular-core-forms-inline-text.mjs +1 -1
- package/fesm2022/sd-angular-core-forms-inline-text.mjs.map +1 -1
- package/fesm2022/sd-angular-core-forms-input-color.mjs +1 -1
- package/fesm2022/sd-angular-core-forms-input-color.mjs.map +1 -1
- package/fesm2022/sd-angular-core-forms-input-number.mjs +1 -1
- package/fesm2022/sd-angular-core-forms-input-number.mjs.map +1 -1
- package/fesm2022/sd-angular-core-forms-input.mjs +1 -1
- package/fesm2022/sd-angular-core-forms-input.mjs.map +1 -1
- package/fesm2022/sd-angular-core-forms-select.mjs +1 -1
- package/fesm2022/sd-angular-core-forms-select.mjs.map +1 -1
- package/fesm2022/sd-angular-core-i18n.mjs +10 -10
- package/fesm2022/sd-angular-core-i18n.mjs.map +1 -1
- package/fesm2022/sd-angular-core-modules-auth.mjs +35 -3
- package/fesm2022/sd-angular-core-modules-auth.mjs.map +1 -1
- package/fesm2022/sd-angular-core-modules-authom.mjs +847 -241
- package/fesm2022/sd-angular-core-modules-authom.mjs.map +1 -1
- package/fesm2022/sd-angular-core-modules-layout.mjs +4 -4
- package/fesm2022/sd-angular-core-modules-layout.mjs.map +1 -1
- package/fesm2022/sd-angular-core-services-cache.mjs +1 -1
- package/fesm2022/sd-angular-core-services-cache.mjs.map +1 -1
- package/fesm2022/sd-angular-core-services-confirm.mjs +2 -2
- package/fesm2022/sd-angular-core-services-confirm.mjs.map +1 -1
- package/fesm2022/sd-angular-core-services-storage.mjs.map +1 -1
- package/i18n/src/en.d.ts +1 -1
- package/modules/authom/authom.auth-configuration.d.ts +18 -0
- package/modules/authom/authom.configuration.d.ts +158 -2
- package/modules/authom/authom.event.d.ts +44 -0
- package/modules/authom/authom.guard.d.ts +13 -0
- package/modules/authom/authom.interceptor.d.ts +7 -1
- package/modules/authom/authom.module.d.ts +35 -5
- package/modules/authom/authom.return-url.d.ts +37 -0
- package/modules/authom/authom.service.d.ts +109 -15
- package/modules/authom/authom.session-channel.d.ts +26 -0
- package/modules/authom/index.d.ts +19 -9
- package/package.json +17 -19
- package/utilities/extensions/src/string.extension.d.ts +6 -2
- package/utilities/models/src/maybe-async.model.d.ts +1 -1
- package/components/base/index.d.ts +0 -1
- package/components/base/src/base-secure.component.d.ts +0 -8
- package/components/history/index.d.ts +0 -1
- package/components/history/src/history.component.d.ts +0 -7
- package/fesm2022/sd-angular-core-components-base.mjs +0 -23
- package/fesm2022/sd-angular-core-components-base.mjs.map +0 -1
- package/fesm2022/sd-angular-core-components-history.mjs +0 -51
- package/fesm2022/sd-angular-core-components-history.mjs.map +0 -1
- package/fesm2022/sd-angular-core-services-license.mjs +0 -97
- package/fesm2022/sd-angular-core-services-license.mjs.map +0 -1
- package/services/license/index.d.ts +0 -1
- package/services/license/src/license.service.d.ts +0 -9
- /package/components/{history → timeline}/pipes/view-date.pipe.d.ts +0 -0
|
@@ -1,251 +1,695 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { InjectionToken, inject, PLATFORM_ID, signal, computed, Injectable, provideAppInitializer, makeEnvironmentProviders, APP_INITIALIZER, NgModule } from '@angular/core';
|
|
2
|
+
import { InjectionToken, inject, PLATFORM_ID, DestroyRef, signal, computed, Injectable, provideAppInitializer, makeEnvironmentProviders, APP_INITIALIZER, NgModule } from '@angular/core';
|
|
3
3
|
import { isPlatformBrowser } from '@angular/common';
|
|
4
|
-
import {
|
|
4
|
+
import { OAuthService, OAuthErrorEvent, provideOAuthClient } from 'angular-oauth2-oidc';
|
|
5
|
+
import { Subject, from, switchMap, catchError, throwError, map, of } from 'rxjs';
|
|
6
|
+
import { HttpErrorResponse } from '@angular/common/http';
|
|
7
|
+
import { toObservable } from '@angular/core/rxjs-interop';
|
|
8
|
+
import { SD_AUTH_CONFIGURATION } from '@sd-angular/core/modules/auth';
|
|
5
9
|
|
|
6
10
|
const SD_AUTHOM_CONFIGURATION = new InjectionToken('sd-authom.configuration');
|
|
11
|
+
/** Scope mặc định. `offline_access` giữ cho token endpoint phát hành refresh_token. */
|
|
12
|
+
const SD_AUTHOM_DEFAULT_SCOPE = 'openid profile email offline_access';
|
|
13
|
+
/** Tỷ lệ vòng đời token mà tại đó refresh chủ động được kích hoạt. */
|
|
14
|
+
const SD_AUTHOM_DEFAULT_TIMEOUT_FACTOR = 0.75;
|
|
15
|
+
/** Timeout mặc định của silent refresh bằng iframe, tính bằng giây. */
|
|
16
|
+
const SD_AUTHOM_DEFAULT_SILENT_REFRESH_TIMEOUT_SECONDS = 20;
|
|
7
17
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Giữ deep link sống sót qua vòng redirect đăng nhập.
|
|
20
|
+
*
|
|
21
|
+
* AuthOM chỉ chấp nhận một `redirect_uri` đã đăng ký (các portal hiện dùng `/callback`), nên một
|
|
22
|
+
* user mở thẳng link sâu mà chưa có token sẽ mất đích đến: guard gọi code flow, cookie SSO của
|
|
23
|
+
* AuthOM trả lời ngay không cần hỏi, và trình duyệt quay về `/callback` rồi bị route mặc định đẩy
|
|
24
|
+
* sang trang chủ.
|
|
25
|
+
*
|
|
26
|
+
* Đích đến đi kèm tham số OIDC `state` (IdP round-trip nguyên vẹn) chứ không nằm trong storage:
|
|
27
|
+
* như vậy nó gắn với đúng một lần đăng nhập, không thể cũ đi và không rò sang điều hướng khác.
|
|
28
|
+
*/
|
|
29
|
+
/** Đường dẫn callback mặc định — khôi phục nó chỉ khiến user bị đẩy về trang chủ lần nữa. */
|
|
30
|
+
const DEFAULT_CALLBACK_PATH = '/callback';
|
|
31
|
+
/**
|
|
32
|
+
* Mã hoá trang mà user thực sự muốn vào, để gửi kèm `initCodeFlow`.
|
|
33
|
+
*
|
|
34
|
+
* Trả về `undefined` khi không có gì đáng khôi phục — lúc đó caller khởi động đăng nhập bình thường.
|
|
35
|
+
*
|
|
36
|
+
* Giá trị trả về là chuỗi THÔ, cố ý không percent-encode: client OIDC đã tự
|
|
37
|
+
* `encodeURIComponent(additionalState)` trước khi ghép vào `state`, và chuỗi giải mã của nó chỉ bóc
|
|
38
|
+
* đúng một lớp. Encode thêm ở đây làm giá trị quay về dạng double-encoded (`%252F...`) và
|
|
39
|
+
* `resolveReturnUrl` sẽ loại bỏ nó.
|
|
40
|
+
*/
|
|
41
|
+
function captureReturnUrl(requestedUrl, callbackPath = DEFAULT_CALLBACK_PATH) {
|
|
42
|
+
const url = requestedUrl?.trim();
|
|
43
|
+
if (!url || !isRestorableAppUrl(url, callbackPath)) {
|
|
44
|
+
return undefined;
|
|
24
45
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
return
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
46
|
+
return url;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Đọc lại giá trị mà `captureReturnUrl` đã gửi đi; trả `null` khi `state` rỗng hoặc không khôi phục được.
|
|
50
|
+
*
|
|
51
|
+
* Một lần `decodeURIComponent` để bóc lớp mã hoá mà client OIDC áp lên phần state của app; lớp ngoài
|
|
52
|
+
* bọc cả tham số đã được chính nó gỡ trước khi giá trị tới đây. Chuỗi không mã hoá sẽ giải mã ra
|
|
53
|
+
* chính nó, nên hàm vẫn đúng trong cả hai trường hợp.
|
|
54
|
+
*/
|
|
55
|
+
function resolveReturnUrl(state, callbackPath = DEFAULT_CALLBACK_PATH) {
|
|
56
|
+
const raw = state?.trim();
|
|
57
|
+
if (!raw) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
let url;
|
|
61
|
+
try {
|
|
62
|
+
url = decodeURIComponent(raw);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
// why: percent-encoding hỏng — bị can thiệp trên đường truyền hoặc IdP làm méo.
|
|
66
|
+
// Bỏ hẳn thay vì đoán đích đến.
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
return isRestorableAppUrl(url, callbackPath) ? url : null;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* `state` đi vòng qua IdP nên kẻ tấn công có thể tác động; chỉ chấp nhận path nằm trong chính app này.
|
|
73
|
+
*
|
|
74
|
+
* URL tuyệt đối hoặc protocol-relative (`//evil.example`, hoặc `/\evil.example` — trình duyệt tự
|
|
75
|
+
* chuẩn hoá dấu gạch ngược) sẽ biến vòng đăng nhập thành open redirect.
|
|
76
|
+
*/
|
|
77
|
+
function isRestorableAppUrl(url, callbackPath = DEFAULT_CALLBACK_PATH) {
|
|
78
|
+
if (!url.startsWith('/') || url.startsWith('//') || url.startsWith('/\\')) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
// why: ký tự điều khiển (CR/LF/NUL/DEL) trong path mở đường cho header hoặc log injection.
|
|
82
|
+
if ([...url].some(character => character.charCodeAt(0) < 0x20 || character.charCodeAt(0) === 0x7f)) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
return url.split(/[?#]/)[0] !== callbackPath;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Đồng bộ đăng xuất giữa các tab cùng origin.
|
|
90
|
+
*
|
|
91
|
+
* Phiên AuthOM sống trong bộ nhớ của client OIDC, có `sessionStorage` làm nền — mà `sessionStorage`
|
|
92
|
+
* bị trình duyệt cô lập theo tab. Không có server-side session để thu hồi (AuthOM không công bố
|
|
93
|
+
* `end_session_endpoint`) và cũng không có cơ chế kiểm tra liveness xuyên tab. Hệ quả: đăng xuất ở
|
|
94
|
+
* một tab để các tab còn lại dùng được bình thường cho tới khi access token của chính chúng hết hạn.
|
|
95
|
+
*
|
|
96
|
+
* `BroadcastChannel` không bao giờ gửi message ngược về đối tượng đã post, nên tab kích hoạt đăng
|
|
97
|
+
* xuất không tự xử lý lại sự kiện của chính nó.
|
|
98
|
+
*/
|
|
99
|
+
const LOGOUT_MESSAGE_TYPE = 'sd-authom:logout';
|
|
100
|
+
/**
|
|
101
|
+
* Bọc `BroadcastChannel` cho tín hiệu đăng xuất, và tự vô hiệu hoá khi môi trường không hỗ trợ
|
|
102
|
+
* (SSR, trình duyệt cũ, `BroadcastChannel` bị chặn).
|
|
103
|
+
*/
|
|
104
|
+
class SdAuthOmSessionChannel {
|
|
105
|
+
#channel;
|
|
106
|
+
constructor(name) {
|
|
107
|
+
this.#channel = SdAuthOmSessionChannel.#create(name);
|
|
108
|
+
}
|
|
109
|
+
/** `true` khi môi trường thực sự hỗ trợ kênh; `false` thì mọi thao tác là no-op. */
|
|
110
|
+
get enabled() {
|
|
111
|
+
return this.#channel !== null;
|
|
56
112
|
}
|
|
57
|
-
|
|
58
|
-
|
|
113
|
+
/** Báo cho mọi tab khác rằng phiên vừa kết thúc. */
|
|
114
|
+
notifyLogout() {
|
|
115
|
+
const message = { type: LOGOUT_MESSAGE_TYPE };
|
|
116
|
+
this.#channel?.postMessage(message);
|
|
117
|
+
}
|
|
118
|
+
/** Đăng ký callback chạy khi một tab khác báo đã đăng xuất. */
|
|
119
|
+
onLogout(handler) {
|
|
120
|
+
if (!this.#channel) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
this.#channel.onmessage = (event) => {
|
|
124
|
+
if (event.data?.type === LOGOUT_MESSAGE_TYPE) {
|
|
125
|
+
handler();
|
|
126
|
+
}
|
|
127
|
+
};
|
|
59
128
|
}
|
|
60
|
-
|
|
61
|
-
|
|
129
|
+
close() {
|
|
130
|
+
this.#channel?.close();
|
|
62
131
|
}
|
|
63
|
-
|
|
132
|
+
static #create(name) {
|
|
133
|
+
if (typeof BroadcastChannel === 'undefined') {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
64
136
|
try {
|
|
65
|
-
|
|
66
|
-
if (parts.length !== 3)
|
|
67
|
-
return null;
|
|
68
|
-
const padded = parts[1].replace(/-/g, '+').replace(/_/g, '/');
|
|
69
|
-
const json = atob(padded + '==='.slice((padded.length + 3) % 4));
|
|
70
|
-
return JSON.parse(json);
|
|
137
|
+
return new BroadcastChannel(name);
|
|
71
138
|
}
|
|
72
139
|
catch {
|
|
140
|
+
// why: một số trình duyệt ném khi site data bị chặn. Đăng xuất xuyên tab là tính năng
|
|
141
|
+
// phụ trợ, không được phép làm hỏng bootstrap.
|
|
73
142
|
return null;
|
|
74
143
|
}
|
|
75
144
|
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Sự kiện của client OIDC được coi là lỗi xác thực ngoài luồng refresh. */
|
|
148
|
+
const AUTH_ERROR_EVENT_TYPES = new Set([
|
|
149
|
+
'discovery_document_load_error',
|
|
150
|
+
'discovery_document_validation_error',
|
|
151
|
+
'jwks_load_error',
|
|
152
|
+
'invalid_nonce_in_state',
|
|
153
|
+
'code_error',
|
|
154
|
+
'token_error',
|
|
155
|
+
'token_validation_error',
|
|
156
|
+
]);
|
|
157
|
+
/** Sự kiện của client OIDC báo một lần gia hạn token thất bại. */
|
|
158
|
+
const REFRESH_ERROR_EVENT_TYPES = new Set(['token_refresh_error', 'silent_refresh_error', 'silent_refresh_timeout']);
|
|
159
|
+
/**
|
|
160
|
+
* Client xác thực AuthOM: Authorization Code + PKCE trên discovery document của IdP.
|
|
161
|
+
*
|
|
162
|
+
* Service sở hữu toàn bộ vòng đời phiên — bootstrap, khôi phục deep link, gia hạn token chủ động,
|
|
163
|
+
* phát hiện hết phiên, đăng xuất xuyên tab — nên app chỉ cần khai báo cấu hình tenant.
|
|
164
|
+
*/
|
|
165
|
+
class SdAuthOmService {
|
|
166
|
+
#platformId = inject(PLATFORM_ID);
|
|
167
|
+
#isBrowser = isPlatformBrowser(this.#platformId);
|
|
168
|
+
#destroyRef = inject(DestroyRef);
|
|
169
|
+
/**
|
|
170
|
+
* Optional để một app chỉ đăng ký `SdAuthOmInterceptor` mà quên `provideSdAuthOm()` thì bị hỏng
|
|
171
|
+
* đúng luồng đăng nhập, chứ không ném NullInjectorError trên mọi HTTP request.
|
|
172
|
+
*/
|
|
173
|
+
#oauth = inject(OAuthService, { optional: true });
|
|
174
|
+
/** Cấu hình tenant đang dùng. `undefined` cho tới khi `init()` chạy xong. */
|
|
175
|
+
config;
|
|
176
|
+
accessToken = signal(null);
|
|
177
|
+
idTokenClaims = signal(null);
|
|
178
|
+
isAuthenticated = computed(() => this.accessToken() !== null);
|
|
179
|
+
/** `true` khi gia hạn đã thất bại VÀ access token cuối cùng cũng đã hết hạn. */
|
|
180
|
+
sessionExpired = signal(false);
|
|
181
|
+
/** Sự kiện `auth_error` gần nhất, hoặc `null` khi lần đăng nhập gần nhất sạch. */
|
|
182
|
+
lastAuthError = signal(null);
|
|
183
|
+
#events = new Subject();
|
|
184
|
+
/** Luồng sự kiện xác thực. Xem `SdAuthOmEventType`. */
|
|
185
|
+
events$ = this.#events.asObservable();
|
|
186
|
+
#channel = null;
|
|
187
|
+
#eventsSubscription = null;
|
|
188
|
+
#expiryCheckTimer = null;
|
|
189
|
+
#inFlightRefresh = null;
|
|
190
|
+
#initialized = false;
|
|
191
|
+
constructor() {
|
|
192
|
+
this.#destroyRef.onDestroy(() => this.#teardown());
|
|
193
|
+
}
|
|
194
|
+
// ---------------------------------------------------------------------------------------------
|
|
195
|
+
// Bootstrap
|
|
196
|
+
// ---------------------------------------------------------------------------------------------
|
|
197
|
+
/**
|
|
198
|
+
* Cấu hình client, tải discovery document, xử lý callback nếu có, rồi bật gia hạn chủ động.
|
|
199
|
+
*
|
|
200
|
+
* Chạy trong APP_INITIALIZER (xem `provideSdAuthOm`), tức trước điều hướng đầu tiên của Router —
|
|
201
|
+
* nhờ vậy guard của route đầu tiên đã thấy trạng thái phiên đúng.
|
|
202
|
+
*
|
|
203
|
+
* @returns `true` khi kết thúc với một access token hợp lệ.
|
|
204
|
+
*/
|
|
205
|
+
async init(config) {
|
|
206
|
+
this.config = config;
|
|
207
|
+
if (!this.#isBrowser || !this.#oauth) {
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
const authErrorHref = config.authErrorUrl ? this.#toPublicHtmlUrl(config.authErrorUrl) : null;
|
|
211
|
+
// why: đang đứng sẵn ở trang lỗi thì KHÔNG init lại, tránh vòng lặp redirect khi IdP sập lâu.
|
|
212
|
+
if (authErrorHref && window.location.pathname === new URL(authErrorHref).pathname) {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
this.#oauth.configure(this.buildAuthConfig(config));
|
|
216
|
+
if (!this.#initialized) {
|
|
217
|
+
this.#initialized = true;
|
|
218
|
+
this.#subscribeOAuthEvents();
|
|
219
|
+
}
|
|
220
|
+
try {
|
|
221
|
+
await this.#oauth.loadDiscoveryDocumentAndTryLogin();
|
|
222
|
+
}
|
|
223
|
+
catch (error) {
|
|
224
|
+
this.#emitAuthError('discovery_document_load_error', error);
|
|
225
|
+
if (authErrorHref) {
|
|
226
|
+
this.navigate(authErrorHref);
|
|
227
|
+
}
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
this.#restoreReturnUrl();
|
|
231
|
+
this.#syncFromOAuth();
|
|
232
|
+
this.#emit('ready');
|
|
233
|
+
this.#oauth.setupAutomaticSilentRefresh({}, 'access_token');
|
|
234
|
+
if (config.crossTabLogout !== false) {
|
|
235
|
+
this.#listenCrossTabLogout(config);
|
|
236
|
+
}
|
|
237
|
+
return this.isAuthenticated();
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Quy đổi cấu hình tenant sang `AuthConfig` của client OIDC.
|
|
241
|
+
*
|
|
242
|
+
* Public để app kiểm tra được cấu hình thực tế trong test mà không phải chạy cả `init()`.
|
|
243
|
+
*/
|
|
244
|
+
buildAuthConfig(config) {
|
|
245
|
+
const audience = config.audience?.trim();
|
|
246
|
+
const silentRefreshSeconds = config.silentRefreshTimeoutInSeconds ?? config.authorizeTimeoutInSeconds ?? SD_AUTHOM_DEFAULT_SILENT_REFRESH_TIMEOUT_SECONDS;
|
|
247
|
+
const authConfig = {
|
|
248
|
+
issuer: this.#resolveIssuer(config),
|
|
249
|
+
clientId: config.clientId,
|
|
250
|
+
redirectUri: config.redirectUri || window.location.origin,
|
|
251
|
+
postLogoutRedirectUri: config.postLogoutRedirectUri || window.location.origin,
|
|
252
|
+
responseType: 'code',
|
|
253
|
+
scope: config.scope || SD_AUTHOM_DEFAULT_SCOPE,
|
|
254
|
+
// why: giữ bật (mặc định của client OIDC). Discovery document thật của AuthOM QC đặt mọi
|
|
255
|
+
// endpoint trong cây issuer nên phép kiểm tra này đi qua; chỉ tenant nào công bố endpoint
|
|
256
|
+
// ngoài cây issuer mới cần tắt.
|
|
257
|
+
strictDiscoveryDocumentValidation: config.strictDiscoveryDocumentValidation ?? true,
|
|
258
|
+
// why: 'remoteOnly' vẫn ép HTTPS với IdP thật nhưng cho phép chạy dev trên localhost.
|
|
259
|
+
requireHttps: config.requireHttps ?? 'remoteOnly',
|
|
260
|
+
showDebugInformation: config.showDebugInformation ?? false,
|
|
261
|
+
timeoutFactor: config.timeoutFactor ?? SD_AUTHOM_DEFAULT_TIMEOUT_FACTOR,
|
|
262
|
+
silentRefreshTimeout: silentRefreshSeconds * 1000,
|
|
263
|
+
useSilentRefresh: config.useSilentRefresh ?? false,
|
|
264
|
+
clearHashAfterLogin: true,
|
|
265
|
+
};
|
|
266
|
+
if (config.refreshThresholdSeconds !== undefined) {
|
|
267
|
+
// why: client OIDC đo ngưỡng refresh bằng "coi token hết hạn sớm N giây", không có
|
|
268
|
+
// tham số "refresh trước exp N giây" riêng. Hai cách diễn đạt cho cùng một hành vi.
|
|
269
|
+
authConfig.decreaseExpirationBySec = config.refreshThresholdSeconds;
|
|
270
|
+
}
|
|
271
|
+
if (config.clockSkewInSec !== undefined) {
|
|
272
|
+
authConfig.clockSkewInSec = config.clockSkewInSec;
|
|
273
|
+
}
|
|
274
|
+
if (config.useSilentRefresh) {
|
|
275
|
+
authConfig.silentRefreshRedirectUri = config.silentRefreshRedirectUri || `${window.location.origin}/silent-authom.html`;
|
|
276
|
+
}
|
|
277
|
+
// why: customQueryParams được gắn vào CẢ /authorize, CẢ lần đổi code lấy token, CẢ refresh —
|
|
278
|
+
// đó là đường duy nhất để `audience` bắt buộc của AuthOM tới được token endpoint.
|
|
279
|
+
const customQueryParams = { ...(config.customQueryParams ?? {}) };
|
|
280
|
+
if (audience) {
|
|
281
|
+
customQueryParams['audience'] = audience;
|
|
282
|
+
}
|
|
283
|
+
if (Object.keys(customQueryParams).length > 0) {
|
|
284
|
+
authConfig.customQueryParams = customQueryParams;
|
|
285
|
+
}
|
|
286
|
+
return authConfig;
|
|
287
|
+
}
|
|
288
|
+
// ---------------------------------------------------------------------------------------------
|
|
289
|
+
// Đăng nhập / đăng xuất
|
|
290
|
+
// ---------------------------------------------------------------------------------------------
|
|
291
|
+
/**
|
|
292
|
+
* Khởi động Authorization Code flow.
|
|
293
|
+
*
|
|
294
|
+
* `returnTo` mặc định là URL hiện tại, đi kèm tham số OIDC `state` để deep link sống sót qua
|
|
295
|
+
* vòng redirect (xem `authom.return-url.ts`).
|
|
296
|
+
*/
|
|
76
297
|
async login(options) {
|
|
77
|
-
if (!this
|
|
298
|
+
if (!this.#isBrowser || !this.#oauth) {
|
|
78
299
|
return;
|
|
79
|
-
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
const returnTo = options?.returnTo || (window.location.pathname + window.location.search);
|
|
83
|
-
sessionStorage.setItem(STORAGE_KEY_STATE, state);
|
|
84
|
-
sessionStorage.setItem(STORAGE_KEY_CODE_VERIFIER, codeVerifier);
|
|
85
|
-
sessionStorage.setItem(STORAGE_KEY_RETURN_TO, returnTo);
|
|
86
|
-
window.location.href = this.buildAuthorizeUrl({
|
|
87
|
-
state,
|
|
88
|
-
codeChallenge,
|
|
89
|
-
redirectUri: this.getDefaultRedirectUri(),
|
|
90
|
-
});
|
|
300
|
+
}
|
|
301
|
+
const requested = options?.returnTo ?? `${window.location.pathname}${window.location.search}${window.location.hash}`;
|
|
302
|
+
this.#oauth.initCodeFlow(captureReturnUrl(requested), this.#authorizeParams(options?.prompt));
|
|
91
303
|
}
|
|
304
|
+
/**
|
|
305
|
+
* Kết thúc phiên cục bộ rồi (mặc định) bắt đăng nhập lại với `prompt=login`.
|
|
306
|
+
*
|
|
307
|
+
* AuthOM không công bố `end_session_endpoint` và không hỗ trợ thu hồi token, nên đăng xuất
|
|
308
|
+
* KHÔNG gọi endpoint nào của IdP trừ khi `endSessionOnLogout` được bật rõ ràng.
|
|
309
|
+
*/
|
|
92
310
|
logout(options) {
|
|
93
|
-
if (!this
|
|
311
|
+
if (!this.#isBrowser || !this.#oauth) {
|
|
94
312
|
return;
|
|
95
|
-
|
|
96
|
-
this.
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
313
|
+
}
|
|
314
|
+
const config = this.config;
|
|
315
|
+
const prompt = options?.prompt ?? config?.logoutPrompt ?? 'login';
|
|
316
|
+
this.#clearExpiryCheck();
|
|
317
|
+
this.#inFlightRefresh = null;
|
|
318
|
+
if (config?.endSessionOnLogout) {
|
|
319
|
+
this.#resetSessionState();
|
|
320
|
+
this.#emit('logout');
|
|
321
|
+
this.#notifyOtherTabs(config);
|
|
322
|
+
// logOut() không tham số sẽ redirect tới end_session_endpoint khi IdP có công bố.
|
|
323
|
+
this.#oauth.logOut();
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
// why: logOut(true) chỉ dọn phiên cục bộ, không thử redirect tới logout URL không tồn tại.
|
|
327
|
+
this.#oauth.logOut(true);
|
|
328
|
+
this.#resetSessionState();
|
|
329
|
+
this.#emit('logout');
|
|
330
|
+
this.#notifyOtherTabs(config);
|
|
331
|
+
if (prompt === false) {
|
|
332
|
+
const returnTo = options?.returnTo ?? config?.postLogoutRedirectUri ?? window.location.origin;
|
|
333
|
+
this.navigate(returnTo);
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
// why: cookie SSO của AuthOM không bị xoá cùng phiên cục bộ. Không ép prompt thì lần
|
|
337
|
+
// /authorize kế tiếp được cookie đó trả lời ngay và user bị đăng nhập lại im lặng.
|
|
338
|
+
this.#oauth.initCodeFlow(undefined, this.#authorizeParams(prompt));
|
|
339
|
+
}
|
|
340
|
+
// ---------------------------------------------------------------------------------------------
|
|
341
|
+
// Gia hạn token
|
|
342
|
+
// ---------------------------------------------------------------------------------------------
|
|
343
|
+
/**
|
|
344
|
+
* Gia hạn access token ngay lập tức.
|
|
345
|
+
*
|
|
346
|
+
* Dùng refresh_token grant khi IdP đã phát hành refresh token, ngược lại rơi về silent refresh
|
|
347
|
+
* bằng iframe ẩn. Các lời gọi chồng nhau dùng chung một request đang bay.
|
|
348
|
+
*
|
|
349
|
+
* @returns `true` khi kết thúc với access token hợp lệ.
|
|
350
|
+
*/
|
|
351
|
+
refresh() {
|
|
352
|
+
if (!this.#isBrowser || !this.#oauth) {
|
|
353
|
+
return Promise.resolve(false);
|
|
354
|
+
}
|
|
355
|
+
if (this.#inFlightRefresh) {
|
|
356
|
+
return this.#inFlightRefresh;
|
|
357
|
+
}
|
|
358
|
+
const pending = this.#runRefresh().finally(() => {
|
|
359
|
+
this.#inFlightRefresh = null;
|
|
105
360
|
});
|
|
106
|
-
|
|
361
|
+
this.#inFlightRefresh = pending;
|
|
362
|
+
return pending;
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Tên cũ của `refresh()`, giữ để tương thích ngược.
|
|
366
|
+
*
|
|
367
|
+
* @deprecated Dùng `refresh()`.
|
|
368
|
+
*/
|
|
369
|
+
silentRefresh() {
|
|
370
|
+
return this.refresh();
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Ghi nhận một `401` từ route đã được bảo vệ.
|
|
374
|
+
*
|
|
375
|
+
* Đây là tín hiệu hết phiên đáng tin nhất: một lần refresh hỏng vẫn có thể xảy ra khi token cũ
|
|
376
|
+
* còn hạn, nhưng `401` nghĩa là resource server đã từ chối đúng token đang dùng.
|
|
377
|
+
*
|
|
378
|
+
* - Token đã hết hạn ⇒ phiên chết, phát `session_expired` (và đăng nhập lại nếu cấu hình cho phép).
|
|
379
|
+
* - Token vẫn còn hạn ⇒ server từ chối vì lý do khác (sai `audience`, client bị thu hồi...);
|
|
380
|
+
* phát `auth_error` với `reason = 'unauthorized'` để app tự hiển thị thông báo.
|
|
381
|
+
*
|
|
382
|
+
* `SdAuthOmInterceptor` gọi sẵn; chỉ cần gọi tay khi app dùng HTTP client riêng.
|
|
383
|
+
*/
|
|
384
|
+
handleUnauthorized() {
|
|
385
|
+
if (!this.#oauth) {
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
if (this.#oauth.hasValidAccessToken()) {
|
|
389
|
+
this.#emitAuthError('unauthorized');
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
this.#markSessionExpired();
|
|
393
|
+
}
|
|
394
|
+
/** `true` khi có thể thử gia hạn mà không cần user tương tác. */
|
|
395
|
+
canRefresh() {
|
|
396
|
+
if (!this.#oauth) {
|
|
397
|
+
return false;
|
|
398
|
+
}
|
|
399
|
+
return Boolean(this.#oauth.getRefreshToken()) || this.config?.useSilentRefresh === true;
|
|
107
400
|
}
|
|
108
|
-
|
|
401
|
+
// ---------------------------------------------------------------------------------------------
|
|
402
|
+
// Callback
|
|
403
|
+
// ---------------------------------------------------------------------------------------------
|
|
404
|
+
/**
|
|
405
|
+
* Xử lý callback `?code=` thủ công.
|
|
406
|
+
*
|
|
407
|
+
* `init()` đã tự làm việc này; chỉ cần gọi tay khi app tách riêng một route `/callback` chạy
|
|
408
|
+
* ngoài APP_INITIALIZER.
|
|
409
|
+
*/
|
|
410
|
+
async handleCallback() {
|
|
411
|
+
if (!this.#isBrowser || !this.#oauth) {
|
|
412
|
+
return false;
|
|
413
|
+
}
|
|
109
414
|
try {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
415
|
+
await this.#oauth.tryLogin();
|
|
416
|
+
}
|
|
417
|
+
catch (error) {
|
|
418
|
+
this.#emitAuthError('code_error', error);
|
|
419
|
+
return false;
|
|
420
|
+
}
|
|
421
|
+
this.#restoreReturnUrl();
|
|
422
|
+
this.#syncFromOAuth();
|
|
423
|
+
return this.isAuthenticated();
|
|
424
|
+
}
|
|
425
|
+
// ---------------------------------------------------------------------------------------------
|
|
426
|
+
// Đọc trạng thái
|
|
427
|
+
// ---------------------------------------------------------------------------------------------
|
|
428
|
+
getAccessToken() {
|
|
429
|
+
return this.accessToken();
|
|
430
|
+
}
|
|
431
|
+
/** `true` khi không còn access token dùng được (chưa đăng nhập, hoặc đã quá hạn). */
|
|
432
|
+
isAccessTokenExpired() {
|
|
433
|
+
return !this.#oauth?.hasValidAccessToken();
|
|
434
|
+
}
|
|
435
|
+
/** URL có nằm trong `secureRoutes` không — tức có được đính Bearer token không. */
|
|
436
|
+
isSecureUrl(url) {
|
|
437
|
+
const patterns = this.config?.secureRoutes;
|
|
438
|
+
if (!patterns?.length) {
|
|
439
|
+
return false;
|
|
440
|
+
}
|
|
441
|
+
return patterns.some(pattern => matchSecureRoute(pattern, url));
|
|
442
|
+
}
|
|
443
|
+
/** Origin của IdP, để interceptor không bao giờ tự đính token vào chính request lấy token. */
|
|
444
|
+
getIssuerOrigin() {
|
|
445
|
+
const issuer = this.config ? this.#resolveIssuer(this.config) : null;
|
|
446
|
+
if (!issuer) {
|
|
447
|
+
return null;
|
|
448
|
+
}
|
|
449
|
+
try {
|
|
450
|
+
return new URL(issuer).origin;
|
|
133
451
|
}
|
|
134
452
|
catch {
|
|
453
|
+
return null;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
/** Claims của user hiện tại ở dạng `SdAuthInfo` của module auth, hoặc `null` khi chưa đăng nhập. */
|
|
457
|
+
authInfo() {
|
|
458
|
+
const claims = this.idTokenClaims();
|
|
459
|
+
if (!claims) {
|
|
460
|
+
return null;
|
|
461
|
+
}
|
|
462
|
+
return {
|
|
463
|
+
id: claims['sub'],
|
|
464
|
+
username: resolveUsername(claims),
|
|
465
|
+
email: claims['email'],
|
|
466
|
+
firstName: claims['given_name'],
|
|
467
|
+
lastName: claims['family_name'],
|
|
468
|
+
data: claims,
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
/** Claims của user hiện tại ở dạng `SdLayoutUserInfo` của module layout. */
|
|
472
|
+
userInfo() {
|
|
473
|
+
const claims = this.idTokenClaims();
|
|
474
|
+
if (!claims) {
|
|
475
|
+
return {};
|
|
476
|
+
}
|
|
477
|
+
return {
|
|
478
|
+
username: resolveUsername(claims),
|
|
479
|
+
email: claims['email'],
|
|
480
|
+
fullName: resolveFullName(claims),
|
|
481
|
+
avatar: claims['picture'],
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
// ---------------------------------------------------------------------------------------------
|
|
485
|
+
// Mối nối với trình duyệt (tách riêng để test ghi đè được)
|
|
486
|
+
// ---------------------------------------------------------------------------------------------
|
|
487
|
+
/** Điều hướng full-page. */
|
|
488
|
+
navigate(url) {
|
|
489
|
+
window.location.href = url;
|
|
490
|
+
}
|
|
491
|
+
/** Viết lại URL trên thanh địa chỉ mà không tạo entry mới trong history. */
|
|
492
|
+
replaceHistoryState(url) {
|
|
493
|
+
window.history.replaceState(null, '', url);
|
|
494
|
+
}
|
|
495
|
+
// ---------------------------------------------------------------------------------------------
|
|
496
|
+
// Nội bộ
|
|
497
|
+
// ---------------------------------------------------------------------------------------------
|
|
498
|
+
async #runRefresh() {
|
|
499
|
+
const oauth = this.#oauth;
|
|
500
|
+
if (!oauth) {
|
|
501
|
+
return false;
|
|
502
|
+
}
|
|
503
|
+
try {
|
|
504
|
+
if (!this.config?.useSilentRefresh && oauth.getRefreshToken()) {
|
|
505
|
+
await oauth.refreshToken();
|
|
506
|
+
}
|
|
507
|
+
else {
|
|
508
|
+
await oauth.silentRefresh();
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
catch (error) {
|
|
512
|
+
this.#emit('token_refresh_error', undefined, error);
|
|
513
|
+
this.#evaluateSessionExpired();
|
|
135
514
|
return false;
|
|
136
515
|
}
|
|
516
|
+
this.#syncFromOAuth();
|
|
517
|
+
return this.isAuthenticated();
|
|
518
|
+
}
|
|
519
|
+
#subscribeOAuthEvents() {
|
|
520
|
+
const oauth = this.#oauth;
|
|
521
|
+
if (!oauth) {
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
// why: KHÔNG đăng ký onDestroy ở đây — hàm này chạy bên trong init() bất đồng bộ, và
|
|
525
|
+
// DestroyRef.onDestroy ném NG0205 nếu injector đã bị huỷ trước khi init() kịp chạy xong.
|
|
526
|
+
// Dọn dẹp dồn hết về #teardown(), vốn đã đăng ký một lần trong constructor.
|
|
527
|
+
this.#eventsSubscription = oauth.events.subscribe(event => this.#onOAuthEvent(event));
|
|
528
|
+
}
|
|
529
|
+
#onOAuthEvent(event) {
|
|
530
|
+
if (event.type === 'token_received') {
|
|
531
|
+
this.#clearExpiryCheck();
|
|
532
|
+
this.sessionExpired.set(false);
|
|
533
|
+
this.lastAuthError.set(null);
|
|
534
|
+
this.#syncFromOAuth();
|
|
535
|
+
this.#emit('token_received');
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
if (event.type === 'token_refreshed' || event.type === 'silently_refreshed') {
|
|
539
|
+
this.#clearExpiryCheck();
|
|
540
|
+
this.sessionExpired.set(false);
|
|
541
|
+
this.#syncFromOAuth();
|
|
542
|
+
this.#emit('token_refreshed');
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
if (REFRESH_ERROR_EVENT_TYPES.has(event.type)) {
|
|
546
|
+
this.#emit('token_refresh_error', event.type, reasonOf(event));
|
|
547
|
+
this.#evaluateSessionExpired();
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
if (AUTH_ERROR_EVENT_TYPES.has(event.type)) {
|
|
551
|
+
this.#emitAuthError(event.type, reasonOf(event));
|
|
552
|
+
}
|
|
137
553
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
554
|
+
/**
|
|
555
|
+
* Quyết định một lần gia hạn thất bại có phải là hết phiên hay không.
|
|
556
|
+
*
|
|
557
|
+
* Refresh hỏng CHƯA phải hết phiên: access token đang giữ thường vẫn còn hạn thêm một lúc, và
|
|
558
|
+
* báo "phiên hết hạn" ngay lúc đó là báo sớm — app vẫn chạy bình thường cho tới khi có một
|
|
559
|
+
* request thật trả 401. Chỉ khi token đó cũng chết thì phiên mới thực sự kết thúc.
|
|
560
|
+
*/
|
|
561
|
+
#evaluateSessionExpired() {
|
|
562
|
+
if (!this.#oauth) {
|
|
563
|
+
return;
|
|
142
564
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
console.warn('[SdAuthOmService] Token không có exp — bỏ qua auto-refresh');
|
|
565
|
+
if (this.#oauth.hasValidAccessToken()) {
|
|
566
|
+
// why: lần gia hạn chủ động này đã hỏng và sẽ không tự lặp lại (timer chỉ lên lịch lại khi
|
|
567
|
+
// nhận token mới), nên phải tự hẹn kiểm tra đúng lúc token hiện tại hết hạn.
|
|
568
|
+
this.#scheduleExpiryCheck();
|
|
148
569
|
return;
|
|
149
570
|
}
|
|
150
|
-
|
|
151
|
-
const nowSeconds = Math.floor(Date.now() / 1000);
|
|
152
|
-
const delayMs = Math.max(0, (exp - nowSeconds - threshold) * 1000);
|
|
153
|
-
this.refreshTimer = setTimeout(() => {
|
|
154
|
-
this.silentRefresh();
|
|
155
|
-
}, delayMs);
|
|
571
|
+
this.#markSessionExpired();
|
|
156
572
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
iframe.src = this.buildAuthorizeUrl({
|
|
201
|
-
state,
|
|
202
|
-
codeChallenge,
|
|
203
|
-
redirectUri,
|
|
204
|
-
prompt: 'none',
|
|
205
|
-
});
|
|
206
|
-
document.body.appendChild(iframe);
|
|
573
|
+
#scheduleExpiryCheck() {
|
|
574
|
+
this.#clearExpiryCheck();
|
|
575
|
+
const expiresAt = this.#oauth?.getAccessTokenExpiration();
|
|
576
|
+
if (!expiresAt) {
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
const delay = Math.max(0, expiresAt - Date.now()) + 1000;
|
|
580
|
+
this.#expiryCheckTimer = setTimeout(() => {
|
|
581
|
+
this.#expiryCheckTimer = null;
|
|
582
|
+
if (!this.#oauth?.hasValidAccessToken()) {
|
|
583
|
+
this.#markSessionExpired();
|
|
584
|
+
}
|
|
585
|
+
}, delay);
|
|
586
|
+
}
|
|
587
|
+
#clearExpiryCheck() {
|
|
588
|
+
if (this.#expiryCheckTimer !== null) {
|
|
589
|
+
clearTimeout(this.#expiryCheckTimer);
|
|
590
|
+
this.#expiryCheckTimer = null;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
#markSessionExpired() {
|
|
594
|
+
if (this.sessionExpired()) {
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
this.#clearExpiryCheck();
|
|
598
|
+
this.#resetSessionState();
|
|
599
|
+
this.sessionExpired.set(true);
|
|
600
|
+
this.#emit('session_expired');
|
|
601
|
+
if ((this.config?.onSessionExpired ?? 'login') === 'login') {
|
|
602
|
+
void this.login();
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
#listenCrossTabLogout(config) {
|
|
606
|
+
if (this.#channel) {
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
this.#channel = new SdAuthOmSessionChannel(config.sessionChannelName || `sd-authom-session:${config.clientId}`);
|
|
610
|
+
this.#channel.onLogout(() => {
|
|
611
|
+
this.#clearExpiryCheck();
|
|
612
|
+
this.#oauth?.logOut(true);
|
|
613
|
+
this.#resetSessionState();
|
|
614
|
+
this.#emit('logout_other_tab');
|
|
615
|
+
this.#oauth?.initCodeFlow(undefined, this.#authorizeParams('login'));
|
|
207
616
|
});
|
|
208
617
|
}
|
|
209
|
-
|
|
210
|
-
if (
|
|
211
|
-
return
|
|
212
|
-
const params = new URLSearchParams(window.location.search);
|
|
213
|
-
const code = params.get('code');
|
|
214
|
-
const state = params.get('state');
|
|
215
|
-
if (!code)
|
|
216
|
-
return false;
|
|
217
|
-
const savedState = sessionStorage.getItem(STORAGE_KEY_STATE);
|
|
218
|
-
const codeVerifier = sessionStorage.getItem(STORAGE_KEY_CODE_VERIFIER);
|
|
219
|
-
const returnTo = sessionStorage.getItem(STORAGE_KEY_RETURN_TO);
|
|
220
|
-
sessionStorage.removeItem(STORAGE_KEY_STATE);
|
|
221
|
-
sessionStorage.removeItem(STORAGE_KEY_CODE_VERIFIER);
|
|
222
|
-
if (state !== savedState || !codeVerifier) {
|
|
223
|
-
window.history.replaceState({}, '', window.location.pathname);
|
|
224
|
-
sessionStorage.removeItem(STORAGE_KEY_RETURN_TO);
|
|
225
|
-
return false;
|
|
618
|
+
#notifyOtherTabs(config) {
|
|
619
|
+
if (config?.crossTabLogout === false) {
|
|
620
|
+
return;
|
|
226
621
|
}
|
|
227
|
-
|
|
228
|
-
|
|
622
|
+
this.#channel?.notifyLogout();
|
|
623
|
+
}
|
|
624
|
+
#restoreReturnUrl() {
|
|
625
|
+
const returnTo = resolveReturnUrl(this.#oauth?.state);
|
|
229
626
|
if (returnTo) {
|
|
230
|
-
|
|
231
|
-
|
|
627
|
+
// why: vẫn đang trong APP_INITIALIZER, tức trước điều hướng đầu tiên của Router — viết lại
|
|
628
|
+
// URL ở đây khiến lần điều hướng đó nhắm thẳng trang user yêu cầu và chạy guard bình thường.
|
|
629
|
+
this.replaceHistoryState(window.location.origin + returnTo);
|
|
232
630
|
}
|
|
233
|
-
|
|
234
|
-
|
|
631
|
+
}
|
|
632
|
+
#syncFromOAuth() {
|
|
633
|
+
const oauth = this.#oauth;
|
|
634
|
+
if (!oauth?.hasValidAccessToken()) {
|
|
635
|
+
this.accessToken.set(null);
|
|
636
|
+
this.idTokenClaims.set(null);
|
|
637
|
+
return;
|
|
235
638
|
}
|
|
236
|
-
|
|
639
|
+
this.accessToken.set(oauth.getAccessToken() || null);
|
|
640
|
+
this.idTokenClaims.set(oauth.getIdentityClaims() ?? null);
|
|
237
641
|
}
|
|
238
|
-
|
|
239
|
-
this.
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
642
|
+
#resetSessionState() {
|
|
643
|
+
this.accessToken.set(null);
|
|
644
|
+
this.idTokenClaims.set(null);
|
|
645
|
+
}
|
|
646
|
+
#authorizeParams(prompt) {
|
|
647
|
+
const params = {};
|
|
648
|
+
if (this.config?.organization) {
|
|
649
|
+
params['organization'] = this.config.organization;
|
|
650
|
+
}
|
|
651
|
+
if (prompt) {
|
|
652
|
+
params['prompt'] = prompt;
|
|
653
|
+
}
|
|
654
|
+
return params;
|
|
655
|
+
}
|
|
656
|
+
#resolveIssuer(config) {
|
|
657
|
+
const issuer = config.issuer?.trim();
|
|
658
|
+
if (issuer) {
|
|
659
|
+
return issuer;
|
|
660
|
+
}
|
|
661
|
+
const domain = config.domain?.trim();
|
|
662
|
+
if (domain) {
|
|
663
|
+
return /^https?:\/\//i.test(domain) ? domain : `https://${domain}/`;
|
|
247
664
|
}
|
|
248
|
-
|
|
665
|
+
throw new Error('[SdAuthOmService] Missing tenant "issuer" (or legacy "domain") in SdAuthOmTenantConfig.');
|
|
666
|
+
}
|
|
667
|
+
/**
|
|
668
|
+
* Chuẩn hoá basename file tĩnh trong `public/` thành URL tuyệt đối.
|
|
669
|
+
* `'auth-error'` | `'/auth-error'` | `'auth-error.html'` → `${origin}/auth-error.html`.
|
|
670
|
+
*/
|
|
671
|
+
#toPublicHtmlUrl(name) {
|
|
672
|
+
const base = name
|
|
673
|
+
.trim()
|
|
674
|
+
.replace(/^\/+/, '')
|
|
675
|
+
.replace(/\.html$/i, '');
|
|
676
|
+
return `${window.location.origin}/${base}.html`;
|
|
677
|
+
}
|
|
678
|
+
#emit(type, reason, error) {
|
|
679
|
+
this.#events.next({ type, reason, error });
|
|
680
|
+
}
|
|
681
|
+
#emitAuthError(reason, error) {
|
|
682
|
+
const event = { type: 'auth_error', reason, error };
|
|
683
|
+
this.lastAuthError.set(event);
|
|
684
|
+
this.#events.next(event);
|
|
685
|
+
}
|
|
686
|
+
#teardown() {
|
|
687
|
+
this.#clearExpiryCheck();
|
|
688
|
+
this.#eventsSubscription?.unsubscribe();
|
|
689
|
+
this.#eventsSubscription = null;
|
|
690
|
+
this.#channel?.close();
|
|
691
|
+
this.#channel = null;
|
|
692
|
+
this.#events.complete();
|
|
249
693
|
}
|
|
250
694
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.21", ngImport: i0, type: SdAuthOmService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
251
695
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.21", ngImport: i0, type: SdAuthOmService, providedIn: 'root' });
|
|
@@ -253,59 +697,216 @@ class SdAuthOmService {
|
|
|
253
697
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.21", ngImport: i0, type: SdAuthOmService, decorators: [{
|
|
254
698
|
type: Injectable,
|
|
255
699
|
args: [{ providedIn: 'root' }]
|
|
256
|
-
}] });
|
|
257
|
-
|
|
258
|
-
|
|
700
|
+
}], ctorParameters: () => [] });
|
|
701
|
+
/**
|
|
702
|
+
* Khớp một URL với một phần tử `secureRoutes`.
|
|
703
|
+
*
|
|
704
|
+
* Có `*` → glob trên toàn bộ URL. Không có `*` → khớp tiền tố, không phân biệt hoa thường
|
|
705
|
+
* (đây là dạng "danh sách host" mà các portal đang dùng).
|
|
706
|
+
*/
|
|
707
|
+
function matchSecureRoute(pattern, url) {
|
|
708
|
+
if (!pattern) {
|
|
709
|
+
return false;
|
|
710
|
+
}
|
|
711
|
+
if (pattern.includes('*')) {
|
|
712
|
+
return matchGlob(pattern, url);
|
|
713
|
+
}
|
|
714
|
+
return url.toLowerCase().startsWith(pattern.toLowerCase());
|
|
715
|
+
}
|
|
716
|
+
const escapeRegExp = (value) => value.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
|
|
717
|
+
/** Khớp glob đơn giản, `*` thay cho một đoạn bất kỳ. Giữ nguyên hành vi từ bản trước của module. */
|
|
259
718
|
const matchGlob = (pattern, url) => {
|
|
260
719
|
const regexBody = pattern.split('*').map(escapeRegExp).join('.*');
|
|
261
720
|
return new RegExp(`^${regexBody}$`).test(url);
|
|
262
721
|
};
|
|
722
|
+
/**
|
|
723
|
+
* AuthOM không phát claim `preferred_username` (xác nhận từ discovery document thật), nên phải có
|
|
724
|
+
* chuỗi fallback thay vì đọc thẳng một claim.
|
|
725
|
+
*/
|
|
726
|
+
function resolveUsername(claims) {
|
|
727
|
+
return (claims['preferred_username'] ?? claims['email'] ?? claims['nickname'] ?? claims['sub']);
|
|
728
|
+
}
|
|
729
|
+
function resolveFullName(claims) {
|
|
730
|
+
const lastName = claims['family_name'];
|
|
731
|
+
const firstName = claims['given_name'];
|
|
732
|
+
const composed = [lastName, firstName].filter(Boolean).join(' ');
|
|
733
|
+
return composed || (claims['name'] ?? claims['nickname']);
|
|
734
|
+
}
|
|
735
|
+
function reasonOf(event) {
|
|
736
|
+
return event instanceof OAuthErrorEvent ? event.reason : event.info;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
/**
|
|
740
|
+
* Đính `Authorization: Bearer <token>` vào các request khớp `secureRoutes`.
|
|
741
|
+
*
|
|
742
|
+
* Interceptor này cũng là chốt gia hạn cuối cùng: nếu access token đã hết hạn ngay trước khi gửi
|
|
743
|
+
* (máy vừa ngủ dậy, tab bị treo, timer refresh chủ động lỡ nhịp) thì nó gia hạn trước rồi mới gửi,
|
|
744
|
+
* thay vì để request chết 401 rồi mới xử lý.
|
|
745
|
+
*/
|
|
263
746
|
const SdAuthOmInterceptor = (req, next) => {
|
|
264
747
|
const authom = inject(SdAuthOmService);
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
748
|
+
if (!authom.config) {
|
|
749
|
+
return next(req);
|
|
750
|
+
}
|
|
751
|
+
// why: request lấy/gia hạn token cũng đi qua HttpClient. Không loại trừ origin của IdP thì một
|
|
752
|
+
// `secureRoutes` rộng tay sẽ khiến refresh tự gọi lại chính nó.
|
|
753
|
+
const issuerOrigin = authom.getIssuerOrigin();
|
|
754
|
+
if (issuerOrigin && req.url.toLowerCase().startsWith(issuerOrigin.toLowerCase())) {
|
|
268
755
|
return next(req);
|
|
269
|
-
|
|
270
|
-
if (!
|
|
756
|
+
}
|
|
757
|
+
if (!authom.isSecureUrl(req.url)) {
|
|
271
758
|
return next(req);
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
759
|
+
}
|
|
760
|
+
const send = (token) => forward(req, next, authom, token);
|
|
761
|
+
if (!authom.isAccessTokenExpired()) {
|
|
762
|
+
return send(authom.getAccessToken());
|
|
763
|
+
}
|
|
764
|
+
if (!authom.canRefresh()) {
|
|
765
|
+
return send(null);
|
|
766
|
+
}
|
|
767
|
+
return from(authom.refresh()).pipe(switchMap(refreshed => send(refreshed ? authom.getAccessToken() : null)));
|
|
276
768
|
};
|
|
769
|
+
function forward(req, next, authom, token) {
|
|
770
|
+
const outgoing = token ? req.clone({ headers: req.headers.set('Authorization', `Bearer ${token}`) }) : req;
|
|
771
|
+
return next(outgoing).pipe(catchError((error) => {
|
|
772
|
+
// why: 401 từ một route đã được bảo vệ là tín hiệu đáng tin cậy nhất cho biết phiên đã chết
|
|
773
|
+
// thật — đáng tin hơn một lần refresh hỏng, vì lúc đó token cũ thường vẫn còn hạn.
|
|
774
|
+
if (error instanceof HttpErrorResponse && error.status === 401) {
|
|
775
|
+
authom.handleUnauthorized();
|
|
776
|
+
}
|
|
777
|
+
return throwError(() => error);
|
|
778
|
+
}));
|
|
779
|
+
}
|
|
277
780
|
|
|
278
|
-
|
|
279
|
-
|
|
781
|
+
/**
|
|
782
|
+
* Chặn route khi chưa đăng nhập và khởi động code flow, mang theo URL được yêu cầu.
|
|
783
|
+
*
|
|
784
|
+
* Dùng trực tiếp trên route:
|
|
785
|
+
* ```ts
|
|
786
|
+
* { path: 'orders', canActivate: [sdAuthOmGuard], loadChildren: ... }
|
|
787
|
+
* ```
|
|
788
|
+
*
|
|
789
|
+
* App đã dùng `SdAuthGuard` của module auth thì không cần guard này — hãy để
|
|
790
|
+
* `SdAuthOmAuthConfiguration` cấp `SD_AUTH_CONFIGURATION` (mặc định `provideSdAuthOm` làm sẵn).
|
|
791
|
+
*/
|
|
792
|
+
const sdAuthOmGuard = (_route, state) => {
|
|
793
|
+
const authom = inject(SdAuthOmService);
|
|
794
|
+
if (authom.isAuthenticated()) {
|
|
795
|
+
return true;
|
|
796
|
+
}
|
|
797
|
+
void authom.login({ returnTo: state.url });
|
|
798
|
+
return false;
|
|
799
|
+
};
|
|
800
|
+
|
|
801
|
+
/**
|
|
802
|
+
* Nối AuthOM vào module `auth` dùng chung.
|
|
803
|
+
*
|
|
804
|
+
* Cấp token này (mặc định `provideSdAuthOm` đã làm) là đủ để `SdAuthGuard`, `SdPortalGuard` và
|
|
805
|
+
* `SdAuthService.signout()` chạy đúng trên AuthOM mà app không phải viết thêm gì.
|
|
806
|
+
*
|
|
807
|
+
* `changePassword` cố ý KHÔNG được cài đặt: AuthOM chưa công bố cơ chế đổi mật khẩu, và
|
|
808
|
+
* `SdAuthService.changePassword()` tự bỏ qua khi thiếu action — đúng hơn là ném lỗi lúc chạy.
|
|
809
|
+
*/
|
|
810
|
+
class SdAuthOmAuthConfiguration {
|
|
811
|
+
#authom = inject(SdAuthOmService);
|
|
812
|
+
/**
|
|
813
|
+
* Phát lại mỗi khi claims đổi, thay vì trả một giá trị chụp một lần.
|
|
814
|
+
*
|
|
815
|
+
* `SdAuthService` bọc giá trị này bằng `toSignal` trong constructor của chính nó, nên một
|
|
816
|
+
* Observable sống giữ cho `getAuthInfo()` đúng cả khi service được tạo trước lúc đăng nhập xong.
|
|
817
|
+
*/
|
|
818
|
+
#authInfo$ = toObservable(this.#authom.idTokenClaims).pipe(map(() => this.#authom.authInfo() ?? {}));
|
|
819
|
+
guard = {
|
|
820
|
+
auth: (_route, state) => {
|
|
821
|
+
if (this.#authom.isAuthenticated()) {
|
|
822
|
+
return of(true);
|
|
823
|
+
}
|
|
824
|
+
void this.#authom.login({ returnTo: state.url });
|
|
825
|
+
return of(false);
|
|
826
|
+
},
|
|
827
|
+
/** AuthOM không có khái niệm portal riêng — mặc định cho qua, app tự override nếu cần. */
|
|
828
|
+
portal: () => of(true),
|
|
829
|
+
authInfo: () => this.#authInfo$,
|
|
830
|
+
};
|
|
831
|
+
action = {
|
|
832
|
+
signout: () => this.#authom.logout(),
|
|
833
|
+
};
|
|
834
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.21", ngImport: i0, type: SdAuthOmAuthConfiguration, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
835
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.21", ngImport: i0, type: SdAuthOmAuthConfiguration, providedIn: 'root' });
|
|
836
|
+
}
|
|
837
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.21", ngImport: i0, type: SdAuthOmAuthConfiguration, decorators: [{
|
|
838
|
+
type: Injectable,
|
|
839
|
+
args: [{ providedIn: 'root' }]
|
|
840
|
+
}] });
|
|
841
|
+
|
|
842
|
+
function buildConfigurationProvider(options) {
|
|
280
843
|
if (options.useFactory) {
|
|
281
|
-
|
|
844
|
+
return { provide: SD_AUTHOM_CONFIGURATION, useFactory: options.useFactory, deps: options.deps || [] };
|
|
845
|
+
}
|
|
846
|
+
if (options.useClass) {
|
|
847
|
+
return { provide: SD_AUTHOM_CONFIGURATION, useClass: options.useClass };
|
|
282
848
|
}
|
|
283
|
-
|
|
284
|
-
|
|
849
|
+
throw new Error('[provideSdAuthOm] Either "useClass" or "useFactory" must be supplied.');
|
|
850
|
+
}
|
|
851
|
+
function buildCommonProviders(options) {
|
|
852
|
+
const providers = [
|
|
853
|
+
// why: config = null nên interceptor DI mặc định của client OIDC không đính token cho ai cả.
|
|
854
|
+
// Việc đính token thuộc về SdAuthOmInterceptor, nhờ đó app chỉ cần withInterceptors([...])
|
|
855
|
+
// và không phải nhớ thêm withInterceptorsFromDi().
|
|
856
|
+
provideOAuthClient(),
|
|
857
|
+
SdAuthOmService,
|
|
858
|
+
buildConfigurationProvider(options),
|
|
859
|
+
];
|
|
860
|
+
if (options.withAuthConfiguration !== false) {
|
|
861
|
+
providers.push({ provide: SD_AUTH_CONFIGURATION, useExisting: SdAuthOmAuthConfiguration });
|
|
285
862
|
}
|
|
863
|
+
return providers;
|
|
864
|
+
}
|
|
865
|
+
/**
|
|
866
|
+
* Đăng ký AuthOM cho app standalone (gọi trong `app.config.ts`).
|
|
867
|
+
*
|
|
868
|
+
* ```ts
|
|
869
|
+
* export const appConfig: ApplicationConfig = {
|
|
870
|
+
* providers: [
|
|
871
|
+
* provideHttpClient(withInterceptors([SdAuthOmInterceptor])),
|
|
872
|
+
* provideSdAuthOm({
|
|
873
|
+
* useFactory: () => ({
|
|
874
|
+
* loadTenantConfig: () => Promise.resolve({
|
|
875
|
+
* issuer: 'https://api-qc.onemount.dev/auth-om/iam/v1/mag-qc/',
|
|
876
|
+
* clientId: 'YOUR_CLIENT_ID',
|
|
877
|
+
* redirectUri: `${location.origin}/callback`,
|
|
878
|
+
* audience: 'http://api-qc.int.onemount.dev/auth-om/api/v1/mag-qc',
|
|
879
|
+
* secureRoutes: ['https://api-qc.onemount.dev'],
|
|
880
|
+
* }),
|
|
881
|
+
* }),
|
|
882
|
+
* }),
|
|
883
|
+
* ],
|
|
884
|
+
* };
|
|
885
|
+
* ```
|
|
886
|
+
*/
|
|
887
|
+
function provideSdAuthOm(options) {
|
|
888
|
+
const providers = buildCommonProviders(options);
|
|
286
889
|
providers.push(provideAppInitializer(() => {
|
|
287
890
|
const configLoader = inject(SD_AUTHOM_CONFIGURATION);
|
|
288
891
|
const authom = inject(SdAuthOmService);
|
|
289
|
-
return configLoader.loadTenantConfig().then(
|
|
892
|
+
return configLoader.loadTenantConfig().then(config => authom.init(config));
|
|
290
893
|
}));
|
|
291
894
|
return makeEnvironmentProviders(providers);
|
|
292
895
|
}
|
|
896
|
+
/** Đăng ký AuthOM cho kiến trúc `NgModule` cũ. */
|
|
293
897
|
class SdAuthOmModule {
|
|
294
898
|
static forRoot(options) {
|
|
295
899
|
return {
|
|
296
900
|
ngModule: SdAuthOmModule,
|
|
297
901
|
providers: [
|
|
298
|
-
|
|
299
|
-
...(options.useFactory
|
|
300
|
-
? [{ provide: SD_AUTHOM_CONFIGURATION, useFactory: options.useFactory, deps: options.deps || [] }]
|
|
301
|
-
: [{ provide: SD_AUTHOM_CONFIGURATION, useClass: options.useClass }]),
|
|
902
|
+
...buildCommonProviders(options),
|
|
302
903
|
{
|
|
303
904
|
provide: APP_INITIALIZER,
|
|
304
905
|
multi: true,
|
|
305
906
|
useFactory: () => {
|
|
306
907
|
const configLoader = inject(SD_AUTHOM_CONFIGURATION);
|
|
307
908
|
const authom = inject(SdAuthOmService);
|
|
308
|
-
return () => configLoader.loadTenantConfig().then(
|
|
909
|
+
return () => configLoader.loadTenantConfig().then(config => authom.init(config));
|
|
309
910
|
},
|
|
310
911
|
},
|
|
311
912
|
],
|
|
@@ -321,7 +922,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.21", ngImpo
|
|
|
321
922
|
}] });
|
|
322
923
|
|
|
323
924
|
/**
|
|
324
|
-
* Module `authom` — OAuth 2.0 + PKCE
|
|
925
|
+
* Module `authom` — client xác thực OAuth 2.0 Authorization Code + PKCE cho AuthOM.
|
|
926
|
+
*
|
|
927
|
+
* Module sở hữu trọn vòng đời phiên: bootstrap qua discovery document, đổi authorization code,
|
|
928
|
+
* khôi phục deep link, gia hạn token chủ động, phát hiện hết phiên, xử lý lỗi xác thực và đăng
|
|
929
|
+
* xuất đồng bộ giữa các tab.
|
|
325
930
|
*
|
|
326
931
|
* Cách dùng (standalone):
|
|
327
932
|
* ```ts
|
|
@@ -334,12 +939,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.21", ngImpo
|
|
|
334
939
|
* provideSdAuthOm({
|
|
335
940
|
* useFactory: () => ({
|
|
336
941
|
* loadTenantConfig: () => Promise.resolve({
|
|
337
|
-
*
|
|
942
|
+
* issuer: 'https://api-qc.onemount.dev/auth-om/iam/v1/mag-qc/',
|
|
338
943
|
* clientId: 'YOUR_CLIENT_ID',
|
|
339
|
-
*
|
|
340
|
-
*
|
|
341
|
-
*
|
|
342
|
-
* secureRoutes: ['https://api.example.com/*'],
|
|
944
|
+
* redirectUri: `${location.origin}/callback`,
|
|
945
|
+
* audience: 'http://api-qc.int.onemount.dev/auth-om/api/v1/mag-qc',
|
|
946
|
+
* secureRoutes: ['https://api-qc.onemount.dev'],
|
|
343
947
|
* }),
|
|
344
948
|
* }),
|
|
345
949
|
* }),
|
|
@@ -347,14 +951,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.21", ngImpo
|
|
|
347
951
|
* };
|
|
348
952
|
* ```
|
|
349
953
|
*
|
|
350
|
-
* Yêu cầu
|
|
351
|
-
* 1.
|
|
352
|
-
* 2. App
|
|
954
|
+
* Yêu cầu phía app:
|
|
955
|
+
* 1. Cài `angular-oauth2-oidc` (peer dependency, chọn major khớp Angular của app).
|
|
956
|
+
* 2. App chạy trên HTTPS hoặc localhost — Web Crypto API cần secure context để tạo PKCE.
|
|
957
|
+
* 3. `redirectUri` phải trùng giá trị đã đăng ký trên client AuthOM.
|
|
958
|
+
* 4. Chỉ khi bật `useSilentRefresh`: copy `silent-authom.html` cạnh source module này vào `public/`.
|
|
353
959
|
*/
|
|
354
960
|
|
|
355
961
|
/**
|
|
356
962
|
* Generated bundle index. Do not edit.
|
|
357
963
|
*/
|
|
358
964
|
|
|
359
|
-
export { SD_AUTHOM_CONFIGURATION, SdAuthOmInterceptor, SdAuthOmModule, SdAuthOmService, matchGlob, provideSdAuthOm };
|
|
965
|
+
export { SD_AUTHOM_CONFIGURATION, SD_AUTHOM_DEFAULT_SCOPE, SD_AUTHOM_DEFAULT_SILENT_REFRESH_TIMEOUT_SECONDS, SD_AUTHOM_DEFAULT_TIMEOUT_FACTOR, SdAuthOmAuthConfiguration, SdAuthOmInterceptor, SdAuthOmModule, SdAuthOmService, SdAuthOmSessionChannel, captureReturnUrl, isRestorableAppUrl, matchGlob, matchSecureRoute, provideSdAuthOm, resolveReturnUrl, sdAuthOmGuard };
|
|
360
966
|
//# sourceMappingURL=sd-angular-core-modules-authom.mjs.map
|