@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 +317 -0
- package/keycloak-theme/login/components/Template/Template.tsx +1 -1
- package/keycloak-theme/login/components/Template/useInitializeTemplate.ts +3 -19
- package/keycloak-theme/login/pages/register/Form.tsx +3 -3
- package/keycloak-theme/public/keycloak-theme/login/js/authChecker.js +95 -0
- package/keycloak-theme/public/keycloak-theme/login/js/passkeysConditionalAuth.js +86 -0
- package/keycloak-theme/public/keycloak-theme/login/js/rfc4648.js +185 -0
- package/keycloak-theme/public/keycloak-theme/login/js/webauthnAuthenticate.js +113 -0
- package/keycloak-theme/public/keycloak-theme/login/js/webauthnRegister.js +153 -0
- package/package.json +1 -1
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 {
|
|
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:
|
|
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
|
-
|
|
65
|
-
|
|
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
|
+
|