Add WebAuthn/Passkey support to Login and LoginPassword flows#960
Conversation
WalkthroughAdds WebAuthn (passkey) support to login flows: expands KcContext types and mocks with WebAuthn fields, conditionally renders WebAuthn UI in Login and LoginPassword pages, and adds two hooks (Login.useScript and LoginPassword.useScript) that inject a module script to perform client-side WebAuthn authentication. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant Page as Login / LoginPassword
participant Hook as useScript
participant DOM
participant Script as Injected Module
participant Auth as authenticateByWebAuthn
User->>Page: load (enableWebAuthnConditionalUI = true)
Page->>Hook: call useScript(webAuthnButtonId, kcContext, i18n)
Hook->>Hook: wait for translations & validate kcContext
Hook->>DOM: wait for button element to mount
Hook->>DOM: insert module script (imports authenticateByWebAuthn)
User->>DOM: click authenticate button
DOM->>Script: click handler executes
Script->>Auth: call authenticateByWebAuthn({challenge,userVerification,rpId,createTimeout,isUserIdentified,errmsg})
Auth-->>Script: result (success/error)
Script->>DOM: populate hidden inputs and submit form
DOM-->>User: proceed with login flow
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~30 minutes
Possibly related PRs
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 📒 Files selected for processing (3)
🚧 Files skipped from review as they are similar to previous changes (1)
🧰 Additional context used🧬 Code graph analysis (1)src/login/pages/LoginPassword.tsx (1)
🔇 Additional comments (5)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (4)
src/login/pages/Login.useScript.tsx (1)
58-75: Align guard comment with actual condition (optional)The comment says “Only insert script if
enableWebAuthnConditionalUIis true”, but the guard actually checks for presence of WebAuthn challenge data (challenge,userVerification,rpId,createTimeout). To avoid confusion, you can update the comment to reflect what’s really gating:- // Only insert script if enableWebAuthnConditionalUI is true + // Only insert script once WebAuthn challenge data is presentThis keeps behavior as‑is while making the intent clearer (same applies to the LoginPassword hook).
src/login/pages/LoginPassword.tsx (1)
10-33: WebAuthn UI integration is consistent; tiny optional tweak on authenticator checkThe
useScriptwiring and conditional WebAuthn block mirror theLogin/WebauthnAuthenticatepatterns and look correct: hiddenwebauth+authn_selectforms and a dedicated button with the expectedauthButtonId.Minor nit: the
authn_selectguardauthenticators !== undefined && Object.keys(authenticators).length !== 0is effectively equivalent to just checking
authenticators, since the object will always have at least theauthenticatorskey. If you actually want to gate on having at least one credential, consider:authenticators?.authenticators.lengthNot required for correctness, but a bit clearer.
Also applies to: 110-139
src/login/pages/Login.tsx (1)
10-33: Login WebAuthn wiring matches other flows; consider small cleanup on authenticator guardThe
useScriptintegration and conditional WebAuthn section align with the LoginPassword/WebauthnAuthenticate implementations and should work as expected with the new KC context fields.As in
LoginPassword.tsx, the conditionauthenticators !== undefined && Object.keys(authenticators).length !== 0doesn’t really filter out the “no authenticators” case—
Object.keyswill still be["authenticators"]even when the inner array is empty. If you want to require at least one credential, you can simplify/clarify to:authenticators?.authenticators.lengthAlso, the WebAuthn JSX block is now duplicated between
LoginandLoginPassword; extracting a tiny shared fragment (e.g.WebauthnConditionalBlock) would reduce duplication, but that’s optional.Also applies to: 204-233
src/login/KcContext/KcContext.ts (1)
175-218: KcContext WebAuthn extensions are consistent; optional type tighteningThe added WebAuthn fields on
Login,LoginUsername,LoginPassword, andLoginPasskeysConditionalAuthenticateline up with how the new hooks and mocks consume them, and keep existing pages backward‑compatible via optional properties where needed.If you want stricter typing, you could consider reusing the same
userVerificationunion (UserVerificationRequirement | "not specified") used onWebauthnAuthenticate/LoginUsernameforLogin,LoginPassword, andLoginPasskeysConditionalAuthenticateinstead of plainstring, but that’s purely a type‑safety nicety.Also applies to: 329-359, 361-385, 611-635
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
src/login/KcContext/KcContext.ts(2 hunks)src/login/KcContext/kcContextMocks.ts(2 hunks)src/login/pages/Login.tsx(3 hunks)src/login/pages/Login.useScript.tsx(1 hunks)src/login/pages/LoginPassword.tsx(3 hunks)src/login/pages/LoginPassword.useScript.tsx(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (5)
src/login/pages/Login.useScript.tsx (6)
src/login/KcContext/KcContext.ts (2)
KcContext(28-65)Login(175-218)src/login/pages/Login.tsx (1)
Login(12-236)src/login/pages/LoginPassword.useScript.tsx (1)
useScript(26-76)src/tools/useInsertScriptTags.ts (1)
useInsertScriptTags(32-120)src/tools/waitForElementMountedOnDom.ts (1)
waitForElementMountedOnDom(1-30)src/login/pages/LoginUsername.useScript.tsx (1)
useScript(26-71)
src/login/pages/LoginPassword.tsx (1)
src/login/pages/LoginPassword.useScript.tsx (1)
useScript(26-76)
src/login/pages/LoginPassword.useScript.tsx (5)
src/login/KcContext/KcContext.ts (2)
KcContext(28-65)LoginPassword(361-386)src/login/pages/LoginPassword.tsx (1)
LoginPassword(12-142)src/login/pages/Login.useScript.tsx (1)
useScript(26-76)src/tools/useInsertScriptTags.ts (1)
useInsertScriptTags(32-120)src/tools/waitForElementMountedOnDom.ts (1)
waitForElementMountedOnDom(1-30)
src/login/pages/Login.tsx (1)
src/login/pages/Login.useScript.tsx (1)
useScript(26-76)
src/login/KcContext/KcContext.ts (1)
src/login/pages/WebauthnAuthenticate.tsx (1)
WebauthnAuthenticate(9-138)
🔇 Additional comments (3)
src/login/pages/Login.useScript.tsx (1)
7-56: WebAuthn script wiring and KcContext typing look consistentThe
KcContextLike/I18nLikeshapes, compile‑timeassertchecks, and injected module script closely mirror the existingLoginUsername.useScriptpattern while safely coercingisUserIdentifiedandcreateTimeoutinto JS literals. The dynamicerrmsgviaJSON.stringifyis also a good choice to avoid quoting issues.src/login/KcContext/kcContextMocks.ts (1)
201-231: Mocks correctly extended for WebAuthn flowsThe added WebAuthn fields on
Login,LoginUsername, andLoginPasswordmocks match the newKcContexttypings and give sane defaults (enableWebAuthnConditionalUI: false, empty challenge, etc.), so the new hooks can short‑circuit cleanly in dev/Storybook while still exercising the shapes.Also applies to: 339-362, 363-381
src/login/pages/LoginPassword.useScript.tsx (1)
7-75: Hook implementation is consistent with Login.useScript and types
KcContextLike/I18nLikeand the injected module script mirrorLogin.useScriptwhile being correctly tied toKcContext.LoginPasswordthrough theassertcalls. The effect guards on translations + WebAuthn data before inserting the script, which should avoid running the flow when KC hasn’t provided the necessary fields.
|
@coderabbitai generate docstrings |
✅ Actions performedInitiated docstring generation; will generate only if new commits exist. |
|
Note Docstrings generation - SUCCESS |
Docstrings generation was requested by @discostur. * #960 (comment) The following files were modified: * `src/login/pages/Login.tsx` * `src/login/pages/Login.useScript.tsx` * `src/login/pages/LoginPassword.tsx` * `src/login/pages/LoginPassword.useScript.tsx`
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (2)
src/login/pages/LoginPassword.useScript.tsx (1)
63-81: Add missing dependencies to useEffect array.The effect uses
authButtonId(line 75) andinsertScriptTags(line 78) but doesn't list them in the dependency array (line 80). WhileauthButtonIdis a constant andinsertScriptTagsshould be stable fromuseCallback, React expects all dependencies to be declared.- }, [isFetchingTranslations, challenge, userVerification, rpId, createTimeout]); + }, [isFetchingTranslations, challenge, userVerification, rpId, createTimeout, authButtonId, insertScriptTags]);This satisfies the exhaustive-deps rule and makes dependencies explicit.
src/login/pages/Login.useScript.tsx (1)
64-82: Add missing dependencies to useEffect array.Same issue as in
LoginPassword.useScript.tsx: the effect usesauthButtonId(line 76) andinsertScriptTags(line 79) but they're not in the dependency array (line 81).- }, [isFetchingTranslations, challenge, userVerification, rpId, createTimeout]); + }, [isFetchingTranslations, challenge, userVerification, rpId, createTimeout, authButtonId, insertScriptTags]);
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
src/login/KcContext/KcContext.ts(2 hunks)src/login/pages/Login.tsx(4 hunks)src/login/pages/Login.useScript.tsx(1 hunks)src/login/pages/LoginPassword.tsx(4 hunks)src/login/pages/LoginPassword.useScript.tsx(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- src/login/pages/LoginPassword.tsx
🧰 Additional context used
🧬 Code graph analysis (4)
src/login/pages/Login.tsx (1)
src/login/pages/Login.useScript.tsx (1)
useScript(32-82)
src/login/pages/Login.useScript.tsx (6)
src/login/KcContext/KcContext.ts (2)
KcContext(28-65)Login(175-219)src/login/pages/Login.tsx (1)
Login(16-240)src/login/pages/LoginPassword.useScript.tsx (1)
useScript(31-81)src/tools/useInsertScriptTags.ts (1)
useInsertScriptTags(32-120)src/tools/waitForElementMountedOnDom.ts (1)
waitForElementMountedOnDom(1-30)src/login/pages/LoginUsername.useScript.tsx (1)
useScript(26-71)
src/login/pages/LoginPassword.useScript.tsx (4)
src/login/KcContext/KcContext.ts (2)
KcContext(28-65)LoginPassword(362-388)src/login/pages/LoginPassword.tsx (1)
LoginPassword(16-146)src/tools/useInsertScriptTags.ts (1)
useInsertScriptTags(32-120)src/tools/waitForElementMountedOnDom.ts (1)
waitForElementMountedOnDom(1-30)
src/login/KcContext/KcContext.ts (1)
src/login/pages/WebauthnAuthenticate.tsx (1)
WebauthnAuthenticate(9-138)
🔇 Additional comments (6)
src/login/pages/Login.tsx (4)
14-14: LGTM: useScript hook imported.The import path is correct and aligns with the new hook introduced in this PR.
24-25: LGTM: WebAuthn context fields destructured.The WebAuthn fields are correctly extracted from kcContext, matching the type extensions in KcContext.ts.
31-37: Consider conditionally invoking useScript hook.The
useScripthook is invoked unconditionally even when WebAuthn is disabled. While the hook internally guards script insertion, you could optimize by conditionally calling it:const authButtonId = "authenticateWebAuthnButton"; - useScript({ - authButtonId, - kcContext, - i18n - }); + if (enableWebAuthnConditionalUI) { + useScript({ + authButtonId, + kcContext, + i18n + }); + }However, this would violate the Rules of Hooks. A better approach is to keep the hook unconditional (as it is now) since the internal guards prevent unnecessary work when WebAuthn is disabled.
208-237: LGTM: WebAuthn UI properly integrated.The conditional WebAuthn block is well-structured:
- Properly gated by
enableWebAuthnConditionalUI- Hidden forms correctly configured for credential submission
- Authenticator list rendered conditionally
- Button wired with the expected ID
src/login/KcContext/KcContext.ts (2)
377-387: LGTM: WebAuthn fields added to LoginPassword type.The fields mirror those added to the
Logintype, maintaining consistency between the two password-based flows.
208-218: Verify WebAuthn field optionality and type alignment reflects actual Keycloak behavior.The inconsistencies flagged in the review are confirmed:
- LoginUsername requires
isUserIdentified,challenge,userVerification,rpId,createTimeoutas non-optional fields with typeUserVerificationRequirement | "not specified"- Login and LoginPassword make all WebAuthn fields optional and use type
WebauthnAuthenticate["userVerification"]instead- Login.isUserIdentified also includes
booleanin its union, while LoginUsername.isUserIdentified restricts to"true" | "false"Verify whether these differences are intentional to match Keycloak's actual API contract for each login flow, or whether alignment is needed.
| /** | ||
| * Hook to wire WebAuthn passkey authentication on the password step (login-password.ftl). | ||
| * Mirrors Login.useScript but scoped to the second step of the flow. | ||
| */ | ||
| import { useEffect } from "react"; | ||
| import { useInsertScriptTags } from "keycloakify/tools/useInsertScriptTags"; | ||
| import { assert } from "keycloakify/tools/assert"; | ||
| import { KcContext } from "keycloakify/login/KcContext/KcContext"; | ||
| import { waitForElementMountedOnDom } from "keycloakify/tools/waitForElementMountedOnDom"; | ||
|
|
||
| type KcContextLike = { | ||
| url: { | ||
| resourcesPath: string; | ||
| }; | ||
| isUserIdentified?: "true" | "false" | boolean; | ||
| challenge?: string; | ||
| userVerification?: string; | ||
| rpId?: string; | ||
| createTimeout?: number | string; | ||
| }; | ||
|
|
||
| assert<keyof KcContextLike extends keyof KcContext.LoginPassword ? true : false>(); | ||
| assert<KcContext.LoginPassword extends KcContextLike ? true : false>(); | ||
|
|
||
| type I18nLike = { | ||
| msgStr: (key: "webauthn-unsupported-browser-text") => string; | ||
| isFetchingTranslations: boolean; | ||
| }; | ||
|
|
||
| /** Injects the WebAuthn authentication script and binds click listener to the passkey button. */ | ||
| export function useScript(params: { authButtonId: string; kcContext: KcContextLike; i18n: I18nLike }) { | ||
| const { authButtonId, kcContext, i18n } = params; | ||
|
|
||
| const { url, isUserIdentified, challenge, userVerification, rpId, createTimeout } = kcContext; | ||
|
|
||
| const { msgStr, isFetchingTranslations } = i18n; | ||
|
|
||
| const { insertScriptTags } = useInsertScriptTags({ | ||
| componentOrHookName: "LoginPassword", | ||
| scriptTags: [ | ||
| { | ||
| type: "module", | ||
| textContent: () => ` | ||
|
|
||
| import { authenticateByWebAuthn } from "${url.resourcesPath}/js/webauthnAuthenticate.js"; | ||
| const authButton = document.getElementById('${authButtonId}'); | ||
| authButton.addEventListener("click", function() { | ||
| const input = { | ||
| isUserIdentified : ${isUserIdentified ?? false}, | ||
| challenge : '${challenge ?? ""}', | ||
| userVerification : '${userVerification ?? ""}', | ||
| rpId : '${rpId ?? ""}', | ||
| createTimeout : ${createTimeout ?? 0}, | ||
| errmsg : ${JSON.stringify(msgStr("webauthn-unsupported-browser-text"))} | ||
| }; | ||
| authenticateByWebAuthn(input); | ||
| }); | ||
| ` | ||
| } | ||
| ] | ||
| }); | ||
|
|
||
| useEffect(() => { | ||
| if (isFetchingTranslations) { | ||
| return; | ||
| } | ||
|
|
||
| // Only insert script if enableWebAuthnConditionalUI is true | ||
| if (!challenge || !userVerification || !rpId || createTimeout === undefined) { | ||
| return; | ||
| } | ||
|
|
||
| (async () => { | ||
| await waitForElementMountedOnDom({ | ||
| elementId: authButtonId | ||
| }); | ||
|
|
||
| insertScriptTags(); | ||
| })(); | ||
| }, [isFetchingTranslations, challenge, userVerification, rpId, createTimeout]); | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Extract shared logic to eliminate duplication.
This file is nearly identical to src/login/pages/Login.useScript.tsx (only difference: componentOrHookName). Consider extracting the shared logic:
// src/login/pages/shared/useWebAuthnScript.tsx
function useWebAuthnScript(params: {
authButtonId: string;
kcContext: KcContextLike;
i18n: I18nLike;
componentOrHookName: string;
}) {
// ... shared implementation
}
// Then in Login.useScript.tsx:
export function useScript(params: { authButtonId: string; kcContext: KcContextLike; i18n: I18nLike }) {
return useWebAuthnScript({ ...params, componentOrHookName: "Login" });
}
// And in LoginPassword.useScript.tsx:
export function useScript(params: { authButtonId: string; kcContext: KcContextLike; i18n: I18nLike }) {
return useWebAuthnScript({ ...params, componentOrHookName: "LoginPassword" });
}This eliminates ~70 lines of duplication and makes future maintenance easier.
🤖 Prompt for AI Agents
src/login/pages/LoginPassword.useScript.tsx lines 1-81, the hook duplicates
Login.useScript.tsx; extract the shared logic into a new module (e.g.,
src/login/pages/shared/useWebAuthnScript.tsx) that accepts { authButtonId,
kcContext, i18n, componentOrHookName } and contains the existing implementation
(including types KcContextLike and I18nLike, useInsertScriptTags usage,
waitForElementMountedOnDom logic and effect dependencies); then replace this
file’s useScript with a thin wrapper that calls the shared hook with
componentOrHookName: "LoginPassword" and update Login.useScript.tsx to call the
same shared hook with componentOrHookName: "Login"; adjust imports accordingly.
There was a problem hiding this comment.
not sure about that ... will wait for feedback from maintainer
There was a problem hiding this comment.
@discostur That sounds reasonable! Waiting for maintainer feedback on architectural decisions like this is a good practice. The current implementation works correctly, so there's no urgency to refactor.
Feel free to ping me if you or the maintainer would like to proceed with the refactoring later, or if you need any assistance with it. Happy to help when you're ready! 🐰
|
@garronej could you please have a look - thanks |
|
Hey @discostur, Thank you very much for taking this on, and sorry for the late review. I skimmed through the changes, and I’m a bit concerned about how the implementation diverges from the reference version in Keycloakify: From my understanding, this part should behave exactly the same. I’m guessing some of the structure may come from @coderabbitai suggestions. While those can be helpful for catching certain classes of bugs, they often add defensive checks that don’t match the actual asserted state of the system, and that can lead to misleading logic paths. In patterns like these, it’s usually better to mirror Keycloak’s behavior precisely. For example, in the snippet below: useEffect(() => {
if (isFetchingTranslations) {
return;
}
// Only insert script if enableWebAuthnConditionalUI is true
if (!challenge || !userVerification || !rpId || createTimeout === undefined) {
return;
}
(async () => {
await waitForElementMountedOnDom({
elementId: authButtonId
});
insertScriptTags();
})();
}, [isFetchingTranslations, challenge, userVerification, rpId, createTimeout]);A few points:
Because of this, the safest and clearest approach is simply to mirror the logic from Could you refactor the code to match that version as closely as possible? Thanks again for the contribution, it’s genuinely appreciated! |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: Length of output: 506 🏁 Script executed: Length of output: 6158 🏁 Script executed: Length of output: 3259 🏁 Script executed: Length of output: 3170 🏁 Script executed: Length of output: 2552 @garronej, thank you for the detailed feedback! You're absolutely right about the divergence from the reference implementation. I can see the issue clearly now when comparing with The current
Here's how both files should be refactored to match the src/login/pages/Login.useScript.tsx: import { useEffect } from "react";
import { useInsertScriptTags } from "keycloakify/tools/useInsertScriptTags";
import { assert } from "keycloakify/tools/assert";
import { KcContext } from "keycloakify/login/KcContext/KcContext";
import { waitForElementMountedOnDom } from "keycloakify/tools/waitForElementMountedOnDom";
type KcContextLike = {
url: {
resourcesPath: string;
};
isUserIdentified: "true" | "false" | boolean;
challenge: string;
userVerification: string | KcContext.WebauthnAuthenticate["userVerification"];
rpId: string;
createTimeout: number | string;
};
assert<keyof KcContextLike extends keyof KcContext.Login ? true : false>();
assert<KcContext.Login extends KcContextLike ? true : false>();
type I18nLike = {
msgStr: (key: "webauthn-unsupported-browser-text") => string;
isFetchingTranslations: boolean;
};
export function useScript(params: { authButtonId: string; kcContext: KcContextLike; i18n: I18nLike }) {
const { authButtonId, kcContext, i18n } = params;
const { url, isUserIdentified, challenge, userVerification, rpId, createTimeout } = kcContext;
const { msgStr, isFetchingTranslations } = i18n;
const { insertScriptTags } = useInsertScriptTags({
componentOrHookName: "Login",
scriptTags: [
{
type: "module",
textContent: () => `
import { authenticateByWebAuthn } from "${url.resourcesPath}/js/webauthnAuthenticate.js";
const authButton = document.getElementById('${authButtonId}');
authButton.addEventListener("click", function() {
const input = {
isUserIdentified : ${isUserIdentified},
challenge : '${challenge}',
userVerification : '${userVerification}',
rpId : '${rpId}',
createTimeout : ${createTimeout},
errmsg : ${JSON.stringify(msgStr("webauthn-unsupported-browser-text"))}
};
authenticateByWebAuthn(input);
});
`
}
]
});
useEffect(() => {
if (isFetchingTranslations) {
return;
}
(async () => {
await waitForElementMountedOnDom({
elementId: authButtonId
});
insertScriptTags();
})();
}, [isFetchingTranslations]);
}src/login/pages/LoginPassword.useScript.tsx: import { useEffect } from "react";
import { useInsertScriptTags } from "keycloakify/tools/useInsertScriptTags";
import { assert } from "keycloakify/tools/assert";
import { KcContext } from "keycloakify/login/KcContext/KcContext";
import { waitForElementMountedOnDom } from "keycloakify/tools/waitForElementMountedOnDom";
type KcContextLike = {
url: {
resourcesPath: string;
};
isUserIdentified: "true" | "false" | boolean;
challenge: string;
userVerification: string | KcContext.WebauthnAuthenticate["userVerification"];
rpId: string;
createTimeout: number | string;
};
assert<keyof KcContextLike extends keyof KcContext.LoginPassword ? true : false>();
assert<KcContext.LoginPassword extends KcContextLike ? true : false>();
type I18nLike = {
msgStr: (key: "webauthn-unsupported-browser-text") => string;
isFetchingTranslations: boolean;
};
export function useScript(params: { authButtonId: string; kcContext: KcContextLike; i18n: I18nLike }) {
const { authButtonId, kcContext, i18n } = params;
const { url, isUserIdentified, challenge, userVerification, rpId, createTimeout } = kcContext;
const { msgStr, isFetchingTranslations } = i18n;
const { insertScriptTags } = useInsertScriptTags({
componentOrHookName: "LoginPassword",
scriptTags: [
{
type: "module",
textContent: () => `
import { authenticateByWebAuthn } from "${url.resourcesPath}/js/webauthnAuthenticate.js";
const authButton = document.getElementById('${authButtonId}');
authButton.addEventListener("click", function() {
const input = {
isUserIdentified : ${isUserIdentified},
challenge : '${challenge}',
userVerification : '${userVerification}',
rpId : '${rpId}',
createTimeout : ${createTimeout},
errmsg : ${JSON.stringify(msgStr("webauthn-unsupported-browser-text"))}
};
authenticateByWebAuthn(input);
});
`
}
]
});
useEffect(() => {
if (isFetchingTranslations) {
return;
}
(async () => {
await waitForElementMountedOnDom({
elementId: authButtonId
});
insertScriptTags();
})();
}, [isFetchingTranslations]);
}The key changes:
This now mirrors the |
|
@garronej thanks for your feedback. i just provided the changes based on your feedback: Key Changes
|
|
Thank you very much! If I may ask a last little thing, I made an error. And if I might ask, would you apply those changes here as well? https://github.com/keycloakify/login-ui Thank you in advance, I'm a bit under and you're saving me a lot of time with this! |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (2)
src/login/pages/LoginPassword.tsx (1)
114-143: Consider extracting shared WebAuthn UI block.The WebAuthn UI block (lines 114–143) is nearly identical to the one in
Login.tsx(lines 208–237). Extracting this into a shared component would eliminate ~30 lines of duplication and ensure consistency.Example structure:
// src/login/components/WebAuthnAuthenticator.tsx export function WebAuthnAuthenticator(props: { webAuthnButtonId: string; url: { loginAction: string }; authenticators?: { authenticators: Array<...> }; kcClsx: KcClsx; msgStr: (key: string) => string; }) { const { webAuthnButtonId, url, authenticators, kcClsx, msgStr } = props; return ( <> <form id="webauth" action={url.loginAction} method="post"> {/* hidden inputs */} </form> {/* authenticators form */} <input id={webAuthnButtonId} ... /> </> ); }Then use
<WebAuthnAuthenticator ... />in bothLogin.tsxandLoginPassword.tsx.src/login/pages/Login.useScript.tsx (1)
1-71: Consider extracting shared useScript logic.
Login.useScript.tsxandLoginPassword.useScript.tsxare nearly identical (~70 lines of duplication). The only difference is thecomponentOrHookNameparameter passed touseInsertScriptTags. Consider extracting the shared logic into a common hook:// src/login/pages/shared/useWebAuthnScript.tsx export function useWebAuthnScript(params: { webAuthnButtonId: string; kcContext: KcContextLike; i18n: I18nLike; componentOrHookName: string; }) { // ... current implementation } // Login.useScript.tsx export function useScript(params: { webAuthnButtonId: string; kcContext: KcContextLike; i18n: I18nLike }) { return useWebAuthnScript({ ...params, componentOrHookName: "Login" }); } // LoginPassword.useScript.tsx export function useScript(params: { webAuthnButtonId: string; kcContext: KcContextLike; i18n: I18nLike }) { return useWebAuthnScript({ ...params, componentOrHookName: "LoginPassword" }); }Note: This was previously discussed; awaiting maintainer decision.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
src/login/KcContext/KcContext.ts(2 hunks)src/login/pages/Login.tsx(4 hunks)src/login/pages/Login.useScript.tsx(1 hunks)src/login/pages/LoginPassword.tsx(4 hunks)src/login/pages/LoginPassword.useScript.tsx(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (5)
src/login/pages/Login.useScript.tsx (5)
src/login/KcContext/KcContext.ts (3)
KcContext(28-65)WebauthnAuthenticate(388-408)Login(175-218)src/login/pages/Login.tsx (1)
Login(16-240)src/login/pages/LoginPassword.useScript.tsx (1)
useScript(26-71)src/tools/useInsertScriptTags.ts (1)
useInsertScriptTags(32-120)src/tools/waitForElementMountedOnDom.ts (1)
waitForElementMountedOnDom(1-30)
src/login/pages/Login.tsx (1)
src/login/pages/Login.useScript.tsx (1)
useScript(26-71)
src/login/pages/LoginPassword.useScript.tsx (4)
src/login/KcContext/KcContext.ts (3)
KcContext(28-65)WebauthnAuthenticate(388-408)LoginPassword(361-386)src/login/pages/Login.useScript.tsx (1)
useScript(26-71)src/tools/useInsertScriptTags.ts (1)
useInsertScriptTags(32-120)src/tools/waitForElementMountedOnDom.ts (1)
waitForElementMountedOnDom(1-30)
src/login/pages/LoginPassword.tsx (1)
src/login/pages/LoginPassword.useScript.tsx (1)
useScript(26-71)
src/login/KcContext/KcContext.ts (1)
src/login/pages/WebauthnAuthenticate.tsx (1)
WebauthnAuthenticate(9-138)
🔇 Additional comments (3)
src/login/pages/Login.useScript.tsx (1)
1-71: LGTM! Implementation aligns with maintainer requirements.The hook correctly:
- Defines constrained types (
KcContextLike,I18nLike) with compile-time assertions ensuring compatibility withKcContext.Login- Uses direct value interpolation in the script template as requested by the maintainer to match the reference implementation
- Defers script insertion until translations are ready and the button is mounted
- Keeps the effect dependency array minimal (
isFetchingTranslationsonly), treatingkcContextfields as immutable per maintainer feedbacksrc/login/pages/LoginPassword.useScript.tsx (1)
1-71: LGTM! Correctly mirrors Login.useScript pattern.This hook properly adapts the WebAuthn script injection for the
LoginPasswordflow:
- Type assertions validate compatibility with
KcContext.LoginPasswordinstead ofLogin- Uses
"LoginPassword"as thecomponentOrHookNameto ensure proper script tracking- Follows the same implementation pattern as
Login.useScript.tsxper maintainer requirementssrc/login/KcContext/KcContext.ts (1)
208-217: LGTM! WebAuthn type additions are well-structured.The WebAuthn field additions to
Login(lines 208–217) andLoginPassword(lines 376–385) correctly:
- Mark core WebAuthn parameters (
challenge,userVerification,rpId,createTimeout,isUserIdentified) as required, reflecting that they're immutable and always present per maintainer guidance- Keep UI control flags (
enableWebAuthnConditionalUI,authenticators,shouldDisplayAuthenticators) optional to support conditional rendering- Reuse the
WebauthnAuthenticate["userVerification"]type for consistency across contexts- Align with the existing
WebauthnAuthenticatetype structureAlso applies to: 376-385
| {authenticators !== undefined && Object.keys(authenticators).length !== 0 && ( | ||
| <> | ||
| <form id="authn_select" className={kcClsx("kcFormClass")}> | ||
| {authenticators.authenticators.map((authenticator, i) => ( | ||
| <input key={i} type="hidden" name="authn_use_chk" readOnly value={authenticator.credentialId} /> | ||
| ))} | ||
| </form> | ||
| </> | ||
| )} |
There was a problem hiding this comment.
Correct the authenticators array check.
Same issue as in LoginPassword.tsx: the condition Object.keys(authenticators).length !== 0 does not correctly validate that the authenticators.authenticators array is non-empty. This will incorrectly render the form even when the array is empty.
Apply this diff:
- {authenticators !== undefined && Object.keys(authenticators).length !== 0 && (
+ {authenticators !== undefined && authenticators.authenticators.length !== 0 && (
<>
<form id="authn_select" className={kcClsx("kcFormClass")}>
{authenticators.authenticators.map((authenticator, i) => (
<input key={i} type="hidden" name="authn_use_chk" readOnly value={authenticator.credentialId} />
))}
</form>
</>
)}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| {authenticators !== undefined && Object.keys(authenticators).length !== 0 && ( | |
| <> | |
| <form id="authn_select" className={kcClsx("kcFormClass")}> | |
| {authenticators.authenticators.map((authenticator, i) => ( | |
| <input key={i} type="hidden" name="authn_use_chk" readOnly value={authenticator.credentialId} /> | |
| ))} | |
| </form> | |
| </> | |
| )} | |
| {authenticators !== undefined && authenticators.authenticators.length !== 0 && ( | |
| <> | |
| <form id="authn_select" className={kcClsx("kcFormClass")}> | |
| {authenticators.authenticators.map((authenticator, i) => ( | |
| <input key={i} type="hidden" name="authn_use_chk" readOnly value={authenticator.credentialId} /> | |
| ))} | |
| </form> | |
| </> | |
| )} |
🤖 Prompt for AI Agents
In src/login/pages/Login.tsx around lines 219 to 227, the current conditional
uses Object.keys(authenticators).length !== 0 which does not verify that the
authenticators.authenticators array is present or non-empty; update the
condition to specifically check that authenticators.authenticators is an array
with length > 0 (e.g., Array.isArray(authenticators.authenticators) &&
authenticators.authenticators.length > 0 or
authenticators?.authenticators?.length > 0) so the form only renders when the
authenticators array actually contains items.
|
@garronej done ;) |
|
Thank you @discostur, Beleve me when I say that I appreciate that very much. But my god, again, this is not you, it's me but coderabbit last suggestion is fully valid. It's write about this it's Would you fix that (including in my implementation) after that I swear I'll merge and add you to the contributor list. Sorry again it's no fault of your own, my mistrakes just surface through your PR... |
|
@garronej of course - changes are implemented ;) |
|
Thank you @discostur! @allcontributors add @discostur for code |
|
I've put up a pull request to add @discostur! 🎉 |
Add Passkey Integration to Login and Login-Password Forms
Completes passkey integration for Keycloakify, addressing issues #918 and #69.
What's Changed:
Implementation:
Regards,
Kilian
Summary by CodeRabbit
New Features
Tests
✏️ Tip: You can customize this high-level summary in your review settings.