@things-factory/auth-ui 7.0.1-rc.1 → 7.0.1-rc.10

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.
@@ -3,7 +3,8 @@ import { css } from 'lit'
3
3
  export const AUTH_STYLE_SIGN = css`
4
4
  :host {
5
5
  display: flex;
6
- background-color: var(--auth-background, var(--md-sys-color-primary));
6
+ background-color: var(--md-sys-color-primary);
7
+ color: var(--md-sys-color-on-primary);
7
8
  }
8
9
 
9
10
  :host *:focus {
@@ -1,8 +1,8 @@
1
- import { html, nothing } from 'lit'
2
- import base64url from 'base64url'
3
-
4
1
  import '@operato/i18n/ox-i18n.js'
5
2
 
3
+ import { html, nothing } from 'lit'
4
+ import { startAuthentication } from '@simplewebauthn/browser'
5
+
6
6
  import { i18next } from '@operato/i18n'
7
7
  import { notify } from '@operato/layout'
8
8
 
@@ -85,84 +85,42 @@ export abstract class AbstractSign extends AbstractAuthPage {
85
85
  <ox-i18n msgid="field.${this.pageName}"> </ox-i18n>
86
86
  </md-elevated-button>
87
87
  ${isAvailableWebauthn
88
- ? html` <md-icon class="fingerprint" raised @click=${e => this.signinWithAuthn()}> fingerprint </md-icon>`
88
+ ? html` <md-icon class="fingerprint" raised @click=${e => this.authenticateUser()}> fingerprint </md-icon>`
89
89
  : nothing}
90
90
  </div>
91
91
  `
92
92
  }
93
93
 
94
- async signinWithAuthn() {
95
- const response = await fetch('/auth/signin-webauthn/challenge', {
96
- method: 'POST',
97
- headers: {
98
- 'Content-Type': 'application/json',
99
- Accept: 'application/json'
100
- },
101
- credentials: 'include'
102
- })
103
-
104
- if (!response.ok) {
105
- notify({
106
- level: 'error',
107
- message: await response.text()
108
- })
109
-
110
- return
111
- }
112
-
113
- const options = await response.json()
114
- const credential = (await navigator.credentials.get({
115
- publicKey: {
116
- challenge: Buffer.from(options.challenge, 'base64')
117
- }
118
- })) as PublicKeyCredential
119
-
120
- if (!credential) {
121
- notify({
122
- level: 'error',
123
- message: 'can not get user credential'
124
- })
125
-
126
- return
127
- }
128
-
129
- const assertionResponse = {
130
- id: credential.id,
131
- response: {
132
- clientDataJSON: base64url.encode(Buffer.from(credential.response.clientDataJSON)),
133
- authenticatorData: base64url.encode((credential.response as any).authenticatorData),
134
- signature: base64url.encode((credential.response as any).signature),
135
- userHandle: (credential.response as any).userHandle
136
- ? base64url.encode((credential.response as any).userHandle)
137
- : null
94
+ async authenticateUser() {
95
+ try {
96
+ const options = await fetch('/auth/signin-webauthn/challenge').then(res => res.json())
97
+ const assertionResp = await startAuthentication(options)
98
+ const verification = await fetch('/auth/signin-webauthn', {
99
+ method: 'POST',
100
+ headers: {
101
+ 'Content-Type': 'application/json'
102
+ },
103
+ body: JSON.stringify(assertionResp)
104
+ }).then(res => res.json())
105
+
106
+ if (verification.verified) {
107
+ const { redirectURL } = verification
108
+
109
+ if (redirectURL) {
110
+ window.location.href = redirectURL
111
+ }
112
+ } else {
113
+ notify({
114
+ level: 'error',
115
+ message: verification.message
116
+ })
138
117
  }
139
- } as AssertionResponse
140
-
141
- if (credential.authenticatorAttachment) {
142
- assertionResponse.authenticatorAttachment = credential.authenticatorAttachment as AuthenticatorAttachment
143
- }
144
-
145
- const signinResponse = await fetch('/auth/signin-webauthn', {
146
- method: 'POST',
147
- headers: {
148
- 'Content-Type': 'application/json',
149
- Accept: 'application/json'
150
- },
151
- body: JSON.stringify(assertionResponse),
152
- credentials: 'include'
153
- })
154
-
155
- if (!signinResponse.ok) {
118
+
119
+ } catch (error) {
156
120
  notify({
157
121
  level: 'error',
158
- message: await signinResponse.text()
122
+ message: i18next.t('error.authn verification failed')
159
123
  })
160
- } else {
161
- const { redirectURL } = await signinResponse.json()
162
-
163
- if (redirectURL) {
164
- window.location.href = redirectURL
165
- }
166
124
  }
167
125
  }
168
126
  }
@@ -7,6 +7,7 @@ import './my-login-history'
7
7
  import base64url from 'base64url'
8
8
  import { css, html, LitElement, nothing } from 'lit'
9
9
  import { customElement, property, query, state } from 'lit/decorators.js'
10
+ import { startRegistration } from '@simplewebauthn/browser'
10
11
 
11
12
  import { i18next, localize } from '@operato/i18n'
12
13
  import { notify, openPopup } from '@operato/layout'
@@ -164,7 +165,7 @@ export class ProfileComponent extends localize(i18next)(LitElement) {
164
165
 
165
166
  ${isAvailableWebauthn
166
167
  ? html`
167
- <md-text-button @click=${() => this.registerWebAuthn()}
168
+ <md-text-button @click=${() => this.registerUser()}
168
169
  >${i18next.t('button.security-key registration')}</md-text-button
169
170
  >
170
171
  `
@@ -244,96 +245,35 @@ export class ProfileComponent extends localize(i18next)(LitElement) {
244
245
  })
245
246
  }
246
247
 
247
- async registerWebAuthn() {
248
- const challenge = await fetch('/auth/register-webauthn/challenge', {
249
- method: 'POST',
250
- headers: {
251
- 'Content-Type': 'application/json',
252
- Accept: 'application/json'
253
- },
254
- body: JSON.stringify({ id: this.userId }),
255
- credentials: 'include'
256
- })
257
-
258
- if (!challenge.ok) {
259
- notify({
260
- level: 'error',
261
- message: await challenge.text()
262
- })
263
-
264
- return
265
- }
266
-
267
- const options = await challenge.json()
268
-
269
- const credential = (await navigator.credentials.create({
270
- publicKey: {
271
- rp: {
272
- name: this.applicationMeta.title
273
- },
274
- user: {
275
- id: Uint8Array.from(base64url.toBuffer(options.user.id)).buffer,
276
- name: options.user.name,
277
- displayName: options.user.displayName
248
+ async registerUser() {
249
+ try {
250
+ const options = await fetch('/auth/register-webauthn/challenge').then(res => res.json())
251
+ const attResp = await startRegistration(options)
252
+ const verification = await fetch('/auth/verify-registration', {
253
+ method: 'POST',
254
+ headers: {
255
+ 'Content-Type': 'application/json'
278
256
  },
279
- challenge: Uint8Array.from(atob(options.challenge), c => c.charCodeAt(0)),
280
- pubKeyCredParams: [
281
- {
282
- type: 'public-key',
283
- alg: -7 // ES256
284
- },
285
- {
286
- type: 'public-key',
287
- alg: -257 // RS256
288
- }
289
- ],
290
- authenticatorSelection: {
291
- userVerification: 'preferred'
292
- }
257
+ body: JSON.stringify(attResp)
258
+ }).then(res => res.json())
259
+
260
+ if (verification.verified) {
261
+ notify({
262
+ level: 'info',
263
+ message: i18next.t('text.user credential registered successfully')
264
+ })
265
+ } else {
266
+ console.error(await verification.text())
267
+
268
+ notify({
269
+ level: 'error',
270
+ message: i18next.t('error.user credential registeration not allowed')
271
+ })
293
272
  }
294
- })) as PublicKeyCredential
295
-
296
- if (!credential) {
273
+ } catch (error) {
297
274
  notify({
298
275
  level: 'error',
299
- message: 'can not get user credential'
300
- })
301
-
302
- return
303
- }
304
-
305
- const response = credential.response as AuthenticatorAttestationResponse
306
-
307
- var body = {
308
- response: {
309
- clientDataJSON: Buffer.from(response.clientDataJSON).toString('base64'),
310
- attestationObject: Buffer.from(response.attestationObject).toString('base64')
311
- } as any
312
- }
313
-
314
- if (response.getTransports) {
315
- body.response.transports = response.getTransports()
316
- }
317
-
318
- const signinResponse = await fetch('/auth/signin-webauthn', {
319
- method: 'POST',
320
- headers: {
321
- 'Content-Type': 'application/json',
322
- Accept: 'application/json'
323
- },
324
- body: JSON.stringify(body),
325
- credentials: 'include'
326
- })
327
-
328
- if (!signinResponse.ok) {
329
- notify({
330
- level: 'error',
331
- message: await signinResponse.text()
332
- })
333
- } else {
334
- notify({
335
- level: 'info',
336
- message: i18next.t('text.user credential registered successfully')
276
+ message: i18next.t('error.user credential registeration failed')
337
277
  })
338
278
  }
339
279
  }
@@ -20,7 +20,8 @@ export class AuthActivate extends localize(i18next)(LitElement) {
20
20
  width: 100vw;
21
21
  height: 100vh;
22
22
  height: 100dvh;
23
- background-color: var(--auth-background, var(--md-sys-color-primary));
23
+ background-color: var(--md-sys-color-primary);
24
+ color: var(--md-sys-color-on-primary);
24
25
  }
25
26
 
26
27
  .wrap {
@@ -21,12 +21,14 @@ export class AuthCheckIn extends localize(i18next)(LitElement) {
21
21
  display: flex;
22
22
  flex-direction: column;
23
23
  margin: auto;
24
- background-color: var(--auth-background, var(--md-sys-color-primary));
24
+ background-color: var(--md-sys-color-primary);
25
+ color: var(--md-sys-color-on-primary);
25
26
  height: 100vh;
26
27
  height: 100dvh;
27
28
  }
28
29
  .header {
29
30
  background-color: var(--md-sys-color-primary);
31
+ color: var(--md-sys-color-on-primary);
30
32
  height: var(--checkin-header-height);
31
33
  }
32
34
  .content {
@@ -38,6 +40,7 @@ export class AuthCheckIn extends localize(i18next)(LitElement) {
38
40
  margin: var(--margin-wide) 0;
39
41
  padding: 0;
40
42
  background-color: var(--md-sys-color-surface);
43
+ color: var(--md-sys-color-on-surface);
41
44
  border-radius: var(--border-radius);
42
45
  border: var(--border-dim-color);
43
46
  list-style: none;
@@ -47,13 +50,13 @@ export class AuthCheckIn extends localize(i18next)(LitElement) {
47
50
  margin-bottom: -1px;
48
51
  padding: var(--padding-default) var(--padding-wide);
49
52
  font-size: 18px;
50
- color: var(--md-sys-color-secondary);
51
53
  text-align: left;
52
54
 
53
55
  cursor: pointer;
54
56
  }
55
57
  li:hover {
56
58
  background-color: var(--md-sys-color-primary-container);
59
+ color: var(--md-sys-color-on-primary-container);
57
60
  }
58
61
  li span {
59
62
  display: block;
@@ -18,7 +18,8 @@ export class AuthResult extends localize(i18next)(LitElement) {
18
18
  width: 100vw;
19
19
  height: 100vh;
20
20
  height: 100dvh;
21
- background-color: var(--auth-background, var(--md-sys-color-primary));
21
+ background-color: var(--md-sys-color-primary);
22
+ color: var(--md-sys-color-on-primary);
22
23
  }
23
24
  .wrap {
24
25
  display: block;
@@ -2,7 +2,8 @@ import { css } from 'lit';
2
2
  export const AUTH_STYLE_SIGN = css `
3
3
  :host {
4
4
  display: flex;
5
- background-color: var(--auth-background, var(--md-sys-color-primary));
5
+ background-color: var(--md-sys-color-primary);
6
+ color: var(--md-sys-color-on-primary);
6
7
  }
7
8
 
8
9
  :host *:focus {
@@ -1 +1 @@
1
- {"version":3,"file":"auth-style-sign.js","sourceRoot":"","sources":["../client/auth-style-sign.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,CAAA;AAEzB,MAAM,CAAC,MAAM,eAAe,GAAG,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4LjC,CAAA","sourcesContent":["import { css } from 'lit'\n\nexport const AUTH_STYLE_SIGN = css`\n :host {\n display: flex;\n background-color: var(--auth-background, var(--md-sys-color-primary));\n }\n\n :host *:focus {\n outline: none;\n }\n\n :host * {\n box-sizing: border-box;\n }\n\n .wrap {\n display: block;\n width: 450px;\n min-width: 350px;\n margin: 0 auto;\n padding-bottom: 100px;\n text-align: center;\n }\n\n .auth-brand {\n color: var(--md-sys-color-on-primary);\n }\n\n .auth-brand img {\n margin: 15% auto 5px auto;\n width: 100px;\n border: 3px solid var(--md-sys-color-on-primary);\n border-radius: 25px;\n box-shadow: var(--box-shadow);\n }\n .name {\n display: block;\n font: var(--auth-brand-name);\n text-shadow: var(--auth-brand-name-shadow);\n }\n .auth-brand .welcome-msg {\n font: var(--auth-brand-welcome-msg);\n }\n .auth-form {\n display: grid;\n grid-gap: var(--margin-default);\n grid-template-columns: 1fr 1fr;\n }\n\n form {\n grid-column: 1 / -1;\n display: grid;\n grid-template-columns: 1fr 1fr;\n grid-gap: var(--margin-default);\n align-items: center;\n }\n\n h3 {\n grid-column: 1 / -1;\n margin: 50px 0 0 0;\n font: var(--auth-title-font);\n color: var(--auth-title-color, var(--md-sys-color-on-primary));\n text-transform: uppercase;\n }\n\n .field {\n grid-column: 1 / -1;\n text-align: left;\n }\n\n .submit-buttons-container {\n grid-column: 1 / -1;\n text-align: center;\n\n display: flex;\n align-items: center;\n gap: 10px;\n }\n\n .fingerprint {\n color: var(--md-sys-color-on-primary);\n border: 1.5px solid var(--md-sys-color-on-primary);\n border-radius: 20%;\n width: 36px;\n height: 36px;\n }\n\n .field md-filled-text-field {\n grid-column: 1 / -1;\n width: 100%;\n }\n\n md-text-button,\n md-elevated-button {\n grid-column: 1 / -1;\n flex: 1;\n }\n\n .wrap .link {\n text-decoration: none;\n justify-self: flex-start;\n }\n\n .wrap .link md-text-button {\n --md-text-button-label-text-color: var(--md-sys-color-on-primary);\n --md-text-button-focus-label-text-color: var(--md-sys-color-on-primary);\n --md-text-button-hover-label-text-color: var(--md-sys-color-on-primary);\n }\n\n .wrap .link md-icon {\n color: var(--md-sys-color-on-primary);\n }\n\n #locale-area {\n display: flex;\n grid-column: 1 / -1;\n padding: 0 var(--padding-default);\n }\n\n #locale-area > label {\n display: flex;\n align-items: center;\n color: var(--md-sys-color-on-primary);\n --md-icon-size: 16px;\n }\n\n #locale-selector {\n font-size: 16px;\n width: 100%;\n }\n\n #locale-selector {\n --i18n-selector-field-border: none;\n --i18n-selector-field-background-color: none;\n --i18n-selector-field-font-size: 14px;\n --i18n-selector-field-color: var(--md-sys-color-on-primary);\n }\n\n .lottie-container {\n width: 100%;\n height: 300px;\n position: absolute;\n left: 0;\n bottom: 0;\n pointer-events: none;\n }\n .lottie-container lottie-player {\n position: absolute;\n bottom: -6%;\n width: 100%;\n height: auto;\n }\n\n @media (max-width: 450px) {\n .wrap {\n width: 85%;\n min-width: 320px;\n }\n .auth-form {\n grid-template-columns: 1fr;\n }\n .auth-brand img {\n margin: 12% auto 5px auto;\n width: 75px;\n }\n h3 {\n margin: 15px 0 0 0;\n }\n .lottie-container {\n overflow: hidden;\n height: 200px;\n pointer-events: none;\n }\n .lottie-container lottie-player {\n width: 1200px;\n left: -30%;\n }\n }\n\n @media screen and (min-width: 1400px) {\n .wrap {\n padding-bottom: 150px;\n }\n }\n @media screen and (min-width: 2500px) {\n .wrap {\n padding-bottom: 280px;\n }\n }\n`\n"]}
1
+ {"version":3,"file":"auth-style-sign.js","sourceRoot":"","sources":["../client/auth-style-sign.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,CAAA;AAEzB,MAAM,CAAC,MAAM,eAAe,GAAG,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6LjC,CAAA","sourcesContent":["import { css } from 'lit'\n\nexport const AUTH_STYLE_SIGN = css`\n :host {\n display: flex;\n background-color: var(--md-sys-color-primary);\n color: var(--md-sys-color-on-primary);\n }\n\n :host *:focus {\n outline: none;\n }\n\n :host * {\n box-sizing: border-box;\n }\n\n .wrap {\n display: block;\n width: 450px;\n min-width: 350px;\n margin: 0 auto;\n padding-bottom: 100px;\n text-align: center;\n }\n\n .auth-brand {\n color: var(--md-sys-color-on-primary);\n }\n\n .auth-brand img {\n margin: 15% auto 5px auto;\n width: 100px;\n border: 3px solid var(--md-sys-color-on-primary);\n border-radius: 25px;\n box-shadow: var(--box-shadow);\n }\n .name {\n display: block;\n font: var(--auth-brand-name);\n text-shadow: var(--auth-brand-name-shadow);\n }\n .auth-brand .welcome-msg {\n font: var(--auth-brand-welcome-msg);\n }\n .auth-form {\n display: grid;\n grid-gap: var(--margin-default);\n grid-template-columns: 1fr 1fr;\n }\n\n form {\n grid-column: 1 / -1;\n display: grid;\n grid-template-columns: 1fr 1fr;\n grid-gap: var(--margin-default);\n align-items: center;\n }\n\n h3 {\n grid-column: 1 / -1;\n margin: 50px 0 0 0;\n font: var(--auth-title-font);\n color: var(--auth-title-color, var(--md-sys-color-on-primary));\n text-transform: uppercase;\n }\n\n .field {\n grid-column: 1 / -1;\n text-align: left;\n }\n\n .submit-buttons-container {\n grid-column: 1 / -1;\n text-align: center;\n\n display: flex;\n align-items: center;\n gap: 10px;\n }\n\n .fingerprint {\n color: var(--md-sys-color-on-primary);\n border: 1.5px solid var(--md-sys-color-on-primary);\n border-radius: 20%;\n width: 36px;\n height: 36px;\n }\n\n .field md-filled-text-field {\n grid-column: 1 / -1;\n width: 100%;\n }\n\n md-text-button,\n md-elevated-button {\n grid-column: 1 / -1;\n flex: 1;\n }\n\n .wrap .link {\n text-decoration: none;\n justify-self: flex-start;\n }\n\n .wrap .link md-text-button {\n --md-text-button-label-text-color: var(--md-sys-color-on-primary);\n --md-text-button-focus-label-text-color: var(--md-sys-color-on-primary);\n --md-text-button-hover-label-text-color: var(--md-sys-color-on-primary);\n }\n\n .wrap .link md-icon {\n color: var(--md-sys-color-on-primary);\n }\n\n #locale-area {\n display: flex;\n grid-column: 1 / -1;\n padding: 0 var(--padding-default);\n }\n\n #locale-area > label {\n display: flex;\n align-items: center;\n color: var(--md-sys-color-on-primary);\n --md-icon-size: 16px;\n }\n\n #locale-selector {\n font-size: 16px;\n width: 100%;\n }\n\n #locale-selector {\n --i18n-selector-field-border: none;\n --i18n-selector-field-background-color: none;\n --i18n-selector-field-font-size: 14px;\n --i18n-selector-field-color: var(--md-sys-color-on-primary);\n }\n\n .lottie-container {\n width: 100%;\n height: 300px;\n position: absolute;\n left: 0;\n bottom: 0;\n pointer-events: none;\n }\n .lottie-container lottie-player {\n position: absolute;\n bottom: -6%;\n width: 100%;\n height: auto;\n }\n\n @media (max-width: 450px) {\n .wrap {\n width: 85%;\n min-width: 320px;\n }\n .auth-form {\n grid-template-columns: 1fr;\n }\n .auth-brand img {\n margin: 12% auto 5px auto;\n width: 75px;\n }\n h3 {\n margin: 15px 0 0 0;\n }\n .lottie-container {\n overflow: hidden;\n height: 200px;\n pointer-events: none;\n }\n .lottie-container lottie-player {\n width: 1200px;\n left: -30%;\n }\n }\n\n @media screen and (min-width: 1400px) {\n .wrap {\n padding-bottom: 150px;\n }\n }\n @media screen and (min-width: 2500px) {\n .wrap {\n padding-bottom: 280px;\n }\n }\n`\n"]}
@@ -4,5 +4,5 @@ export declare abstract class AbstractSign extends AbstractAuthPage {
4
4
  submit(): Promise<void>;
5
5
  updated(changed: any): void;
6
6
  get formfields(): import("lit-html").TemplateResult<1>;
7
- signinWithAuthn(): Promise<void>;
7
+ authenticateUser(): Promise<void>;
8
8
  }
@@ -1,6 +1,6 @@
1
- import { html, nothing } from 'lit';
2
- import base64url from 'base64url';
3
1
  import '@operato/i18n/ox-i18n.js';
2
+ import { html, nothing } from 'lit';
3
+ import { startAuthentication } from '@simplewebauthn/browser';
4
4
  import { i18next } from '@operato/i18n';
5
5
  import { notify } from '@operato/layout';
6
6
  import { AbstractAuthPage } from './abstract-auth-page.js';
@@ -66,75 +66,41 @@ export class AbstractSign extends AbstractAuthPage {
66
66
  <ox-i18n msgid="field.${this.pageName}"> </ox-i18n>
67
67
  </md-elevated-button>
68
68
  ${isAvailableWebauthn
69
- ? html ` <md-icon class="fingerprint" raised @click=${e => this.signinWithAuthn()}> fingerprint </md-icon>`
69
+ ? html ` <md-icon class="fingerprint" raised @click=${e => this.authenticateUser()}> fingerprint </md-icon>`
70
70
  : nothing}
71
71
  </div>
72
72
  `;
73
73
  }
74
- async signinWithAuthn() {
75
- const response = await fetch('/auth/signin-webauthn/challenge', {
76
- method: 'POST',
77
- headers: {
78
- 'Content-Type': 'application/json',
79
- Accept: 'application/json'
80
- },
81
- credentials: 'include'
82
- });
83
- if (!response.ok) {
84
- notify({
85
- level: 'error',
86
- message: await response.text()
87
- });
88
- return;
89
- }
90
- const options = await response.json();
91
- const credential = (await navigator.credentials.get({
92
- publicKey: {
93
- challenge: Buffer.from(options.challenge, 'base64')
74
+ async authenticateUser() {
75
+ try {
76
+ const options = await fetch('/auth/signin-webauthn/challenge').then(res => res.json());
77
+ const assertionResp = await startAuthentication(options);
78
+ const verification = await fetch('/auth/signin-webauthn', {
79
+ method: 'POST',
80
+ headers: {
81
+ 'Content-Type': 'application/json'
82
+ },
83
+ body: JSON.stringify(assertionResp)
84
+ }).then(res => res.json());
85
+ if (verification.verified) {
86
+ const { redirectURL } = verification;
87
+ if (redirectURL) {
88
+ window.location.href = redirectURL;
89
+ }
94
90
  }
95
- }));
96
- if (!credential) {
97
- notify({
98
- level: 'error',
99
- message: 'can not get user credential'
100
- });
101
- return;
102
- }
103
- const assertionResponse = {
104
- id: credential.id,
105
- response: {
106
- clientDataJSON: base64url.encode(Buffer.from(credential.response.clientDataJSON)),
107
- authenticatorData: base64url.encode(credential.response.authenticatorData),
108
- signature: base64url.encode(credential.response.signature),
109
- userHandle: credential.response.userHandle
110
- ? base64url.encode(credential.response.userHandle)
111
- : null
91
+ else {
92
+ notify({
93
+ level: 'error',
94
+ message: verification.message
95
+ });
112
96
  }
113
- };
114
- if (credential.authenticatorAttachment) {
115
- assertionResponse.authenticatorAttachment = credential.authenticatorAttachment;
116
97
  }
117
- const signinResponse = await fetch('/auth/signin-webauthn', {
118
- method: 'POST',
119
- headers: {
120
- 'Content-Type': 'application/json',
121
- Accept: 'application/json'
122
- },
123
- body: JSON.stringify(assertionResponse),
124
- credentials: 'include'
125
- });
126
- if (!signinResponse.ok) {
98
+ catch (error) {
127
99
  notify({
128
100
  level: 'error',
129
- message: await signinResponse.text()
101
+ message: i18next.t('error.authn verification failed')
130
102
  });
131
103
  }
132
- else {
133
- const { redirectURL } = await signinResponse.json();
134
- if (redirectURL) {
135
- window.location.href = redirectURL;
136
- }
137
- }
138
104
  }
139
105
  }
140
106
  //# sourceMappingURL=abstract-sign.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"abstract-sign.js","sourceRoot":"","sources":["../../client/components/abstract-sign.ts"],"names":[],"mappings":"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","sourcesContent":["import { html, nothing } from 'lit'\nimport base64url from 'base64url'\n\nimport '@operato/i18n/ox-i18n.js'\n\nimport { i18next } from '@operato/i18n'\nimport { notify } from '@operato/layout'\n\nimport { AbstractAuthPage } from './abstract-auth-page.js'\n\nconst isAvailableWebauthn = 'PublicKeyCredential' in window\n\ninterface AssertionResponse {\n id: string\n rawId?: number[]\n response: {\n authenticatorData: string\n clientDataJSON: string\n signature: string\n userHandle: string | null\n }\n type: PublicKeyCredentialType\n authenticatorAttachment?: AuthenticatorAttachment\n}\n\nexport abstract class AbstractSign extends AbstractAuthPage {\n async submit() {\n this.formEl.submit()\n }\n\n updated(changed) {\n super.updated(changed)\n\n if (changed.has('message')) {\n if (!this.message) {\n this.hideSnackbar()\n } else {\n this.showSnackbar({\n level: 'error',\n timer: -1\n })\n }\n }\n }\n\n get formfields() {\n const email = this.data?.email || ''\n\n return html`\n <input id=\"redirectTo\" type=\"hidden\" name=\"redirectTo\" .value=${this.redirectTo || '/'} />\n\n <div class=\"field\">\n <md-filled-text-field\n name=\"email\"\n type=\"email\"\n label=${String(i18next.t('field.email'))}\n required\n .value=${email}\n autocomplete=\"username\"\n autocapitalize=\"off\"\n @input=${(e: Event) => {\n const target = e.target as HTMLInputElement\n if (target.validity.typeMismatch) {\n target.setCustomValidity(i18next.t('text.invalid-email'))\n } else {\n target.setCustomValidity('')\n }\n }}\n ><md-icon slot=\"leading-icon\">mail</md-icon></md-filled-text-field\n >\n </div>\n <div class=\"field\">\n <md-filled-text-field\n name=\"password\"\n type=\"password\"\n label=${String(i18next.t('field.password'))}\n autocomplete=\"current-password\"\n required\n ><md-icon slot=\"leading-icon\">vpn_key</md-icon></md-filled-text-field\n >\n </div>\n\n <div class=\"submit-buttons-container\">\n <md-elevated-button class=\"submit-button\" type=\"submit\" raised @click=${e => this._onSubmit(e)}>\n <ox-i18n msgid=\"field.${this.pageName}\"> </ox-i18n>\n </md-elevated-button>\n ${isAvailableWebauthn\n ? html` <md-icon class=\"fingerprint\" raised @click=${e => this.signinWithAuthn()}> fingerprint </md-icon>`\n : nothing}\n </div>\n `\n }\n\n async signinWithAuthn() {\n const response = await fetch('/auth/signin-webauthn/challenge', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Accept: 'application/json'\n },\n credentials: 'include'\n })\n\n if (!response.ok) {\n notify({\n level: 'error',\n message: await response.text()\n })\n\n return\n }\n\n const options = await response.json()\n const credential = (await navigator.credentials.get({\n publicKey: {\n challenge: Buffer.from(options.challenge, 'base64')\n }\n })) as PublicKeyCredential\n\n if (!credential) {\n notify({\n level: 'error',\n message: 'can not get user credential'\n })\n\n return\n }\n\n const assertionResponse = {\n id: credential.id,\n response: {\n clientDataJSON: base64url.encode(Buffer.from(credential.response.clientDataJSON)),\n authenticatorData: base64url.encode((credential.response as any).authenticatorData),\n signature: base64url.encode((credential.response as any).signature),\n userHandle: (credential.response as any).userHandle\n ? base64url.encode((credential.response as any).userHandle)\n : null\n }\n } as AssertionResponse\n\n if (credential.authenticatorAttachment) {\n assertionResponse.authenticatorAttachment = credential.authenticatorAttachment as AuthenticatorAttachment\n }\n\n const signinResponse = await fetch('/auth/signin-webauthn', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Accept: 'application/json'\n },\n body: JSON.stringify(assertionResponse),\n credentials: 'include'\n })\n\n if (!signinResponse.ok) {\n notify({\n level: 'error',\n message: await signinResponse.text()\n })\n } else {\n const { redirectURL } = await signinResponse.json()\n\n if (redirectURL) {\n window.location.href = redirectURL\n }\n }\n }\n}\n"]}
1
+ {"version":3,"file":"abstract-sign.js","sourceRoot":"","sources":["../../client/components/abstract-sign.ts"],"names":[],"mappings":"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","sourcesContent":["import '@operato/i18n/ox-i18n.js'\n\nimport { html, nothing } from 'lit'\nimport { startAuthentication } from '@simplewebauthn/browser'\n\nimport { i18next } from '@operato/i18n'\nimport { notify } from '@operato/layout'\n\nimport { AbstractAuthPage } from './abstract-auth-page.js'\n\nconst isAvailableWebauthn = 'PublicKeyCredential' in window\n\ninterface AssertionResponse {\n id: string\n rawId?: number[]\n response: {\n authenticatorData: string\n clientDataJSON: string\n signature: string\n userHandle: string | null\n }\n type: PublicKeyCredentialType\n authenticatorAttachment?: AuthenticatorAttachment\n}\n\nexport abstract class AbstractSign extends AbstractAuthPage {\n async submit() {\n this.formEl.submit()\n }\n\n updated(changed) {\n super.updated(changed)\n\n if (changed.has('message')) {\n if (!this.message) {\n this.hideSnackbar()\n } else {\n this.showSnackbar({\n level: 'error',\n timer: -1\n })\n }\n }\n }\n\n get formfields() {\n const email = this.data?.email || ''\n\n return html`\n <input id=\"redirectTo\" type=\"hidden\" name=\"redirectTo\" .value=${this.redirectTo || '/'} />\n\n <div class=\"field\">\n <md-filled-text-field\n name=\"email\"\n type=\"email\"\n label=${String(i18next.t('field.email'))}\n required\n .value=${email}\n autocomplete=\"username\"\n autocapitalize=\"off\"\n @input=${(e: Event) => {\n const target = e.target as HTMLInputElement\n if (target.validity.typeMismatch) {\n target.setCustomValidity(i18next.t('text.invalid-email'))\n } else {\n target.setCustomValidity('')\n }\n }}\n ><md-icon slot=\"leading-icon\">mail</md-icon></md-filled-text-field\n >\n </div>\n <div class=\"field\">\n <md-filled-text-field\n name=\"password\"\n type=\"password\"\n label=${String(i18next.t('field.password'))}\n autocomplete=\"current-password\"\n required\n ><md-icon slot=\"leading-icon\">vpn_key</md-icon></md-filled-text-field\n >\n </div>\n\n <div class=\"submit-buttons-container\">\n <md-elevated-button class=\"submit-button\" type=\"submit\" raised @click=${e => this._onSubmit(e)}>\n <ox-i18n msgid=\"field.${this.pageName}\"> </ox-i18n>\n </md-elevated-button>\n ${isAvailableWebauthn\n ? html` <md-icon class=\"fingerprint\" raised @click=${e => this.authenticateUser()}> fingerprint </md-icon>`\n : nothing}\n </div>\n `\n }\n\n async authenticateUser() {\n try {\n const options = await fetch('/auth/signin-webauthn/challenge').then(res => res.json())\n const assertionResp = await startAuthentication(options)\n const verification = await fetch('/auth/signin-webauthn', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json'\n },\n body: JSON.stringify(assertionResp)\n }).then(res => res.json())\n\n if (verification.verified) {\n const { redirectURL } = verification\n \n if (redirectURL) {\n window.location.href = redirectURL\n }\n } else {\n notify({\n level: 'error',\n message: verification.message\n })\n }\n \n } catch (error) {\n notify({\n level: 'error',\n message: i18next.t('error.authn verification failed')\n })\n }\n }\n}\n"]}
@@ -23,7 +23,7 @@ export declare class ProfileComponent extends ProfileComponent_base {
23
23
  onLocaleChanged(value: any): Promise<void>;
24
24
  openLoginHistory(): void;
25
25
  deleteUser(): void;
26
- registerWebAuthn(): Promise<void>;
26
+ registerUser(): Promise<void>;
27
27
  get applicationMeta(): {
28
28
  icon: string;
29
29
  title: string;