Realm settings(tokens): Add tokens tab (#934)

* wip tokens tab

* tokens

* add data-testids to cypress doc

* uncomment

* uncomment

* finish tests

* remove logs

* fix help text

* fix form panel title for sessions

* removed unused useEffect

* wip tokens updates to convert function

* fix flattened attributes to match call

* interpolate timespan

* delete unused variables
This commit is contained in:
Jenny 2021-08-09 15:28:24 -04:00 committed by GitHub
parent c7e72abfc1
commit 45e07266eb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 905 additions and 8 deletions

View file

@ -329,4 +329,49 @@ describe("Realm settings", () => {
10 10
); );
}); });
it("add token data", () => {
sidebarPage.goToRealmSettings();
cy.getId("rs-tokens-tab").click();
realmSettingsPage.populateTokensPage();
realmSettingsPage.save("tokens-tab-save");
masthead.checkNotificationMessage("Realm successfully updated");
});
it("check that token data was saved", () => {
sidebarPage.goToRealmSettings();
cy.getId("rs-tokens-tab").click();
cy.getId(realmSettingsPage.accessTokenLifespanInput).should(
"have.value",
1
);
cy.getId(realmSettingsPage.accessTokenLifespanImplicitInput).should(
"have.value",
2
);
cy.getId(realmSettingsPage.clientLoginTimeoutInput).should("have.value", 3);
cy.getId(realmSettingsPage.userInitiatedActionLifespanInput).should(
"have.value",
4
);
cy.getId(realmSettingsPage.defaultAdminInitatedInput).should(
"have.value",
5
);
cy.getId(realmSettingsPage.emailVerificationInput).should("have.value", 6);
cy.getId(realmSettingsPage.idpEmailVerificationInput).should(
"have.value",
7
);
cy.getId(realmSettingsPage.forgotPasswordInput).should("have.value", 8);
cy.getId(realmSettingsPage.executeActionsInput).should("have.value", 9);
});
}); });

View file

@ -87,6 +87,59 @@ export default class RealmSettingsPage {
offlineSessionMaxSwitch = "offline-session-max-switch"; offlineSessionMaxSwitch = "offline-session-max-switch";
loginTimeoutInput = "login-timeout-input"; loginTimeoutInput = "login-timeout-input";
loginActionTimeoutInput = "login-action-timeout-input"; loginActionTimeoutInput = "login-action-timeout-input";
selectDefaultSignatureAlgorithm = "#kc-default-sig-alg";
revokeRefreshTokenSwitch = "revoke-refresh-token-switch";
accessTokenLifespanInput = "access-token-lifespan-input";
accessTokenLifespanImplicitInput = "access-token-lifespan-implicit-input";
clientLoginTimeoutInput = "client-login-timeout-input";
offlineSessionMaxInput = "offline-session-max-input";
userInitiatedActionLifespanInput = "user-initiated-action-lifespan";
defaultAdminInitatedInput = "default-admin-initated-input";
emailVerificationInput = "email-verification-input";
idpEmailVerificationInput = "idp-email-verification-input";
forgotPasswordInput = "forgot-pw-input";
executeActionsInput = "execute-actions-input";
accessTokenLifespanSelectMenu = "#kc-access-token-lifespan-select-menu";
accessTokenLifespanSelectMenuList =
"#kc-access-token-lifespan-select-menu > div > ul";
accessTokenLifespanImplicitSelectMenu =
"#kc-access-token-lifespan-implicit-select-menu";
accessTokenLifespanImplicitSelectMenuList =
"#kc-access-token-lifespan-implicit-select-menu > div > ul";
clientLoginTimeoutSelectMenu = "#kc-client-login-timeout-select-menu";
clientLoginTimeoutSelectMenuList =
"#kc-client-login-timeout-select-menu > div > ul";
offlineSessionMaxSelectMenu = "#kc-offline-session-max-select-menu";
offlineSessionMaxSelectMenuList =
"#kc-offline-session-max-select-menu > div > ul";
userInitiatedActionLifespanSelectMenu =
"#kc-user-initiated-action-lifespan-select-menu";
userInitiatedActionLifespanSelectMenuList =
"#kc-user-initiated-action-lifespan-select-menu > div > ul";
defaultAdminInitatedInputSelectMenu =
"#kc-default-admin-initiated-select-menu";
defaultAdminInitatedInputSelectMenuList =
"#kc-default-admin-initiated-select-menu";
emailVerificationSelectMenu = "#kc-email-verification-select-menu";
emailVerificationSelectMenuList =
"#kc-email-verification-select-menu > div > ul";
idpEmailVerificationSelectMenu = "#kc-idp-email-verification-select-menu";
idpEmailVerificationSelectMenuList =
"#kc-idp-email-verification-select-menu > div > ul";
forgotPasswordSelectMenu = "#kc-forgot-pw-select-menu";
forgotPasswordSelectMenuList = "#kc-forgot-pw-select-menu > div > ul";
executeActionsSelectMenu = "#kc-execute-actions-select-menu";
executeActionsSelectMenuList = "#kc-execute-actions-select-menu > div > ul";
selectLoginThemeType(themeType: string) { selectLoginThemeType(themeType: string) {
cy.get(this.selectLoginTheme).click(); cy.get(this.selectLoginTheme).click();
@ -314,6 +367,76 @@ export default class RealmSettingsPage {
); );
} }
populateTokensPage() {
this.toggleSwitch(this.revokeRefreshTokenSwitch);
cy.getId(this.accessTokenLifespanInput)
.focus()
.clear({ force: true })
.getId(this.accessTokenLifespanInput)
.clear()
.type("1");
this.changeTimeUnit(
"Days",
this.accessTokenLifespanSelectMenu,
this.accessTokenLifespanSelectMenuList
);
cy.getId(this.accessTokenLifespanImplicitInput).clear().type("2");
this.changeTimeUnit(
"Minutes",
this.accessTokenLifespanImplicitSelectMenu,
this.accessTokenLifespanImplicitSelectMenuList
);
cy.getId(this.clientLoginTimeoutInput).clear().type("3");
this.changeTimeUnit(
"Hours",
this.clientLoginTimeoutSelectMenu,
this.clientLoginTimeoutSelectMenuList
);
cy.getId(this.userInitiatedActionLifespanInput).clear().type("4");
this.changeTimeUnit(
"Minutes",
this.userInitiatedActionLifespanSelectMenu,
this.userInitiatedActionLifespanSelectMenuList
);
cy.getId(this.defaultAdminInitatedInput).clear().type("5");
this.changeTimeUnit(
"Days",
this.defaultAdminInitatedInputSelectMenu,
this.defaultAdminInitatedInputSelectMenuList
);
cy.getId(this.emailVerificationInput).clear().type("6");
this.changeTimeUnit(
"Days",
this.emailVerificationSelectMenu,
this.emailVerificationSelectMenuList
);
cy.getId(this.idpEmailVerificationInput).clear().type("7");
this.changeTimeUnit(
"Days",
this.idpEmailVerificationSelectMenu,
this.idpEmailVerificationSelectMenuList
);
cy.getId(this.forgotPasswordInput).clear().type("8");
this.changeTimeUnit(
"Days",
this.forgotPasswordSelectMenu,
this.forgotPasswordSelectMenuList
);
cy.getId(this.executeActionsInput).clear().type("9");
this.changeTimeUnit(
"Days",
this.executeActionsSelectMenu,
this.executeActionsSelectMenuList
);
}
checkUserEvents(events: string[]) { checkUserEvents(events: string[]) {
cy.get(this.eventTypeColumn).should((event) => { cy.get(this.eventTypeColumn).should((event) => {
for (const user of events) { for (const user of events) {

View file

@ -107,6 +107,7 @@ export default {
minutes: "Minutes", minutes: "Minutes",
hours: "Hours", hours: "Hours",
days: "Days", days: "Days",
years: "Years",
}, },
attributes: "Attributes", attributes: "Attributes",

View file

@ -120,7 +120,17 @@ div#offline-session-max-label > .pf-c-form__group-label {
.kc-client-session-idle-input, .kc-client-session-idle-input,
.kc-client-session-max-input, .kc-client-session-max-input,
.kc-login-timeout-input, .kc-login-timeout-input,
.kc-login-action-timeout-input { .kc-login-action-timeout-input,
.kc-access-token-lifespan-input,
.kc-user-initiated-action-lifespan-input,
.kc-default-admin-initiated-input,
.kc-access-token-lifespan-implicit-input,
.kc-client-login-timeout-input,
.kc-email-verification-input,
.kc-idp-email-verification-input,
.kc-idp-forgot-password-input,
.kc-forgot-pw-input,
.kc-execute-actions-input {
width: 170px; width: 170px;
margin-right: 12px; margin-right: 12px;
} }
@ -157,3 +167,8 @@ article.pf-c-card.pf-m-flat.kc-login-settings-template
> .pf-c-card__body.kc-form-panel__body { > .pf-c-card__body.kc-form-panel__body {
padding: 0px; padding: 0px;
} }
.kc-override-action-tokens-subtitle {
font-size: var(--pf-global--FontSize--md);
font-weight: bold;
}

View file

@ -37,6 +37,7 @@ import { PartialImportDialog } from "./PartialImport";
import { SecurityDefences } from "./security-defences/SecurityDefences"; import { SecurityDefences } from "./security-defences/SecurityDefences";
import { RealmSettingsSessionsTab } from "./SessionsTab"; import { RealmSettingsSessionsTab } from "./SessionsTab";
import { RealmSettingsThemesTab } from "./ThemesTab"; import { RealmSettingsThemesTab } from "./ThemesTab";
import { RealmSettingsTokensTab } from "./TokensTab";
type RealmSettingsHeaderProps = { type RealmSettingsHeaderProps = {
onChange: (value: boolean) => void; onChange: (value: boolean) => void;
@ -227,7 +228,7 @@ export const RealmSettingsSection = () => {
> >
<RealmSettingsGeneralTab <RealmSettingsGeneralTab
save={save} save={save}
reset={() => resetForm(realm!)} reset={() => resetForm(realm)}
/> />
</Tab> </Tab>
<Tab <Tab
@ -256,7 +257,7 @@ export const RealmSettingsSection = () => {
> >
<RealmSettingsThemesTab <RealmSettingsThemesTab
save={save} save={save}
reset={() => resetForm(realm!)} reset={() => resetForm(realm)}
realm={realm!} realm={realm!}
/> />
</Tab> </Tab>
@ -341,6 +342,15 @@ export const RealmSettingsSection = () => {
> >
<RealmSettingsSessionsTab key={key} realm={realm} /> <RealmSettingsSessionsTab key={key} realm={realm} />
</Tab> </Tab>
<Tab
id="tokens"
eventKey="tokens"
data-testid="rs-tokens-tab"
aria-label="tokens-tab"
title={<TabTitleText>{t("realm-settings:tokens")}</TabTitleText>}
>
<RealmSettingsTokensTab reset={() => resetForm(realm)} />
</Tab>
</KeycloakTabs> </KeycloakTabs>
</FormProvider> </FormProvider>
</PageSection> </PageSection>

View file

@ -199,7 +199,7 @@ export const RealmSettingsSessionsTab = ({
</FormAccess> </FormAccess>
</FormPanel> </FormPanel>
<FormPanel <FormPanel
title={t(".pf-c-data-list__item-draggable-iconclientSessionSettings")} title={t("clientSessionSettings")}
className="kc-client-session-template" className="kc-client-session-template"
> >
<FormAccess <FormAccess
@ -331,7 +331,7 @@ export const RealmSettingsSessionsTab = ({
label={t("common:enabled")} label={t("common:enabled")}
labelOff={t("common:disabled")} labelOff={t("common:disabled")}
isChecked={value} isChecked={value}
onChange={onChange} onChange={(value) => onChange(value.toString())}
/> />
)} )}
/> />

View file

@ -0,0 +1,569 @@
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { Controller, useForm, useFormContext, useWatch } from "react-hook-form";
import {
ActionGroup,
AlertVariant,
Button,
FormGroup,
NumberInput,
PageSection,
Select,
SelectOption,
SelectVariant,
Switch,
Text,
TextVariants,
} from "@patternfly/react-core";
import type RealmRepresentation from "keycloak-admin/lib/defs/realmRepresentation";
import { FormAccess } from "../components/form-access/FormAccess";
import { HelpItem } from "../components/help-enabler/HelpItem";
import { FormPanel } from "../components/scroll-form/FormPanel";
import { useAdminClient, useFetch } from "../context/auth/AdminClient";
import { useAlerts } from "../components/alert/Alerts";
import { useRealm } from "../context/realm-context/RealmContext";
import "./RealmSettingsSection.css";
import type UserRepresentation from "keycloak-admin/lib/defs/userRepresentation";
import { TimeSelector } from "../components/time-selector/TimeSelector";
import { useServerInfo } from "../context/server-info/ServerInfoProvider";
import {
convertToFormValues,
forHumans,
flatten,
convertFormValuesToObject,
interpolateTimespan,
} from "../util";
type RealmSettingsSessionsTabProps = {
realm?: RealmRepresentation;
user?: UserRepresentation;
reset?: () => void;
};
export const RealmSettingsTokensTab = ({
realm: initialRealm,
reset,
}: RealmSettingsSessionsTabProps) => {
const { t } = useTranslation("realm-settings");
const adminClient = useAdminClient();
const { realm: realmName } = useRealm();
const { addAlert, addError } = useAlerts();
const serverInfo = useServerInfo();
const [realm, setRealm] = useState(initialRealm);
const [defaultSigAlgDrpdwnIsOpen, setDefaultSigAlgDrpdwnOpen] =
useState(false);
const allComponentTypes =
serverInfo.componentTypes?.["org.keycloak.keys.KeyProvider"] ?? [];
const esOptions = ["ES256", "ES384", "ES512"];
const hmacAlgorithmOptions = allComponentTypes[2].properties[4].options;
const javaKeystoreAlgOptions = allComponentTypes[3].properties[3].options;
const defaultSigAlgOptions = esOptions.concat(
hmacAlgorithmOptions!,
javaKeystoreAlgOptions!
);
const form = useForm<RealmRepresentation>();
const { control } = useFormContext();
const offlineSessionMaxEnabled = useWatch({
control,
name: "offlineSessionMaxLifespanEnabled",
defaultValue: realm?.offlineSessionMaxLifespanEnabled,
});
const setupForm = (realm: RealmRepresentation) => {
const { ...formValues } = realm;
form.reset(formValues);
Object.entries(realm).map((entry) => {
if (entry[0] === "attributes") {
convertToFormValues(entry[1], "attributes", form.setValue);
} else {
form.setValue(entry[0], entry[1]);
}
});
};
useFetch(
() => adminClient.realms.findOne({ realm: realmName }),
(realm) => {
setRealm(realm);
setupForm(realm);
},
[realmName]
);
const save = async () => {
const firstInstanceOnly = true;
const flattenedAttributes = convertFormValuesToObject(
flatten(form.getValues()["attributes"]),
firstInstanceOnly
);
const attributes = { ...flattenedAttributes, ...realm?.attributes };
try {
const newRealm: RealmRepresentation = {
...realm,
...form.getValues(),
attributes,
};
await adminClient.realms.update({ realm: realmName }, newRealm);
setupForm(newRealm);
setRealm(newRealm);
addAlert(t("saveSuccess"), AlertVariant.success);
} catch (error) {
addError("realm-settings:saveError", error);
}
};
return (
<>
<PageSection variant="light">
<FormPanel
title={t("realm-settings:general")}
className="kc-sso-session-template"
>
<FormAccess
isHorizontal
role="manage-realm"
onSubmit={form.handleSubmit(save)}
>
<FormGroup
label={t("defaultSigAlg")}
fieldId="kc-default-signature-algorithm"
labelIcon={
<HelpItem
helpText="realm-settings-help:defaultSigAlg"
forLabel={t("defaultSigAlg")}
forID={t("common:helpLabel", { label: t("algorithm") })}
/>
}
>
<Controller
name="defaultSignatureAlgorithm"
defaultValue={"RS256"}
control={form.control}
render={({ onChange, value }) => (
<Select
toggleId="kc-default-sig-alg"
onToggle={() =>
setDefaultSigAlgDrpdwnOpen(!defaultSigAlgDrpdwnIsOpen)
}
onSelect={(_, value) => {
onChange(value.toString());
setDefaultSigAlgDrpdwnOpen(false);
}}
selections={[value.toString()]}
variant={SelectVariant.single}
aria-label={t("defaultSigAlg")}
isOpen={defaultSigAlgDrpdwnIsOpen}
data-testid="select-default-sig-alg"
>
{defaultSigAlgOptions!.map((p, idx) => (
<SelectOption
selected={p === value}
key={`default-sig-alg-${idx}`}
value={p}
></SelectOption>
))}
</Select>
)}
/>
</FormGroup>
</FormAccess>
</FormPanel>
<FormPanel
title={t("realm-settings:refreshTokens")}
className="kc-client-session-template"
>
<FormAccess
isHorizontal
role="manage-realm"
className="pf-u-mt-lg"
onSubmit={form.handleSubmit(save)}
>
<FormGroup
hasNoPaddingTop
label={t("revokeRefreshToken")}
fieldId="kc-revoke-refresh-token"
labelIcon={
<HelpItem
helpText="realm-settings-help:revokeRefreshToken"
forLabel={t("revokeRefreshToken")}
forID="revokeRefreshToken"
id="revokeRefreshToken"
/>
}
>
<Controller
name="revokeRefreshToken"
control={form.control}
defaultValue={false}
render={({ onChange, value }) => (
<Switch
id="kc-revoke-refresh-token"
data-testid="revoke-refresh-token-switch"
aria-label="revoke-refresh-token-switch"
label={t("common:enabled")}
labelOff={t("common:disabled")}
isChecked={value}
onChange={onChange}
/>
)}
/>
</FormGroup>
<FormGroup
label={t("refreshTokenMaxReuse")}
labelIcon={
<HelpItem
helpText="realm-settings-help:refreshTokenMaxReuse"
forLabel={t("refreshTokenMaxReuse")}
forID="refreshTokenMaxReuse"
/>
}
fieldId="refreshTokenMaxReuse"
>
<Controller
name="refreshTokenMaxReuse"
defaultValue={0}
control={form.control}
render={({ onChange, value }) => (
<NumberInput
type="text"
id="refreshTokenMaxReuseMs"
value={value}
onPlus={() => onChange(value + 1)}
onMinus={() => onChange(value - 1)}
onChange={(event) =>
onChange(Number((event.target as HTMLInputElement).value))
}
/>
)}
/>
</FormGroup>
</FormAccess>
</FormPanel>
<FormPanel
title={t("realm-settings:accessTokens")}
className="kc-offline-session-template"
>
<FormAccess
isHorizontal
role="manage-realm"
className="pf-u-mt-lg"
onSubmit={form.handleSubmit(save)}
>
<FormGroup
label={t("accessTokenLifespan")}
fieldId="accessTokenLifespan"
helperText={`It is recommended for this value to be shorter than the SSO session idle timeout: ${interpolateTimespan(
forHumans(realm?.ssoSessionIdleTimeout!)
)}`}
labelIcon={
<HelpItem
helpText="realm-settings-help:accessTokenLifespan"
forLabel={t("accessTokenLifespan")}
forID="accessTokenLifespan"
id="accessTokenLifespan"
/>
}
>
<Controller
name="accessTokenLifespan"
defaultValue=""
helperTextInvalid={t("common:required")}
control={form.control}
render={({ onChange, value }) => (
<TimeSelector
validated={
value > realm?.ssoSessionIdleTimeout!
? "warning"
: "default"
}
className="kc-access-token-lifespan"
data-testid="access-token-lifespan-input"
aria-label="access-token-lifespan"
value={value}
onChange={onChange}
units={["minutes", "hours", "days"]}
/>
)}
/>
</FormGroup>
<FormGroup
label={t("accessTokenLifespanImplicitFlow")}
fieldId="accessTokenLifespanImplicitFlow"
labelIcon={
<HelpItem
helpText="realm-settings-help:accessTokenLifespanImplicitFlow"
forLabel={t("accessTokenLifespanImplicitFlow")}
forID="accessTokenLifespanImplicitFlow"
id="accessTokenLifespanImplicitFlow"
/>
}
>
<Controller
name="accessTokenLifespanForImplicitFlow"
defaultValue=""
control={form.control}
render={({ onChange, value }) => (
<TimeSelector
className="kc-access-token-lifespan-implicit"
data-testid="access-token-lifespan-implicit-input"
aria-label="access-token-lifespan-implicit"
value={value}
onChange={onChange}
units={["minutes", "hours", "days"]}
/>
)}
/>
</FormGroup>
<FormGroup
label={t("clientLoginTimeout")}
fieldId="clientLoginTimeout"
labelIcon={
<HelpItem
helpText="realm-settings-help:clientLoginTimeout"
forLabel={t("clientLoginTimeout")}
forID="clientLoginTimeout"
id="clientLoginTimeout"
/>
}
>
<Controller
name="accessCodeLifespan"
defaultValue=""
control={form.control}
render={({ onChange, value }) => (
<TimeSelector
className="kc-client-login-timeout"
data-testid="client-login-timeout-input"
aria-label="client-login-timeout"
value={value}
onChange={onChange}
units={["minutes", "hours", "days"]}
/>
)}
/>
</FormGroup>
{offlineSessionMaxEnabled && (
<FormGroup
label={t("offlineSessionMax")}
fieldId="offlineSessionMax"
id="offline-session-max-label"
labelIcon={
<HelpItem
helpText="realm-settings-help:offlineSessionMax"
forLabel={t("offlineSessionMax")}
forID="offlineSessionMax"
id="offlineSessionMax"
/>
}
>
<Controller
name="offlineSessionMaxLifespan"
defaultValue=""
control={form.control}
render={({ onChange, value }) => (
<TimeSelector
className="kc-offline-session-max"
data-testid="offline-session-max-input"
aria-label="offline-session-max-input"
value={value}
onChange={onChange}
units={["minutes", "hours", "days"]}
/>
)}
/>
</FormGroup>
)}
</FormAccess>
</FormPanel>
<FormPanel
className="kc-login-settings-template"
title={t("actionTokens")}
>
<FormAccess
isHorizontal
role="manage-realm"
className="pf-u-mt-lg"
onSubmit={form.handleSubmit(save)}
>
<FormGroup
label={t("userInitiatedActionLifespan")}
id="kc-user-initiated-action-lifespan"
fieldId="userInitiatedActionLifespan"
labelIcon={
<HelpItem
helpText="realm-settings-help:userInitiatedActionLifespan"
forLabel={t("userInitiatedActionLifespan")}
forID="userInitiatedActionLifespan"
id="userInitiatedActionLifespan"
/>
}
>
<Controller
name="actionTokenGeneratedByUserLifespan"
defaultValue={""}
control={form.control}
render={({ onChange, value }) => (
<TimeSelector
className="kc-user-initiated-action-lifespan"
data-testid="user-initiated-action-lifespan"
aria-label="user-initiated-action-lifespan"
value={value}
onChange={onChange}
units={["minutes", "hours", "days"]}
/>
)}
/>
</FormGroup>
<FormGroup
label={t("defaultAdminInitiated")}
fieldId="defaultAdminInitiated"
id="default-admin-initiated-label"
labelIcon={
<HelpItem
helpText="realm-settings-help:defaultAdminInitiatedActionLifespan"
forLabel={t("defaultAdminInitiated")}
forID="defaultAdminInitiated"
id="defaultAdminInitiated"
/>
}
>
<Controller
name="actionTokenGeneratedByAdminLifespan"
defaultValue={""}
control={form.control}
render={({ onChange, value }) => (
<TimeSelector
className="kc-default-admin-initiated"
data-testid="default-admin-initated-input"
aria-label="default-admin-initated-input"
value={value}
onChange={onChange}
units={["minutes", "hours", "days"]}
/>
)}
/>
</FormGroup>
<Text
className="kc-override-action-tokens-subtitle"
component={TextVariants.h1}
>
{t("overrideActionTokens")}
</Text>
<FormGroup
label={t("emailVerification")}
fieldId="emailVerification"
id="email-verification"
>
<Controller
name="attributes.actionTokenGeneratedByUserLifespan-verify-email"
defaultValue={""}
control={form.control}
render={({ onChange, value }) => (
<TimeSelector
className="kc-email-verification"
data-testid="email-verification-input"
aria-label="email-verification-input"
value={value}
onChange={(value: any) => onChange(value.toString())}
units={["minutes", "hours", "days"]}
/>
)}
/>
</FormGroup>
<FormGroup
label={t("idpAccountEmailVerification")}
fieldId="idpAccountEmailVerification"
id="idp-acct-label"
>
<Controller
name="attributes.actionTokenGeneratedByUserLifespan-idp-verify-account-via-email"
defaultValue={""}
control={form.control}
render={({ onChange, value }) => (
<TimeSelector
className="kc-idp-email-verification"
data-testid="idp-email-verification-input"
aria-label="idp-email-verification"
value={value}
onChange={onChange}
units={["minutes", "hours", "days"]}
/>
)}
/>
</FormGroup>
<FormGroup
label={t("forgotPassword")}
fieldId="forgotPassword"
id="forgot-password-label"
>
<Controller
name="attributes.actionTokenGeneratedByUserLifespan-reset-credentials"
defaultValue={""}
control={form.control}
render={({ onChange, value }) => (
<TimeSelector
className="kc-forgot-pw"
data-testid="forgot-pw-input"
aria-label="forgot-pw-input"
value={value}
onChange={onChange}
units={["minutes", "hours", "days"]}
/>
)}
/>
</FormGroup>
<FormGroup
label={t("executeActions")}
fieldId="executeActions"
id="execute-actions"
>
<Controller
name="attributes.actionTokenGeneratedByUserLifespan-execute-actions"
defaultValue={""}
control={form.control}
render={({ onChange, value }) => (
<TimeSelector
className="kc-execute-actions"
data-testid="execute-actions-input"
aria-label="execute-actions-input"
value={value}
onChange={onChange}
units={["minutes", "hours", "days"]}
/>
)}
/>
</FormGroup>
<ActionGroup>
<Button
variant="primary"
type="submit"
data-testid="tokens-tab-save"
isDisabled={!form.formState.isDirty}
>
{t("common:save")}
</Button>
<Button variant="link" onClick={reset}>
{t("common:revert")}
</Button>
</ActionGroup>
</FormAccess>
</FormPanel>
</PageSection>
</>
);
};

View file

@ -88,5 +88,22 @@ export default {
"Max time a user has to complete a login. This is recommended to be relatively long, such as 30 minutes or more", "Max time a user has to complete a login. This is recommended to be relatively long, such as 30 minutes or more",
loginActionTimeout: loginActionTimeout:
"Max time a user has to complete login related actions like update password or configure totp. This is recommended to be relatively long, such as 5 minutes or more", "Max time a user has to complete login related actions like update password or configure totp. This is recommended to be relatively long, such as 5 minutes or more",
defaultSigAlg: "Default algorithm used to sign tokens for the realm",
revokeRefreshToken:
"If enabled a refresh token can only be used up to 'Refresh Token Max Reuse' and is revoked when a different token is used. Otherwise refresh tokens are not revoked when used and can be used multiple times.",
refreshTokenMaxReuse:
"Maximum number of times a refresh token can be reused. When a different token is used, revocation is immediate.",
accessTokenLifespan:
"Max time before an access token is expired. This value is recommended to be short relative to the SSO timeout",
accessTokenLifespanImplicitFlow:
"Max time before an access token issued during OpenID Connect Implicit Flow is expired. This value is recommended to be shorter than the SSO timeout. There is no possibility to refresh token during implicit flow, that's why there is a separate timeout different to 'Access Token Lifespan'",
clientLoginTimeout:
"Max time a client has to finish the access token protocol. This should normally be 1 minute.",
userInitiatedActionLifespan:
"Maximum time before an action permit sent by a user (such as a forgot password e-mail) is expired. This value is recommended to be short because it's expected that the user would react to self-created action quickly.",
defaultAdminInitiatedActionLifespan:
"Maximum time before an action permit sent to a user by administrator is expired. This value is recommended to be long to allow administrators to send e-mails for users that are currently offline. The default timeout can be overridden immediately before issuing the token.",
overrideActionTokens:
"Override default settings of maximum time before an action permit sent by a user (such as a forgot password e-mail) is expired for specific action. This value is recommended to be short because it's expected that the user would react to self-created action quickly.",
}, },
}; };

View file

@ -171,8 +171,29 @@ export default {
loginSettings: "Login settings", loginSettings: "Login settings",
loginTimeout: "Login timeout", loginTimeout: "Login timeout",
loginActionTimeout: "Login action timeout", loginActionTimeout: "Login action timeout",
refreshTokens: "Refresh tokens",
accessTokens: "Access tokens",
actionTokens: "Action tokens",
overrideActionTokens: "Override Action Tokens",
defaultSigAlg: "Default Signature Algorithm",
revokeRefreshToken: "Revoke Refresh Token",
refreshTokenMaxReuse: "Refresh Token Max Reuse",
accessTokenLifespan: "Access Token Lifespan",
accessTokenLifespanImplicitFlow: "Access Token Lifespan For Implicit Flow",
clientLoginTimeout: "Client Login Timeout",
userInitiatedActionLifespan: "User-Initiated Action Lifespan",
defaultAdminInitiated: "Default Admin-Initated Action Lifespan",
emailVerification: "Email Verification",
idpAccountEmailVerification: "IdP account email verification",
executeActions: "Execute actions",
tokens: "Tokens",
key: "Key", key: "Key",
value: "Value", value: "Value",
convertedToYearsValue: "{{convertedToYears}}",
convertedToDaysValue: "{{convertedToDays}}",
convertedToHoursValue: "{{convertedToHours}}",
convertedToMinutesValue: "{{convertedToMinutes}}",
convertedToSecondsValue: "{{convertedToSeconds}}",
pairCreatedSuccess: "Success! The localization text has been created.", pairCreatedSuccess: "Success! The localization text has been created.",
pairCreatedError: "Error creating localization text.", pairCreatedError: "Error creating localization text.",
supportedLocales: "Supported locales", supportedLocales: "Supported locales",

View file

@ -11,7 +11,8 @@ export type RealmSettingsTab =
| "keys" | "keys"
| "events" | "events"
| "securityDefences" | "securityDefences"
| "sessions"; | "sessions"
| "tokens";
export type RealmSettingsParams = { export type RealmSettingsParams = {
realm: string; realm: string;

View file

@ -4,6 +4,7 @@ import _ from "lodash";
import type ClientRepresentation from "keycloak-admin/lib/defs/clientRepresentation"; import type ClientRepresentation from "keycloak-admin/lib/defs/clientRepresentation";
import type { ProviderRepresentation } from "keycloak-admin/lib/defs/serverInfoRepesentation"; import type { ProviderRepresentation } from "keycloak-admin/lib/defs/serverInfoRepesentation";
import type KeycloakAdminClient from "keycloak-admin"; import type KeycloakAdminClient from "keycloak-admin";
import { useTranslation } from "react-i18next";
export const sortProviders = (providers: { export const sortProviders = (providers: {
[index: string]: ProviderRepresentation; [index: string]: ProviderRepresentation;
@ -64,9 +65,31 @@ export const convertToFormValues = (
}); });
}; };
export const convertFormValuesToObject = (obj: any) => { export const flatten = (
obj: Record<string, any> | undefined,
path = ""
): {} => {
if (!(obj instanceof Object)) return { [path.replace(/\.$/g, "")]: obj };
return Object.keys(obj).reduce((output, key) => {
return obj instanceof Array
? {
...output,
...flatten(obj[key as unknown as number], path + "[" + key + "]."),
}
: { ...output, ...flatten(obj[key], path + key + ".") };
}, {});
};
export const convertFormValuesToObject = (
obj: any,
firstInstanceOnly?: boolean
) => {
const keyValues = Object.keys(obj).map((key) => { const keyValues = Object.keys(obj).map((key) => {
const newKey = key.replace(/-/g, "."); const newKey = firstInstanceOnly
? key.replace(/-/, ".")
: key.replace(/-/g, ".");
console.log(newKey);
return { [newKey]: obj[key] }; return { [newKey]: obj[key] };
}); });
return Object.assign({}, ...keyValues); return Object.assign({}, ...keyValues);
@ -94,3 +117,75 @@ export const getBaseUrl = (adminClient: KeycloakAdminClient) => {
export const emailRegexPattern = export const emailRegexPattern =
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
export const forHumans = (seconds: number) => {
const { t } = useTranslation();
const levels: [
[number, string],
[number, string],
[number, string],
[number, string],
[number, string]
] = [
[Math.floor(seconds / 31536000), t("common:times.years")],
[Math.floor((seconds % 31536000) / 86400), t("common:times.days")],
[
Math.floor(((seconds % 31536000) % 86400) / 3600),
t("common:times.hours"),
],
[
Math.floor((((seconds % 31536000) % 86400) % 3600) / 60),
t("common:times.minutes"),
],
[(((seconds % 31536000) % 86400) % 3600) % 60, t("common:times.seconds")],
];
let returntext = "";
for (let i = 0, max = levels.length; i < max; i++) {
if (levels[i][0] === 0) continue;
returntext +=
" " +
levels[i][0] +
" " +
(levels[i][0] === 1
? levels[i][1].substr(0, levels[i][1].length - 1)
: levels[i][1]);
}
return returntext.trim();
};
export const interpolateTimespan = (forHumans: string) => {
const { t } = useTranslation();
const timespan = forHumans.split(" ");
if (timespan[1] === "Years") {
return t(`realm-settings:convertedToYearsValue`, {
convertedToYears: forHumans,
});
}
if (timespan[1] === "Days") {
return t(`realm-settings:convertedToDaysValue`, {
convertedToYears: forHumans,
});
}
if (timespan[1] === "Hours") {
return t(`realm-settings:convertedToHoursValue`, {
convertedToHours: forHumans,
});
}
if (timespan[1] === "Minutes") {
return t(`realm-settings:convertedToMinutesValue`, {
convertedToMinutes: forHumans,
});
}
if (timespan[1] === "Seconds") {
return t(`realm-settings:convertedToSecondsValue`, {
convertedToSeconds: forHumans,
});
}
};