@andreyyolkin/keycloakify-vue 0.1.0 → 0.2.0

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.
Files changed (29) hide show
  1. package/keycloakify-vue/account/pages/Log.vue +30 -30
  2. package/keycloakify-vue/login/Template.vue +42 -40
  3. package/keycloakify-vue/login/Template.vue.d.ts +6 -4
  4. package/keycloakify-vue/login/components/PasswordWrapper.vue +2 -0
  5. package/keycloakify-vue/login/components/PasswordWrapper.vue.d.ts +1 -0
  6. package/keycloakify-vue/login/pages/Login.useScript.d.ts +25 -0
  7. package/keycloakify-vue/login/pages/Login.useScript.js +75 -0
  8. package/keycloakify-vue/login/pages/Login.vue +88 -3
  9. package/keycloakify-vue/login/pages/LoginPasskeysConditionalAuthenticate.useScript.js +1 -1
  10. package/keycloakify-vue/login/pages/LoginPassword.useScript.d.ts +2 -0
  11. package/keycloakify-vue/login/pages/LoginPassword.useScript.js +13 -2
  12. package/keycloakify-vue/login/pages/LoginUsername.useScript.d.ts +2 -0
  13. package/keycloakify-vue/login/pages/LoginUsername.useScript.js +13 -2
  14. package/keycloakify-vue/login/pages/WebauthnAuthenticate.useScript.js +5 -5
  15. package/keycloakify-vue/login/pages/WebauthnRegister.useScript.d.ts +2 -1
  16. package/keycloakify-vue/login/pages/WebauthnRegister.useScript.js +7 -5
  17. package/keycloakify-vue/login/pages/WebauthnRegister.vue +5 -0
  18. package/package.json +11 -4
  19. package/src/account/pages/Log.vue +30 -30
  20. package/src/login/Template.vue +42 -40
  21. package/src/login/components/PasswordWrapper.vue +2 -0
  22. package/src/login/pages/Login.useScript.ts +101 -0
  23. package/src/login/pages/Login.vue +88 -3
  24. package/src/login/pages/LoginPasskeysConditionalAuthenticate.useScript.ts +1 -1
  25. package/src/login/pages/LoginPassword.useScript.ts +15 -2
  26. package/src/login/pages/LoginUsername.useScript.ts +15 -2
  27. package/src/login/pages/WebauthnAuthenticate.useScript.ts +5 -5
  28. package/src/login/pages/WebauthnRegister.useScript.ts +9 -6
  29. package/src/login/pages/WebauthnRegister.vue +5 -0
@@ -28,37 +28,37 @@ const { log } = kcContext;
28
28
  <div class="col-md-10">
29
29
  <h2><component :is="props.i18n.msg('accountLogHtmlTitle')" /></h2>
30
30
  </div>
31
- </div>
32
31
 
33
- <table class="table table-striped table-bordered">
34
- <thead>
35
- <tr>
36
- <td><component :is="props.i18n.msg('date')" /></td>
37
- <td><component :is="props.i18n.msg('event')" /></td>
38
- <td><component :is="props.i18n.msg('ip')" /></td>
39
- <td><component :is="props.i18n.msg('client')" /></td>
40
- <td><component :is="props.i18n.msg('details')" /></td>
41
- </tr>
42
- </thead>
32
+ <table class="table table-striped table-bordered">
33
+ <thead>
34
+ <tr>
35
+ <td><component :is="props.i18n.msg('date')" /></td>
36
+ <td><component :is="props.i18n.msg('event')" /></td>
37
+ <td><component :is="props.i18n.msg('ip')" /></td>
38
+ <td><component :is="props.i18n.msg('client')" /></td>
39
+ <td><component :is="props.i18n.msg('details')" /></td>
40
+ </tr>
41
+ </thead>
43
42
 
44
- <tbody>
45
- <tr
46
- v-for="(event, index) in log.events"
47
- :key="index"
48
- >
49
- <td>{{ event.date ? new Date(event.date).toLocaleString() : '' }}</td>
50
- <td>{{ event.event }}</td>
51
- <td>{{ event.ipAddress }}</td>
52
- <td>{{ event.client || '' }}</td>
53
- <td>
54
- <span
55
- v-for="(detail, detailIndex) in event.details"
56
- :key="detailIndex"
57
- >{{ `${detail.key} = ${detail.value}` }}{{ detailIndex < event.details.length - 1 ? ', ' : '' }}</span
58
- >
59
- </td>
60
- </tr>
61
- </tbody>
62
- </table>
43
+ <tbody>
44
+ <tr
45
+ v-for="(event, index) in log.events"
46
+ :key="index"
47
+ >
48
+ <td>{{ event.date ? new Date(event.date).toLocaleString() : '' }}</td>
49
+ <td>{{ event.event }}</td>
50
+ <td>{{ event.ipAddress }}</td>
51
+ <td>{{ event.client || '' }}</td>
52
+ <td>
53
+ <span
54
+ v-for="(detail, detailIndex) in event.details"
55
+ :key="detailIndex"
56
+ >{{ `${detail.key} = ${detail.value}` }}{{ detailIndex < event.details.length - 1 ? ', ' : '' }}</span
57
+ >
58
+ </td>
59
+ </tr>
60
+ </tbody>
61
+ </table>
62
+ </div>
63
63
  </component>
64
64
  </template>
@@ -27,7 +27,8 @@ const { realm, auth, url, message, isAppInitiatedAction } = kcContext;
27
27
  const slots = useSlots();
28
28
 
29
29
  onMounted(() => {
30
- document.title = props.documentTitle ?? props.i18n.msgStr('loginTitle', kcContext.realm.displayName);
30
+ document.title =
31
+ props.documentTitle ?? props.i18n.msgStr('loginTitle', kcContext.realm.displayName || kcContext.realm.name);
31
32
  });
32
33
 
33
34
  useSetClassName({ qualifiedName: 'html', className: kcClsx('kcHtmlClass') });
@@ -81,9 +82,8 @@ function onTryAnotherWay() {
81
82
  <div
82
83
  id="kc-header-wrapper"
83
84
  :class="kcClsx('kcHeaderWrapperClass')"
84
- >
85
- {{ i18n.msgStr('loginTitleHtml', realm.displayNameHtml) }}
86
- </div>
85
+ v-html="kcSanitize(i18n.msgStr('loginTitleHtml', realm.displayNameHtml || realm.name))"
86
+ ></div>
87
87
  </div>
88
88
 
89
89
  <div :class="kcClsx('kcFormCardClass')">
@@ -97,45 +97,47 @@ function onTryAnotherWay() {
97
97
  id="kc-locale-wrapper"
98
98
  :class="kcClsx('kcLocaleWrapperClass')"
99
99
  >
100
- <div
101
- id="kc-locale-dropdown"
102
- :class="clsx('menu-button-links', kcClsx('kcLocaleDropDownClass'))"
103
- >
104
- <button
105
- :tabindex="1"
106
- id="kc-current-locale-link"
107
- :aria-label="i18n.msgStr('languages')"
108
- aria-haspopup="true"
109
- aria-expanded="false"
110
- aria-controls="language-switch1"
100
+ <slot name="localeSelector">
101
+ <div
102
+ id="kc-locale-dropdown"
103
+ :class="clsx('menu-button-links', kcClsx('kcLocaleDropDownClass'))"
111
104
  >
112
- {{ i18n.currentLanguage.label }}
113
- </button>
114
- <ul
115
- role="menu"
116
- :tabindex="-1"
117
- aria-labelledby="kc-current-locale-link"
118
- aria-activedescendant=""
119
- id="language-switch1"
120
- :class="kcClsx('kcLocaleListClass')"
121
- >
122
- <li
123
- v-for="(enabledLanguage, i) in i18n.enabledLanguages"
124
- :key="enabledLanguage.languageTag"
125
- :class="kcClsx('kcLocaleListItemClass')"
126
- role="none"
105
+ <button
106
+ :tabindex="1"
107
+ id="kc-current-locale-link"
108
+ :aria-label="i18n.msgStr('languages')"
109
+ aria-haspopup="true"
110
+ aria-expanded="false"
111
+ aria-controls="language-switch1"
112
+ >
113
+ {{ i18n.currentLanguage.label }}
114
+ </button>
115
+ <ul
116
+ role="menu"
117
+ :tabindex="-1"
118
+ aria-labelledby="kc-current-locale-link"
119
+ aria-activedescendant=""
120
+ id="language-switch1"
121
+ :class="kcClsx('kcLocaleListClass')"
127
122
  >
128
- <a
129
- role="menuitem"
130
- :id="`language-${i + 1}`"
131
- :class="kcClsx('kcLocaleItemClass')"
132
- :href="enabledLanguage.href"
123
+ <li
124
+ v-for="(enabledLanguage, i) in i18n.enabledLanguages"
125
+ :key="enabledLanguage.languageTag"
126
+ :class="kcClsx('kcLocaleListItemClass')"
127
+ role="none"
133
128
  >
134
- {{ enabledLanguage.label }}
135
- </a>
136
- </li>
137
- </ul>
138
- </div>
129
+ <a
130
+ role="menuitem"
131
+ :id="`language-${i + 1}`"
132
+ :class="kcClsx('kcLocaleItemClass')"
133
+ :href="enabledLanguage.href"
134
+ >
135
+ {{ enabledLanguage.label }}
136
+ </a>
137
+ </li>
138
+ </ul>
139
+ </div>
140
+ </slot>
139
141
  </div>
140
142
  </div>
141
143
 
@@ -2,13 +2,15 @@ import type { KcContext } from 'keycloakify/login/KcContext';
2
2
  import type { I18n } from './i18n/i18n';
3
3
  import type { TemplateProps } from './TemplateProps';
4
4
  type __VLS_Props = TemplateProps<KcContext, I18n>;
5
- declare var __VLS_11: {}, __VLS_13: {}, __VLS_15: {};
5
+ declare var __VLS_1: {}, __VLS_13: {}, __VLS_15: {}, __VLS_17: {};
6
6
  type __VLS_Slots = {} & {
7
- default?: (props: typeof __VLS_11) => any;
7
+ localeSelector?: (props: typeof __VLS_1) => any;
8
8
  } & {
9
- socialProviders?: (props: typeof __VLS_13) => any;
9
+ default?: (props: typeof __VLS_13) => any;
10
10
  } & {
11
- info?: (props: typeof __VLS_15) => any;
11
+ socialProviders?: (props: typeof __VLS_15) => any;
12
+ } & {
13
+ info?: (props: typeof __VLS_17) => any;
12
14
  };
13
15
  declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
14
16
  displayInfo: boolean;
@@ -8,6 +8,7 @@ const props = defineProps<{
8
8
  kcClsx: KcClsx;
9
9
  i18n: I18n;
10
10
  passwordInputId: string;
11
+ tabIndex?: number;
11
12
  }>();
12
13
 
13
14
  const isPasswordRevealed = ref(false);
@@ -28,6 +29,7 @@ watch(
28
29
  <slot />
29
30
  <button
30
31
  type="button"
32
+ :tabindex="tabIndex"
31
33
  :class="kcClsx('kcFormPasswordVisibilityButtonClass')"
32
34
  :aria-label="i18n.msgStr(isPasswordRevealed ? 'hidePassword' : 'showPassword')"
33
35
  :aria-controls="passwordInputId"
@@ -4,6 +4,7 @@ type __VLS_Props = {
4
4
  kcClsx: KcClsx;
5
5
  i18n: I18n;
6
6
  passwordInputId: string;
7
+ tabIndex?: number;
7
8
  };
8
9
  declare var __VLS_1: {};
9
10
  type __VLS_Slots = {} & {
@@ -0,0 +1,25 @@
1
+ import { type Ref } from 'vue';
2
+ import type { KcContext } from 'keycloakify/login/KcContext';
3
+ type KcContextLike = {
4
+ url: {
5
+ resourcesPath: string;
6
+ };
7
+ isUserIdentified: 'true' | 'false';
8
+ challenge: string;
9
+ userVerification: KcContext.WebauthnAuthenticate['userVerification'];
10
+ rpId: string;
11
+ createTimeout: number | string;
12
+ mediation?: string;
13
+ authenticatorAttachment?: string;
14
+ enableWebAuthnConditionalUI?: boolean;
15
+ };
16
+ type I18nLike = {
17
+ msgStr: (key: 'webauthn-unsupported-browser-text' | 'passkey-unsupported-browser-text') => string;
18
+ isFetchingTranslations: boolean;
19
+ };
20
+ export declare function useScript(params: {
21
+ webAuthnButtonId: string;
22
+ kcContext: KcContextLike;
23
+ i18n: Ref<I18nLike>;
24
+ }): void;
25
+ export {};
@@ -0,0 +1,75 @@
1
+ import { onMounted, watch } from "vue";
2
+ import { useInsertScriptTags } from "../../tools/useInsertScriptTags";
3
+ import { assert } from "keycloakify/tools/assert";
4
+ import { waitForElementMountedOnDom } from "keycloakify/tools/waitForElementMountedOnDom";
5
+ assert();
6
+ assert();
7
+ function useScript(params) {
8
+ const { webAuthnButtonId, kcContext, i18n } = params;
9
+ const {
10
+ url,
11
+ isUserIdentified,
12
+ challenge,
13
+ userVerification,
14
+ rpId,
15
+ createTimeout,
16
+ mediation,
17
+ authenticatorAttachment
18
+ } = kcContext;
19
+ const { insertScriptTags } = useInsertScriptTags({
20
+ componentOrHookName: "Login",
21
+ scriptTags: [
22
+ {
23
+ type: "module",
24
+ textContent: () => `
25
+
26
+ import { authenticateByWebAuthn } from "${url.resourcesPath}/js/webauthnAuthenticate.js";
27
+ import { initAuthenticate } from "${url.resourcesPath}/js/passkeysConditionalAuth.js";
28
+ const authButton = document.getElementById('${webAuthnButtonId}');
29
+ const input = {
30
+ isUserIdentified : ${isUserIdentified},
31
+ challenge : ${JSON.stringify(challenge)},
32
+ userVerification : ${JSON.stringify(userVerification)},
33
+ rpId : ${JSON.stringify(rpId)},
34
+ createTimeout : ${JSON.stringify(createTimeout)},
35
+ mediation : ${JSON.stringify(mediation)},
36
+ authenticatorAttachment : ${JSON.stringify(authenticatorAttachment)}
37
+ };
38
+ authButton.addEventListener("click", () => {
39
+ authenticateByWebAuthn({
40
+ ...input,
41
+ errmsg : ${JSON.stringify(i18n.value.msgStr("webauthn-unsupported-browser-text"))}
42
+ });
43
+ }, { once: true });
44
+
45
+ initAuthenticate({
46
+ ...input,
47
+ errmsg : ${JSON.stringify(i18n.value.msgStr("passkey-unsupported-browser-text"))}
48
+ });
49
+ `
50
+ }
51
+ ]
52
+ });
53
+ onMounted(() => {
54
+ if (kcContext.enableWebAuthnConditionalUI !== true) {
55
+ return;
56
+ }
57
+ const stopWatch = watch(
58
+ () => i18n.value.isFetchingTranslations,
59
+ async (isFetching) => {
60
+ if (isFetching) {
61
+ return;
62
+ }
63
+ stopWatch();
64
+ await waitForElementMountedOnDom({
65
+ elementId: webAuthnButtonId
66
+ });
67
+ insertScriptTags();
68
+ },
69
+ { immediate: true }
70
+ );
71
+ });
72
+ }
73
+ export {
74
+ useScript
75
+ };
@@ -3,10 +3,11 @@ import { kcSanitize } from 'keycloakify/lib/kcSanitize';
3
3
  import type { KcContext } from 'keycloakify/login/KcContext';
4
4
  import { getKcClsx } from 'keycloakify/login/lib/kcClsx';
5
5
  import { clsx } from 'keycloakify/tools/clsx';
6
- import { computed, ref } from 'vue';
6
+ import { computed, ref, toRef } from 'vue';
7
7
  import PasswordWrapper from '../components/PasswordWrapper.vue';
8
8
  import type { I18n } from '../i18n/i18n';
9
9
  import type { PageProps } from './PageProps';
10
+ import { useScript } from './Login.useScript';
10
11
 
11
12
  const props = defineProps<PageProps<Extract<KcContext, { pageId: 'login.ftl' }>, I18n>>();
12
13
 
@@ -16,7 +17,18 @@ const { kcClsx } = getKcClsx({
16
17
  });
17
18
 
18
19
  const kcContext = props.kcContext;
19
- const { social, realm, url, usernameHidden, login, auth, registrationDisabled, messagesPerField } = kcContext;
20
+ const {
21
+ social,
22
+ realm,
23
+ url,
24
+ usernameHidden,
25
+ login,
26
+ auth,
27
+ registrationDisabled,
28
+ messagesPerField,
29
+ enableWebAuthnConditionalUI,
30
+ authenticators,
31
+ } = kcContext;
20
32
 
21
33
  const isLoginButtonDisabled = ref(false);
22
34
 
@@ -28,6 +40,14 @@ function onSubmit() {
28
40
  isLoginButtonDisabled.value = true;
29
41
  return true;
30
42
  }
43
+
44
+ const webAuthnButtonId = 'authenticateWebAuthnButton';
45
+
46
+ useScript({
47
+ webAuthnButtonId,
48
+ kcContext,
49
+ i18n: toRef(props, 'i18n'),
50
+ });
31
51
  </script>
32
52
 
33
53
  <template>
@@ -130,7 +150,7 @@ function onSubmit() {
130
150
  :value="login.username ?? ''"
131
151
  type="text"
132
152
  autofocus
133
- autocomplete="username"
153
+ :autocomplete="enableWebAuthnConditionalUI ? 'username webauthn' : 'username'"
134
154
  :aria-invalid="messagesPerField.existsError('username', 'password')"
135
155
  />
136
156
  <span
@@ -151,6 +171,7 @@ function onSubmit() {
151
171
  </label>
152
172
  <PasswordWrapper
153
173
  :kcClsx="kcClsx"
174
+ :tabIndex="4"
154
175
  :i18n="i18n"
155
176
  passwordInputId="password"
156
177
  >
@@ -226,5 +247,69 @@ function onSubmit() {
226
247
  </form>
227
248
  </div>
228
249
  </div>
250
+
251
+ <template v-if="enableWebAuthnConditionalUI">
252
+ <form
253
+ id="webauth"
254
+ :action="url.loginAction"
255
+ method="post"
256
+ >
257
+ <input
258
+ type="hidden"
259
+ id="clientDataJSON"
260
+ name="clientDataJSON"
261
+ />
262
+ <input
263
+ type="hidden"
264
+ id="authenticatorData"
265
+ name="authenticatorData"
266
+ />
267
+ <input
268
+ type="hidden"
269
+ id="signature"
270
+ name="signature"
271
+ />
272
+ <input
273
+ type="hidden"
274
+ id="credentialId"
275
+ name="credentialId"
276
+ />
277
+ <input
278
+ type="hidden"
279
+ id="userHandle"
280
+ name="userHandle"
281
+ />
282
+ <input
283
+ type="hidden"
284
+ id="error"
285
+ name="error"
286
+ />
287
+ </form>
288
+
289
+ <template v-if="authenticators !== undefined && authenticators.authenticators.length !== 0">
290
+ <form
291
+ id="authn_select"
292
+ :class="kcClsx('kcFormClass')"
293
+ >
294
+ <input
295
+ v-for="(authenticator, i) in authenticators.authenticators"
296
+ :key="i"
297
+ type="hidden"
298
+ name="authn_use_chk"
299
+ :value="authenticator.credentialId"
300
+ readonly
301
+ />
302
+ </form>
303
+ </template>
304
+
305
+ <br />
306
+
307
+ <input
308
+ :id="webAuthnButtonId"
309
+ type="button"
310
+ :class="kcClsx('kcButtonClass', 'kcButtonDefaultClass', 'kcButtonBlockClass', 'kcButtonLargeClass')"
311
+ :value="i18n.msgStr('passkey-doAuthenticate')"
312
+ />
313
+ </template>
229
314
  </component>
230
315
  </template>
@@ -22,7 +22,7 @@ function useScript(params) {
22
22
  challenge : ${JSON.stringify(challenge)},
23
23
  userVerification : ${JSON.stringify(userVerification)},
24
24
  rpId : ${JSON.stringify(rpId)},
25
- createTimeout : ${createTimeout}
25
+ createTimeout : ${JSON.stringify(createTimeout)}
26
26
  };
27
27
  authButton.addEventListener("click", () => {
28
28
  authenticateByWebAuthn({
@@ -9,6 +9,8 @@ type KcContextLike = {
9
9
  userVerification: KcContext.WebauthnAuthenticate['userVerification'];
10
10
  rpId: string;
11
11
  createTimeout: number | string;
12
+ mediation?: string;
13
+ authenticatorAttachment?: string;
12
14
  enableWebAuthnConditionalUI?: boolean;
13
15
  };
14
16
  type I18nLike = {
@@ -6,7 +6,16 @@ assert();
6
6
  assert();
7
7
  function useScript(params) {
8
8
  const { webAuthnButtonId, kcContext, i18n } = params;
9
- const { url, isUserIdentified, challenge, userVerification, rpId, createTimeout } = kcContext;
9
+ const {
10
+ url,
11
+ isUserIdentified,
12
+ challenge,
13
+ userVerification,
14
+ rpId,
15
+ createTimeout,
16
+ mediation,
17
+ authenticatorAttachment
18
+ } = kcContext;
10
19
  const { insertScriptTags } = useInsertScriptTags({
11
20
  componentOrHookName: "LoginPassword",
12
21
  scriptTags: [
@@ -22,7 +31,9 @@ function useScript(params) {
22
31
  challenge : ${JSON.stringify(challenge)},
23
32
  userVerification : ${JSON.stringify(userVerification)},
24
33
  rpId : ${JSON.stringify(rpId)},
25
- createTimeout : ${createTimeout}
34
+ createTimeout : ${JSON.stringify(createTimeout)},
35
+ mediation : ${JSON.stringify(mediation)},
36
+ authenticatorAttachment : ${JSON.stringify(authenticatorAttachment)}
26
37
  };
27
38
  authButton.addEventListener("click", () => {
28
39
  authenticateByWebAuthn({
@@ -9,6 +9,8 @@ type KcContextLike = {
9
9
  userVerification: KcContext.WebauthnAuthenticate['userVerification'];
10
10
  rpId: string;
11
11
  createTimeout: number | string;
12
+ mediation?: string;
13
+ authenticatorAttachment?: string;
12
14
  enableWebAuthnConditionalUI?: boolean;
13
15
  };
14
16
  type I18nLike = {
@@ -6,7 +6,16 @@ assert();
6
6
  assert();
7
7
  function useScript(params) {
8
8
  const { webAuthnButtonId, kcContext, i18n } = params;
9
- const { url, isUserIdentified, challenge, userVerification, rpId, createTimeout } = kcContext;
9
+ const {
10
+ url,
11
+ isUserIdentified,
12
+ challenge,
13
+ userVerification,
14
+ rpId,
15
+ createTimeout,
16
+ mediation,
17
+ authenticatorAttachment
18
+ } = kcContext;
10
19
  const { insertScriptTags } = useInsertScriptTags({
11
20
  componentOrHookName: "LoginUsername",
12
21
  scriptTags: [
@@ -22,7 +31,9 @@ function useScript(params) {
22
31
  challenge : ${JSON.stringify(challenge)},
23
32
  userVerification : ${JSON.stringify(userVerification)},
24
33
  rpId : ${JSON.stringify(rpId)},
25
- createTimeout : ${createTimeout}
34
+ createTimeout : ${JSON.stringify(createTimeout)},
35
+ mediation : ${JSON.stringify(mediation)},
36
+ authenticatorAttachment : ${JSON.stringify(authenticatorAttachment)}
26
37
  };
27
38
  authButton.addEventListener("click", () => {
28
39
  authenticateByWebAuthn({
@@ -19,14 +19,14 @@ function useScript(params) {
19
19
  authButton.addEventListener("click", function() {
20
20
  const input = {
21
21
  isUserIdentified : ${isUserIdentified},
22
- challenge : '${challenge}',
23
- userVerification : '${userVerification}',
24
- rpId : '${rpId}',
25
- createTimeout : ${createTimeout},
22
+ challenge : ${JSON.stringify(challenge)},
23
+ userVerification : ${JSON.stringify(userVerification)},
24
+ rpId : ${JSON.stringify(rpId)},
25
+ createTimeout : ${JSON.stringify(createTimeout)},
26
26
  errmsg : ${JSON.stringify(i18n.value.msgStr("webauthn-unsupported-browser-text"))}
27
27
  };
28
28
  authenticateByWebAuthn(input);
29
- });
29
+ }, { once: true });
30
30
  `
31
31
  }
32
32
  ]
@@ -11,7 +11,8 @@ type KcContextLike = {
11
11
  rpId: string;
12
12
  attestationConveyancePreference: string;
13
13
  authenticatorAttachment: string;
14
- requireResidentKey: string;
14
+ requireResidentKey?: string;
15
+ residentKey?: string;
15
16
  userVerificationRequirement: string;
16
17
  createTimeout: number | string;
17
18
  excludeCredentialIds: string;
@@ -17,6 +17,7 @@ function useScript(params) {
17
17
  attestationConveyancePreference,
18
18
  authenticatorAttachment,
19
19
  requireResidentKey,
20
+ residentKey,
20
21
  userVerificationRequirement,
21
22
  createTimeout,
22
23
  excludeCredentialIds
@@ -31,24 +32,25 @@ function useScript(params) {
31
32
  const registerButton = document.getElementById('${authButtonId}');
32
33
  registerButton.addEventListener("click", function() {
33
34
  const input = {
34
- challenge : '${challenge}',
35
- userid : '${userid}',
36
- username : '${username}',
35
+ challenge : ${JSON.stringify(challenge)},
36
+ userid : ${JSON.stringify(userid)},
37
+ username : ${JSON.stringify(username)},
37
38
  signatureAlgorithms : ${JSON.stringify(signatureAlgorithms)},
38
39
  rpEntityName : ${JSON.stringify(rpEntityName)},
39
40
  rpId : ${JSON.stringify(rpId)},
40
41
  attestationConveyancePreference : ${JSON.stringify(attestationConveyancePreference)},
41
42
  authenticatorAttachment : ${JSON.stringify(authenticatorAttachment)},
42
43
  requireResidentKey : ${JSON.stringify(requireResidentKey)},
44
+ residentKey : ${JSON.stringify(residentKey)},
43
45
  userVerificationRequirement : ${JSON.stringify(userVerificationRequirement)},
44
- createTimeout : ${createTimeout},
46
+ createTimeout : ${JSON.stringify(createTimeout)},
45
47
  excludeCredentialIds : ${JSON.stringify(excludeCredentialIds)},
46
48
  initLabel : ${JSON.stringify(i18n.value.msgStr("webauthn-registration-init-label"))},
47
49
  initLabelPrompt : ${JSON.stringify(i18n.value.msgStr("webauthn-registration-init-label-prompt"))},
48
50
  errmsg : ${JSON.stringify(i18n.value.msgStr("webauthn-unsupported-browser-text"))}
49
51
  };
50
52
  registerByWebAuthn(input);
51
- });
53
+ }, { once: true });
52
54
  `
53
55
  }
54
56
  ]
@@ -67,6 +67,11 @@ useScript({ authButtonId, kcContext, i18n: toRef(props, 'i18n') });
67
67
  id="transports"
68
68
  name="transports"
69
69
  />
70
+ <input
71
+ type="hidden"
72
+ id="authenticatorAttachment"
73
+ name="authenticatorAttachment"
74
+ />
70
75
  <input
71
76
  type="hidden"
72
77
  id="error"
package/package.json CHANGED
@@ -16,7 +16,7 @@
16
16
  "publishConfig": {
17
17
  "access": "public"
18
18
  },
19
- "version": "0.1.0",
19
+ "version": "0.2.0",
20
20
  "type": "module",
21
21
  "files": [
22
22
  "keycloakify-vue",
@@ -48,9 +48,12 @@
48
48
  "_keycloakify-custom-handler": "keycloakify-vue/bin/index.js"
49
49
  },
50
50
  "peerDependencies": {
51
- "keycloakify": "^11.15.11",
51
+ "keycloakify": "^11.16.0",
52
52
  "vue": "^3.5.0"
53
53
  },
54
+ "dependencies": {
55
+ "tsafe": "^1.8.12"
56
+ },
54
57
  "devDependencies": {
55
58
  "@eslint/js": "^10.0.1",
56
59
  "@storybook/addon-docs": "^10",
@@ -69,11 +72,10 @@
69
72
  "eslint-plugin-vue": "^10.2.0",
70
73
  "globals": "^17.6.0",
71
74
  "jsdom": "^29.1.1",
72
- "keycloakify": "^11.15.11",
75
+ "keycloakify": "^11.16.0",
73
76
  "prettier": "^3.8.3",
74
77
  "publint": "^0.3.21",
75
78
  "storybook": "^10",
76
- "tsafe": "^1.8.12",
77
79
  "tsx": "^4.22.4",
78
80
  "typescript": "^6.0.3",
79
81
  "typescript-eslint": "^8.60.1",
@@ -329,6 +331,11 @@
329
331
  "import": "./keycloakify-vue/login/pages/LinkIdpAction.vue",
330
332
  "default": "./keycloakify-vue/login/pages/LinkIdpAction.vue"
331
333
  },
334
+ "./login/pages/Login.useScript": {
335
+ "types": "./keycloakify-vue/login/pages/Login.useScript.d.ts",
336
+ "import": "./keycloakify-vue/login/pages/Login.useScript.js",
337
+ "default": "./keycloakify-vue/login/pages/Login.useScript.js"
338
+ },
332
339
  "./login/pages/Login.vue": {
333
340
  "types": "./keycloakify-vue/login/pages/Login.vue.d.ts",
334
341
  "import": "./keycloakify-vue/login/pages/Login.vue",
@@ -28,37 +28,37 @@ const { log } = kcContext;
28
28
  <div class="col-md-10">
29
29
  <h2><component :is="props.i18n.msg('accountLogHtmlTitle')" /></h2>
30
30
  </div>
31
- </div>
32
31
 
33
- <table class="table table-striped table-bordered">
34
- <thead>
35
- <tr>
36
- <td><component :is="props.i18n.msg('date')" /></td>
37
- <td><component :is="props.i18n.msg('event')" /></td>
38
- <td><component :is="props.i18n.msg('ip')" /></td>
39
- <td><component :is="props.i18n.msg('client')" /></td>
40
- <td><component :is="props.i18n.msg('details')" /></td>
41
- </tr>
42
- </thead>
32
+ <table class="table table-striped table-bordered">
33
+ <thead>
34
+ <tr>
35
+ <td><component :is="props.i18n.msg('date')" /></td>
36
+ <td><component :is="props.i18n.msg('event')" /></td>
37
+ <td><component :is="props.i18n.msg('ip')" /></td>
38
+ <td><component :is="props.i18n.msg('client')" /></td>
39
+ <td><component :is="props.i18n.msg('details')" /></td>
40
+ </tr>
41
+ </thead>
43
42
 
44
- <tbody>
45
- <tr
46
- v-for="(event, index) in log.events"
47
- :key="index"
48
- >
49
- <td>{{ event.date ? new Date(event.date).toLocaleString() : '' }}</td>
50
- <td>{{ event.event }}</td>
51
- <td>{{ event.ipAddress }}</td>
52
- <td>{{ event.client || '' }}</td>
53
- <td>
54
- <span
55
- v-for="(detail, detailIndex) in event.details"
56
- :key="detailIndex"
57
- >{{ `${detail.key} = ${detail.value}` }}{{ detailIndex < event.details.length - 1 ? ', ' : '' }}</span
58
- >
59
- </td>
60
- </tr>
61
- </tbody>
62
- </table>
43
+ <tbody>
44
+ <tr
45
+ v-for="(event, index) in log.events"
46
+ :key="index"
47
+ >
48
+ <td>{{ event.date ? new Date(event.date).toLocaleString() : '' }}</td>
49
+ <td>{{ event.event }}</td>
50
+ <td>{{ event.ipAddress }}</td>
51
+ <td>{{ event.client || '' }}</td>
52
+ <td>
53
+ <span
54
+ v-for="(detail, detailIndex) in event.details"
55
+ :key="detailIndex"
56
+ >{{ `${detail.key} = ${detail.value}` }}{{ detailIndex < event.details.length - 1 ? ', ' : '' }}</span
57
+ >
58
+ </td>
59
+ </tr>
60
+ </tbody>
61
+ </table>
62
+ </div>
63
63
  </component>
64
64
  </template>
@@ -27,7 +27,8 @@ const { realm, auth, url, message, isAppInitiatedAction } = kcContext;
27
27
  const slots = useSlots();
28
28
 
29
29
  onMounted(() => {
30
- document.title = props.documentTitle ?? props.i18n.msgStr('loginTitle', kcContext.realm.displayName);
30
+ document.title =
31
+ props.documentTitle ?? props.i18n.msgStr('loginTitle', kcContext.realm.displayName || kcContext.realm.name);
31
32
  });
32
33
 
33
34
  useSetClassName({ qualifiedName: 'html', className: kcClsx('kcHtmlClass') });
@@ -81,9 +82,8 @@ function onTryAnotherWay() {
81
82
  <div
82
83
  id="kc-header-wrapper"
83
84
  :class="kcClsx('kcHeaderWrapperClass')"
84
- >
85
- {{ i18n.msgStr('loginTitleHtml', realm.displayNameHtml) }}
86
- </div>
85
+ v-html="kcSanitize(i18n.msgStr('loginTitleHtml', realm.displayNameHtml || realm.name))"
86
+ ></div>
87
87
  </div>
88
88
 
89
89
  <div :class="kcClsx('kcFormCardClass')">
@@ -97,45 +97,47 @@ function onTryAnotherWay() {
97
97
  id="kc-locale-wrapper"
98
98
  :class="kcClsx('kcLocaleWrapperClass')"
99
99
  >
100
- <div
101
- id="kc-locale-dropdown"
102
- :class="clsx('menu-button-links', kcClsx('kcLocaleDropDownClass'))"
103
- >
104
- <button
105
- :tabindex="1"
106
- id="kc-current-locale-link"
107
- :aria-label="i18n.msgStr('languages')"
108
- aria-haspopup="true"
109
- aria-expanded="false"
110
- aria-controls="language-switch1"
100
+ <slot name="localeSelector">
101
+ <div
102
+ id="kc-locale-dropdown"
103
+ :class="clsx('menu-button-links', kcClsx('kcLocaleDropDownClass'))"
111
104
  >
112
- {{ i18n.currentLanguage.label }}
113
- </button>
114
- <ul
115
- role="menu"
116
- :tabindex="-1"
117
- aria-labelledby="kc-current-locale-link"
118
- aria-activedescendant=""
119
- id="language-switch1"
120
- :class="kcClsx('kcLocaleListClass')"
121
- >
122
- <li
123
- v-for="(enabledLanguage, i) in i18n.enabledLanguages"
124
- :key="enabledLanguage.languageTag"
125
- :class="kcClsx('kcLocaleListItemClass')"
126
- role="none"
105
+ <button
106
+ :tabindex="1"
107
+ id="kc-current-locale-link"
108
+ :aria-label="i18n.msgStr('languages')"
109
+ aria-haspopup="true"
110
+ aria-expanded="false"
111
+ aria-controls="language-switch1"
112
+ >
113
+ {{ i18n.currentLanguage.label }}
114
+ </button>
115
+ <ul
116
+ role="menu"
117
+ :tabindex="-1"
118
+ aria-labelledby="kc-current-locale-link"
119
+ aria-activedescendant=""
120
+ id="language-switch1"
121
+ :class="kcClsx('kcLocaleListClass')"
127
122
  >
128
- <a
129
- role="menuitem"
130
- :id="`language-${i + 1}`"
131
- :class="kcClsx('kcLocaleItemClass')"
132
- :href="enabledLanguage.href"
123
+ <li
124
+ v-for="(enabledLanguage, i) in i18n.enabledLanguages"
125
+ :key="enabledLanguage.languageTag"
126
+ :class="kcClsx('kcLocaleListItemClass')"
127
+ role="none"
133
128
  >
134
- {{ enabledLanguage.label }}
135
- </a>
136
- </li>
137
- </ul>
138
- </div>
129
+ <a
130
+ role="menuitem"
131
+ :id="`language-${i + 1}`"
132
+ :class="kcClsx('kcLocaleItemClass')"
133
+ :href="enabledLanguage.href"
134
+ >
135
+ {{ enabledLanguage.label }}
136
+ </a>
137
+ </li>
138
+ </ul>
139
+ </div>
140
+ </slot>
139
141
  </div>
140
142
  </div>
141
143
 
@@ -8,6 +8,7 @@ const props = defineProps<{
8
8
  kcClsx: KcClsx;
9
9
  i18n: I18n;
10
10
  passwordInputId: string;
11
+ tabIndex?: number;
11
12
  }>();
12
13
 
13
14
  const isPasswordRevealed = ref(false);
@@ -28,6 +29,7 @@ watch(
28
29
  <slot />
29
30
  <button
30
31
  type="button"
32
+ :tabindex="tabIndex"
31
33
  :class="kcClsx('kcFormPasswordVisibilityButtonClass')"
32
34
  :aria-label="i18n.msgStr(isPasswordRevealed ? 'hidePassword' : 'showPassword')"
33
35
  :aria-controls="passwordInputId"
@@ -0,0 +1,101 @@
1
+ import { onMounted, watch, type Ref } from 'vue';
2
+ import { useInsertScriptTags } from '../../tools/useInsertScriptTags';
3
+ import { assert } from 'keycloakify/tools/assert';
4
+ import { waitForElementMountedOnDom } from 'keycloakify/tools/waitForElementMountedOnDom';
5
+ import type { KcContext } from 'keycloakify/login/KcContext';
6
+
7
+ type KcContextLike = {
8
+ url: {
9
+ resourcesPath: string;
10
+ };
11
+ isUserIdentified: 'true' | 'false';
12
+ challenge: string;
13
+ userVerification: KcContext.WebauthnAuthenticate['userVerification'];
14
+ rpId: string;
15
+ createTimeout: number | string;
16
+ mediation?: string;
17
+ authenticatorAttachment?: string;
18
+ enableWebAuthnConditionalUI?: boolean;
19
+ };
20
+
21
+ assert<keyof KcContextLike extends keyof KcContext.Login ? true : false>();
22
+ assert<KcContext.Login extends KcContextLike ? true : false>();
23
+
24
+ type I18nLike = {
25
+ msgStr: (key: 'webauthn-unsupported-browser-text' | 'passkey-unsupported-browser-text') => string;
26
+ isFetchingTranslations: boolean;
27
+ };
28
+
29
+ export function useScript(params: { webAuthnButtonId: string; kcContext: KcContextLike; i18n: Ref<I18nLike> }) {
30
+ const { webAuthnButtonId, kcContext, i18n } = params;
31
+
32
+ const {
33
+ url,
34
+ isUserIdentified,
35
+ challenge,
36
+ userVerification,
37
+ rpId,
38
+ createTimeout,
39
+ mediation,
40
+ authenticatorAttachment,
41
+ } = kcContext;
42
+
43
+ const { insertScriptTags } = useInsertScriptTags({
44
+ componentOrHookName: 'Login',
45
+ scriptTags: [
46
+ {
47
+ type: 'module',
48
+ textContent: () => `
49
+
50
+ import { authenticateByWebAuthn } from "${url.resourcesPath}/js/webauthnAuthenticate.js";
51
+ import { initAuthenticate } from "${url.resourcesPath}/js/passkeysConditionalAuth.js";
52
+ const authButton = document.getElementById('${webAuthnButtonId}');
53
+ const input = {
54
+ isUserIdentified : ${isUserIdentified},
55
+ challenge : ${JSON.stringify(challenge)},
56
+ userVerification : ${JSON.stringify(userVerification)},
57
+ rpId : ${JSON.stringify(rpId)},
58
+ createTimeout : ${JSON.stringify(createTimeout)},
59
+ mediation : ${JSON.stringify(mediation)},
60
+ authenticatorAttachment : ${JSON.stringify(authenticatorAttachment)}
61
+ };
62
+ authButton.addEventListener("click", () => {
63
+ authenticateByWebAuthn({
64
+ ...input,
65
+ errmsg : ${JSON.stringify(i18n.value.msgStr('webauthn-unsupported-browser-text'))}
66
+ });
67
+ }, { once: true });
68
+
69
+ initAuthenticate({
70
+ ...input,
71
+ errmsg : ${JSON.stringify(i18n.value.msgStr('passkey-unsupported-browser-text'))}
72
+ });
73
+ `,
74
+ },
75
+ ],
76
+ });
77
+
78
+ onMounted(() => {
79
+ if (kcContext.enableWebAuthnConditionalUI !== true) {
80
+ return;
81
+ }
82
+
83
+ const stopWatch = watch(
84
+ () => i18n.value.isFetchingTranslations,
85
+ async (isFetching) => {
86
+ if (isFetching) {
87
+ return;
88
+ }
89
+
90
+ stopWatch();
91
+
92
+ await waitForElementMountedOnDom({
93
+ elementId: webAuthnButtonId,
94
+ });
95
+
96
+ insertScriptTags();
97
+ },
98
+ { immediate: true },
99
+ );
100
+ });
101
+ }
@@ -3,10 +3,11 @@ import { kcSanitize } from 'keycloakify/lib/kcSanitize';
3
3
  import type { KcContext } from 'keycloakify/login/KcContext';
4
4
  import { getKcClsx } from 'keycloakify/login/lib/kcClsx';
5
5
  import { clsx } from 'keycloakify/tools/clsx';
6
- import { computed, ref } from 'vue';
6
+ import { computed, ref, toRef } from 'vue';
7
7
  import PasswordWrapper from '../components/PasswordWrapper.vue';
8
8
  import type { I18n } from '../i18n/i18n';
9
9
  import type { PageProps } from './PageProps';
10
+ import { useScript } from './Login.useScript';
10
11
 
11
12
  const props = defineProps<PageProps<Extract<KcContext, { pageId: 'login.ftl' }>, I18n>>();
12
13
 
@@ -16,7 +17,18 @@ const { kcClsx } = getKcClsx({
16
17
  });
17
18
 
18
19
  const kcContext = props.kcContext;
19
- const { social, realm, url, usernameHidden, login, auth, registrationDisabled, messagesPerField } = kcContext;
20
+ const {
21
+ social,
22
+ realm,
23
+ url,
24
+ usernameHidden,
25
+ login,
26
+ auth,
27
+ registrationDisabled,
28
+ messagesPerField,
29
+ enableWebAuthnConditionalUI,
30
+ authenticators,
31
+ } = kcContext;
20
32
 
21
33
  const isLoginButtonDisabled = ref(false);
22
34
 
@@ -28,6 +40,14 @@ function onSubmit() {
28
40
  isLoginButtonDisabled.value = true;
29
41
  return true;
30
42
  }
43
+
44
+ const webAuthnButtonId = 'authenticateWebAuthnButton';
45
+
46
+ useScript({
47
+ webAuthnButtonId,
48
+ kcContext,
49
+ i18n: toRef(props, 'i18n'),
50
+ });
31
51
  </script>
32
52
 
33
53
  <template>
@@ -130,7 +150,7 @@ function onSubmit() {
130
150
  :value="login.username ?? ''"
131
151
  type="text"
132
152
  autofocus
133
- autocomplete="username"
153
+ :autocomplete="enableWebAuthnConditionalUI ? 'username webauthn' : 'username'"
134
154
  :aria-invalid="messagesPerField.existsError('username', 'password')"
135
155
  />
136
156
  <span
@@ -151,6 +171,7 @@ function onSubmit() {
151
171
  </label>
152
172
  <PasswordWrapper
153
173
  :kcClsx="kcClsx"
174
+ :tabIndex="4"
154
175
  :i18n="i18n"
155
176
  passwordInputId="password"
156
177
  >
@@ -226,5 +247,69 @@ function onSubmit() {
226
247
  </form>
227
248
  </div>
228
249
  </div>
250
+
251
+ <template v-if="enableWebAuthnConditionalUI">
252
+ <form
253
+ id="webauth"
254
+ :action="url.loginAction"
255
+ method="post"
256
+ >
257
+ <input
258
+ type="hidden"
259
+ id="clientDataJSON"
260
+ name="clientDataJSON"
261
+ />
262
+ <input
263
+ type="hidden"
264
+ id="authenticatorData"
265
+ name="authenticatorData"
266
+ />
267
+ <input
268
+ type="hidden"
269
+ id="signature"
270
+ name="signature"
271
+ />
272
+ <input
273
+ type="hidden"
274
+ id="credentialId"
275
+ name="credentialId"
276
+ />
277
+ <input
278
+ type="hidden"
279
+ id="userHandle"
280
+ name="userHandle"
281
+ />
282
+ <input
283
+ type="hidden"
284
+ id="error"
285
+ name="error"
286
+ />
287
+ </form>
288
+
289
+ <template v-if="authenticators !== undefined && authenticators.authenticators.length !== 0">
290
+ <form
291
+ id="authn_select"
292
+ :class="kcClsx('kcFormClass')"
293
+ >
294
+ <input
295
+ v-for="(authenticator, i) in authenticators.authenticators"
296
+ :key="i"
297
+ type="hidden"
298
+ name="authn_use_chk"
299
+ :value="authenticator.credentialId"
300
+ readonly
301
+ />
302
+ </form>
303
+ </template>
304
+
305
+ <br />
306
+
307
+ <input
308
+ :id="webAuthnButtonId"
309
+ type="button"
310
+ :class="kcClsx('kcButtonClass', 'kcButtonDefaultClass', 'kcButtonBlockClass', 'kcButtonLargeClass')"
311
+ :value="i18n.msgStr('passkey-doAuthenticate')"
312
+ />
313
+ </template>
229
314
  </component>
230
315
  </template>
@@ -43,7 +43,7 @@ export function useScript(params: { authButtonId: string; kcContext: KcContextLi
43
43
  challenge : ${JSON.stringify(challenge)},
44
44
  userVerification : ${JSON.stringify(userVerification)},
45
45
  rpId : ${JSON.stringify(rpId)},
46
- createTimeout : ${createTimeout}
46
+ createTimeout : ${JSON.stringify(createTimeout)}
47
47
  };
48
48
  authButton.addEventListener("click", () => {
49
49
  authenticateByWebAuthn({
@@ -13,6 +13,8 @@ type KcContextLike = {
13
13
  userVerification: KcContext.WebauthnAuthenticate['userVerification'];
14
14
  rpId: string;
15
15
  createTimeout: number | string;
16
+ mediation?: string;
17
+ authenticatorAttachment?: string;
16
18
  enableWebAuthnConditionalUI?: boolean;
17
19
  };
18
20
 
@@ -27,7 +29,16 @@ type I18nLike = {
27
29
  export function useScript(params: { webAuthnButtonId: string; kcContext: KcContextLike; i18n: Ref<I18nLike> }) {
28
30
  const { webAuthnButtonId, kcContext, i18n } = params;
29
31
 
30
- const { url, isUserIdentified, challenge, userVerification, rpId, createTimeout } = kcContext;
32
+ const {
33
+ url,
34
+ isUserIdentified,
35
+ challenge,
36
+ userVerification,
37
+ rpId,
38
+ createTimeout,
39
+ mediation,
40
+ authenticatorAttachment,
41
+ } = kcContext;
31
42
 
32
43
  const { insertScriptTags } = useInsertScriptTags({
33
44
  componentOrHookName: 'LoginPassword',
@@ -44,7 +55,9 @@ export function useScript(params: { webAuthnButtonId: string; kcContext: KcConte
44
55
  challenge : ${JSON.stringify(challenge)},
45
56
  userVerification : ${JSON.stringify(userVerification)},
46
57
  rpId : ${JSON.stringify(rpId)},
47
- createTimeout : ${createTimeout}
58
+ createTimeout : ${JSON.stringify(createTimeout)},
59
+ mediation : ${JSON.stringify(mediation)},
60
+ authenticatorAttachment : ${JSON.stringify(authenticatorAttachment)}
48
61
  };
49
62
  authButton.addEventListener("click", () => {
50
63
  authenticateByWebAuthn({
@@ -13,6 +13,8 @@ type KcContextLike = {
13
13
  userVerification: KcContext.WebauthnAuthenticate['userVerification'];
14
14
  rpId: string;
15
15
  createTimeout: number | string;
16
+ mediation?: string;
17
+ authenticatorAttachment?: string;
16
18
  enableWebAuthnConditionalUI?: boolean;
17
19
  };
18
20
 
@@ -27,7 +29,16 @@ type I18nLike = {
27
29
  export function useScript(params: { webAuthnButtonId: string; kcContext: KcContextLike; i18n: Ref<I18nLike> }) {
28
30
  const { webAuthnButtonId, kcContext, i18n } = params;
29
31
 
30
- const { url, isUserIdentified, challenge, userVerification, rpId, createTimeout } = kcContext;
32
+ const {
33
+ url,
34
+ isUserIdentified,
35
+ challenge,
36
+ userVerification,
37
+ rpId,
38
+ createTimeout,
39
+ mediation,
40
+ authenticatorAttachment,
41
+ } = kcContext;
31
42
 
32
43
  const { insertScriptTags } = useInsertScriptTags({
33
44
  componentOrHookName: 'LoginUsername',
@@ -44,7 +55,9 @@ export function useScript(params: { webAuthnButtonId: string; kcContext: KcConte
44
55
  challenge : ${JSON.stringify(challenge)},
45
56
  userVerification : ${JSON.stringify(userVerification)},
46
57
  rpId : ${JSON.stringify(rpId)},
47
- createTimeout : ${createTimeout}
58
+ createTimeout : ${JSON.stringify(createTimeout)},
59
+ mediation : ${JSON.stringify(mediation)},
60
+ authenticatorAttachment : ${JSON.stringify(authenticatorAttachment)}
48
61
  };
49
62
  authButton.addEventListener("click", () => {
50
63
  authenticateByWebAuthn({
@@ -40,14 +40,14 @@ export function useScript(params: { authButtonId: string; kcContext: KcContextLi
40
40
  authButton.addEventListener("click", function() {
41
41
  const input = {
42
42
  isUserIdentified : ${isUserIdentified},
43
- challenge : '${challenge}',
44
- userVerification : '${userVerification}',
45
- rpId : '${rpId}',
46
- createTimeout : ${createTimeout},
43
+ challenge : ${JSON.stringify(challenge)},
44
+ userVerification : ${JSON.stringify(userVerification)},
45
+ rpId : ${JSON.stringify(rpId)},
46
+ createTimeout : ${JSON.stringify(createTimeout)},
47
47
  errmsg : ${JSON.stringify(i18n.value.msgStr('webauthn-unsupported-browser-text'))}
48
48
  };
49
49
  authenticateByWebAuthn(input);
50
- });
50
+ }, { once: true });
51
51
  `,
52
52
  },
53
53
  ],
@@ -16,7 +16,8 @@ type KcContextLike = {
16
16
  rpId: string;
17
17
  attestationConveyancePreference: string;
18
18
  authenticatorAttachment: string;
19
- requireResidentKey: string;
19
+ requireResidentKey?: string;
20
+ residentKey?: string;
20
21
  userVerificationRequirement: string;
21
22
  createTimeout: number | string;
22
23
  excludeCredentialIds: string;
@@ -49,6 +50,7 @@ export function useScript(params: { authButtonId: string; kcContext: KcContextLi
49
50
  attestationConveyancePreference,
50
51
  authenticatorAttachment,
51
52
  requireResidentKey,
53
+ residentKey,
52
54
  userVerificationRequirement,
53
55
  createTimeout,
54
56
  excludeCredentialIds,
@@ -64,24 +66,25 @@ export function useScript(params: { authButtonId: string; kcContext: KcContextLi
64
66
  const registerButton = document.getElementById('${authButtonId}');
65
67
  registerButton.addEventListener("click", function() {
66
68
  const input = {
67
- challenge : '${challenge}',
68
- userid : '${userid}',
69
- username : '${username}',
69
+ challenge : ${JSON.stringify(challenge)},
70
+ userid : ${JSON.stringify(userid)},
71
+ username : ${JSON.stringify(username)},
70
72
  signatureAlgorithms : ${JSON.stringify(signatureAlgorithms)},
71
73
  rpEntityName : ${JSON.stringify(rpEntityName)},
72
74
  rpId : ${JSON.stringify(rpId)},
73
75
  attestationConveyancePreference : ${JSON.stringify(attestationConveyancePreference)},
74
76
  authenticatorAttachment : ${JSON.stringify(authenticatorAttachment)},
75
77
  requireResidentKey : ${JSON.stringify(requireResidentKey)},
78
+ residentKey : ${JSON.stringify(residentKey)},
76
79
  userVerificationRequirement : ${JSON.stringify(userVerificationRequirement)},
77
- createTimeout : ${createTimeout},
80
+ createTimeout : ${JSON.stringify(createTimeout)},
78
81
  excludeCredentialIds : ${JSON.stringify(excludeCredentialIds)},
79
82
  initLabel : ${JSON.stringify(i18n.value.msgStr('webauthn-registration-init-label'))},
80
83
  initLabelPrompt : ${JSON.stringify(i18n.value.msgStr('webauthn-registration-init-label-prompt'))},
81
84
  errmsg : ${JSON.stringify(i18n.value.msgStr('webauthn-unsupported-browser-text'))}
82
85
  };
83
86
  registerByWebAuthn(input);
84
- });
87
+ }, { once: true });
85
88
  `,
86
89
  },
87
90
  ],
@@ -67,6 +67,11 @@ useScript({ authButtonId, kcContext, i18n: toRef(props, 'i18n') });
67
67
  id="transports"
68
68
  name="transports"
69
69
  />
70
+ <input
71
+ type="hidden"
72
+ id="authenticatorAttachment"
73
+ name="authenticatorAttachment"
74
+ />
70
75
  <input
71
76
  type="hidden"
72
77
  id="error"