@oussemasahbeni/keycloakify-login-shadcn 250004.0.9 → 250004.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,317 @@
1
+ # Keycloakify Shadcn Starter
2
+
3
+ A modern, production-ready Keycloak login theme built with React, TypeScript, Tailwind CSS v4, shadcn/ui, and Keycloakify v11.
4
+
5
+ **npm Package:** [@oussemasahbeni/keycloakify-login-shadcn](https://www.npmjs.com/package/@oussemasahbeni/keycloakify-login-shadcn)
6
+
7
+ ---
8
+
9
+ ## ✨ Features
10
+
11
+ - 🎨 **Modern UI** - Beautiful, responsive design using Tailwind CSS v4 and shadcn/ui components
12
+ - 🌙 **Dark Mode** - Built-in dark/light/system theme toggle with persistent preferences
13
+ - 🌍 **Multi-language Support** - i18n ready with English, French, and Arabic translations (RTL supported)
14
+ - 📧 **Custom Email Templates** - Styled email templates using jsx-email for all Keycloak events
15
+ - 🔐 **Complete Login Flow** - All 35+ Keycloak login pages fully customized
16
+ - 🎭 **Social Login Providers** - Pre-styled icons for 16+ OAuth providers (Google, GitHub, Microsoft, etc.)
17
+ - 📖 **Storybook Integration** - Visual testing and documentation for all components
18
+ - ⚡ **Vite Powered** - Fast development with HMR and optimized builds
19
+ - 🔧 **Type-Safe** - Full TypeScript support throughout the codebase
20
+
21
+ ---
22
+
23
+ ## 🚀 Quick Start with npm
24
+
25
+ Get started quickly by using the published npm package in your own project.
26
+
27
+ ### Step 1: Create a new Vite + React + TypeScript project
28
+
29
+ ```bash
30
+ pnpm create vite
31
+ ```
32
+
33
+ When prompted:
34
+
35
+ - **Project name:** `keycloak-theme` (or your preferred name)
36
+ - **Select a framework:** Choose **React**
37
+ - **Select a variant:** Choose **TypeScript**
38
+
39
+ ```bash
40
+ cd keycloak-theme
41
+ ```
42
+
43
+ ### Step 2: Install dependencies
44
+
45
+ ```bash
46
+ pnpm add keycloakify @oussemasahbeni/keycloakify-login-shadcn
47
+ pnpm install
48
+ ```
49
+
50
+ ### Step 3: Initialize Keycloakify
51
+
52
+ ```bash
53
+ npx keycloakify init
54
+ ```
55
+
56
+ When prompted:
57
+
58
+ - **Which theme type would you like to initialize?** Select **(x) login**
59
+ - **Do you want to install the Stories?** Select **(x) Yes (Recommended)**
60
+
61
+ ### Step 4: Configure Vite
62
+
63
+ Update your `vite.config.ts` to include Tailwind CSS, path aliases, and the Keycloakify plugin:
64
+
65
+ ```typescript
66
+ import react from "@vitejs/plugin-react";
67
+ import { keycloakify } from "keycloakify/vite-plugin";
68
+ import { defineConfig } from "vite";
69
+ import path from "node:path";
70
+ import tailwindcss from "@tailwindcss/vite";
71
+
72
+ // https://vite.dev/config/
73
+ export default defineConfig({
74
+ plugins: [
75
+ react(),
76
+ tailwindcss(),
77
+ keycloakify({
78
+ accountThemeImplementation: "none"
79
+ })
80
+ ],
81
+ resolve: {
82
+ alias: {
83
+ "@": path.resolve(__dirname, "./src")
84
+ }
85
+ }
86
+ });
87
+ ```
88
+
89
+ ### Step 5: Configure TypeScript paths
90
+
91
+ Add the path alias to your `tsconfig.app.json`:
92
+
93
+ ```json
94
+ {
95
+ "compilerOptions": {
96
+ "paths": {
97
+ "@/*": ["./src/*"]
98
+ }
99
+ }
100
+ }
101
+ ```
102
+
103
+ ### Step 6: Run Storybook and build
104
+
105
+ ```bash
106
+ # Run Storybook for component development and testing
107
+ pnpm storybook
108
+
109
+ # Build the Keycloak theme JAR file
110
+ pnpm build-keycloak-theme
111
+ ```
112
+
113
+ That's it! You now have a fully functional Keycloak login theme using the published package.
114
+
115
+ ---
116
+
117
+ ## 🛠️ Development (for contributors)
118
+
119
+ If you want to clone this repository and develop/customize the theme locally:
120
+
121
+ ### Prerequisites
122
+
123
+ - Node.js 18+
124
+ - pnpm (or npm/yarn)
125
+ - [Maven](https://maven.apache.org/) (for building the theme JAR)
126
+
127
+ ### Clone and Install
128
+
129
+ ```bash
130
+ # Clone the repository
131
+ git clone https://github.com/Oussemasahbeni/keycloakify-shadcn-starter.git
132
+ cd keycloakify-shadcn-starter
133
+
134
+ # Install dependencies
135
+ pnpm install
136
+ ```
137
+
138
+ ### Development Commands
139
+
140
+ ```bash
141
+ # Start development server with hot reload
142
+ pnpm dev
143
+
144
+ # Run Storybook for component development
145
+ pnpm storybook
146
+
147
+ # Preview email templates
148
+ pnpm emails:preview
149
+
150
+ # Build the Keycloak theme JAR
151
+ pnpm build-keycloak-theme
152
+ ```
153
+
154
+ ---
155
+
156
+ ## 🖼️ Supported Pages
157
+
158
+ This theme includes custom implementations for all Keycloak login pages:
159
+
160
+ | Authentication | Account Management | Security |
161
+ | ------------------- | ------------------- | --------------------- |
162
+ | Login | Register | WebAuthn Authenticate |
163
+ | Login with Username | Update Profile | WebAuthn Register |
164
+ | Login with Password | Update Email | Configure TOTP |
165
+ | Login OTP | Delete Account | Recovery Codes |
166
+ | Login with Passkeys | Logout Confirm | Reset OTP |
167
+ | OAuth Grant | Terms & Conditions | X509 Info |
168
+ | Device Verification | Select Organization | Delete Credential |
169
+
170
+ ---
171
+
172
+ ### Branding
173
+
174
+ 1. **Logo**: Replace `src/login/assets/img/auth-logo.svg` with your company logo
175
+ 2. **Colors**: Modify CSS variables in `src/login/index.css`
176
+ 3. **Fonts**: Update font imports in `src/login/assets/fonts/`
177
+
178
+ ### Internationalization
179
+
180
+ Add or modify translations in `src/login/i18n.ts`:
181
+
182
+ ```typescript
183
+ .withCustomTranslations({
184
+ en: {
185
+ welcomeMessage: "Welcome to Your App",
186
+ loginAccountTitle: "Login to your account",
187
+ // ... more translations
188
+ },
189
+ fr: { /* French translations */ },
190
+ ar: { /* Arabic translations */ }
191
+ })
192
+ ```
193
+
194
+ ### UI Components
195
+
196
+ The theme uses shadcn/ui components located in `src/components/ui/`:
197
+
198
+ - `alert.tsx` - Alert messages
199
+ - `button.tsx` - Buttons with variants
200
+ - `card.tsx` - Card containers
201
+ - `checkbox.tsx` - Checkbox inputs
202
+ - `input.tsx` - Text inputs
203
+ - `label.tsx` - Form labels
204
+ - `dropdown-menu.tsx` - Dropdown menus
205
+ - `radio-group.tsx` - Radio button groups
206
+ - `tooltip.tsx` - Tooltips
207
+
208
+ ---
209
+
210
+ ## 📧 Email Templates
211
+
212
+ Custom email templates are built with [jsx-email](https://jsx.email/) and support multiple languages.
213
+
214
+ ### Available Templates
215
+
216
+ | Template | Description |
217
+ | ---------------------------- | ------------------------------- |
218
+ | `email-verification.tsx` | Email verification |
219
+ | `password-reset.tsx` | Password reset link |
220
+ | `executeActions.tsx` | Required actions |
221
+ | `identity-provider-link.tsx` | IDP linking |
222
+ | `org-invite.tsx` | Organization invitation |
223
+ | `event-login_error.tsx` | Login error notification |
224
+ | `event-update_password.tsx` | Password change notification |
225
+ | `event-update_totp.tsx` | TOTP configuration notification |
226
+ | And more... | |
227
+
228
+ ### Preview Emails Locally
229
+
230
+ ```bash
231
+ pnpm emails:preview
232
+ ```
233
+
234
+ ### Email Locales
235
+
236
+ Translations are in `src/email/locales/{locale}/translation.json`:
237
+
238
+ - `en/` - English
239
+ - `fr/` - French
240
+ - `ar/` - Arabic
241
+
242
+ ---
243
+
244
+ ## 🔨 Building for Production
245
+
246
+ ### Install Maven
247
+
248
+ Required for building the Keycloak theme JAR file.
249
+
250
+ - **macOS**: `brew install maven`
251
+ - **Ubuntu/Debian**: `sudo apt-get install maven`
252
+ - **Windows**: `choco install openjdk && choco install maven`
253
+
254
+ ### Build the Theme
255
+
256
+ ```bash
257
+ pnpm build-keycloak-theme
258
+ ```
259
+
260
+ The built theme will be output as a `.jar` file in the `dist_keycloak/` directory.
261
+
262
+ ### Deploy to Keycloak
263
+
264
+ 1. Copy the `.jar` file to your Keycloak's `providers/` directory
265
+ 2. Restart Keycloak
266
+ 3. Go to Keycloak Admin Console → **Realm Settings** → **Themes**
267
+ 4. Select your custom theme from the dropdown
268
+
269
+ ---
270
+
271
+ ## 🧪 Testing
272
+
273
+ ### Storybook
274
+
275
+ Run Storybook for visual testing and component documentation:
276
+
277
+ ```bash
278
+ pnpm storybook
279
+ ```
280
+
281
+ ### Local Keycloak Testing
282
+
283
+ For local testing with a Keycloak instance, see the [Keycloakify documentation](https://docs.keycloakify.dev/testing-your-theme).
284
+
285
+ ---
286
+
287
+ ## 🤝 Contributing
288
+
289
+ Contributions are welcome! Please feel free to submit a Pull Request.
290
+
291
+ 1. Fork the repository
292
+ 2. Create your feature branch (`git checkout -b feature/amazing-feature`)
293
+ 3. Commit your changes (`git commit -m 'Add some amazing feature'`)
294
+ 4. Push to the branch (`git push origin feature/amazing-feature`)
295
+ 5. Open a Pull Request
296
+
297
+ ---
298
+
299
+ ## 📄 License
300
+
301
+ This project is licensed under the MIT License.
302
+
303
+ ---
304
+
305
+ ## 🙏 Acknowledgments
306
+
307
+ - [Keycloakify](https://keycloakify.dev) - For making Keycloak theming with React possible
308
+ - [shadcn/ui](https://ui.shadcn.com) - For the beautiful UI components
309
+ - [Tailwind CSS](https://tailwindcss.com) - For the utility-first CSS framework
310
+ - [jsx-email](https://jsx.email) - For React email templates
311
+
312
+ ---
313
+
314
+ ## 📦 Package Information
315
+
316
+ **npm:** [@oussemasahbeni/keycloakify-login-shadcn](https://www.npmjs.com/package/@oussemasahbeni/keycloakify-login-shadcn)
317
+ **GitHub:** [Oussemasahbeni/keycloakify-shadcn-starter](https://github.com/Oussemasahbeni/keycloakify-shadcn-starter)
@@ -83,7 +83,7 @@ export function Template(props: {
83
83
  <div className="flex flex-col gap-4 px-0 py-0 pb-6 lg:p-6 lg:md:p-10 lg:pt-10 min-h-screen lg:min-h-0">
84
84
  {/* navigation */}
85
85
  <div className="absolute top-4 right-4 lg:left-4 z-20 flex gap-2">
86
- <Button variant="outline" size="icon">
86
+ <Button type="button" variant="outline" size="icon" asChild>
87
87
  <a href={kcContext.client.baseUrl ?? redirectUrlOrigin}>
88
88
  <FiHome />
89
89
  </a>
@@ -1,36 +1,20 @@
1
- import { useEffect } from "react";
2
- import { useInsertScriptTags } from "@keycloakify/login-ui/tools/useInsertScriptTags";
3
1
  import { useInsertLinkTags } from "@keycloakify/login-ui/tools/useInsertLinkTags";
4
- import { useKcClsx } from "@keycloakify/login-ui/useKcClsx";
2
+ import { useInsertScriptTags } from "@keycloakify/login-ui/tools/useInsertScriptTags";
3
+ import { useEffect } from "react";
5
4
  import { BASE_URL } from "../../../kc.gen";
6
5
  import { useKcContext } from "../../KcContext";
7
6
 
8
7
  export function useInitializeTemplate() {
9
8
  const { kcContext } = useKcContext();
10
9
 
11
- const { doUseDefaultCss } = useKcClsx();
12
-
13
10
  const { areAllStyleSheetsLoaded } = useInsertLinkTags({
14
11
  effectId: "Template",
15
- hrefs: !doUseDefaultCss
16
- ? []
17
- : [
18
- `${BASE_URL}keycloak-theme/login/resources-common/node_modules/@patternfly/patternfly/patternfly.min.css`,
19
- `${BASE_URL}keycloak-theme/login/resources-common/node_modules/patternfly/dist/css/patternfly.min.css`,
20
- `${BASE_URL}keycloak-theme/login/resources-common/node_modules/patternfly/dist/css/patternfly-additions.min.css`,
21
- `${BASE_URL}keycloak-theme/login/resources-common/lib/pficon/pficon.css`,
22
- `${BASE_URL}keycloak-theme/login/css/login.css`
23
- ]
12
+ hrefs: []
24
13
  });
25
14
 
26
15
  const { insertScriptTags } = useInsertScriptTags({
27
16
  effectId: "Template",
28
17
  scriptTags: [
29
- // NOTE: The importmap is added in by the FTL script because it's too late to add it here.
30
- {
31
- type: "module",
32
- src: `${BASE_URL}keycloak-theme/login/js/menu-button-links.js`
33
- },
34
18
  ...(kcContext.scripts === undefined
35
19
  ? []
36
20
  : kcContext.scripts.map(src => ({
@@ -61,8 +61,8 @@ export function Form() {
61
61
  )}
62
62
  <div className={kcClsx("kcFormGroupClass")}>
63
63
  {kcContext.recaptchaRequired &&
64
- !kcContext.recaptchaVisible &&
65
- kcContext.recaptchaAction !== undefined ? (
64
+ !kcContext.recaptchaVisible &&
65
+ kcContext.recaptchaAction !== undefined ? (
66
66
  <div id="kc-form-buttons" className={kcClsx("kcFormButtonsClass")}>
67
67
  <button
68
68
  className={clsx(
@@ -98,7 +98,7 @@ export function Form() {
98
98
  </div>
99
99
 
100
100
  <div className=" flex justify-end">
101
- <Button variant="ghost">
101
+ <Button type="button" variant="ghost">
102
102
  <a href={kcContext.url.loginUrl}>{msg("backToLogin")}</a>
103
103
  </Button>
104
104
  </div>
@@ -0,0 +1,95 @@
1
+ /**
2
+ * This file has been claimed for ownership from @keycloakify/login-ui version 250004.6.5.
3
+ * To relinquish ownership and restore this file to its original content, run the following command:
4
+ *
5
+ * $ npx keycloakify own --path "login/js/authChecker.js" --public --revert
6
+ */
7
+
8
+
9
+
10
+ const SESSION_POLLING_INTERVAL = 2000;
11
+ const AUTH_SESSION_TIMEOUT_MILLISECS = 1000;
12
+ const initialSession = getSession();
13
+ const forms = Array.from(document.forms);
14
+ let timeout;
15
+
16
+ // Stop polling for a session when a form is submitted to prevent unexpected redirects.
17
+ // This is required as Safari does not support the 'beforeunload' event properly.
18
+ // See: https://bugs.webkit.org/show_bug.cgi?id=219102
19
+ forms.forEach((form) =>
20
+ form.addEventListener("submit", () => stopSessionPolling()),
21
+ );
22
+
23
+ // Stop polling for a session when the page is unloaded to prevent unexpected redirects.
24
+ globalThis.addEventListener("beforeunload", () => stopSessionPolling());
25
+
26
+ /**
27
+ * Starts polling to check if a new session was started in another context (e.g. a tab or window), and redirects to the specified URL if a session is detected.
28
+ * @param {string} redirectUrl - The URL to redirect to if a new session is detected.
29
+ */
30
+ export function startSessionPolling(redirectUrl) {
31
+ if (initialSession) {
32
+ // We started with a session, so there is nothing to do, exit.
33
+ return;
34
+ }
35
+
36
+ const session = getSession();
37
+
38
+ if (!session) {
39
+ // No new session detected, check again later.
40
+ timeout = setTimeout(
41
+ () => startSessionPolling(redirectUrl),
42
+ SESSION_POLLING_INTERVAL,
43
+ );
44
+ } else {
45
+ // A new session was detected, redirect to the specified URL and stop polling.
46
+ location.href = redirectUrl;
47
+ stopSessionPolling();
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Stops polling the session.
53
+ */
54
+ function stopSessionPolling() {
55
+ if (timeout) {
56
+ clearTimeout(timeout);
57
+ timeout = undefined;
58
+ }
59
+ }
60
+
61
+ export function checkAuthSession(pageAuthSessionHash) {
62
+ setTimeout(() => {
63
+ const cookieAuthSessionHash = getKcAuthSessionHash();
64
+ if (
65
+ cookieAuthSessionHash &&
66
+ cookieAuthSessionHash !== pageAuthSessionHash
67
+ ) {
68
+ location.reload();
69
+ }
70
+ }, AUTH_SESSION_TIMEOUT_MILLISECS);
71
+ }
72
+
73
+ function getKcAuthSessionHash() {
74
+ return getCookieByName("KC_AUTH_SESSION_HASH");
75
+ }
76
+
77
+ function getSession() {
78
+ return getCookieByName("KEYCLOAK_SESSION");
79
+ }
80
+
81
+ function getCookieByName(name) {
82
+ for (const cookie of document.cookie.split(";")) {
83
+ const [key, value] = cookie.split("=").map((value) => value.trim());
84
+ if (key === name) {
85
+ return value.startsWith('"') && value.endsWith('"')
86
+ ? value.slice(1, -1)
87
+ : value;
88
+ }
89
+ }
90
+ return null;
91
+ }
92
+
93
+
94
+
95
+
@@ -0,0 +1,86 @@
1
+ /**
2
+ * This file has been claimed for ownership from @keycloakify/login-ui version 250004.6.5.
3
+ * To relinquish ownership and restore this file to its original content, run the following command:
4
+ *
5
+ * $ npx keycloakify own --path "login/js/passkeysConditionalAuth.js" --public --revert
6
+ */
7
+
8
+ import { base64url } from "./rfc4648.js";
9
+ import { returnSuccess, returnFailure } from "./webauthnAuthenticate.js";
10
+
11
+ export function initAuthenticate(input) {
12
+ // Check if WebAuthn is supported by this browser
13
+ if (!window.PublicKeyCredential) {
14
+ returnFailure(input.errmsg);
15
+ return;
16
+ }
17
+ if (input.isUserIdentified || typeof PublicKeyCredential.isConditionalMediationAvailable === "undefined") {
18
+ document.getElementById("kc-form-passkey-button").style.display = 'block';
19
+ } else {
20
+ tryAutoFillUI(input);
21
+ }
22
+ }
23
+
24
+ function doAuthenticate(input) {
25
+ // Check if WebAuthn is supported by this browser
26
+ if (!window.PublicKeyCredential) {
27
+ returnFailure(input.errmsg);
28
+ return;
29
+ }
30
+
31
+ const publicKey = {
32
+ rpId : input.rpId,
33
+ challenge: base64url.parse(input.challenge, { loose: true })
34
+ };
35
+
36
+ publicKey.allowCredentials = !input.isUserIdentified ? [] : getAllowCredentials();
37
+
38
+ if (input.createTimeout !== 0) {
39
+ publicKey.timeout = input.createTimeout * 1000;
40
+ }
41
+
42
+ if (input.userVerification !== 'not specified') {
43
+ publicKey.userVerification = input.userVerification;
44
+ }
45
+
46
+ return navigator.credentials.get({
47
+ publicKey: publicKey,
48
+ ...input.additionalOptions
49
+ });
50
+ }
51
+
52
+ async function tryAutoFillUI(input) {
53
+ const isConditionalMediationAvailable = await PublicKeyCredential.isConditionalMediationAvailable();
54
+ if (isConditionalMediationAvailable) {
55
+ document.getElementById("kc-form-login").style.display = "block";
56
+ input.additionalOptions = { mediation: 'conditional'};
57
+ try {
58
+ const result = await doAuthenticate(input);
59
+ returnSuccess(result);
60
+ } catch (error) {
61
+ returnFailure(error);
62
+ }
63
+ } else {
64
+ document.getElementById("kc-form-passkey-button").style.display = 'block';
65
+ }
66
+ }
67
+
68
+ function getAllowCredentials() {
69
+ const allowCredentials = [];
70
+ const authnUse = document.forms['authn_select'].authn_use_chk;
71
+ if (authnUse !== undefined) {
72
+ if (authnUse.length === undefined) {
73
+ allowCredentials.push({
74
+ id: base64url.parse(authnUse.value, {loose: true}),
75
+ type: 'public-key',
76
+ });
77
+ } else {
78
+ authnUse.forEach((entry) =>
79
+ allowCredentials.push({
80
+ id: base64url.parse(entry.value, {loose: true}),
81
+ type: 'public-key',
82
+ }));
83
+ }
84
+ }
85
+ return allowCredentials;
86
+ }
@@ -0,0 +1,185 @@
1
+ /**
2
+ * This file has been claimed for ownership from @keycloakify/login-ui version 250004.6.5.
3
+ * To relinquish ownership and restore this file to its original content, run the following command:
4
+ *
5
+ * $ npx keycloakify own --path "login/js/rfc4648.js" --public --revert
6
+ */
7
+
8
+ /* eslint-disable @typescript-eslint/strict-boolean-expressions */
9
+ function parse(string, encoding, opts) {
10
+ var _opts$out;
11
+
12
+ if (opts === void 0) {
13
+ opts = {};
14
+ }
15
+
16
+ // Build the character lookup table:
17
+ if (!encoding.codes) {
18
+ encoding.codes = {};
19
+
20
+ for (var i = 0; i < encoding.chars.length; ++i) {
21
+ encoding.codes[encoding.chars[i]] = i;
22
+ }
23
+ } // The string must have a whole number of bytes:
24
+
25
+
26
+ if (!opts.loose && string.length * encoding.bits & 7) {
27
+ throw new SyntaxError('Invalid padding');
28
+ } // Count the padding bytes:
29
+
30
+
31
+ var end = string.length;
32
+
33
+ while (string[end - 1] === '=') {
34
+ --end; // If we get a whole number of bytes, there is too much padding:
35
+
36
+ if (!opts.loose && !((string.length - end) * encoding.bits & 7)) {
37
+ throw new SyntaxError('Invalid padding');
38
+ }
39
+ } // Allocate the output:
40
+
41
+
42
+ var out = new ((_opts$out = opts.out) != null ? _opts$out : Uint8Array)(end * encoding.bits / 8 | 0); // Parse the data:
43
+
44
+ var bits = 0; // Number of bits currently in the buffer
45
+
46
+ var buffer = 0; // Bits waiting to be written out, MSB first
47
+
48
+ var written = 0; // Next byte to write
49
+
50
+ for (var _i = 0; _i < end; ++_i) {
51
+ // Read one character from the string:
52
+ var value = encoding.codes[string[_i]];
53
+
54
+ if (value === undefined) {
55
+ throw new SyntaxError('Invalid character ' + string[_i]);
56
+ } // Append the bits to the buffer:
57
+
58
+
59
+ buffer = buffer << encoding.bits | value;
60
+ bits += encoding.bits; // Write out some bits if the buffer has a byte's worth:
61
+
62
+ if (bits >= 8) {
63
+ bits -= 8;
64
+ out[written++] = 0xff & buffer >> bits;
65
+ }
66
+ } // Verify that we have received just enough bits:
67
+
68
+
69
+ if (bits >= encoding.bits || 0xff & buffer << 8 - bits) {
70
+ throw new SyntaxError('Unexpected end of data');
71
+ }
72
+
73
+ return out;
74
+ }
75
+ function stringify(data, encoding, opts) {
76
+ if (opts === void 0) {
77
+ opts = {};
78
+ }
79
+
80
+ var _opts = opts,
81
+ _opts$pad = _opts.pad,
82
+ pad = _opts$pad === void 0 ? true : _opts$pad;
83
+ var mask = (1 << encoding.bits) - 1;
84
+ var out = '';
85
+ var bits = 0; // Number of bits currently in the buffer
86
+
87
+ var buffer = 0; // Bits waiting to be written out, MSB first
88
+
89
+ for (var i = 0; i < data.length; ++i) {
90
+ // Slurp data into the buffer:
91
+ buffer = buffer << 8 | 0xff & data[i];
92
+ bits += 8; // Write out as much as we can:
93
+
94
+ while (bits > encoding.bits) {
95
+ bits -= encoding.bits;
96
+ out += encoding.chars[mask & buffer >> bits];
97
+ }
98
+ } // Partial character:
99
+
100
+
101
+ if (bits) {
102
+ out += encoding.chars[mask & buffer << encoding.bits - bits];
103
+ } // Add padding characters until we hit a byte boundary:
104
+
105
+
106
+ if (pad) {
107
+ while (out.length * encoding.bits & 7) {
108
+ out += '=';
109
+ }
110
+ }
111
+
112
+ return out;
113
+ }
114
+
115
+ /* eslint-disable @typescript-eslint/strict-boolean-expressions */
116
+ var base16Encoding = {
117
+ chars: '0123456789ABCDEF',
118
+ bits: 4
119
+ };
120
+ var base32Encoding = {
121
+ chars: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567',
122
+ bits: 5
123
+ };
124
+ var base32HexEncoding = {
125
+ chars: '0123456789ABCDEFGHIJKLMNOPQRSTUV',
126
+ bits: 5
127
+ };
128
+ var base64Encoding = {
129
+ chars: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/',
130
+ bits: 6
131
+ };
132
+ var base64UrlEncoding = {
133
+ chars: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_',
134
+ bits: 6
135
+ };
136
+ var base16 = {
137
+ parse: function parse$1(string, opts) {
138
+ return parse(string.toUpperCase(), base16Encoding, opts);
139
+ },
140
+ stringify: function stringify$1(data, opts) {
141
+ return stringify(data, base16Encoding, opts);
142
+ }
143
+ };
144
+ var base32 = {
145
+ parse: function parse$1(string, opts) {
146
+ if (opts === void 0) {
147
+ opts = {};
148
+ }
149
+
150
+ return parse(opts.loose ? string.toUpperCase().replace(/0/g, 'O').replace(/1/g, 'L').replace(/8/g, 'B') : string, base32Encoding, opts);
151
+ },
152
+ stringify: function stringify$1(data, opts) {
153
+ return stringify(data, base32Encoding, opts);
154
+ }
155
+ };
156
+ var base32hex = {
157
+ parse: function parse$1(string, opts) {
158
+ return parse(string, base32HexEncoding, opts);
159
+ },
160
+ stringify: function stringify$1(data, opts) {
161
+ return stringify(data, base32HexEncoding, opts);
162
+ }
163
+ };
164
+ var base64 = {
165
+ parse: function parse$1(string, opts) {
166
+ return parse(string, base64Encoding, opts);
167
+ },
168
+ stringify: function stringify$1(data, opts) {
169
+ return stringify(data, base64Encoding, opts);
170
+ }
171
+ };
172
+ var base64url = {
173
+ parse: function parse$1(string, opts) {
174
+ return parse(string, base64UrlEncoding, opts);
175
+ },
176
+ stringify: function stringify$1(data, opts) {
177
+ return stringify(data, base64UrlEncoding, opts);
178
+ }
179
+ };
180
+ var codec = {
181
+ parse: parse,
182
+ stringify: stringify
183
+ };
184
+
185
+ export { base16, base32, base32hex, base64, base64url, codec };
@@ -0,0 +1,113 @@
1
+ /**
2
+ * This file has been claimed for ownership from @keycloakify/login-ui version 250004.6.5.
3
+ * To relinquish ownership and restore this file to its original content, run the following command:
4
+ *
5
+ * $ npx keycloakify own --path "login/js/webauthnAuthenticate.js" --public --revert
6
+ */
7
+
8
+
9
+
10
+ import { base64url } from "./rfc4648.js";
11
+
12
+ // singleton
13
+ let abortController = undefined;
14
+
15
+ export function signal() {
16
+ if (abortController) {
17
+ // abort the previous call
18
+ const abortError = new Error("Cancelling pending WebAuthn call");
19
+ abortError.name = "AbortError";
20
+ abortController.abort(abortError);
21
+ }
22
+
23
+ abortController = new AbortController();
24
+ return abortController.signal;
25
+ }
26
+
27
+ export async function authenticateByWebAuthn(input) {
28
+ if (!input.isUserIdentified) {
29
+ try {
30
+ const result = await doAuthenticate([], input.challenge, input.userVerification, input.rpId, input.createTimeout, input.errmsg);
31
+ returnSuccess(result);
32
+ } catch (error) {
33
+ returnFailure(error);
34
+ }
35
+ return;
36
+ }
37
+ checkAllowCredentials(input.challenge, input.userVerification, input.rpId, input.createTimeout, input.errmsg);
38
+ }
39
+
40
+ async function checkAllowCredentials(challenge, userVerification, rpId, createTimeout, errmsg) {
41
+ const allowCredentials = [];
42
+ const authnUse = document.forms['authn_select'].authn_use_chk;
43
+ if (authnUse !== undefined) {
44
+ if (authnUse.length === undefined) {
45
+ allowCredentials.push({
46
+ id: base64url.parse(authnUse.value, {loose: true}),
47
+ type: 'public-key',
48
+ });
49
+ } else {
50
+ authnUse.forEach((entry) =>
51
+ allowCredentials.push({
52
+ id: base64url.parse(entry.value, {loose: true}),
53
+ type: 'public-key',
54
+ }));
55
+ }
56
+ }
57
+ try {
58
+ const result = await doAuthenticate(allowCredentials, challenge, userVerification, rpId, createTimeout, errmsg);
59
+ returnSuccess(result);
60
+ } catch (error) {
61
+ returnFailure(error);
62
+ }
63
+ }
64
+
65
+ function doAuthenticate(allowCredentials, challenge, userVerification, rpId, createTimeout, errmsg) {
66
+ // Check if WebAuthn is supported by this browser
67
+ if (!window.PublicKeyCredential) {
68
+ returnFailure(errmsg);
69
+ return;
70
+ }
71
+
72
+ const publicKey = {
73
+ rpId : rpId,
74
+ challenge: base64url.parse(challenge, { loose: true })
75
+ };
76
+
77
+ if (createTimeout !== 0) {
78
+ publicKey.timeout = createTimeout * 1000;
79
+ }
80
+
81
+ if (allowCredentials.length) {
82
+ publicKey.allowCredentials = allowCredentials;
83
+ }
84
+
85
+ if (userVerification !== 'not specified') {
86
+ publicKey.userVerification = userVerification;
87
+ }
88
+
89
+ return navigator.credentials.get({
90
+ publicKey: publicKey,
91
+ signal: signal()
92
+ });
93
+ }
94
+
95
+ export function returnSuccess(result) {
96
+ document.getElementById("clientDataJSON").value = base64url.stringify(new Uint8Array(result.response.clientDataJSON), { pad: false });
97
+ document.getElementById("authenticatorData").value = base64url.stringify(new Uint8Array(result.response.authenticatorData), { pad: false });
98
+ document.getElementById("signature").value = base64url.stringify(new Uint8Array(result.response.signature), { pad: false });
99
+ document.getElementById("credentialId").value = result.id;
100
+ if (result.response.userHandle) {
101
+ document.getElementById("userHandle").value = base64url.stringify(new Uint8Array(result.response.userHandle), { pad: false });
102
+ }
103
+ document.getElementById("webauth").requestSubmit();
104
+ }
105
+
106
+ export function returnFailure(err) {
107
+ document.getElementById("error").value = err;
108
+ document.getElementById("webauth").requestSubmit();
109
+ }
110
+
111
+
112
+
113
+
@@ -0,0 +1,153 @@
1
+ /**
2
+ * This file has been claimed for ownership from @keycloakify/login-ui version 250004.6.5.
3
+ * To relinquish ownership and restore this file to its original content, run the following command:
4
+ *
5
+ * $ npx keycloakify own --path "login/js/webauthnRegister.js" --public --revert
6
+ */
7
+
8
+
9
+
10
+ import { base64url } from "./rfc4648.js";
11
+
12
+ export async function registerByWebAuthn(input) {
13
+
14
+ // Check if WebAuthn is supported by this browser
15
+ if (!window.PublicKeyCredential) {
16
+ returnFailure(input.errmsg);
17
+ return;
18
+ }
19
+
20
+ const publicKey = {
21
+ challenge: base64url.parse(input.challenge, {loose: true}),
22
+ rp: {id: input.rpId, name: input.rpEntityName},
23
+ user: {
24
+ id: base64url.parse(input.userid, {loose: true}),
25
+ name: input.username,
26
+ displayName: input.username
27
+ },
28
+ pubKeyCredParams: getPubKeyCredParams(input.signatureAlgorithms),
29
+ };
30
+
31
+ if (input.attestationConveyancePreference !== 'not specified') {
32
+ publicKey.attestation = input.attestationConveyancePreference;
33
+ }
34
+
35
+ const authenticatorSelection = {};
36
+ let isAuthenticatorSelectionSpecified = false;
37
+
38
+ if (input.authenticatorAttachment !== 'not specified') {
39
+ authenticatorSelection.authenticatorAttachment = input.authenticatorAttachment;
40
+ isAuthenticatorSelectionSpecified = true;
41
+ }
42
+
43
+ if (input.requireResidentKey !== 'not specified') {
44
+ if (input.requireResidentKey === 'Yes') {
45
+ authenticatorSelection.requireResidentKey = true;
46
+ } else {
47
+ authenticatorSelection.requireResidentKey = false;
48
+ }
49
+ isAuthenticatorSelectionSpecified = true;
50
+ }
51
+
52
+ if (input.userVerificationRequirement !== 'not specified') {
53
+ authenticatorSelection.userVerification = input.userVerificationRequirement;
54
+ isAuthenticatorSelectionSpecified = true;
55
+ }
56
+
57
+ if (isAuthenticatorSelectionSpecified) {
58
+ publicKey.authenticatorSelection = authenticatorSelection;
59
+ }
60
+
61
+ if (input.createTimeout !== 0) {
62
+ publicKey.timeout = input.createTimeout * 1000;
63
+ }
64
+
65
+ const excludeCredentials = getExcludeCredentials(input.excludeCredentialIds);
66
+ if (excludeCredentials.length > 0) {
67
+ publicKey.excludeCredentials = excludeCredentials;
68
+ }
69
+
70
+ try {
71
+ const result = await doRegister(publicKey);
72
+ returnSuccess(result, input.initLabel, input.initLabelPrompt);
73
+ } catch (error) {
74
+ returnFailure(error);
75
+ }
76
+ }
77
+
78
+ function doRegister(publicKey) {
79
+ return navigator.credentials.create({publicKey});
80
+ }
81
+
82
+ function getPubKeyCredParams(signatureAlgorithmsList) {
83
+ const pubKeyCredParams = [];
84
+ if (signatureAlgorithmsList.length === 0) {
85
+ pubKeyCredParams.push({type: "public-key", alg: -7});
86
+ return pubKeyCredParams;
87
+ }
88
+
89
+ for (const entry of signatureAlgorithmsList) {
90
+ pubKeyCredParams.push({
91
+ type: "public-key",
92
+ alg: entry
93
+ });
94
+ }
95
+
96
+ return pubKeyCredParams;
97
+ }
98
+
99
+ function getExcludeCredentials(excludeCredentialIds) {
100
+ const excludeCredentials = [];
101
+ if (excludeCredentialIds === "") {
102
+ return excludeCredentials;
103
+ }
104
+
105
+ for (const entry of excludeCredentialIds.split(',')) {
106
+ excludeCredentials.push({
107
+ type: "public-key",
108
+ id: base64url.parse(entry, {loose: true})
109
+ });
110
+ }
111
+
112
+ return excludeCredentials;
113
+ }
114
+
115
+ function getTransportsAsString(transportsList) {
116
+ if (!Array.isArray(transportsList)) {
117
+ return "";
118
+ }
119
+
120
+ return transportsList.join();
121
+ }
122
+
123
+ function returnSuccess(result, initLabel, initLabelPrompt) {
124
+ document.getElementById("clientDataJSON").value = base64url.stringify(new Uint8Array(result.response.clientDataJSON), {pad: false});
125
+ document.getElementById("attestationObject").value = base64url.stringify(new Uint8Array(result.response.attestationObject), {pad: false});
126
+ document.getElementById("publicKeyCredentialId").value = base64url.stringify(new Uint8Array(result.rawId), {pad: false});
127
+
128
+ if (typeof result.response.getTransports === "function") {
129
+ const transports = result.response.getTransports();
130
+ if (transports) {
131
+ document.getElementById("transports").value = getTransportsAsString(transports);
132
+ }
133
+ } else {
134
+ console.log("Your browser is not able to recognize supported transport media for the authenticator.");
135
+ }
136
+
137
+ let labelResult = window.prompt(initLabelPrompt, initLabel);
138
+ if (labelResult === null) {
139
+ labelResult = initLabel;
140
+ }
141
+ document.getElementById("authenticatorLabel").value = labelResult;
142
+
143
+ document.getElementById("register").requestSubmit();
144
+ }
145
+
146
+ function returnFailure(err) {
147
+ document.getElementById("error").value = err;
148
+ document.getElementById("register").requestSubmit();
149
+ }
150
+
151
+
152
+
153
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oussemasahbeni/keycloakify-login-shadcn",
3
- "version": "250004.0.9",
3
+ "version": "250004.0.11",
4
4
  "description": "Keycloakify Shadcn Theme extensions",
5
5
  "license": "MIT",
6
6
  "repository": {