@andreyyolkin/keycloakify-vue 0.1.2 → 0.2.1

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 (34) hide show
  1. package/README.md +8 -0
  2. package/keycloakify-vue/account/pages/Log.vue +30 -30
  3. package/keycloakify-vue/account/pages/PageProps.d.ts +2 -2
  4. package/keycloakify-vue/login/Template.vue +42 -40
  5. package/keycloakify-vue/login/Template.vue.d.ts +6 -4
  6. package/keycloakify-vue/login/components/PasswordWrapper.vue +2 -0
  7. package/keycloakify-vue/login/components/PasswordWrapper.vue.d.ts +1 -0
  8. package/keycloakify-vue/login/pages/Login.useScript.d.ts +25 -0
  9. package/keycloakify-vue/login/pages/Login.useScript.js +75 -0
  10. package/keycloakify-vue/login/pages/Login.vue +88 -3
  11. package/keycloakify-vue/login/pages/LoginPasskeysConditionalAuthenticate.useScript.js +1 -1
  12. package/keycloakify-vue/login/pages/LoginPassword.useScript.d.ts +2 -0
  13. package/keycloakify-vue/login/pages/LoginPassword.useScript.js +13 -2
  14. package/keycloakify-vue/login/pages/LoginUsername.useScript.d.ts +2 -0
  15. package/keycloakify-vue/login/pages/LoginUsername.useScript.js +13 -2
  16. package/keycloakify-vue/login/pages/PageProps.d.ts +2 -2
  17. package/keycloakify-vue/login/pages/WebauthnAuthenticate.useScript.js +5 -5
  18. package/keycloakify-vue/login/pages/WebauthnRegister.useScript.d.ts +2 -1
  19. package/keycloakify-vue/login/pages/WebauthnRegister.useScript.js +7 -5
  20. package/keycloakify-vue/login/pages/WebauthnRegister.vue +5 -0
  21. package/package.json +8 -3
  22. package/src/account/pages/Log.vue +30 -30
  23. package/src/account/pages/PageProps.ts +9 -2
  24. package/src/login/Template.vue +42 -40
  25. package/src/login/components/PasswordWrapper.vue +2 -0
  26. package/src/login/pages/Login.useScript.ts +101 -0
  27. package/src/login/pages/Login.vue +88 -3
  28. package/src/login/pages/LoginPasskeysConditionalAuthenticate.useScript.ts +1 -1
  29. package/src/login/pages/LoginPassword.useScript.ts +15 -2
  30. package/src/login/pages/LoginUsername.useScript.ts +15 -2
  31. package/src/login/pages/PageProps.ts +9 -2
  32. package/src/login/pages/WebauthnAuthenticate.useScript.ts +5 -5
  33. package/src/login/pages/WebauthnRegister.useScript.ts +9 -6
  34. package/src/login/pages/WebauthnRegister.vue +5 -0
package/README.md CHANGED
@@ -41,6 +41,14 @@ The easiest way to get started is the official starter template:
41
41
  and `IdpReviewUserProfile`.
42
42
  - **`i18nBuilder`** — factory for wiring Keycloakify's `noJsx` i18n engine into Vue (returns `msg()` /
43
43
  `advancedMsg()` that produce sanitized `VNode`s).
44
+ > **`withCustomTranslations` must be called from a plain `.ts`/`.tsx`/`.js` file, never from a `.vue` SFC.**
45
+ > At build time, `keycloakify`'s `generateMessageProperties` step statically scans your theme source for a
46
+ > `withCustomTranslations({...})` call to produce the server-side `messages_<lang>.properties` bundle — but
47
+ > it only scans files matching `/\.(js|ts|tsx)$/`, so a call placed inside a `<script setup>` block of a
48
+ > `.vue` file is silently skipped and those keys never reach the server bundle (client-side `msg()` still
49
+ > works, which can make the gap easy to miss). Keep your `i18nBuilder(...).withCustomTranslations(...)`
50
+ > call in a dedicated `i18n.ts` (as in this repo's `stories/login/i18n.ts`) and import it from your `.vue`
51
+ > components instead.
44
52
  - **Account theme** — importable from `@andreyyolkin/keycloakify-vue/account`: its own `DefaultPage` + `Template`,
45
53
  the 7 account pages at `@andreyyolkin/keycloakify-vue/account/pages/*.vue`, and an account `i18nBuilder`.
46
54
  - **`bin` handlers** — `_keycloakify-custom-handler` delegates `update-kc-gen`, `eject-page`,
@@ -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>
@@ -1,7 +1,7 @@
1
- import { type ClassKey, type TemplateProps } from '../TemplateProps';
1
+ import type { ClassKey } from '../TemplateProps';
2
2
  import type { Component } from 'vue';
3
3
  export type PageProps<NarrowedKcContext, I18n> = {
4
- Template: Component<TemplateProps<NarrowedKcContext, I18n>>;
4
+ Template: Component;
5
5
  kcContext: NarrowedKcContext;
6
6
  i18n: I18n;
7
7
  doUseDefaultCss: boolean;
@@ -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({
@@ -1,7 +1,7 @@
1
- import { type ClassKey, type TemplateProps } from '../TemplateProps';
1
+ import type { ClassKey } from '../TemplateProps';
2
2
  import type { Component } from 'vue';
3
3
  export type PageProps<NarrowedKcContext, I18n> = {
4
- Template: Component<TemplateProps<NarrowedKcContext, I18n>>;
4
+ Template: Component;
5
5
  kcContext: NarrowedKcContext;
6
6
  i18n: I18n;
7
7
  doUseDefaultCss: boolean;
@@ -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
  ]