Merge pull request #52 from InseeFrLab/main

Update fork
This commit is contained in:
Bystrova Ann 2022-07-02 10:39:39 +03:00 committed by GitHub
commit 6c14758e33
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 774 additions and 347 deletions

View File

@ -14,7 +14,7 @@ jobs:
steps:
- uses: actions/checkout@v2.3.4
- uses: actions/setup-node@v2.1.3
- uses: bahmutov/npm-install@v1
- uses: bahmutov/npm-install@v1.8.15
- name: If this step fails run 'yarn format' then commit again.
run: |
PACKAGE_MANAGER=npm
@ -41,7 +41,7 @@ jobs:
- uses: actions/setup-node@v2.1.3
with:
node-version: ${{ matrix.node }}
- uses: bahmutov/npm-install@v1
- uses: bahmutov/npm-install@v1.8.15
- if: steps.step1.outputs.npm_or_yarn == 'yarn'
run: |
yarn build
@ -123,7 +123,7 @@ jobs:
with:
node-version: '15'
registry-url: https://registry.npmjs.org/
- uses: bahmutov/npm-install@v1
- uses: bahmutov/npm-install@v1.8.15
- run: |
PACKAGE_MANAGER=npm
if [ -f "./yarn.lock" ]; then

View File

@ -1,3 +1,22 @@
## **5.6.0** (2022-06-28)
- Merge pull request #127 from aidangilmore/add-totp-support
feat: add login-config-totp.ftl page
- Fix unknown algorithm name lookup in LoginConfigTotp
- Add totp config support
## **5.5.0** (2022-06-28)
- Make it possible to redirect to login by repacing the url (should be default in most case)
### **5.4.7** (2022-06-19)
- #121
- fmt
- Create CONTRIBUTING.md
- Enable users to link keycloak in their own app
### **5.4.6** (2022-06-16)
- Use keycloak 18.0.1 i18n resources #120

View File

@ -38,6 +38,10 @@
# Changelog highlights
## v5.6.0
Add support for `login-config-totp.ftl` page [#127](https://github.com/InseeFrLab/keycloakify/pull/127).
## v5.3.0
Rename `keycloak_theme_email` to `keycloak_email`.

View File

@ -1,6 +1,6 @@
{
"name": "keycloakify",
"version": "5.4.6",
"version": "5.6.0",
"description": "Keycloak theme generator for Reacts app",
"repository": {
"type": "git",
@ -63,13 +63,13 @@
"@emotion/react": "^11.4.1",
"@types/memoizee": "^0.4.7",
"@types/node": "^17.0.25",
"@types/react": "^17.0.0",
"@types/react": "18.0.9",
"copyfiles": "^2.4.1",
"husky": "^4.3.8",
"lint-staged": "^11.0.0",
"prettier": "^2.3.0",
"properties-parser": "^0.3.1",
"react": "^17.0.1",
"react": "18.1.0",
"rimraf": "^3.0.2",
"typescript": "^4.2.3"
},

View File

@ -155,33 +155,6 @@ ${ftl_object_to_js_code_declaring_an_object(.data_model, [])?no_esc};
</#if>
<#if key == "showUsername" && are_same_path(path, ["auth"])>
<#attempt>
<#return auth.showUsername()?c>
<#recover>
<#local out_seq += ["/*Couldn't evaluate auth.showUsername()*/"]>
<#continue>
</#attempt>
</#if>
<#if key == "showResetCredentials" && are_same_path(path, ["auth"])>
<#attempt>
<#return auth.showResetCredentials()?c>
<#recover>
<#local out_seq += ["/*Couldn't evaluate auth.showResetCredentials()*/"]>
<#continue>
</#attempt>
</#if>
<#if key == "showTryAnotherWayLink" && are_same_path(path, ["auth"])>
<#attempt>
<#return auth.showTryAnotherWayLink()?c>
<#recover>
<#local out_seq += ["/*Couldn't evaluate auth.showTryAnotherWayLink()*/"]>
<#continue>
</#attempt>
</#if>
<#attempt>
<#if !object[key]??>
<#continue>
@ -229,6 +202,31 @@ ${ftl_object_to_js_code_declaring_an_object(.data_model, [])?no_esc};
</#attempt>
<#if isMethod>
<#if are_same_path(path, ["auth", "showUsername"])>
<#attempt>
<#return auth.showUsername()?c>
<#recover>
<#return "ABORT: Couldn't evaluate auth.showUsername()">
</#attempt>
</#if>
<#if are_same_path(path, ["auth", "showResetCredentials"])>
<#attempt>
<#return auth.showResetCredentials()?c>
<#recover>
<#return "ABORT: Couldn't evaluate auth.showResetCredentials()">
</#attempt>
</#if>
<#if are_same_path(path, ["auth", "showTryAnotherWayLink"])>
<#attempt>
<#return auth.showTryAnotherWayLink()?c>
<#recover>
<#return "ABORT: Couldn't evaluate auth.showTryAnotherWayLink()">
</#attempt>
</#if>
<#return "ABORT: It's a method">
</#if>

View File

@ -20,6 +20,7 @@ export const pageIds = [
"login-idp-link-confirm.ftl",
"login-idp-link-email.ftl",
"login-page-expired.ftl",
"login-config-totp.ftl",
] as const;
export type PageId = typeof pageIds[number];

View File

@ -15,6 +15,7 @@ import { LoginUpdateProfile } from "./LoginUpdateProfile";
import { LoginIdpLinkConfirm } from "./LoginIdpLinkConfirm";
import { LoginPageExpired } from "./LoginPageExpired";
import { LoginIdpLinkEmail } from "./LoginIdpLinkEmail";
import { LoginConfigTotp } from "./LoginConfigTotp";
export const KcApp = memo(({ kcContext, ...props }: { kcContext: KcContextBase } & KcProps) => {
switch (kcContext.pageId) {
@ -46,5 +47,7 @@ export const KcApp = memo(({ kcContext, ...props }: { kcContext: KcContextBase }
return <LoginIdpLinkEmail {...{ kcContext, ...props }} />;
case "login-page-expired.ftl":
return <LoginPageExpired {...{ kcContext, ...props }} />;
case "login-config-totp.ftl":
return <LoginConfigTotp {...{ kcContext, ...props }} />;
}
});

View File

@ -0,0 +1,183 @@
import { memo } from "react";
import { Template } from "./Template";
import type { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase";
import { getMsg } from "../i18n";
import { useCssAndCx } from "tss-react";
export const LoginConfigTotp = memo(({ kcContext, ...props }: { kcContext: KcContextBase.LoginConfigTotp } & KcProps) => {
const { url, isAppInitiatedAction, totp, mode, messagesPerField } = kcContext;
const { cx } = useCssAndCx();
const { msg, msgStr } = getMsg(kcContext);
const algToKeyUriAlg: Record<KcContextBase.LoginConfigTotp["totp"]["policy"]["algorithm"], string> = {
HmacSHA1: "SHA1",
HmacSHA256: "SHA256",
HmacSHA512: "SHA512",
};
return (
<Template
{...{ kcContext, ...props }}
doFetchDefaultThemeResources={true}
headerNode={msg("loginTotpTitle")}
formNode={
<>
<ol id="kc-totp-settings">
<li>
<p>{msg("loginTotpStep1")}</p>
<ul id="kc-totp-supported-apps">
{totp.policy.supportedApplications.map(app => (
<li>{app}</li>
))}
</ul>
</li>
{mode && mode == "manual" ? (
<>
<li>
<p>{msg("loginTotpManualStep2")}</p>
<p>
<span id="kc-totp-secret-key">{totp.totpSecretEncoded}</span>
</p>
<p>
<a href={totp.qrUrl} id="mode-barcode">
{msg("loginTotpScanBarcode")}
</a>
</p>
</li>
<li>
<p>{msg("loginTotpManualStep3")}</p>
<p>
<ul>
<li id="kc-totp-type">
{msg("loginTotpType")}: {msg(`loginTotp.${totp.policy.type}`)}
</li>
<li id="kc-totp-algorithm">
{msg("loginTotpAlgorithm")}: {algToKeyUriAlg?.[totp.policy.algorithm] ?? totp.policy.algorithm}
</li>
<li id="kc-totp-digits">
{msg("loginTotpDigits")}: {totp.policy.digits}
</li>
{totp.policy.type === "totp" ? (
<li id="kc-totp-period">
{msg("loginTotpInterval")}: {totp.policy.period}
</li>
) : (
<li id="kc-totp-counter">
{msg("loginTotpCounter")}: {totp.policy.initialCounter}
</li>
)}
</ul>
</p>
</li>
</>
) : (
<li>
<p>{msg("loginTotpStep2")}</p>
<img id="kc-totp-secret-qr-code" src={`data:image/png;base64, ${totp.totpSecretQrCode}`} alt="Figure: Barcode" />
<br />
<p>
<a href={totp.manualUrl} id="mode-manual">
{msg("loginTotpUnableToScan")}
</a>
</p>
</li>
)}
<li>
<p>{msg("loginTotpStep3")}</p>
<p>{msg("loginTotpStep3DeviceName")}</p>
</li>
</ol>
<form action={url.loginAction} className={cx(props.kcFormClass)} id="kc-totp-settings-form" method="post">
<div className={cx(props.kcFormGroupClass)}>
<div className={cx(props.kcInputWrapperClass)}>
<label htmlFor="totp" className={cx(props.kcLabelClass)}>
{msg("authenticatorCode")}
</label>{" "}
<span className="required">*</span>
</div>
<div className={cx(props.kcInputWrapperClass)}>
<input
type="text"
id="totp"
name="totp"
autoComplete="off"
className={cx(props.kcInputClass)}
aria-invalid={messagesPerField.existsError("totp")}
/>
{messagesPerField.existsError("totp") && (
<span id="input-error-otp-code" className={cx(props.kcInputErrorMessageClass)} aria-live="polite">
{messagesPerField.get("totp")}
</span>
)}
</div>
<input type="hidden" id="totpSecret" name="totpSecret" value={totp.totpSecret} />
{mode && <input type="hidden" id="mode" value={mode} />}
</div>
<div className={cx(props.kcFormGroupClass)}>
<div className={cx(props.kcInputWrapperClass)}>
<label htmlFor="userLabel" className={cx(props.kcLabelClass)}>
{msg("loginTotpDeviceName")}
</label>{" "}
{totp.otpCredentials.length >= 1 && <span className="required">*</span>}
</div>
<div className={cx(props.kcInputWrapperClass)}>
<input
type="text"
id="userLabel"
name="userLabel"
autoComplete="off"
className={cx(props.kcInputClass)}
aria-invalid={messagesPerField.existsError("userLabel")}
/>
{messagesPerField.existsError("userLabel") && (
<span id="input-error-otp-label" className={cx(props.kcInputErrorMessageClass)} aria-live="polite">
{messagesPerField.get("userLabel")}
</span>
)}
</div>
</div>
{isAppInitiatedAction ? (
<>
<input
type="submit"
className={cx(props.kcButtonClass, props.kcButtonPrimaryClass, props.kcButtonLargeClass)}
id="saveTOTPBtn"
value={msgStr("doSubmit")}
/>
<button
type="submit"
className={cx(
props.kcButtonClass,
props.kcButtonDefaultClass,
props.kcButtonLargeClass,
props.kcButtonLargeClass,
)}
id="cancelTOTPBtn"
name="cancel-aia"
value="true"
>
${msg("doCancel")}
</button>
</>
) : (
<input
type="submit"
className={cx(props.kcButtonClass, props.kcButtonPrimaryClass, props.kcButtonLargeClass)}
id="saveTOTPBtn"
value={msgStr("doSubmit")}
/>
)}
</form>
</>
}
/>
);
});

View File

@ -24,7 +24,8 @@ export type KcContextBase =
| KcContextBase.LoginUpdateProfile
| KcContextBase.LoginIdpLinkConfirm
| KcContextBase.LoginIdpLinkEmail
| KcContextBase.LoginPageExpired;
| KcContextBase.LoginPageExpired
| KcContextBase.LoginConfigTotp;
export declare namespace KcContextBase {
export type Common = {
@ -223,6 +224,34 @@ export declare namespace KcContextBase {
export type LoginPageExpired = Common & {
pageId: "login-page-expired.ftl";
};
export type LoginConfigTotp = Common & {
pageId: "login-config-totp.ftl";
mode?: "qr" | "manual" | undefined | null;
totp: {
totpSecretEncoded: string;
qrUrl: string;
policy: {
supportedApplications: string[];
algorithm: "HmacSHA1" | "HmacSHA256" | "HmacSHA512";
digits: number;
lookAheadWindow: number;
} & (
| {
type: "totp";
period: number;
}
| {
type: "hotp";
initialCounter: number;
}
);
totpSecretQrCode: string;
manualUrl: string;
totpSecret: string;
otpCredentials: { id: string; userLabel: string }[];
};
};
}
export type Attribute = {

View File

@ -387,4 +387,25 @@ export const kcContextMocks: KcContextBase[] = [
"username": "anUsername",
},
}),
id<KcContextBase.LoginConfigTotp>({
...kcContextCommonMock,
"pageId": "login-config-totp.ftl",
totp: {
totpSecretEncoded: "KVVF G2BY N4YX S6LB IUYT K2LH IFYE 4SBV",
qrUrl: "#",
totpSecretQrCode:
"iVBORw0KGgoAAAANSUhEUgAAAPYAAAD2AQAAAADNaUdlAAACM0lEQVR4Xu3OIZJgOQwDUDFd2UxiurLAVnnbHw4YGDKtSiWOn4Gxf81//7r/+q8b4HfLGBZDK9d85NmNR+sB42sXvOYrN5P1DcgYYFTGfOlbzE8gzwy3euweGizw7cfdl34/GRhlkxjKNV+5AebPXPORX1JuB9x8ZfbyyD2y1krWAKsbMq1HnqQDaLfa77p4+MqvzEGSqvSAD/2IHW2yHaigR9tX3m8dDIYGcNf3f+gDpVBZbZU77zyJ6Rlcy+qoTMG887KAPD9hsh6a1Sv3gJUHGHUAxSMzj7zqDDe7Phmt2eG+8UsMxjRGm816MAO+8VMl1R1jGHOrZB/5Zo/WXAPgxixm9Mo96vDGrM1eOto8c4Ax4wF437mifOXlpiPzCnN7Y9l95NnEMxgMY9AAGA8fucH14Y1aVb6N/cqrmyh0BVht7k1e+bU8LK0Cg5vmVq9c5vHIjOfqxDIfeTraNVTwewa4wVe+SW5N+uP1qACeudUZbqGOfA6VZV750Noq2Xx3kpveV44ZelSV1V7KFHzkWyVrrlUwG0Pl9pWnoy3vsQoME6vKI69i5osVgwWzHT7zjmJtMcNUSVn1oYMd7ZodbgowZl45VG0uVuLPUr1yc79uaQBag/mqR34xhlWyHm1prplHboCWdZ4TeZjsK8+dI+jbz1C5hl65mcpgB5dhcj8+dGO+0Ko68+lD37JDD83dpDLzzK+TrQyaVwGj6pUboGV+7+AyN8An/pf84/7rv/4/1l4OCc/1BYMAAAAASUVORK5CYII=",
manualUrl: "#",
totpSecret: "G4nsI8lQagRMUchH8jEG",
otpCredentials: [],
policy: {
supportedApplications: ["FreeOTP", "Google Authenticator"],
algorithm: "HmacSHA1",
digits: 6,
lookAheadWindow: 1,
type: "totp",
period: 30,
},
},
}),
];

View File

@ -44,8 +44,9 @@ export declare namespace keycloak_js {
export function createKeycloakAdapter(params: {
keycloakInstance: keycloak_js.KeycloakInstance;
transformUrlBeforeRedirect(url: string): string;
getRedirectMethod?: () => "overwrite location.href" | "location.replace";
}): keycloak_js.KeycloakAdapter {
const { keycloakInstance, transformUrlBeforeRedirect } = params;
const { keycloakInstance, transformUrlBeforeRedirect, getRedirectMethod = () => "overwrite location.href" } = params;
const neverResolvingPromise: keycloak_js.KeycloakPromise<void, void> = Object.defineProperties(new Promise(() => {}), {
"success": { "value": () => {} },
@ -54,25 +55,50 @@ export function createKeycloakAdapter(params: {
return {
"login": options => {
window.location.href = transformUrlBeforeRedirect(keycloakInstance.createLoginUrl(options));
const newHref = transformUrlBeforeRedirect(keycloakInstance.createLoginUrl(options));
switch (getRedirectMethod()) {
case "location.replace":
window.location.replace(newHref);
break;
case "overwrite location.href":
window.location.href = newHref;
break;
}
return neverResolvingPromise;
},
"register": options => {
const newHref = transformUrlBeforeRedirect(keycloakInstance.createRegisterUrl(options));
switch (getRedirectMethod()) {
case "location.replace":
window.location.replace(newHref);
break;
case "overwrite location.href":
window.location.href = newHref;
break;
}
return neverResolvingPromise;
},
"logout": options => {
window.location.replace(transformUrlBeforeRedirect(keycloakInstance.createLogoutUrl(options)));
return neverResolvingPromise;
},
"register": options => {
window.location.href = transformUrlBeforeRedirect(keycloakInstance.createRegisterUrl(options));
return neverResolvingPromise;
},
"accountManagement": () => {
var accountUrl = transformUrlBeforeRedirect(keycloakInstance.createAccountUrl());
if (typeof accountUrl !== "undefined") {
window.location.href = accountUrl;
} else {
const accountUrl = transformUrlBeforeRedirect(keycloakInstance.createAccountUrl());
if (accountUrl === "undefined") {
throw new Error("Not supported by the OIDC server");
}
switch (getRedirectMethod()) {
case "location.replace":
window.location.replace(accountUrl);
break;
case "overwrite location.href":
window.location.href = accountUrl;
break;
}
return neverResolvingPromise;
},
"redirectUri": options => {

747
yarn.lock

File diff suppressed because it is too large Load Diff