@masterteam/gateway-auth 0.0.1 → 0.0.3
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.
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
import { HttpClient, HttpContextToken, HttpBackend } from '@angular/common/http';
|
|
2
|
+
import * as i0 from '@angular/core';
|
|
3
|
+
import { InjectionToken, inject, Injectable, computed, ChangeDetectionStrategy, Component, signal, input } from '@angular/core';
|
|
4
|
+
import { of, EMPTY, isObservable, from, map, tap as tap$1, shareReplay, finalize, switchMap, catchError as catchError$1, throwError } from 'rxjs';
|
|
5
|
+
import { Action, Selector, State, Store, select } from '@ngxs/store';
|
|
6
|
+
import { Router, ActivatedRoute } from '@angular/router';
|
|
7
|
+
import { mergeMap, catchError, tap } from 'rxjs/operators';
|
|
8
|
+
import * as i2 from '@angular/common';
|
|
9
|
+
import { CommonModule } from '@angular/common';
|
|
10
|
+
import * as i1 from '@angular/forms';
|
|
11
|
+
import { FormBuilder, Validators, ReactiveFormsModule } from '@angular/forms';
|
|
12
|
+
import { Button } from '@masterteam/components/button';
|
|
13
|
+
import { TextField } from '@masterteam/components/text-field';
|
|
14
|
+
import { Icon } from '@masterteam/icons';
|
|
15
|
+
|
|
1
16
|
const GATEWAY_AUTH_DEVICE_TOKEN = 'web-app';
|
|
2
17
|
const GATEWAY_AUTH_ENDPOINTS = {
|
|
3
18
|
login: 'auth/login',
|
|
@@ -105,6 +120,97 @@ function createSecureClientState() {
|
|
|
105
120
|
return `${Date.now().toString(36)}${Math.random().toString(36).slice(2)}`;
|
|
106
121
|
}
|
|
107
122
|
|
|
123
|
+
class Login {
|
|
124
|
+
payload;
|
|
125
|
+
static type = '[Auth] Login';
|
|
126
|
+
constructor(payload) {
|
|
127
|
+
this.payload = payload;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
class VerifyMfa {
|
|
131
|
+
code;
|
|
132
|
+
static type = '[Auth] Verify MFA';
|
|
133
|
+
constructor(code) {
|
|
134
|
+
this.code = code;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
class ResendMfa {
|
|
138
|
+
static type = '[Auth] Resend MFA';
|
|
139
|
+
}
|
|
140
|
+
class LoadSsoProviders {
|
|
141
|
+
static type = '[Auth] Load SSO Providers';
|
|
142
|
+
}
|
|
143
|
+
class StartSso {
|
|
144
|
+
provider;
|
|
145
|
+
static type = '[Auth] Start SSO';
|
|
146
|
+
constructor(provider) {
|
|
147
|
+
this.provider = provider;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
class ExchangeSsoCode {
|
|
151
|
+
code;
|
|
152
|
+
state;
|
|
153
|
+
static type = '[Auth] Exchange SSO Code';
|
|
154
|
+
constructor(code, state) {
|
|
155
|
+
this.code = code;
|
|
156
|
+
this.state = state;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
class LoginSuccess {
|
|
160
|
+
session;
|
|
161
|
+
static type = '[Auth] Login Success';
|
|
162
|
+
constructor(session) {
|
|
163
|
+
this.session = session;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
class LoginFailure {
|
|
167
|
+
error;
|
|
168
|
+
static type = '[Auth] Login Failure';
|
|
169
|
+
constructor(error) {
|
|
170
|
+
this.error = error;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
class Logout {
|
|
174
|
+
remote;
|
|
175
|
+
static type = '[Auth] Logout';
|
|
176
|
+
constructor(remote = false) {
|
|
177
|
+
this.remote = remote;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
class UpdateUserData {
|
|
181
|
+
user;
|
|
182
|
+
static type = '[Auth] Update User Data';
|
|
183
|
+
constructor(user) {
|
|
184
|
+
this.user = user;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
class UpdateTokens {
|
|
188
|
+
tokens;
|
|
189
|
+
static type = '[Auth] Update Tokens';
|
|
190
|
+
constructor(tokens) {
|
|
191
|
+
this.tokens = tokens;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
class ClearError {
|
|
195
|
+
static type = '[Auth] Clear Error';
|
|
196
|
+
}
|
|
197
|
+
class ClearPendingMfa {
|
|
198
|
+
static type = '[Auth] Clear Pending MFA';
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const GATEWAY_AUTH_OPTIONS = new InjectionToken('GATEWAY_AUTH_OPTIONS', {
|
|
202
|
+
factory: () => ({
|
|
203
|
+
getGatewayApiBaseUrl: () => null,
|
|
204
|
+
deviceToken: 'web-app',
|
|
205
|
+
platform: 'web',
|
|
206
|
+
loginRoute: '/auth/login',
|
|
207
|
+
mfaRoute: '/auth/mfa',
|
|
208
|
+
ssoCallbackPath: '/auth/sso/callback',
|
|
209
|
+
defaultAuthenticatedRoute: '/',
|
|
210
|
+
preserveSsoProvidersOnLogout: true,
|
|
211
|
+
}),
|
|
212
|
+
});
|
|
213
|
+
|
|
108
214
|
const PENDING_SSO_STATE_KEY = 'gatewayAuth.pendingSsoState';
|
|
109
215
|
class GatewaySsoSession {
|
|
110
216
|
createAndStoreState() {
|
|
@@ -140,9 +246,825 @@ class GatewaySsoSession {
|
|
|
140
246
|
}
|
|
141
247
|
}
|
|
142
248
|
|
|
249
|
+
const AUTH_STATE_DEFAULTS = {
|
|
250
|
+
user: null,
|
|
251
|
+
token: null,
|
|
252
|
+
refreshToken: null,
|
|
253
|
+
accessTokenExpiresAt: null,
|
|
254
|
+
refreshTokenExpiresAt: null,
|
|
255
|
+
loading: false,
|
|
256
|
+
mfaResendLoading: false,
|
|
257
|
+
ssoLoading: false,
|
|
258
|
+
ssoCallbackLoading: false,
|
|
259
|
+
error: null,
|
|
260
|
+
twoFactorRequired: false,
|
|
261
|
+
pendingMfa: null,
|
|
262
|
+
ssoProviders: [],
|
|
263
|
+
};
|
|
264
|
+
function sanitizePersistedAuthState(obj) {
|
|
265
|
+
return {
|
|
266
|
+
...AUTH_STATE_DEFAULTS,
|
|
267
|
+
...(obj ?? {}),
|
|
268
|
+
loading: false,
|
|
269
|
+
mfaResendLoading: false,
|
|
270
|
+
ssoLoading: false,
|
|
271
|
+
ssoCallbackLoading: false,
|
|
272
|
+
error: null,
|
|
273
|
+
twoFactorRequired: false,
|
|
274
|
+
pendingMfa: null,
|
|
275
|
+
ssoProviders: [],
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
280
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
281
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
282
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
283
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
284
|
+
};
|
|
285
|
+
let GatewayAuthState = class GatewayAuthState {
|
|
286
|
+
http = inject(HttpClient);
|
|
287
|
+
options = inject(GATEWAY_AUTH_OPTIONS);
|
|
288
|
+
router = inject(Router);
|
|
289
|
+
ssoSession = new GatewaySsoSession();
|
|
290
|
+
static user(state) {
|
|
291
|
+
return state.user;
|
|
292
|
+
}
|
|
293
|
+
static loading(state) {
|
|
294
|
+
return state.loading;
|
|
295
|
+
}
|
|
296
|
+
static mfaResendLoading(state) {
|
|
297
|
+
return state.mfaResendLoading;
|
|
298
|
+
}
|
|
299
|
+
static ssoLoading(state) {
|
|
300
|
+
return state.ssoLoading;
|
|
301
|
+
}
|
|
302
|
+
static ssoCallbackLoading(state) {
|
|
303
|
+
return state.ssoCallbackLoading;
|
|
304
|
+
}
|
|
305
|
+
static error(state) {
|
|
306
|
+
return state.error;
|
|
307
|
+
}
|
|
308
|
+
static token(state) {
|
|
309
|
+
return state.token;
|
|
310
|
+
}
|
|
311
|
+
static refreshToken(state) {
|
|
312
|
+
return state.refreshToken;
|
|
313
|
+
}
|
|
314
|
+
static accessTokenExpiresAt(state) {
|
|
315
|
+
return state.accessTokenExpiresAt;
|
|
316
|
+
}
|
|
317
|
+
static refreshTokenExpiresAt(state) {
|
|
318
|
+
return state.refreshTokenExpiresAt;
|
|
319
|
+
}
|
|
320
|
+
static twoFactorRequired(state) {
|
|
321
|
+
return state.twoFactorRequired;
|
|
322
|
+
}
|
|
323
|
+
static pendingMfa(state) {
|
|
324
|
+
return state.pendingMfa;
|
|
325
|
+
}
|
|
326
|
+
static ssoProviders(state) {
|
|
327
|
+
return state.ssoProviders;
|
|
328
|
+
}
|
|
329
|
+
static isAdmin(state) {
|
|
330
|
+
return state.user?.isAdmin || false;
|
|
331
|
+
}
|
|
332
|
+
static userDetails(state) {
|
|
333
|
+
return state.user?.user || null;
|
|
334
|
+
}
|
|
335
|
+
login(ctx, action) {
|
|
336
|
+
ctx.patchState({
|
|
337
|
+
loading: true,
|
|
338
|
+
error: null,
|
|
339
|
+
pendingMfa: null,
|
|
340
|
+
twoFactorRequired: false,
|
|
341
|
+
});
|
|
342
|
+
return this.http
|
|
343
|
+
.post(this.gatewayUrl(GATEWAY_AUTH_ENDPOINTS.login), {
|
|
344
|
+
userName: action.payload.userName,
|
|
345
|
+
password: action.payload.password,
|
|
346
|
+
isEncrypted: action.payload.isEncrypted ?? false,
|
|
347
|
+
deviceToken: action.payload.deviceToken || this.deviceToken,
|
|
348
|
+
recaptchaToken: action.payload.recaptchaToken,
|
|
349
|
+
recaptchaAction: action.payload.recaptchaAction,
|
|
350
|
+
})
|
|
351
|
+
.pipe(mergeMap((response) => this.handleLoginResponse(ctx, response.data)), catchError((error) => {
|
|
352
|
+
ctx.dispatch(new LoginFailure(getGatewayErrorMessage(error, 'Login failed')));
|
|
353
|
+
return of(null);
|
|
354
|
+
}));
|
|
355
|
+
}
|
|
356
|
+
verifyMfa(ctx, action) {
|
|
357
|
+
const pendingMfa = ctx.getState().pendingMfa;
|
|
358
|
+
if (!pendingMfa) {
|
|
359
|
+
ctx.dispatch(new LoginFailure('The verification session expired.'));
|
|
360
|
+
this.navigateToLogin();
|
|
361
|
+
return EMPTY;
|
|
362
|
+
}
|
|
363
|
+
ctx.patchState({ loading: true, error: null });
|
|
364
|
+
return this.http
|
|
365
|
+
.post(this.gatewayUrl(GATEWAY_AUTH_ENDPOINTS.verifyMfa), {
|
|
366
|
+
userId: pendingMfa.userId,
|
|
367
|
+
tempSessionId: pendingMfa.tempSessionId,
|
|
368
|
+
code: action.code,
|
|
369
|
+
deviceToken: this.deviceToken,
|
|
370
|
+
})
|
|
371
|
+
.pipe(mergeMap((response) => this.handleLoginResponse(ctx, response.data)), catchError((error) => {
|
|
372
|
+
const message = getGatewayErrorMessage(error, 'Invalid verification code');
|
|
373
|
+
const shouldClearMfa = error?.status === 403;
|
|
374
|
+
ctx.patchState({
|
|
375
|
+
loading: false,
|
|
376
|
+
error: message,
|
|
377
|
+
pendingMfa: shouldClearMfa ? null : pendingMfa,
|
|
378
|
+
twoFactorRequired: !shouldClearMfa,
|
|
379
|
+
});
|
|
380
|
+
if (shouldClearMfa) {
|
|
381
|
+
this.navigateToLogin();
|
|
382
|
+
}
|
|
383
|
+
return of(null);
|
|
384
|
+
}));
|
|
385
|
+
}
|
|
386
|
+
resendMfa(ctx) {
|
|
387
|
+
const pendingMfa = ctx.getState().pendingMfa;
|
|
388
|
+
if (!pendingMfa) {
|
|
389
|
+
ctx.dispatch(new LoginFailure('The verification session expired.'));
|
|
390
|
+
this.navigateToLogin();
|
|
391
|
+
return EMPTY;
|
|
392
|
+
}
|
|
393
|
+
ctx.patchState({ mfaResendLoading: true, error: null });
|
|
394
|
+
return this.http
|
|
395
|
+
.post(this.gatewayUrl(GATEWAY_AUTH_ENDPOINTS.resendMfa), {
|
|
396
|
+
userId: pendingMfa.userId,
|
|
397
|
+
tempSessionId: pendingMfa.tempSessionId,
|
|
398
|
+
})
|
|
399
|
+
.pipe(tap((response) => {
|
|
400
|
+
ctx.patchState({
|
|
401
|
+
pendingMfa: response.data,
|
|
402
|
+
twoFactorRequired: true,
|
|
403
|
+
mfaResendLoading: false,
|
|
404
|
+
error: null,
|
|
405
|
+
});
|
|
406
|
+
}), catchError((error) => {
|
|
407
|
+
ctx.patchState({
|
|
408
|
+
mfaResendLoading: false,
|
|
409
|
+
error: getGatewayErrorMessage(error, 'Failed to resend code'),
|
|
410
|
+
});
|
|
411
|
+
return of(null);
|
|
412
|
+
}));
|
|
413
|
+
}
|
|
414
|
+
loadSsoProviders(ctx) {
|
|
415
|
+
ctx.patchState({ ssoLoading: true });
|
|
416
|
+
return this.http
|
|
417
|
+
.get(this.gatewayUrl(GATEWAY_AUTH_ENDPOINTS.ssoProviders), {
|
|
418
|
+
params: { platform: this.options.platform ?? 'web' },
|
|
419
|
+
})
|
|
420
|
+
.pipe(tap((response) => {
|
|
421
|
+
ctx.patchState({
|
|
422
|
+
ssoProviders: (response.data?.providers ?? []).filter((provider) => provider.enabled),
|
|
423
|
+
ssoLoading: false,
|
|
424
|
+
});
|
|
425
|
+
}), catchError(() => {
|
|
426
|
+
ctx.patchState({
|
|
427
|
+
ssoProviders: [],
|
|
428
|
+
ssoLoading: false,
|
|
429
|
+
});
|
|
430
|
+
return of(null);
|
|
431
|
+
}));
|
|
432
|
+
}
|
|
433
|
+
startSso(ctx, action) {
|
|
434
|
+
if (action.provider.flow !== 'browser_redirect') {
|
|
435
|
+
ctx.patchState({
|
|
436
|
+
error: 'This SSO provider is not available for browser sign-in.',
|
|
437
|
+
});
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
const gatewayApiBaseUrl = this.options.getGatewayApiBaseUrl();
|
|
441
|
+
if (!gatewayApiBaseUrl || typeof window === 'undefined') {
|
|
442
|
+
ctx.patchState({ error: 'SSO is not configured.' });
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
const clientState = this.ssoSession.createAndStoreState();
|
|
446
|
+
const callbackPath = this.options.ssoCallbackPath ?? '/auth/sso/callback';
|
|
447
|
+
const startUrl = buildSsoStartUrl({
|
|
448
|
+
gatewayApiBaseUrl,
|
|
449
|
+
provider: action.provider,
|
|
450
|
+
returnUrl: `${window.location.origin}${callbackPath}`,
|
|
451
|
+
clientState,
|
|
452
|
+
platform: this.options.platform ?? 'web',
|
|
453
|
+
});
|
|
454
|
+
window.location.assign(startUrl);
|
|
455
|
+
}
|
|
456
|
+
exchangeSsoCode(ctx, action) {
|
|
457
|
+
ctx.patchState({ ssoCallbackLoading: true, error: null });
|
|
458
|
+
return this.http
|
|
459
|
+
.post(this.gatewayUrl(GATEWAY_AUTH_ENDPOINTS.ssoExchange), {
|
|
460
|
+
code: action.code,
|
|
461
|
+
state: action.state,
|
|
462
|
+
deviceToken: this.deviceToken,
|
|
463
|
+
})
|
|
464
|
+
.pipe(mergeMap((response) => this.handleLoginResponse(ctx, response.data)), catchError((error) => {
|
|
465
|
+
ctx.patchState({
|
|
466
|
+
ssoCallbackLoading: false,
|
|
467
|
+
error: getGatewayErrorMessage(error, 'SSO sign-in failed. Please try again.'),
|
|
468
|
+
});
|
|
469
|
+
return of(null);
|
|
470
|
+
}));
|
|
471
|
+
}
|
|
472
|
+
loginSuccess(ctx, action) {
|
|
473
|
+
return this.handleLoginResponse(ctx, action.session);
|
|
474
|
+
}
|
|
475
|
+
loginFailure(ctx, action) {
|
|
476
|
+
ctx.patchState({
|
|
477
|
+
loading: false,
|
|
478
|
+
ssoCallbackLoading: false,
|
|
479
|
+
error: action.error,
|
|
480
|
+
user: null,
|
|
481
|
+
token: null,
|
|
482
|
+
refreshToken: null,
|
|
483
|
+
accessTokenExpiresAt: null,
|
|
484
|
+
refreshTokenExpiresAt: null,
|
|
485
|
+
pendingMfa: null,
|
|
486
|
+
twoFactorRequired: false,
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
logout(ctx, action) {
|
|
490
|
+
const { token, refreshToken, ssoProviders } = ctx.getState();
|
|
491
|
+
this.ssoSession.clearAll();
|
|
492
|
+
this.toObservable(this.options.beforeLocalLogout?.(ctx)).subscribe();
|
|
493
|
+
ctx.patchState({
|
|
494
|
+
...AUTH_STATE_DEFAULTS,
|
|
495
|
+
ssoProviders: this.options.preserveSsoProvidersOnLogout
|
|
496
|
+
? ssoProviders
|
|
497
|
+
: [],
|
|
498
|
+
});
|
|
499
|
+
this.navigateToLogin();
|
|
500
|
+
if (action.remote && token) {
|
|
501
|
+
this.http
|
|
502
|
+
.post(this.gatewayUrl(GATEWAY_AUTH_ENDPOINTS.logout), {
|
|
503
|
+
refreshToken,
|
|
504
|
+
deviceToken: this.deviceToken,
|
|
505
|
+
}, {
|
|
506
|
+
headers: {
|
|
507
|
+
Authorization: `Bearer ${token}`,
|
|
508
|
+
noMessage: 'true',
|
|
509
|
+
},
|
|
510
|
+
})
|
|
511
|
+
.pipe(catchError(() => EMPTY))
|
|
512
|
+
.subscribe();
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
updateUserData(ctx, action) {
|
|
516
|
+
ctx.patchState({
|
|
517
|
+
user: action.user,
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
updateTokens(ctx, action) {
|
|
521
|
+
const tokenData = mapGatewayTokens(action.tokens);
|
|
522
|
+
const state = ctx.getState();
|
|
523
|
+
const updatedUser = state.user
|
|
524
|
+
? {
|
|
525
|
+
...state.user,
|
|
526
|
+
token: tokenData.accessToken ?? state.user.token,
|
|
527
|
+
accessToken: tokenData.accessToken ?? state.user.accessToken,
|
|
528
|
+
refreshToken: tokenData.refreshToken ?? state.user.refreshToken,
|
|
529
|
+
accessTokenExpiresAt: tokenData.accessTokenExpiresAt ?? undefined,
|
|
530
|
+
refreshTokenExpiresAt: tokenData.refreshTokenExpiresAt ?? undefined,
|
|
531
|
+
}
|
|
532
|
+
: state.user;
|
|
533
|
+
ctx.patchState({
|
|
534
|
+
token: tokenData.accessToken,
|
|
535
|
+
refreshToken: tokenData.refreshToken,
|
|
536
|
+
accessTokenExpiresAt: tokenData.accessTokenExpiresAt,
|
|
537
|
+
refreshTokenExpiresAt: tokenData.refreshTokenExpiresAt,
|
|
538
|
+
user: updatedUser,
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
clearError(ctx) {
|
|
542
|
+
ctx.patchState({ error: null });
|
|
543
|
+
}
|
|
544
|
+
clearPendingMfa(ctx) {
|
|
545
|
+
ctx.patchState({ pendingMfa: null, twoFactorRequired: false });
|
|
546
|
+
}
|
|
547
|
+
handleLoginResponse(ctx, session) {
|
|
548
|
+
if (session.requiresTwoFactor) {
|
|
549
|
+
ctx.patchState({
|
|
550
|
+
user: null,
|
|
551
|
+
token: null,
|
|
552
|
+
refreshToken: null,
|
|
553
|
+
accessTokenExpiresAt: null,
|
|
554
|
+
refreshTokenExpiresAt: null,
|
|
555
|
+
pendingMfa: session.twoFactor,
|
|
556
|
+
twoFactorRequired: true,
|
|
557
|
+
loading: false,
|
|
558
|
+
ssoCallbackLoading: false,
|
|
559
|
+
error: null,
|
|
560
|
+
});
|
|
561
|
+
this.router.navigate([this.options.mfaRoute ?? '/auth/mfa'], {
|
|
562
|
+
replaceUrl: true,
|
|
563
|
+
});
|
|
564
|
+
return of(null);
|
|
565
|
+
}
|
|
566
|
+
if (!hasGatewayTokens(session.tokens)) {
|
|
567
|
+
ctx.dispatch(new LoginFailure('Login failed'));
|
|
568
|
+
return of(null);
|
|
569
|
+
}
|
|
570
|
+
const tokenData = mapGatewayTokens(session.tokens);
|
|
571
|
+
const user = mapGatewayUser(session.user, tokenData, session.delegations);
|
|
572
|
+
ctx.patchState({
|
|
573
|
+
user,
|
|
574
|
+
token: tokenData.accessToken,
|
|
575
|
+
refreshToken: tokenData.refreshToken,
|
|
576
|
+
accessTokenExpiresAt: tokenData.accessTokenExpiresAt,
|
|
577
|
+
refreshTokenExpiresAt: tokenData.refreshTokenExpiresAt,
|
|
578
|
+
loading: false,
|
|
579
|
+
ssoCallbackLoading: false,
|
|
580
|
+
error: null,
|
|
581
|
+
pendingMfa: null,
|
|
582
|
+
twoFactorRequired: false,
|
|
583
|
+
});
|
|
584
|
+
return this.toObservable(this.options.afterLogin?.(session, ctx)).pipe(tap(() => {
|
|
585
|
+
this.router.navigateByUrl(this.resolveAuthenticatedRoute(), {
|
|
586
|
+
replaceUrl: true,
|
|
587
|
+
});
|
|
588
|
+
}));
|
|
589
|
+
}
|
|
590
|
+
get deviceToken() {
|
|
591
|
+
return this.options.deviceToken ?? GATEWAY_AUTH_DEVICE_TOKEN;
|
|
592
|
+
}
|
|
593
|
+
gatewayUrl(path) {
|
|
594
|
+
return buildGatewayUrl(this.options.getGatewayApiBaseUrl(), path);
|
|
595
|
+
}
|
|
596
|
+
navigateToLogin() {
|
|
597
|
+
this.router.navigate([this.options.loginRoute ?? '/auth/login'], {
|
|
598
|
+
replaceUrl: true,
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
resolveAuthenticatedRoute() {
|
|
602
|
+
const tree = this.router.parseUrl(this.router.url);
|
|
603
|
+
const returnUrl = this.sanitizeLocalRoute(tree.queryParams['returnUrl']);
|
|
604
|
+
if (returnUrl !== null) {
|
|
605
|
+
return returnUrl;
|
|
606
|
+
}
|
|
607
|
+
const configuredRoute = typeof this.options.defaultAuthenticatedRoute === 'function'
|
|
608
|
+
? this.options.defaultAuthenticatedRoute()
|
|
609
|
+
: this.options.defaultAuthenticatedRoute;
|
|
610
|
+
return this.sanitizeLocalRoute(configuredRoute) ?? '/';
|
|
611
|
+
}
|
|
612
|
+
sanitizeLocalRoute(route) {
|
|
613
|
+
if (!route) {
|
|
614
|
+
return null;
|
|
615
|
+
}
|
|
616
|
+
if (/^https?:\/\//i.test(route)) {
|
|
617
|
+
return null;
|
|
618
|
+
}
|
|
619
|
+
return route.startsWith('/') ? route : `/${route}`;
|
|
620
|
+
}
|
|
621
|
+
toObservable(result) {
|
|
622
|
+
if (!result) {
|
|
623
|
+
return of(null);
|
|
624
|
+
}
|
|
625
|
+
if (isObservable(result)) {
|
|
626
|
+
return result;
|
|
627
|
+
}
|
|
628
|
+
if (result instanceof Promise) {
|
|
629
|
+
return from(result);
|
|
630
|
+
}
|
|
631
|
+
return of(result);
|
|
632
|
+
}
|
|
633
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: GatewayAuthState, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
634
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: GatewayAuthState });
|
|
635
|
+
};
|
|
636
|
+
__decorate([
|
|
637
|
+
Action(Login)
|
|
638
|
+
], GatewayAuthState.prototype, "login", null);
|
|
639
|
+
__decorate([
|
|
640
|
+
Action(VerifyMfa)
|
|
641
|
+
], GatewayAuthState.prototype, "verifyMfa", null);
|
|
642
|
+
__decorate([
|
|
643
|
+
Action(ResendMfa)
|
|
644
|
+
], GatewayAuthState.prototype, "resendMfa", null);
|
|
645
|
+
__decorate([
|
|
646
|
+
Action(LoadSsoProviders)
|
|
647
|
+
], GatewayAuthState.prototype, "loadSsoProviders", null);
|
|
648
|
+
__decorate([
|
|
649
|
+
Action(StartSso)
|
|
650
|
+
], GatewayAuthState.prototype, "startSso", null);
|
|
651
|
+
__decorate([
|
|
652
|
+
Action(ExchangeSsoCode)
|
|
653
|
+
], GatewayAuthState.prototype, "exchangeSsoCode", null);
|
|
654
|
+
__decorate([
|
|
655
|
+
Action(LoginSuccess)
|
|
656
|
+
], GatewayAuthState.prototype, "loginSuccess", null);
|
|
657
|
+
__decorate([
|
|
658
|
+
Action(LoginFailure)
|
|
659
|
+
], GatewayAuthState.prototype, "loginFailure", null);
|
|
660
|
+
__decorate([
|
|
661
|
+
Action(Logout)
|
|
662
|
+
], GatewayAuthState.prototype, "logout", null);
|
|
663
|
+
__decorate([
|
|
664
|
+
Action(UpdateUserData)
|
|
665
|
+
], GatewayAuthState.prototype, "updateUserData", null);
|
|
666
|
+
__decorate([
|
|
667
|
+
Action(UpdateTokens)
|
|
668
|
+
], GatewayAuthState.prototype, "updateTokens", null);
|
|
669
|
+
__decorate([
|
|
670
|
+
Action(ClearError)
|
|
671
|
+
], GatewayAuthState.prototype, "clearError", null);
|
|
672
|
+
__decorate([
|
|
673
|
+
Action(ClearPendingMfa)
|
|
674
|
+
], GatewayAuthState.prototype, "clearPendingMfa", null);
|
|
675
|
+
__decorate([
|
|
676
|
+
Selector()
|
|
677
|
+
], GatewayAuthState, "user", null);
|
|
678
|
+
__decorate([
|
|
679
|
+
Selector()
|
|
680
|
+
], GatewayAuthState, "loading", null);
|
|
681
|
+
__decorate([
|
|
682
|
+
Selector()
|
|
683
|
+
], GatewayAuthState, "mfaResendLoading", null);
|
|
684
|
+
__decorate([
|
|
685
|
+
Selector()
|
|
686
|
+
], GatewayAuthState, "ssoLoading", null);
|
|
687
|
+
__decorate([
|
|
688
|
+
Selector()
|
|
689
|
+
], GatewayAuthState, "ssoCallbackLoading", null);
|
|
690
|
+
__decorate([
|
|
691
|
+
Selector()
|
|
692
|
+
], GatewayAuthState, "error", null);
|
|
693
|
+
__decorate([
|
|
694
|
+
Selector()
|
|
695
|
+
], GatewayAuthState, "token", null);
|
|
696
|
+
__decorate([
|
|
697
|
+
Selector()
|
|
698
|
+
], GatewayAuthState, "refreshToken", null);
|
|
699
|
+
__decorate([
|
|
700
|
+
Selector()
|
|
701
|
+
], GatewayAuthState, "accessTokenExpiresAt", null);
|
|
702
|
+
__decorate([
|
|
703
|
+
Selector()
|
|
704
|
+
], GatewayAuthState, "refreshTokenExpiresAt", null);
|
|
705
|
+
__decorate([
|
|
706
|
+
Selector()
|
|
707
|
+
], GatewayAuthState, "twoFactorRequired", null);
|
|
708
|
+
__decorate([
|
|
709
|
+
Selector()
|
|
710
|
+
], GatewayAuthState, "pendingMfa", null);
|
|
711
|
+
__decorate([
|
|
712
|
+
Selector()
|
|
713
|
+
], GatewayAuthState, "ssoProviders", null);
|
|
714
|
+
__decorate([
|
|
715
|
+
Selector()
|
|
716
|
+
], GatewayAuthState, "isAdmin", null);
|
|
717
|
+
__decorate([
|
|
718
|
+
Selector()
|
|
719
|
+
], GatewayAuthState, "userDetails", null);
|
|
720
|
+
GatewayAuthState = __decorate([
|
|
721
|
+
State({
|
|
722
|
+
name: 'auth',
|
|
723
|
+
defaults: AUTH_STATE_DEFAULTS,
|
|
724
|
+
})
|
|
725
|
+
], GatewayAuthState);
|
|
726
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: GatewayAuthState, decorators: [{
|
|
727
|
+
type: Injectable
|
|
728
|
+
}], propDecorators: { login: [], verifyMfa: [], resendMfa: [], loadSsoProviders: [], startSso: [], exchangeSsoCode: [], loginSuccess: [], loginFailure: [], logout: [], updateUserData: [], updateTokens: [], clearError: [], clearPendingMfa: [] } });
|
|
729
|
+
|
|
730
|
+
class GatewayAuthFacade {
|
|
731
|
+
store = inject(Store);
|
|
732
|
+
user = select(GatewayAuthState.user);
|
|
733
|
+
loading = select(GatewayAuthState.loading);
|
|
734
|
+
mfaResendLoading = select(GatewayAuthState.mfaResendLoading);
|
|
735
|
+
ssoLoading = select(GatewayAuthState.ssoLoading);
|
|
736
|
+
ssoCallbackLoading = select(GatewayAuthState.ssoCallbackLoading);
|
|
737
|
+
error = select(GatewayAuthState.error);
|
|
738
|
+
token = select(GatewayAuthState.token);
|
|
739
|
+
refreshToken = select(GatewayAuthState.refreshToken);
|
|
740
|
+
accessTokenExpiresAt = select(GatewayAuthState.accessTokenExpiresAt);
|
|
741
|
+
refreshTokenExpiresAt = select(GatewayAuthState.refreshTokenExpiresAt);
|
|
742
|
+
twoFactorRequired = select(GatewayAuthState.twoFactorRequired);
|
|
743
|
+
pendingMfa = select(GatewayAuthState.pendingMfa);
|
|
744
|
+
ssoProviders = select(GatewayAuthState.ssoProviders);
|
|
745
|
+
isAdmin = select(GatewayAuthState.isAdmin);
|
|
746
|
+
userDetails = select(GatewayAuthState.userDetails);
|
|
747
|
+
hasError = computed(() => this.error() !== null, ...(ngDevMode ? [{ debugName: "hasError" }] : /* istanbul ignore next */ []));
|
|
748
|
+
isReady = computed(() => !this.loading() && this.error() === null, ...(ngDevMode ? [{ debugName: "isReady" }] : /* istanbul ignore next */ []));
|
|
749
|
+
userDisplayName = computed(() => this.user()?.user?.displayName || '', ...(ngDevMode ? [{ debugName: "userDisplayName" }] : /* istanbul ignore next */ []));
|
|
750
|
+
userEmail = computed(() => this.user()?.user?.email || '', ...(ngDevMode ? [{ debugName: "userEmail" }] : /* istanbul ignore next */ []));
|
|
751
|
+
hasUser = computed(() => this.user() !== null, ...(ngDevMode ? [{ debugName: "hasUser" }] : /* istanbul ignore next */ []));
|
|
752
|
+
hasPendingMfa = computed(() => this.pendingMfa() !== null, ...(ngDevMode ? [{ debugName: "hasPendingMfa" }] : /* istanbul ignore next */ []));
|
|
753
|
+
isAuthenticated = computed(() => {
|
|
754
|
+
const token = this.token();
|
|
755
|
+
const accessExpiresAt = this.accessTokenExpiresAt();
|
|
756
|
+
if (token && !isExpired(accessExpiresAt)) {
|
|
757
|
+
return true;
|
|
758
|
+
}
|
|
759
|
+
const refreshToken = this.refreshToken();
|
|
760
|
+
const refreshExpiresAt = this.refreshTokenExpiresAt();
|
|
761
|
+
return !!refreshToken && !isExpired(refreshExpiresAt);
|
|
762
|
+
}, ...(ngDevMode ? [{ debugName: "isAuthenticated" }] : /* istanbul ignore next */ []));
|
|
763
|
+
login(loginRequest) {
|
|
764
|
+
this.store.dispatch(new Login(loginRequest));
|
|
765
|
+
}
|
|
766
|
+
verifyMfa(code) {
|
|
767
|
+
this.store.dispatch(new VerifyMfa(code));
|
|
768
|
+
}
|
|
769
|
+
resendMfa() {
|
|
770
|
+
this.store.dispatch(new ResendMfa());
|
|
771
|
+
}
|
|
772
|
+
loadSsoProviders() {
|
|
773
|
+
this.store.dispatch(new LoadSsoProviders());
|
|
774
|
+
}
|
|
775
|
+
startSso(provider) {
|
|
776
|
+
this.store.dispatch(new StartSso(provider));
|
|
777
|
+
}
|
|
778
|
+
exchangeSsoCode(code, state) {
|
|
779
|
+
this.store.dispatch(new ExchangeSsoCode(code, state));
|
|
780
|
+
}
|
|
781
|
+
logout(remote = false) {
|
|
782
|
+
this.store.dispatch(new Logout(remote));
|
|
783
|
+
}
|
|
784
|
+
updateUserData(user) {
|
|
785
|
+
this.store.dispatch(new UpdateUserData(user));
|
|
786
|
+
}
|
|
787
|
+
updateTokens(tokens) {
|
|
788
|
+
this.store.dispatch(new UpdateTokens(tokens));
|
|
789
|
+
}
|
|
790
|
+
clearError() {
|
|
791
|
+
this.store.dispatch(new ClearError());
|
|
792
|
+
}
|
|
793
|
+
clearPendingMfa() {
|
|
794
|
+
this.store.dispatch(new ClearPendingMfa());
|
|
795
|
+
}
|
|
796
|
+
hasRole(role) {
|
|
797
|
+
const userRoles = this.user()?.user?.roles;
|
|
798
|
+
return Array.isArray(userRoles) ? userRoles.includes(role) : false;
|
|
799
|
+
}
|
|
800
|
+
hasGroup(group) {
|
|
801
|
+
const userGroups = this.user()?.user?.groups;
|
|
802
|
+
return Array.isArray(userGroups) ? userGroups.includes(group) : false;
|
|
803
|
+
}
|
|
804
|
+
isTokenExpired() {
|
|
805
|
+
const accessExpiresAt = this.accessTokenExpiresAt();
|
|
806
|
+
return accessExpiresAt ? isExpired(accessExpiresAt) : false;
|
|
807
|
+
}
|
|
808
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: GatewayAuthFacade, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
809
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: GatewayAuthFacade, providedIn: 'root' });
|
|
810
|
+
}
|
|
811
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: GatewayAuthFacade, decorators: [{
|
|
812
|
+
type: Injectable,
|
|
813
|
+
args: [{
|
|
814
|
+
providedIn: 'root',
|
|
815
|
+
}]
|
|
816
|
+
}] });
|
|
817
|
+
|
|
818
|
+
const GATEWAY_AUTH_RETRY_CONTEXT = new HttpContextToken(() => false);
|
|
819
|
+
const GATEWAY_AUTH_ENDPOINT_PATHS = new Set([
|
|
820
|
+
GATEWAY_AUTH_ENDPOINTS.login,
|
|
821
|
+
GATEWAY_AUTH_ENDPOINTS.refresh,
|
|
822
|
+
GATEWAY_AUTH_ENDPOINTS.logout,
|
|
823
|
+
]);
|
|
824
|
+
const GATEWAY_AUTH_ENDPOINT_PREFIXES = ['auth/2fa/', 'auth/sso/'];
|
|
825
|
+
const isAbsoluteUrl = (url) => /^https?:\/\//i.test(url);
|
|
826
|
+
const isAssetRequest = (url) => url.startsWith('/assets') || url.startsWith('assets');
|
|
827
|
+
const normalizeBase = (baseUrl) => baseUrl?.replace(/\/+$/, '') ?? '';
|
|
828
|
+
const buildAbsoluteUrl = (baseUrl, url) => {
|
|
829
|
+
if (isAbsoluteUrl(url) || !baseUrl) {
|
|
830
|
+
return url;
|
|
831
|
+
}
|
|
832
|
+
return `${normalizeBase(baseUrl)}/${url.replace(/^\/+/, '')}`;
|
|
833
|
+
};
|
|
834
|
+
const normalizePath = (path) => {
|
|
835
|
+
const trimmed = path.replace(/^\/+/, '');
|
|
836
|
+
const withoutQuery = trimmed.split('?')[0] || '';
|
|
837
|
+
return withoutQuery.startsWith('api/') ? withoutQuery.slice(4) : withoutQuery;
|
|
838
|
+
};
|
|
839
|
+
function resolveGatewayAuthPath(url, gatewayApiBaseUrl) {
|
|
840
|
+
if (isAbsoluteUrl(url)) {
|
|
841
|
+
try {
|
|
842
|
+
return normalizePath(new URL(url).pathname);
|
|
843
|
+
}
|
|
844
|
+
catch {
|
|
845
|
+
return normalizePath(url);
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
if (gatewayApiBaseUrl && url.startsWith(gatewayApiBaseUrl)) {
|
|
849
|
+
return normalizePath(url.slice(gatewayApiBaseUrl.length));
|
|
850
|
+
}
|
|
851
|
+
return normalizePath(url);
|
|
852
|
+
}
|
|
853
|
+
function isGatewayAuthRequestUrl(url, gatewayApiBaseUrl) {
|
|
854
|
+
const path = resolveGatewayAuthPath(url, gatewayApiBaseUrl).toLowerCase();
|
|
855
|
+
return (GATEWAY_AUTH_ENDPOINT_PATHS.has(path) ||
|
|
856
|
+
GATEWAY_AUTH_ENDPOINT_PREFIXES.some((prefix) => path.startsWith(prefix)));
|
|
857
|
+
}
|
|
858
|
+
let inflightRefresh$ = null;
|
|
859
|
+
const refreshTokens$ = (http, gatewayApiBaseUrl, refreshToken, auth, deviceToken) => {
|
|
860
|
+
if (!inflightRefresh$) {
|
|
861
|
+
const url = buildGatewayUrl(gatewayApiBaseUrl, GATEWAY_AUTH_ENDPOINTS.refresh);
|
|
862
|
+
inflightRefresh$ = http
|
|
863
|
+
.post(url, {
|
|
864
|
+
refreshToken,
|
|
865
|
+
deviceToken,
|
|
866
|
+
})
|
|
867
|
+
.pipe(map((response) => response.data), map((tokens) => {
|
|
868
|
+
if (!tokens?.accessToken || !tokens.refreshToken) {
|
|
869
|
+
throw new Error('Invalid refresh response');
|
|
870
|
+
}
|
|
871
|
+
return tokens;
|
|
872
|
+
}), tap$1((tokens) => auth.updateTokens(tokens)), shareReplay({ bufferSize: 1, refCount: true }), finalize(() => {
|
|
873
|
+
inflightRefresh$ = null;
|
|
874
|
+
}));
|
|
875
|
+
}
|
|
876
|
+
return inflightRefresh$;
|
|
877
|
+
};
|
|
878
|
+
const prepareRequest = (req, token, markRetried, baseUrl) => {
|
|
879
|
+
let modifiedReq = req;
|
|
880
|
+
if (token) {
|
|
881
|
+
modifiedReq = modifiedReq.clone({
|
|
882
|
+
setHeaders: {
|
|
883
|
+
Authorization: `Bearer ${token}`,
|
|
884
|
+
},
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
if (!isAbsoluteUrl(modifiedReq.url) && !isAssetRequest(modifiedReq.url)) {
|
|
888
|
+
modifiedReq = modifiedReq.clone({
|
|
889
|
+
url: buildAbsoluteUrl(baseUrl, modifiedReq.url),
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
if (markRetried) {
|
|
893
|
+
modifiedReq = modifiedReq.clone({
|
|
894
|
+
context: modifiedReq.context.set(GATEWAY_AUTH_RETRY_CONTEXT, true),
|
|
895
|
+
});
|
|
896
|
+
}
|
|
897
|
+
return modifiedReq;
|
|
898
|
+
};
|
|
899
|
+
const gatewayAuthInterceptor = (req, next) => {
|
|
900
|
+
if (isAssetRequest(req.url)) {
|
|
901
|
+
return next(req);
|
|
902
|
+
}
|
|
903
|
+
const auth = inject(GatewayAuthFacade);
|
|
904
|
+
const options = inject(GATEWAY_AUTH_OPTIONS);
|
|
905
|
+
const http = new HttpClient(inject(HttpBackend));
|
|
906
|
+
const gatewayApiBaseUrl = options.getGatewayApiBaseUrl();
|
|
907
|
+
const appApiBaseUrl = options.getApplicationApiBaseUrl?.();
|
|
908
|
+
const useGatewayBaseUrl = options.shouldUseGatewayApiBaseUrl?.(req) ?? false;
|
|
909
|
+
const baseUrl = useGatewayBaseUrl ? gatewayApiBaseUrl : appApiBaseUrl;
|
|
910
|
+
const deviceToken = options.deviceToken ?? 'web-app';
|
|
911
|
+
const accessToken = auth.token();
|
|
912
|
+
const refreshToken = auth.refreshToken();
|
|
913
|
+
const accessTokenExpiresAt = auth.accessTokenExpiresAt();
|
|
914
|
+
const refreshTokenExpiresAt = auth.refreshTokenExpiresAt();
|
|
915
|
+
const accessTokenValid = !!accessToken && !isExpired(accessTokenExpiresAt);
|
|
916
|
+
const canRefresh = !!refreshToken && !isExpired(refreshTokenExpiresAt);
|
|
917
|
+
const alreadyRetried = req.context.get(GATEWAY_AUTH_RETRY_CONTEXT);
|
|
918
|
+
const isAuthRequest = isGatewayAuthRequestUrl(req.url, gatewayApiBaseUrl);
|
|
919
|
+
const tokenToAttach = !isAuthRequest && accessTokenValid ? accessToken : null;
|
|
920
|
+
if (!isAuthRequest && !accessTokenValid && canRefresh) {
|
|
921
|
+
return refreshTokens$(http, gatewayApiBaseUrl, refreshToken, auth, deviceToken).pipe(switchMap((tokens) => next(prepareRequest(req, tokens.accessToken, true, baseUrl))), catchError$1((error) => {
|
|
922
|
+
auth.logout();
|
|
923
|
+
return throwError(() => error);
|
|
924
|
+
}));
|
|
925
|
+
}
|
|
926
|
+
return next(prepareRequest(req, tokenToAttach, false, baseUrl)).pipe(catchError$1((error) => {
|
|
927
|
+
if (error?.status === 401 && !isAuthRequest && !alreadyRetried) {
|
|
928
|
+
const latestRefreshToken = auth.refreshToken();
|
|
929
|
+
const latestRefreshTokenExpiresAt = auth.refreshTokenExpiresAt();
|
|
930
|
+
const canRefreshNow = !!latestRefreshToken && !isExpired(latestRefreshTokenExpiresAt);
|
|
931
|
+
if (canRefreshNow) {
|
|
932
|
+
return refreshTokens$(http, gatewayApiBaseUrl, latestRefreshToken, auth, deviceToken).pipe(switchMap((tokens) => next(prepareRequest(req, tokens.accessToken, true, baseUrl))), catchError$1((refreshError) => {
|
|
933
|
+
auth.logout();
|
|
934
|
+
return throwError(() => refreshError);
|
|
935
|
+
}));
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
if (error?.status === 401 && !isAuthRequest) {
|
|
939
|
+
auth.logout();
|
|
940
|
+
}
|
|
941
|
+
return throwError(() => error);
|
|
942
|
+
}));
|
|
943
|
+
};
|
|
944
|
+
|
|
945
|
+
class GatewayMfa {
|
|
946
|
+
auth = inject(GatewayAuthFacade);
|
|
947
|
+
fb = inject(FormBuilder);
|
|
948
|
+
router = inject(Router);
|
|
949
|
+
options = inject(GATEWAY_AUTH_OPTIONS);
|
|
950
|
+
pendingMfa = this.auth.pendingMfa;
|
|
951
|
+
loading = this.auth.loading;
|
|
952
|
+
resendLoading = this.auth.mfaResendLoading;
|
|
953
|
+
error = this.auth.error;
|
|
954
|
+
deliveryChannel = computed(() => this.pendingMfa()?.deliveryChannel || 'your configured channel', ...(ngDevMode ? [{ debugName: "deliveryChannel" }] : /* istanbul ignore next */ []));
|
|
955
|
+
expiresAt = computed(() => this.pendingMfa()?.expiresAtUtc ?? null, ...(ngDevMode ? [{ debugName: "expiresAt" }] : /* istanbul ignore next */ []));
|
|
956
|
+
form = this.fb.group({
|
|
957
|
+
code: ['', [Validators.required, Validators.pattern(/^[0-9]{4,8}$/)]],
|
|
958
|
+
});
|
|
959
|
+
ngOnInit() {
|
|
960
|
+
if (!this.pendingMfa()) {
|
|
961
|
+
this.router.navigate([this.options.loginRoute ?? '/auth/login'], {
|
|
962
|
+
replaceUrl: true,
|
|
963
|
+
});
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
verify() {
|
|
967
|
+
if (this.form.invalid) {
|
|
968
|
+
this.form.markAllAsTouched();
|
|
969
|
+
return;
|
|
970
|
+
}
|
|
971
|
+
const code = this.form.controls.code.value ?? '';
|
|
972
|
+
this.form.reset();
|
|
973
|
+
this.auth.verifyMfa(code);
|
|
974
|
+
}
|
|
975
|
+
resend() {
|
|
976
|
+
this.form.reset();
|
|
977
|
+
this.auth.resendMfa();
|
|
978
|
+
}
|
|
979
|
+
backToLogin() {
|
|
980
|
+
this.auth.clearPendingMfa();
|
|
981
|
+
this.router.navigate([this.options.loginRoute ?? '/auth/login'], {
|
|
982
|
+
replaceUrl: true,
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: GatewayMfa, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
986
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: GatewayMfa, isStandalone: true, selector: "mt-gateway-mfa", ngImport: i0, template: "<div\n class=\"min-h-screen w-screen flex items-center justify-center p-6 bg-surface-50\"\n>\n <div\n class=\"w-full max-w-md rounded-xl bg-white p-8 shadow-sm border border-surface-200\"\n >\n <div class=\"flex justify-center mb-6\">\n <div\n class=\"h-12 w-12 rounded-full bg-primary/10 flex items-center justify-center\"\n >\n <mt-icon\n icon=\"security.lock-01\"\n class=\"text-primary text-2xl\"\n ></mt-icon>\n </div>\n </div>\n\n <div class=\"text-center space-y-2 mb-6\">\n <h1 class=\"text-2xl font-semibold text-surface-950\">Verification code</h1>\n <p class=\"text-sm text-surface-600\">\n Enter the code sent through {{ deliveryChannel() }}.\n </p>\n @if (expiresAt(); as expiry) {\n <p class=\"text-xs text-surface-500\">\n Expires at {{ expiry | date: \"shortTime\" }}\n </p>\n }\n </div>\n\n <form [formGroup]=\"form\" (ngSubmit)=\"verify()\" class=\"space-y-4\">\n <mt-text-field\n label=\"Code\"\n formControlName=\"code\"\n [pInputs]=\"{ inputMode: 'numeric', autocomplete: 'one-time-code' }\"\n />\n\n @if (error(); as errorMessage) {\n <p class=\"text-sm text-red-500 text-center\">{{ errorMessage }}</p>\n }\n\n <mt-button\n label=\"Verify\"\n type=\"submit\"\n severity=\"primary\"\n size=\"large\"\n fluid\n [loading]=\"loading()\"\n [disabled]=\"form.invalid || loading()\"\n />\n </form>\n\n <div class=\"flex items-center justify-between gap-3 pt-5\">\n <mt-button\n label=\"Back\"\n type=\"button\"\n text\n [disabled]=\"loading()\"\n (onClick)=\"backToLogin()\"\n />\n <mt-button\n label=\"Resend code\"\n type=\"button\"\n variant=\"outlined\"\n [loading]=\"resendLoading()\"\n [disabled]=\"loading() || resendLoading()\"\n (onClick)=\"resend()\"\n />\n </div>\n </div>\n</div>\n", styles: [":host{display:block}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "component", type: Button, selector: "mt-button", inputs: ["icon", "label", "tooltip", "class", "type", "styleClass", "severity", "badge", "variant", "badgeSeverity", "size", "iconPos", "autofocus", "fluid", "raised", "rounded", "text", "plain", "outlined", "link", "disabled", "loading", "pInputs"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "component", type: TextField, selector: "mt-text-field", inputs: ["field", "hint", "label", "placeholder", "class", "type", "readonly", "pInputs", "required", "icon", "iconPosition"] }, { kind: "component", type: Icon, selector: "mt-icon", inputs: ["icon"] }, { kind: "pipe", type: i2.DatePipe, name: "date" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
987
|
+
}
|
|
988
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: GatewayMfa, decorators: [{
|
|
989
|
+
type: Component,
|
|
990
|
+
args: [{ selector: 'mt-gateway-mfa', imports: [CommonModule, ReactiveFormsModule, Button, TextField, Icon], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div\n class=\"min-h-screen w-screen flex items-center justify-center p-6 bg-surface-50\"\n>\n <div\n class=\"w-full max-w-md rounded-xl bg-white p-8 shadow-sm border border-surface-200\"\n >\n <div class=\"flex justify-center mb-6\">\n <div\n class=\"h-12 w-12 rounded-full bg-primary/10 flex items-center justify-center\"\n >\n <mt-icon\n icon=\"security.lock-01\"\n class=\"text-primary text-2xl\"\n ></mt-icon>\n </div>\n </div>\n\n <div class=\"text-center space-y-2 mb-6\">\n <h1 class=\"text-2xl font-semibold text-surface-950\">Verification code</h1>\n <p class=\"text-sm text-surface-600\">\n Enter the code sent through {{ deliveryChannel() }}.\n </p>\n @if (expiresAt(); as expiry) {\n <p class=\"text-xs text-surface-500\">\n Expires at {{ expiry | date: \"shortTime\" }}\n </p>\n }\n </div>\n\n <form [formGroup]=\"form\" (ngSubmit)=\"verify()\" class=\"space-y-4\">\n <mt-text-field\n label=\"Code\"\n formControlName=\"code\"\n [pInputs]=\"{ inputMode: 'numeric', autocomplete: 'one-time-code' }\"\n />\n\n @if (error(); as errorMessage) {\n <p class=\"text-sm text-red-500 text-center\">{{ errorMessage }}</p>\n }\n\n <mt-button\n label=\"Verify\"\n type=\"submit\"\n severity=\"primary\"\n size=\"large\"\n fluid\n [loading]=\"loading()\"\n [disabled]=\"form.invalid || loading()\"\n />\n </form>\n\n <div class=\"flex items-center justify-between gap-3 pt-5\">\n <mt-button\n label=\"Back\"\n type=\"button\"\n text\n [disabled]=\"loading()\"\n (onClick)=\"backToLogin()\"\n />\n <mt-button\n label=\"Resend code\"\n type=\"button\"\n variant=\"outlined\"\n [loading]=\"resendLoading()\"\n [disabled]=\"loading() || resendLoading()\"\n (onClick)=\"resend()\"\n />\n </div>\n </div>\n</div>\n", styles: [":host{display:block}\n"] }]
|
|
991
|
+
}] });
|
|
992
|
+
|
|
993
|
+
class GatewaySsoCallback {
|
|
994
|
+
route = inject(ActivatedRoute);
|
|
995
|
+
router = inject(Router);
|
|
996
|
+
auth = inject(GatewayAuthFacade);
|
|
997
|
+
options = inject(GATEWAY_AUTH_OPTIONS);
|
|
998
|
+
ssoSession = new GatewaySsoSession();
|
|
999
|
+
loading = this.auth.ssoCallbackLoading;
|
|
1000
|
+
exchangeError = this.auth.error;
|
|
1001
|
+
callbackError = signal(null, ...(ngDevMode ? [{ debugName: "callbackError" }] : /* istanbul ignore next */ []));
|
|
1002
|
+
ngOnInit() {
|
|
1003
|
+
const params = this.route.snapshot.queryParamMap;
|
|
1004
|
+
const error = params.get('error');
|
|
1005
|
+
const errorCode = params.get('errorCode');
|
|
1006
|
+
const code = params.get('code');
|
|
1007
|
+
const state = params.get('state');
|
|
1008
|
+
this.ssoSession.clearCallbackQuery(this.options.ssoCallbackPath ?? '/auth/sso/callback');
|
|
1009
|
+
if (!this.ssoSession.consumeExpectedState(state)) {
|
|
1010
|
+
this.callbackError.set(this.safeSsoMessage('invalid_state'));
|
|
1011
|
+
return;
|
|
1012
|
+
}
|
|
1013
|
+
if (error) {
|
|
1014
|
+
this.callbackError.set(this.safeSsoMessage(errorCode ?? 'sso_failed'));
|
|
1015
|
+
return;
|
|
1016
|
+
}
|
|
1017
|
+
if (!code || !state) {
|
|
1018
|
+
this.callbackError.set(this.safeSsoMessage('invalid_state'));
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
1021
|
+
this.auth.exchangeSsoCode(code, state);
|
|
1022
|
+
}
|
|
1023
|
+
retry() {
|
|
1024
|
+
this.router.navigate([this.options.loginRoute ?? '/auth/login'], {
|
|
1025
|
+
replaceUrl: true,
|
|
1026
|
+
});
|
|
1027
|
+
}
|
|
1028
|
+
safeSsoMessage(errorCode) {
|
|
1029
|
+
const messages = {
|
|
1030
|
+
invalid_state: 'The sign-in session expired. Please try again.',
|
|
1031
|
+
sso_exchange_expired: 'The sign-in session expired. Please try again.',
|
|
1032
|
+
sso_exchange_used: 'This sign-in session was already used. Please try again.',
|
|
1033
|
+
sso_user_not_found: 'Your account is not configured for SSO.',
|
|
1034
|
+
sso_provisioning_not_allowed: 'Your account is not configured for SSO.',
|
|
1035
|
+
};
|
|
1036
|
+
return messages[errorCode] ?? 'SSO sign-in failed. Please try again.';
|
|
1037
|
+
}
|
|
1038
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: GatewaySsoCallback, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1039
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: GatewaySsoCallback, isStandalone: true, selector: "mt-gateway-sso-callback", ngImport: i0, template: "<div\n class=\"min-h-screen w-screen flex items-center justify-center p-6 bg-surface-50\"\n>\n <div\n class=\"w-full max-w-md rounded-xl bg-white p-8 shadow-sm border border-surface-200 text-center\"\n >\n @if (callbackError() || exchangeError(); as errorMessage) {\n <div class=\"space-y-5\">\n <mt-icon\n icon=\"alert.alert-circle\"\n class=\"text-red-500 text-4xl\"\n ></mt-icon>\n <div class=\"space-y-2\">\n <h1 class=\"text-xl font-semibold text-surface-950\">\n SSO sign-in failed\n </h1>\n <p class=\"text-sm text-surface-600\">{{ errorMessage }}</p>\n </div>\n <mt-button\n label=\"Back to sign in\"\n severity=\"primary\"\n (onClick)=\"retry()\"\n ></mt-button>\n </div>\n } @else {\n <div class=\"space-y-5\">\n <mt-icon\n icon=\"general.loading-02\"\n class=\"text-primary text-4xl animate-spin animate-duration-2000\"\n >\n </mt-icon>\n <div class=\"space-y-2\">\n <h1 class=\"text-xl font-semibold text-surface-950\">\n Completing sign-in\n </h1>\n <p class=\"text-sm text-surface-600\">\n Please wait while we finish the secure sign-in.\n </p>\n </div>\n </div>\n }\n </div>\n</div>\n", styles: [":host{display:block}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: Button, selector: "mt-button", inputs: ["icon", "label", "tooltip", "class", "type", "styleClass", "severity", "badge", "variant", "badgeSeverity", "size", "iconPos", "autofocus", "fluid", "raised", "rounded", "text", "plain", "outlined", "link", "disabled", "loading", "pInputs"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "component", type: Icon, selector: "mt-icon", inputs: ["icon"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1040
|
+
}
|
|
1041
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: GatewaySsoCallback, decorators: [{
|
|
1042
|
+
type: Component,
|
|
1043
|
+
args: [{ selector: 'mt-gateway-sso-callback', imports: [CommonModule, Button, Icon], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div\n class=\"min-h-screen w-screen flex items-center justify-center p-6 bg-surface-50\"\n>\n <div\n class=\"w-full max-w-md rounded-xl bg-white p-8 shadow-sm border border-surface-200 text-center\"\n >\n @if (callbackError() || exchangeError(); as errorMessage) {\n <div class=\"space-y-5\">\n <mt-icon\n icon=\"alert.alert-circle\"\n class=\"text-red-500 text-4xl\"\n ></mt-icon>\n <div class=\"space-y-2\">\n <h1 class=\"text-xl font-semibold text-surface-950\">\n SSO sign-in failed\n </h1>\n <p class=\"text-sm text-surface-600\">{{ errorMessage }}</p>\n </div>\n <mt-button\n label=\"Back to sign in\"\n severity=\"primary\"\n (onClick)=\"retry()\"\n ></mt-button>\n </div>\n } @else {\n <div class=\"space-y-5\">\n <mt-icon\n icon=\"general.loading-02\"\n class=\"text-primary text-4xl animate-spin animate-duration-2000\"\n >\n </mt-icon>\n <div class=\"space-y-2\">\n <h1 class=\"text-xl font-semibold text-surface-950\">\n Completing sign-in\n </h1>\n <p class=\"text-sm text-surface-600\">\n Please wait while we finish the secure sign-in.\n </p>\n </div>\n </div>\n }\n </div>\n</div>\n", styles: [":host{display:block}\n"] }]
|
|
1044
|
+
}] });
|
|
1045
|
+
|
|
1046
|
+
class GatewaySsoButtons {
|
|
1047
|
+
authFacade = inject(GatewayAuthFacade);
|
|
1048
|
+
dividerLabel = input('or', ...(ngDevMode ? [{ debugName: "dividerLabel" }] : /* istanbul ignore next */ []));
|
|
1049
|
+
providers = this.authFacade.ssoProviders;
|
|
1050
|
+
loading = this.authFacade.ssoLoading;
|
|
1051
|
+
constructor() {
|
|
1052
|
+
this.authFacade.loadSsoProviders();
|
|
1053
|
+
}
|
|
1054
|
+
startSso(provider) {
|
|
1055
|
+
this.authFacade.startSso(provider);
|
|
1056
|
+
}
|
|
1057
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: GatewaySsoButtons, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1058
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: GatewaySsoButtons, isStandalone: true, selector: "mt-gateway-sso-buttons", inputs: { dividerLabel: { classPropertyName: "dividerLabel", publicName: "dividerLabel", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "@if (providers().length > 0) {\n <div class=\"gateway-sso-divider\">\n <span></span>\n <span>{{ dividerLabel() }}</span>\n <span></span>\n </div>\n\n <div class=\"gateway-sso-list\">\n @for (provider of providers(); track provider.key) {\n <mt-button\n [label]=\"provider.displayName\"\n size=\"large\"\n variant=\"outlined\"\n fluid\n type=\"button\"\n [disabled]=\"loading()\"\n (onClick)=\"startSso(provider)\"\n />\n }\n </div>\n}\n", styles: [":host{display:block}.gateway-sso-divider{align-items:center;display:flex;gap:.75rem;padding-top:.5rem}.gateway-sso-divider span:first-child,.gateway-sso-divider span:last-child{background:#ffffff4d;flex:1;height:1px}.gateway-sso-divider span:nth-child(2){font-size:.75rem;letter-spacing:0;text-transform:uppercase}.gateway-sso-list{display:flex;flex-direction:column;gap:.5rem;margin-top:.5rem}\n"], dependencies: [{ kind: "component", type: Button, selector: "mt-button", inputs: ["icon", "label", "tooltip", "class", "type", "styleClass", "severity", "badge", "variant", "badgeSeverity", "size", "iconPos", "autofocus", "fluid", "raised", "rounded", "text", "plain", "outlined", "link", "disabled", "loading", "pInputs"], outputs: ["onClick", "onFocus", "onBlur"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1059
|
+
}
|
|
1060
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: GatewaySsoButtons, decorators: [{
|
|
1061
|
+
type: Component,
|
|
1062
|
+
args: [{ selector: 'mt-gateway-sso-buttons', imports: [Button], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (providers().length > 0) {\n <div class=\"gateway-sso-divider\">\n <span></span>\n <span>{{ dividerLabel() }}</span>\n <span></span>\n </div>\n\n <div class=\"gateway-sso-list\">\n @for (provider of providers(); track provider.key) {\n <mt-button\n [label]=\"provider.displayName\"\n size=\"large\"\n variant=\"outlined\"\n fluid\n type=\"button\"\n [disabled]=\"loading()\"\n (onClick)=\"startSso(provider)\"\n />\n }\n </div>\n}\n", styles: [":host{display:block}.gateway-sso-divider{align-items:center;display:flex;gap:.75rem;padding-top:.5rem}.gateway-sso-divider span:first-child,.gateway-sso-divider span:last-child{background:#ffffff4d;flex:1;height:1px}.gateway-sso-divider span:nth-child(2){font-size:.75rem;letter-spacing:0;text-transform:uppercase}.gateway-sso-list{display:flex;flex-direction:column;gap:.5rem;margin-top:.5rem}\n"] }]
|
|
1063
|
+
}], ctorParameters: () => [], propDecorators: { dividerLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "dividerLabel", required: false }] }] } });
|
|
1064
|
+
|
|
143
1065
|
/**
|
|
144
1066
|
* Generated bundle index. Do not edit.
|
|
145
1067
|
*/
|
|
146
1068
|
|
|
147
|
-
export { GATEWAY_AUTH_DEVICE_TOKEN, GATEWAY_AUTH_ENDPOINTS, GatewaySsoSession, buildGatewayUrl, buildSsoStartUrl, createSecureClientState, getGatewayErrorMessage, hasGatewayTokens, isExpired, mapGatewayTokens, mapGatewayUser, normalizeGatewayBase, resolveApiDateValue };
|
|
1069
|
+
export { AUTH_STATE_DEFAULTS, ClearError, ClearPendingMfa, ExchangeSsoCode, GATEWAY_AUTH_DEVICE_TOKEN, GATEWAY_AUTH_ENDPOINTS, GATEWAY_AUTH_OPTIONS, GATEWAY_AUTH_RETRY_CONTEXT, GatewayAuthFacade, GatewayAuthState, GatewayMfa, GatewaySsoButtons, GatewaySsoCallback, GatewaySsoSession, LoadSsoProviders, Login, LoginFailure, LoginSuccess, Logout, ResendMfa, StartSso, UpdateTokens, UpdateUserData, VerifyMfa, buildGatewayUrl, buildSsoStartUrl, createSecureClientState, gatewayAuthInterceptor, getGatewayErrorMessage, hasGatewayTokens, isExpired, isGatewayAuthRequestUrl, mapGatewayTokens, mapGatewayUser, normalizeGatewayBase, resolveApiDateValue, resolveGatewayAuthPath, sanitizePersistedAuthState };
|
|
148
1070
|
//# sourceMappingURL=masterteam-gateway-auth.mjs.map
|