๐๏ธStyling a Custom Page Not Included In Base Keycloak
Sometimes certain extensions will add new functionality that requires an additional page not originally shipped with Keycloak. Keycloakify out-of-the-box will only provide customization to base pages, so if a new page is introduced by an extension, there is a good chance the page will not be styled correctly.
To account for these cases, Keycloakify supports the ability to add custom pages and configure them such that style preservation is maintained.
For our example on how to customize this, we will be using Phase Two's otp-form.ftl page. Phase Two provides email OTP codes for logging in and as a result has a special page if OTP codes are enabled in the authorization flow.
You can load the extention that you are using in Keycloak container that is started when running npx keycloakify start-keycloak. Use the extensionJars option.
The first thing we will do is create the page under the pages directory, our file name in this case will be OtpForm.tsx and paste in some starter code including the template.
Note the pageId variable specified otp-form.ftl, that should match the exact name of the page file you are trying to implement. Additionally, we will also need to modify the kcContext values to account for certain custom variables, but we will get to that later. For now the last new file we need to add would be the story file for this page:
Next the easiest thing is to just paste the default code for the custom page right into the template and begin modifying it for Keycloakify. In our case, here is the code for that page at the time of writing:
The freemarker, dynamic variables/messages, and classnames will need to be converted to React.
The content in the header section will go in the headerNode prop of <Template> and the form section will be the child of the <Template> element.
@layout.registrationLayout has the prop displayInfo=true which means we need to set that prop in the <Template> element.
The auth and messagesPerField variables and their attributes which need to be provided in kcContext.
1, 2, and 3 require converting code to JSX. The converted code for the page can be found at the bottom. Here are some tips:
Any classname provided as a variable will use kcClsx to resolve, so ${properties.kcFormClass!} would turn into {kcClsx("kcFormGroupClass")}
When dealing with message values, msg may return full blown HTML so it can be used as a child element and msgStr will return straight text.
Example 1, aria-label="${msg("restartLoginTooltip")}" would turn into aria-label={msgStr("restartLoginTooltip")}.
Example 2, msg variables they can inject HTML as a variable, when this happens we need to dangerously set inner html. Specifcally with a piece of code like this:
Unfortunately, a lot of it is up to you to decide with the extension you might be using, but there may be some trial and error.
4 on the other hand requires changing some code in other files.
src/login/KcContext.ts
/* eslint-disable @typescript-eslint/ban-types */importtype { ExtendKcContext } from"keycloakify/login";importtype { KcEnvName, ThemeName } from"../kc.gen";exporttypeKcContextExtension= { themeName:ThemeName; properties:Record<KcEnvName,string> & {};};// added for otp form page, required for the typesexporttypeKcContextExtensionPerPage= {"otp-form.ftl": { auth: { attemptedUsername:string; }; url: { loginRestartFlowUrl:string; loginAction:string; }; };};exporttypeKcContext=ExtendKcContext<KcContextExtension,KcContextExtensionPerPage>;
As seen above, kcContext is where we can add the type definitions for the props passed into the page. In the freemarker we also see msg("doResend") value which is not in the base keycloak i18 library. We would also need add this for mocking purposes.
After all that you should be done! You can view the new component in storybook and check everything looks right and then the next time you bundle and build it, it should be deployed.