@c8y/login 1022.3.2
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/.browserslistrc +16 -0
- package/cumulocity.config.ts +27 -0
- package/jest.config.js +18 -0
- package/package.json +23 -0
- package/public/favicon.ico +0 -0
- package/public/platform-animation.svg +2533 -0
- package/src/app/app.config.ts +11 -0
- package/src/app/bootstrap-login/bootstrap-login.component.html +3 -0
- package/src/app/bootstrap-login/bootstrap-login.component.ts +16 -0
- package/src/app/login/change-password/change-password.component.html +97 -0
- package/src/app/login/change-password/change-password.component.ts +101 -0
- package/src/app/login/credentials/credentials.component.html +141 -0
- package/src/app/login/credentials/credentials.component.ts +148 -0
- package/src/app/login/credentials-component-params.ts +4 -0
- package/src/app/login/credentials-from-query-params.service.ts +86 -0
- package/src/app/login/index.ts +9 -0
- package/src/app/login/login.component.html +128 -0
- package/src/app/login/login.component.less +136 -0
- package/src/app/login/login.component.ts +238 -0
- package/src/app/login/login.model.ts +36 -0
- package/src/app/login/login.service.ts +651 -0
- package/src/app/login/missing-application-access/missing-application-access.component.html +2 -0
- package/src/app/login/missing-application-access/missing-application-access.component.ts +21 -0
- package/src/app/login/password-strength-validator.directive.ts +26 -0
- package/src/app/login/provide-phone-number/provide-phone-number.component.html +39 -0
- package/src/app/login/provide-phone-number/provide-phone-number.component.ts +73 -0
- package/src/app/login/recover-password/recover-password.component.html +53 -0
- package/src/app/login/recover-password/recover-password.component.ts +59 -0
- package/src/app/login/sms-challenge/sms-challenge.component.html +50 -0
- package/src/app/login/sms-challenge/sms-challenge.component.ts +134 -0
- package/src/app/login/strength-validator-service.ts +18 -0
- package/src/app/login/tenant-id-setup/tenant-id-setup.component.html +28 -0
- package/src/app/login/tenant-id-setup/tenant-id-setup.component.ts +94 -0
- package/src/app/login/totp-auth/totp-auth.component.html +18 -0
- package/src/app/login/totp-auth/totp-auth.component.ts +72 -0
- package/src/bootstrap.ts +19 -0
- package/src/i18n.ts +18 -0
- package/src/main.ts +25 -0
- package/src/polyfills.ts +33 -0
- package/tsconfig.app.json +20 -0
|
@@ -0,0 +1,651 @@
|
|
|
1
|
+
import { inject, Injectable } from '@angular/core';
|
|
2
|
+
import {
|
|
3
|
+
ApplicationService,
|
|
4
|
+
BearerAuthFromSessionStorage,
|
|
5
|
+
FetchClient,
|
|
6
|
+
IAuthentication,
|
|
7
|
+
ICredentials,
|
|
8
|
+
ICurrentTenant,
|
|
9
|
+
IFetchResponse,
|
|
10
|
+
ITenantLoginOption,
|
|
11
|
+
IUser,
|
|
12
|
+
TenantLoginOptionsService,
|
|
13
|
+
TenantService,
|
|
14
|
+
UserService
|
|
15
|
+
} from '@c8y/client';
|
|
16
|
+
import { TenantUiService, ModalService, Status, SimplifiedAuthService } from '@c8y/ngx-components';
|
|
17
|
+
import { gettext } from '@c8y/ngx-components/gettext';
|
|
18
|
+
import { ApiService } from '@c8y/ngx-components/api';
|
|
19
|
+
import { switchMap } from 'rxjs/operators';
|
|
20
|
+
import { BehaviorSubject, EMPTY } from 'rxjs';
|
|
21
|
+
import { isEmpty } from 'lodash-es';
|
|
22
|
+
import { TranslateService } from '@ngx-translate/core';
|
|
23
|
+
import { SsoData } from './login.model';
|
|
24
|
+
import {
|
|
25
|
+
getStoredToken,
|
|
26
|
+
getStoredTfaToken,
|
|
27
|
+
TOKEN_KEY,
|
|
28
|
+
TFATOKEN_KEY,
|
|
29
|
+
isLocal
|
|
30
|
+
} from '@c8y/bootstrap';
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Service to manage the login.
|
|
34
|
+
*/
|
|
35
|
+
@Injectable({ providedIn: 'root' })
|
|
36
|
+
export class LoginService extends SimplifiedAuthService {
|
|
37
|
+
rememberMe = false;
|
|
38
|
+
loginMode: ITenantLoginOption;
|
|
39
|
+
managementLoginMode: ITenantLoginOption;
|
|
40
|
+
oauthOptions: ITenantLoginOption;
|
|
41
|
+
isFirstLogin = true;
|
|
42
|
+
automaticLoginInProgress$ = new BehaviorSubject(true);
|
|
43
|
+
|
|
44
|
+
regexp = /\/apps\/(public\/)?([^\/]+)(\/.*)?$/;
|
|
45
|
+
|
|
46
|
+
readonly queryParamsToRemove = [
|
|
47
|
+
'token',
|
|
48
|
+
'email',
|
|
49
|
+
'code',
|
|
50
|
+
'session_state',
|
|
51
|
+
'error',
|
|
52
|
+
'error_description'
|
|
53
|
+
] as const;
|
|
54
|
+
|
|
55
|
+
private translateService = inject(TranslateService);
|
|
56
|
+
ERROR_MESSAGES = {
|
|
57
|
+
minlength: gettext('Password must have at least 8 characters and no more than 32.'),
|
|
58
|
+
password_missmatch: gettext('Passwords do not match.'),
|
|
59
|
+
maxlength: gettext('Password must have at least 8 characters and no more than 32.'),
|
|
60
|
+
password_strength: gettext(
|
|
61
|
+
'Your password is not strong enough. Please include numbers, lower and upper case characters'
|
|
62
|
+
),
|
|
63
|
+
remote_error: gettext('Server error occurred.'),
|
|
64
|
+
email: gettext('Invalid email address.'),
|
|
65
|
+
password_change: gettext('Your password is expired. Please set a new password.'),
|
|
66
|
+
password_reset_token_expired: gettext(
|
|
67
|
+
'Password reset link expired. Please enter your email address to receive a new one.'
|
|
68
|
+
),
|
|
69
|
+
tfa_pin_invalid: gettext('The code you entered is invalid. Please try again.'),
|
|
70
|
+
pattern_newPassword: this.translateService.instant(
|
|
71
|
+
gettext(
|
|
72
|
+
'Password must have at least 8 characters and no more than 32 and can only contain letters, numbers and following symbols: {{ symbols }}'
|
|
73
|
+
),
|
|
74
|
+
{ symbols: '`~!@#$%^&*()_|+-=?;:\'",.<>{}[]\\/' }
|
|
75
|
+
),
|
|
76
|
+
internationalPhoneNumber: gettext(
|
|
77
|
+
'Must be a valid phone number (only digits, spaces, slashes ("/"), dashes ("-"), and plus ("+") allowed, for example: +49 9 876 543 210).'
|
|
78
|
+
),
|
|
79
|
+
phone_number_error: gettext('Could not update phone number.'),
|
|
80
|
+
pinAlreadySent: gettext(
|
|
81
|
+
'The verification code was already sent. For a new verification code, please click on the link above.'
|
|
82
|
+
),
|
|
83
|
+
passwordConfirm: gettext('Passwords do not match.'),
|
|
84
|
+
tfaExpired: gettext('Two-factor authentication token expired.')
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
private SUCCESS_MESSAGES = {
|
|
88
|
+
password_changed: gettext('Password changed. You can now log in using new password.'),
|
|
89
|
+
password_reset_requested: gettext(
|
|
90
|
+
'Password reset request has been sent. Please check your email.'
|
|
91
|
+
),
|
|
92
|
+
resend_sms: gettext('Verification code SMS resent.'),
|
|
93
|
+
send_sms: gettext('Verification code SMS sent.')
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
private showTenantRegExp = new RegExp('showTenant');
|
|
97
|
+
|
|
98
|
+
private user = inject(UserService);
|
|
99
|
+
private tenant = inject(TenantService);
|
|
100
|
+
private api = inject(ApiService);
|
|
101
|
+
private tenantUiService = inject(TenantUiService);
|
|
102
|
+
private tenantLoginOptionsService = inject(TenantLoginOptionsService);
|
|
103
|
+
private modalService = inject(ModalService);
|
|
104
|
+
private applicationService = inject(ApplicationService);
|
|
105
|
+
|
|
106
|
+
constructor() {
|
|
107
|
+
super();
|
|
108
|
+
this.autoLogout();
|
|
109
|
+
this.initLoginOptions();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Returns the current tenant.
|
|
114
|
+
* @return The tenant name.
|
|
115
|
+
*/
|
|
116
|
+
getTenant() {
|
|
117
|
+
return this.client.tenant;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
initLoginOptions() {
|
|
121
|
+
const loginOptions = this.ui.state.loginOptions || [];
|
|
122
|
+
this.loginMode = this.tenantUiService.getPreferredLoginOption(loginOptions);
|
|
123
|
+
this.oauthOptions =
|
|
124
|
+
this.tenantUiService.getOauth2Option(loginOptions) || ({} as ITenantLoginOption);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
redirectToOauth() {
|
|
128
|
+
const { initRequest, flowControlledByUI } = this.oauthOptions;
|
|
129
|
+
const fullPath = `${window.location.origin}${window.location.pathname}`;
|
|
130
|
+
const redirectUrl = encodeURIComponent(fullPath);
|
|
131
|
+
const originUriParam = `${initRequest.includes('?') ? '&' : '?'}originUri=${redirectUrl}`;
|
|
132
|
+
const urlObject = new URL(initRequest);
|
|
133
|
+
|
|
134
|
+
if (flowControlledByUI) {
|
|
135
|
+
this.client
|
|
136
|
+
.fetch(`/tenant/oauth${urlObject.search}${originUriParam}`)
|
|
137
|
+
.then(res => this.handleErrorStatusCodes(res))
|
|
138
|
+
.then(res => res.json())
|
|
139
|
+
.then((res: any) => (window.location.href = res.redirectTo))
|
|
140
|
+
.catch(ex => this.showSsoError(ex));
|
|
141
|
+
} else {
|
|
142
|
+
window.location.href = `${initRequest}${originUriParam}`;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
loginBySso({ code, sessionState }: SsoData) {
|
|
147
|
+
const params = {
|
|
148
|
+
method: 'GET',
|
|
149
|
+
headers: {
|
|
150
|
+
Accept: 'text/html,application/xhtml+xml'
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
let url = `/tenant/oauth?code=${encodeURIComponent(code)}`;
|
|
154
|
+
if (sessionState) {
|
|
155
|
+
url += `&session_state=${encodeURIComponent(sessionState)}`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return this.client
|
|
159
|
+
.fetch(url, params)
|
|
160
|
+
.then(res => this.handleErrorStatusCodes(res))
|
|
161
|
+
.catch(ex => {
|
|
162
|
+
this.showSsoError(ex);
|
|
163
|
+
throw new Error();
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
autoLogout() {
|
|
168
|
+
const errorPattern = /invalid\scredentials.*pin.*generate/i;
|
|
169
|
+
const isTfaExpired = data =>
|
|
170
|
+
data && typeof data.message === 'string' && errorPattern.test(data.message);
|
|
171
|
+
this.ui.currentUser
|
|
172
|
+
.pipe(
|
|
173
|
+
switchMap(u =>
|
|
174
|
+
u ? this.api.hookResponse(({ response }) => response.status === 401) : EMPTY
|
|
175
|
+
)
|
|
176
|
+
)
|
|
177
|
+
.subscribe(async (apiCall: any) => {
|
|
178
|
+
const { response } = apiCall;
|
|
179
|
+
let willLogout = false;
|
|
180
|
+
if (isTfaExpired(response.data)) {
|
|
181
|
+
willLogout = true;
|
|
182
|
+
} else {
|
|
183
|
+
if (typeof response.json === 'function') {
|
|
184
|
+
const data = await response.clone().json();
|
|
185
|
+
if (isTfaExpired(data)) {
|
|
186
|
+
willLogout = true;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (willLogout) {
|
|
191
|
+
this.logout(false);
|
|
192
|
+
setTimeout(() => this.alert.danger(this.ERROR_MESSAGES.tfaExpired), 500);
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Clears all backend errors.
|
|
199
|
+
*/
|
|
200
|
+
cleanMessages() {
|
|
201
|
+
this.alert.clearAll();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Adds a new success message
|
|
206
|
+
* @param successKey The key of the success message as used in SUCCESS_MESSAGES
|
|
207
|
+
*/
|
|
208
|
+
addSuccessMessage(successKey: string) {
|
|
209
|
+
const successMessage = this.SUCCESS_MESSAGES[successKey];
|
|
210
|
+
if (successMessage) {
|
|
211
|
+
this.alert.add({
|
|
212
|
+
text: successMessage,
|
|
213
|
+
type: 'success',
|
|
214
|
+
timeout: 0
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Returns the current strategy. Defaults to cookie, if a token
|
|
221
|
+
* is found in local or session storage we switch to basic auth.
|
|
222
|
+
* @returns The current auth strategy.
|
|
223
|
+
*/
|
|
224
|
+
getAuthStrategy(): IAuthentication {
|
|
225
|
+
try {
|
|
226
|
+
const authStrategy = new BearerAuthFromSessionStorage();
|
|
227
|
+
console.log(`Using BearerAuthFromSessionStorage`);
|
|
228
|
+
return authStrategy;
|
|
229
|
+
} catch (e) {
|
|
230
|
+
// do nothing
|
|
231
|
+
}
|
|
232
|
+
let authStrategy: IAuthentication = this.cookieAuth;
|
|
233
|
+
const token = this.getStoredToken();
|
|
234
|
+
const tfa = this.getStoredTfaToken();
|
|
235
|
+
if (token) {
|
|
236
|
+
authStrategy = this.basicAuth;
|
|
237
|
+
this.setCredentials({ token, tfa }, this.basicAuth);
|
|
238
|
+
}
|
|
239
|
+
return authStrategy;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Forces the use of basic auth as strategy with this credentials.
|
|
244
|
+
* @param credentials The credentials to use.
|
|
245
|
+
*/
|
|
246
|
+
useBasicAuth(credentials: ICredentials) {
|
|
247
|
+
this.setCredentials(credentials, this.basicAuth);
|
|
248
|
+
return this.basicAuth;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Tries to login a user with the given credentials.
|
|
253
|
+
* If successful, the current tenant and user is set. If not an error
|
|
254
|
+
* is thrown. It also verifies if the user is allowed to open the
|
|
255
|
+
* current app.
|
|
256
|
+
* @param auth The authentication strategy used.
|
|
257
|
+
* @param credentials The credentials to try to login.
|
|
258
|
+
*/
|
|
259
|
+
async login(
|
|
260
|
+
auth: IAuthentication = this.getAuthStrategy(),
|
|
261
|
+
credentials?: ICredentials
|
|
262
|
+
): Promise<boolean> {
|
|
263
|
+
// To ensure backward compatibility, we need to verify whether the backend supports TFA
|
|
264
|
+
// without requiring the use of /tenant with auth: base64. The tfaSupported flag indicates
|
|
265
|
+
// whether authentication is possible exclusively via OAI-SECURE.
|
|
266
|
+
// TfaSupported flag should be removed during: MTM-62641
|
|
267
|
+
const isOAISecureAndTFAIsSupported =
|
|
268
|
+
(this.tenantUiService.isOauthInternal(this.loginMode) && this.loginMode.tfaSupported) ||
|
|
269
|
+
false;
|
|
270
|
+
|
|
271
|
+
if (isOAISecureAndTFAIsSupported && (await this.switchLoginMode(credentials))) {
|
|
272
|
+
auth = this.cookieAuth;
|
|
273
|
+
} else {
|
|
274
|
+
this.client.setAuth(auth);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const tenantRes = await this.tenant.current({ withParent: true });
|
|
278
|
+
const tenant = tenantRes.data;
|
|
279
|
+
|
|
280
|
+
if (credentials) {
|
|
281
|
+
credentials.tenant = tenant.name;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (!isOAISecureAndTFAIsSupported && (await this.switchLoginMode(credentials))) {
|
|
285
|
+
auth = this.cookieAuth;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const userRes = await this.user.current();
|
|
289
|
+
const user = userRes.data;
|
|
290
|
+
|
|
291
|
+
const supportUserName = this.getSupportUserName(credentials);
|
|
292
|
+
const token = this.setCredentials(
|
|
293
|
+
{
|
|
294
|
+
tenant: tenant.name,
|
|
295
|
+
user: (supportUserName ? `${supportUserName}$` : '') + user.userName
|
|
296
|
+
},
|
|
297
|
+
auth
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
if (token) {
|
|
301
|
+
this.storeBasicAuthToken(token);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
await this.authFulfilled(tenant, user);
|
|
305
|
+
|
|
306
|
+
return this.ensureUserPermissionsForRedirect(user);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
async ensureUserPermissionsForRedirect(user: IUser) {
|
|
310
|
+
const redirectPath = await this.getRedirectPath();
|
|
311
|
+
if (!redirectPath) {
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
const userHasAccessToApp = await this.userHasAccessToApp(user, redirectPath);
|
|
315
|
+
|
|
316
|
+
if (!userHasAccessToApp) {
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
window.location.href = redirectPath;
|
|
321
|
+
return true;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
async getRedirectPath() {
|
|
325
|
+
let redirectPathFromSessionStorage = sessionStorage.getItem('c8yRedirectAfterLoginPath');
|
|
326
|
+
if (redirectPathFromSessionStorage) {
|
|
327
|
+
sessionStorage.removeItem('c8yRedirectAfterLoginPath');
|
|
328
|
+
if (redirectPathFromSessionStorage.includes('?')) {
|
|
329
|
+
const queryParams = new URLSearchParams(redirectPathFromSessionStorage);
|
|
330
|
+
for (const param of this.queryParamsToRemove) {
|
|
331
|
+
queryParams.delete(param);
|
|
332
|
+
}
|
|
333
|
+
const newQueryParams = queryParams.toString();
|
|
334
|
+
const redirectWithoutQueryParams = redirectPathFromSessionStorage.split('?', 1)[0];
|
|
335
|
+
const hash = redirectPathFromSessionStorage.split('#', 2)[1] || '';
|
|
336
|
+
|
|
337
|
+
const queryParamsToAppend = newQueryParams ? `?${newQueryParams}` : '';
|
|
338
|
+
const hashToAppend = hash ? `#${hash}` : '';
|
|
339
|
+
redirectPathFromSessionStorage =
|
|
340
|
+
redirectWithoutQueryParams + queryParamsToAppend + hashToAppend;
|
|
341
|
+
}
|
|
342
|
+
return Promise.resolve(redirectPathFromSessionStorage);
|
|
343
|
+
}
|
|
344
|
+
return this.getDefaultAppRedirect();
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
async getDefaultAppRedirect() {
|
|
348
|
+
const response = await fetch('/');
|
|
349
|
+
const matches = response.url.match(this.regexp);
|
|
350
|
+
return matches ? matches[0] : null;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
async userHasAccessToApp(user: IUser, redirectPath: string): Promise<false | string> {
|
|
354
|
+
if (!redirectPath) {
|
|
355
|
+
return false;
|
|
356
|
+
}
|
|
357
|
+
const contextPathOfApp = this.extractContextPathFromRedirectUrl(redirectPath);
|
|
358
|
+
|
|
359
|
+
if (!contextPathOfApp) {
|
|
360
|
+
return false;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
try {
|
|
364
|
+
await this.applicationService.getManifestOfContextPath(contextPathOfApp);
|
|
365
|
+
return redirectPath;
|
|
366
|
+
} catch (e) {
|
|
367
|
+
return false;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
extractContextPathFromRedirectUrl(redirectUrl: string): string | null {
|
|
372
|
+
const matches = redirectUrl.match(this.regexp);
|
|
373
|
+
if (matches) {
|
|
374
|
+
return matches[2];
|
|
375
|
+
}
|
|
376
|
+
return null;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Saves tenant, user and support user info to the app state.
|
|
381
|
+
* @param tenant The current tenant object.
|
|
382
|
+
* @param user The current user object.
|
|
383
|
+
* @param supportUserName The current support user name.
|
|
384
|
+
*/
|
|
385
|
+
async authFulfilled(tenant?: ICurrentTenant, user?: IUser) {
|
|
386
|
+
if (!tenant) {
|
|
387
|
+
const { data } = await this.tenant.current({ withParent: true });
|
|
388
|
+
tenant = data;
|
|
389
|
+
this.client.tenant = tenant.name;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (!user) {
|
|
393
|
+
const { data } = await this.user.current();
|
|
394
|
+
user = data;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
this.ui.setUser({ user });
|
|
398
|
+
this.ui.currentTenant.next(tenant);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Switch the login mode to CookieAuth if the
|
|
403
|
+
* user has configured to use it in loginOptions.
|
|
404
|
+
* @param credentials The credentials for that login
|
|
405
|
+
*/
|
|
406
|
+
async switchLoginMode(credentials?: ICredentials) {
|
|
407
|
+
const isPasswordGrantLogin = await this.isPasswordGrantLogin(credentials);
|
|
408
|
+
if (isPasswordGrantLogin && credentials) {
|
|
409
|
+
const res = await this.generateOauthToken(credentials);
|
|
410
|
+
if (!res?.ok) {
|
|
411
|
+
try {
|
|
412
|
+
const data = await res.json();
|
|
413
|
+
throw { res, data };
|
|
414
|
+
} catch (ex) {
|
|
415
|
+
throw ex;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
this.client.setAuth(this.cookieAuth);
|
|
419
|
+
this.cleanLocalStorage();
|
|
420
|
+
this.basicAuth.logout();
|
|
421
|
+
}
|
|
422
|
+
return isPasswordGrantLogin;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
async generateOauthToken(credentials?: ICredentials): Promise<IFetchResponse | null> {
|
|
426
|
+
if ((await this.isPasswordGrantLogin(credentials)) && credentials) {
|
|
427
|
+
const params = new URLSearchParams({
|
|
428
|
+
grant_type: 'PASSWORD',
|
|
429
|
+
username: credentials.user,
|
|
430
|
+
password: credentials.password,
|
|
431
|
+
...(credentials.tfa !== undefined && { tfa_code: credentials.tfa })
|
|
432
|
+
});
|
|
433
|
+
return await new FetchClient().fetch(this.getUrlForOauth(credentials), {
|
|
434
|
+
method: 'POST',
|
|
435
|
+
body: params.toString(),
|
|
436
|
+
headers: {
|
|
437
|
+
'content-type': 'application/x-www-form-urlencoded;charset=UTF-8'
|
|
438
|
+
}
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
return null;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
async isPasswordGrantLogin(credentials?: ICredentials) {
|
|
446
|
+
let loginMode = this.loginMode;
|
|
447
|
+
|
|
448
|
+
if (this.isSupportUser(credentials)) {
|
|
449
|
+
if (!this.managementLoginMode) {
|
|
450
|
+
this.managementLoginMode = await this.getManagementLoginMode();
|
|
451
|
+
}
|
|
452
|
+
loginMode = this.managementLoginMode;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
return this.tenantUiService.isOauthInternal(loginMode);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Verifies if the provided credentials use a support user to log in or not.
|
|
460
|
+
* @param credentials Credentials to check.
|
|
461
|
+
* @returns {boolean} Returns true if user is a support user.
|
|
462
|
+
*/
|
|
463
|
+
isSupportUser(credentials?: ICredentials): boolean {
|
|
464
|
+
return credentials && credentials.user.includes('$');
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Verifies if the tenant input field should be shown
|
|
469
|
+
* or not.
|
|
470
|
+
* @returns If true, show the tenant input.
|
|
471
|
+
*/
|
|
472
|
+
showTenant(): boolean {
|
|
473
|
+
return !this.ui.state.loginOptions || this.isLocal() || this.isShowTenant();
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Verifies if the tenant setup should be shown
|
|
478
|
+
* or not.
|
|
479
|
+
* @returns If true, show the tenant input.
|
|
480
|
+
*/
|
|
481
|
+
showTenantSetup(): boolean {
|
|
482
|
+
return !this.ui.state.loginOptions && !this.isLocal();
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Saves the TFA token to local or session storage.
|
|
487
|
+
* @param tfaToken The tfa token to save.
|
|
488
|
+
* @param storage The storage to use (local or session).
|
|
489
|
+
*/
|
|
490
|
+
saveTFAToken(tfaToken: string, storage: Storage) {
|
|
491
|
+
storage.setItem(TFATOKEN_KEY, tfaToken);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
redirectToDomain(domain) {
|
|
495
|
+
const originUrl = new URL(window.location.href);
|
|
496
|
+
const redirectUrl = originUrl.href.replace(originUrl.hostname, domain);
|
|
497
|
+
window.location.href = redirectUrl;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
showSsoError(error): void {
|
|
501
|
+
const body = error
|
|
502
|
+
? this.translateService.instant(
|
|
503
|
+
gettext(
|
|
504
|
+
'<p><strong>The following error was returned from the external authentication service:</strong></p><p><code>{{ error }}</code></p>.'
|
|
505
|
+
),
|
|
506
|
+
{ error }
|
|
507
|
+
)
|
|
508
|
+
: gettext('SSO login failed. Contact the administrator.');
|
|
509
|
+
|
|
510
|
+
this.modalService.acknowledge(gettext('Login error'), body, Status.DANGER, gettext('OK'));
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
async clearCookies() {
|
|
514
|
+
// clear cookies but avoid redirect on logout
|
|
515
|
+
return await this.cookieAuth.logout({ redirect: 'manual' });
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Sets the tenant to the client and updates the credentials on the
|
|
520
|
+
* auth strategy.
|
|
521
|
+
* @param credentials The name of the tenant.
|
|
522
|
+
* @param authStrategy The authentication strategy used.
|
|
523
|
+
* @return Returns the token if basic auth, otherwise undefined.
|
|
524
|
+
*/
|
|
525
|
+
private setCredentials(credentials: ICredentials, authStrategy: IAuthentication) {
|
|
526
|
+
if (credentials.tenant) {
|
|
527
|
+
this.client.tenant = credentials.tenant;
|
|
528
|
+
}
|
|
529
|
+
// Check if a token is already set (case for support user login)
|
|
530
|
+
// if yes -> we just need to update the user, and reuse the token
|
|
531
|
+
// of the support user.
|
|
532
|
+
// Therefore we need to pass user and tenant, to get
|
|
533
|
+
// just the stored token and nothing else (see BasicAuth.ts:31).
|
|
534
|
+
const token = this.basicAuth.updateCredentials({
|
|
535
|
+
tenant: credentials.tenant,
|
|
536
|
+
user: credentials.user
|
|
537
|
+
});
|
|
538
|
+
const newCredentials = { token, ...credentials };
|
|
539
|
+
|
|
540
|
+
return authStrategy.updateCredentials(newCredentials);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Verifies if the current user is a developer or not.
|
|
545
|
+
* Running on localhost means development mode.
|
|
546
|
+
*/
|
|
547
|
+
private isLocal(): boolean {
|
|
548
|
+
return isLocal();
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* Save the token to local or session storage.
|
|
553
|
+
* @param token The token to save.
|
|
554
|
+
* @param storage The storage to use (local or session).
|
|
555
|
+
*/
|
|
556
|
+
private saveToken(token: string, storage: Storage) {
|
|
557
|
+
storage.setItem(TOKEN_KEY, token);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
private storeBasicAuthToken(token: string) {
|
|
561
|
+
this.saveToken(token, sessionStorage);
|
|
562
|
+
if (this.rememberMe) {
|
|
563
|
+
this.saveToken(token, localStorage);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
private isShowTenant(): boolean {
|
|
568
|
+
return this.showTenantRegExp.test(window.location.href);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Gets support user name from credentials.
|
|
573
|
+
* @param credentials Credentials object (defaults to the stored one).
|
|
574
|
+
* @returns Support user name.
|
|
575
|
+
*/
|
|
576
|
+
private getSupportUserName(credentials: ICredentials = this.getStoredCredentials()): string {
|
|
577
|
+
if (!credentials) {
|
|
578
|
+
return null;
|
|
579
|
+
}
|
|
580
|
+
const supportUserName = credentials.user.match(/^(.+\/)?((.+)\$)?(.+)?$/)[3];
|
|
581
|
+
return supportUserName;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Gets credentials object from the stored token.
|
|
586
|
+
* @returns Credentials object.
|
|
587
|
+
*/
|
|
588
|
+
private getStoredCredentials(): ICredentials {
|
|
589
|
+
const token = this.getStoredToken();
|
|
590
|
+
if (!token) {
|
|
591
|
+
return null;
|
|
592
|
+
}
|
|
593
|
+
return this.decodeToken(token);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* Gets stored token from local storage or session storage.
|
|
598
|
+
* @returns Stored token.
|
|
599
|
+
*/
|
|
600
|
+
private getStoredToken(): string {
|
|
601
|
+
return getStoredToken();
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Gets stored TFA token from local storage or session storage.
|
|
606
|
+
* @returns Stored TFA token.
|
|
607
|
+
*/
|
|
608
|
+
private getStoredTfaToken(): string {
|
|
609
|
+
return getStoredTfaToken();
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* Decodes token to credentials object.
|
|
614
|
+
* @param token Token to decode.
|
|
615
|
+
* @returns Credentials object.
|
|
616
|
+
*/
|
|
617
|
+
private decodeToken(token: string): ICredentials {
|
|
618
|
+
const decoded = decodeURIComponent(escape(window.atob(token)));
|
|
619
|
+
const split = decoded.match(/(([^/]*)\/)?([^/:]+):(.+)/);
|
|
620
|
+
|
|
621
|
+
return {
|
|
622
|
+
tenant: split[2],
|
|
623
|
+
user: split[3],
|
|
624
|
+
password: split[4]
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
private getUrlForOauth(credentials: ICredentials) {
|
|
629
|
+
if (isEmpty(credentials.tenant) && this.loginMode.initRequest) {
|
|
630
|
+
const urlParams = new URLSearchParams(this.loginMode.initRequest.split('?').pop());
|
|
631
|
+
credentials.tenant = urlParams.get('tenant_id');
|
|
632
|
+
}
|
|
633
|
+
return !isEmpty(credentials.tenant)
|
|
634
|
+
? `tenant/oauth?tenant_id=${credentials.tenant}`
|
|
635
|
+
: `tenant/oauth`;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
private async getManagementLoginMode() {
|
|
639
|
+
const managementLoginOptions = (await this.tenantLoginOptionsService.listForManagement()).data;
|
|
640
|
+
return this.tenantUiService.getPreferredLoginOption(managementLoginOptions);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
private async handleErrorStatusCodes(response: IFetchResponse): Promise<IFetchResponse> {
|
|
644
|
+
if (response.status >= 400) {
|
|
645
|
+
const data = await response.json();
|
|
646
|
+
const error = data.message || data.error_description || data.error;
|
|
647
|
+
throw error;
|
|
648
|
+
}
|
|
649
|
+
return response;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Component, inject } from '@angular/core';
|
|
2
|
+
import { AppSwitcherInlineComponent, AlertService } from '@c8y/ngx-components';
|
|
3
|
+
import { gettext } from '@c8y/ngx-components/gettext';
|
|
4
|
+
|
|
5
|
+
@Component({
|
|
6
|
+
selector: 'c8y-missing-application-access',
|
|
7
|
+
templateUrl: './missing-application-access.component.html',
|
|
8
|
+
standalone: true,
|
|
9
|
+
imports: [AppSwitcherInlineComponent]
|
|
10
|
+
})
|
|
11
|
+
export class MissingApplicationAccessComponent {
|
|
12
|
+
private alertService = inject(AlertService);
|
|
13
|
+
|
|
14
|
+
constructor() {
|
|
15
|
+
this.alertService.warning(
|
|
16
|
+
gettext(
|
|
17
|
+
`The application you've been trying to access is not available. Verify if you have the required permissions to access this application.`
|
|
18
|
+
)
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Directive, Input } from '@angular/core';
|
|
2
|
+
import { AbstractControl, NG_VALIDATORS, ValidationErrors, Validator } from '@angular/forms';
|
|
3
|
+
import { StrengthValidatorService } from './strength-validator-service';
|
|
4
|
+
|
|
5
|
+
@Directive({
|
|
6
|
+
selector: '[passwordStrengthEnforced]',
|
|
7
|
+
providers: [
|
|
8
|
+
{ provide: NG_VALIDATORS, useExisting: PasswordStrengthValidatorDirective, multi: true }
|
|
9
|
+
],
|
|
10
|
+
standalone: true
|
|
11
|
+
})
|
|
12
|
+
export class PasswordStrengthValidatorDirective implements Validator {
|
|
13
|
+
private forced: boolean;
|
|
14
|
+
|
|
15
|
+
@Input() set passwordStrengthEnforced(value) {
|
|
16
|
+
this.forced = value;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
constructor(public passwordService: StrengthValidatorService) {}
|
|
20
|
+
|
|
21
|
+
validate(control: AbstractControl): ValidationErrors | null {
|
|
22
|
+
const strengthFulfilled = this.passwordService.isStrong(control.value || '');
|
|
23
|
+
const enforcementForcedAndNotFulfilled = this.forced && !strengthFulfilled;
|
|
24
|
+
return enforcementForcedAndNotFulfilled ? { passwordStrength: true } : null;
|
|
25
|
+
}
|
|
26
|
+
}
|