Repository navigation
Allow configuring "Revoke Refresh Token" per client (#16422) #51798
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
thomasdarimont
wants to merge
16
commits into
keycloak:main
Choose a base branch
from
thomasdarimont:issue/gh-16422-oidc-allow-revoke-refresh-token-client-level
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
12a1ccf
Allow configuring "Revoke Refresh Token" per client (#16422)
thomasdarimont 533cf83
Add admin console test for per-client refresh token revocation override
thomasdarimont 075f3d0
Fix "Revert" in client Advanced settings breaking subsequent save
thomasdarimont 68a94b2
Guard acr.loa.map JSON parsing when resetting the advanced tab form
thomasdarimont 0792e6c
Address review feedback for client-level refresh token revocation
thomasdarimont da5f681
Revise fix for acr.loa.map handling in AdvancedTab.tsx
thomasdarimont b82dd04
Address second round of review feedback for client-level refresh toke…
thomasdarimont 6c73d69
Use double quotes in client validation message to satisfy theme verifier
thomasdarimont 1b80aee
Address third round of review feedback for client-level refresh token…
thomasdarimont 76e3cce
Align client-level refresh token revocation control with TokenLifespa…
thomasdarimont 51140ab
Normalize malformed refresh token max reuse override on client form s…
thomasdarimont acb5a3c
Reject refresh token max reuse overrides above Integer.MAX_VALUE in c…
thomasdarimont 3b7e399
Address review findings on per-client refresh token revocation
thomasdarimont 89867c4
Honour client-level refresh token revocation in the OID4VCI refresh t…
thomasdarimont b8a7526
Move release note for Refresh token revocation configurable per clien…
thomasdarimont 217eaf7
Honour client-level refresh token revocation in the revocation endpoi…
thomasdarimont File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
169 changes: 169 additions & 0 deletions
169
js/apps/admin-ui/src/clients/advanced/RefreshTokenRevocation.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,169 @@ | ||
| import { HelpItem } from "@keycloak/keycloak-ui-shared"; | ||
| import { | ||
| FormGroup, | ||
| MenuToggle, | ||
| NumberInput, | ||
| Select, | ||
| SelectList, | ||
| SelectOption, | ||
| Split, | ||
| SplitItem, | ||
| } from "@patternfly/react-core"; | ||
| import { useState } from "react"; | ||
| import { Controller, useFormContext } from "react-hook-form"; | ||
| import { useTranslation } from "react-i18next"; | ||
| import { useRealm } from "../../context/realm-context/RealmContext"; | ||
| import { convertAttributeNameToForm } from "../../util"; | ||
| import { FormFields } from "../ClientDetails"; | ||
|
|
||
| const REVOKE_REFRESH_TOKEN = convertAttributeNameToForm<FormFields>( | ||
| "attributes.revoke.refresh.token", | ||
| ); | ||
| const REFRESH_TOKEN_MAX_REUSE = convertAttributeNameToForm<FormFields>( | ||
| "attributes.refresh.token.max.reuse", | ||
| ); | ||
|
|
||
| const ENABLED = "true"; | ||
| const DISABLED = "false"; | ||
| const INHERITED = ""; | ||
|
|
||
| const toNumber = (value: unknown, fallback: number) => { | ||
| const number = Number(value); | ||
| return value === "" || value == null || isNaN(number) ? fallback : number; | ||
| }; | ||
|
|
||
| /** | ||
| * Client-level override of the realm settings "Revoke Refresh Token" and "Refresh Token Max Reuse", | ||
| * laid out like {@link TokenLifespan}: a selector to inherit, enable or disable revocation and an inline | ||
| * input for the max reuse that is only shown when revocation is enabled for the client. When revocation | ||
| * is not explicitly enabled, both attributes are cleared so the realm settings apply. | ||
| */ | ||
| export const RefreshTokenRevocation = () => { | ||
| const { t } = useTranslation(); | ||
| const { realmRepresentation: realm } = useRealm(); | ||
| const { control, setValue, getValues } = useFormContext(); | ||
| const [open, setOpen] = useState(false); | ||
|
|
||
| const realmRevokeRefreshToken = realm.revokeRefreshToken ?? false; | ||
| const realmMaxReuse = realm.refreshTokenMaxReuse ?? 0; | ||
|
|
||
| const options = [ | ||
| { | ||
| value: INHERITED, | ||
| label: t("inheritsFromRealmSettings", { | ||
| value: t(realmRevokeRefreshToken ? "enabled" : "disabled"), | ||
| }), | ||
| }, | ||
| { value: ENABLED, label: t("enabled") }, | ||
| { value: DISABLED, label: t("disabled") }, | ||
| ]; | ||
| // Only explicit "true"/"false" are overrides, anything else inherits the realm setting (same as the server side) | ||
| const labelFor = (value?: string) => | ||
| (options.find((option) => option.value === value) ?? options[0]).label; | ||
|
|
||
| return ( | ||
| <FormGroup | ||
| label={t("revokeRefreshToken")} | ||
| fieldId="revokeRefreshToken" | ||
| labelIcon={ | ||
| <HelpItem | ||
| helpText={t("revokeRefreshTokenClientHelp")} | ||
| fieldLabelId="revokeRefreshToken" | ||
| /> | ||
| } | ||
| data-testid="revoke-refresh-token" | ||
| > | ||
| <Controller | ||
| name={REVOKE_REFRESH_TOKEN} | ||
| defaultValue={INHERITED} | ||
| control={control} | ||
| render={({ field }) => { | ||
| const isEnabled = field.value === ENABLED; | ||
| return ( | ||
| <Split hasGutter> | ||
| <SplitItem> | ||
| <Select | ||
| toggle={(ref) => ( | ||
| <MenuToggle | ||
| ref={ref} | ||
| id="revokeRefreshToken" | ||
| => setOpen(!open)} | ||
| isExpanded={open} | ||
| > | ||
| {labelFor(field.value)} | ||
| </MenuToggle> | ||
| )} | ||
| isOpen={open} | ||
| => setOpen(isOpen)} | ||
| value) => { | ||
| field.onChange(value); | ||
| // The max reuse is an override only while revocation is enabled for the client; | ||
| // seed it with the realm value when enabling, like TokenLifespan does with the lifespan | ||
| setValue( | ||
| REFRESH_TOKEN_MAX_REUSE, | ||
| value === ENABLED | ||
| ? toNumber( | ||
| getValues(REFRESH_TOKEN_MAX_REUSE), | ||
| realmMaxReuse, | ||
| ) | ||
| : INHERITED, | ||
|
Copilot marked this conversation as resolved.
Resolved
|
||
| { shouldDirty: true }, | ||
| ); | ||
| setOpen(false); | ||
| }} | ||
| selected={field.value} | ||
| > | ||
| <SelectList> | ||
| {options.map(({ value, label }) => ( | ||
| <SelectOption key={value} value={value}> | ||
| {label} | ||
| </SelectOption> | ||
| ))} | ||
| </SelectList> | ||
| </Select> | ||
| </SplitItem> | ||
| <SplitItem hidden={!isEnabled}> | ||
| <Controller | ||
| name={REFRESH_TOKEN_MAX_REUSE} | ||
| defaultValue={INHERITED} | ||
| control={control} | ||
| render={({ field: maxReuse }) => { | ||
| const value = toNumber(maxReuse.value, realmMaxReuse); | ||
| const update = (newValue: number) => | ||
| maxReuse.onChange(Math.max(0, newValue)); | ||
| return ( | ||
| <NumberInput | ||
| id="refreshTokenMaxReuse" | ||
| inputAriaLabel={t("refreshTokenMaxReuse")} | ||
| inputProps={{ "data-testid": "refreshTokenMaxReuse" }} | ||
| unit={ | ||
| <span style={{ whiteSpace: "nowrap" }}> | ||
| {t("refreshTokenMaxReuseShort")} | ||
| </span> | ||
| } | ||
| unitPosition="before" | ||
| min={0} | ||
| value={value} | ||
| isDisabled={!isEnabled} | ||
| => update(value + 1)} | ||
| => update(value - 1)} | ||
| => | ||
| update( | ||
| toNumber( | ||
| (event.target as HTMLInputElement).value, | ||
| 0, | ||
| ), | ||
| ) | ||
| } | ||
| /> | ||
| ); | ||
| }} | ||
| /> | ||
| </SplitItem> | ||
| </Split> | ||
| ); | ||
| }} | ||
| /> | ||
| </FormGroup> | ||
| ); | ||
| }; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.