Add dedicated feature flag for oauth device grant flow (#23892)

Closes #23891
This commit is contained in:
Thomas Darimont 2023-10-24 10:09:26 +02:00 committed by GitHub
parent e4632c9e78
commit e567210ed1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 337 additions and 192 deletions

View file

@ -92,7 +92,10 @@ public class Profile {
DPOP("OAuth 2.0 Demonstrating Proof-of-Possession at the Application Layer", Type.PREVIEW),
LINKEDIN_OAUTH("LinkedIn Social Identity Provider based on OAuth", Type.DEPRECATED);
LINKEDIN_OAUTH("LinkedIn Social Identity Provider based on OAuth", Type.DEPRECATED),
DEVICE_FLOW("OAuth 2.0 Device Authorization Grant", Type.DEFAULT),
;
private final Type type;
private final String label;

View file

@ -14,6 +14,7 @@ import { FormAccess } from "../../components/form/FormAccess";
import { HelpItem } from "ui-shared";
import { convertAttributeNameToForm } from "../../util";
import { FormFields } from "../ClientDetails";
import useIsFeatureEnabled, { Feature } from "../../utils/useIsFeatureEnabled";
type CapabilityConfigProps = {
unWrap?: boolean;
@ -29,6 +30,7 @@ export const CapabilityConfig = ({
const protocol = type || watch("protocol");
const clientAuthentication = watch("publicClient");
const authorization = watch("authorizationServicesEnabled");
const isFeatureEnabled = useIsFeatureEnabled();
return (
<FormAccess
@ -215,31 +217,33 @@ export const CapabilityConfig = ({
)}
/>
</GridItem>
<GridItem lg={8} sm={6}>
<Controller
name={convertAttributeNameToForm<
Required<ClientRepresentation["attributes"]>
>("attributes.oauth2.device.authorization.grant.enabled")}
defaultValue={false}
control={control}
render={({ field }) => (
<InputGroup>
<Checkbox
data-testid="oauth-device-authorization-grant"
label={t("oauthDeviceAuthorizationGrant")}
id="kc-oauth-device-authorization-grant"
name="oauth2.device.authorization.grant.enabled"
isChecked={field.value.toString() === "true"}
onChange={field.onChange}
/>
<HelpItem
helpText={t("oauthDeviceAuthorizationGrantHelp")}
fieldLabelId="oauthDeviceAuthorizationGrant"
/>
</InputGroup>
)}
/>
</GridItem>
{isFeatureEnabled(Feature.DeviceFlow) && (
<GridItem lg={8} sm={6}>
<Controller
name={convertAttributeNameToForm<
Required<ClientRepresentation["attributes"]>
>("attributes.oauth2.device.authorization.grant.enabled")}
defaultValue={false}
control={control}
render={({ field }) => (
<InputGroup>
<Checkbox
data-testid="oauth-device-authorization-grant"
label={t("oauthDeviceAuthorizationGrant")}
id="kc-oauth-device-authorization-grant"
name="oauth2.device.authorization.grant.enabled"
isChecked={field.value.toString() === "true"}
onChange={field.onChange}
/>
<HelpItem
helpText={t("oauthDeviceAuthorizationGrantHelp")}
fieldLabelId="oauthDeviceAuthorizationGrant"
/>
</InputGroup>
)}
/>
</GridItem>
)}
<GridItem lg={8} sm={6}>
<Controller
name={convertAttributeNameToForm<FormFields>(

View file

@ -27,6 +27,7 @@ import {
import { useServerInfo } from "../context/server-info/ServerInfoProvider";
import { useWhoAmI } from "../context/whoami/WhoAmI";
import { convertToFormValues, sortProviders } from "../util";
import useIsFeatureEnabled, { Feature } from "../utils/useIsFeatureEnabled";
import "./realm-settings-section.css";
@ -43,6 +44,7 @@ export const RealmSettingsTokensTab = ({
}: RealmSettingsSessionsTabProps) => {
const { t } = useTranslation();
const serverInfo = useServerInfo();
const isFeatureEnabled = useIsFeatureEnabled();
const { whoAmI } = useWhoAmI();
const [defaultSigAlgDrpdwnIsOpen, setDefaultSigAlgDrpdwnOpen] =
@ -127,77 +129,81 @@ export const RealmSettingsTokensTab = ({
/>
</FormGroup>
<FormGroup
label={t("oAuthDeviceCodeLifespan")}
fieldId="oAuthDeviceCodeLifespan"
labelIcon={
<HelpItem
helpText={t("oAuthDeviceCodeLifespanHelp")}
fieldLabelId="oAuthDeviceCodeLifespan"
/>
}
>
<Controller
name="oauth2DeviceCodeLifespan"
defaultValue={0}
control={form.control}
render={({ field }) => (
<TimeSelector
id="oAuthDeviceCodeLifespan"
data-testid="oAuthDeviceCodeLifespan"
value={field.value || 0}
onChange={field.onChange}
units={["minute", "hour", "day"]}
{isFeatureEnabled(Feature.DeviceFlow) && (
<>
<FormGroup
label={t("oAuthDeviceCodeLifespan")}
fieldId="oAuthDeviceCodeLifespan"
labelIcon={
<HelpItem
helpText={t("oAuthDeviceCodeLifespanHelp")}
fieldLabelId="oAuthDeviceCodeLifespan"
/>
}
>
<Controller
name="oauth2DeviceCodeLifespan"
defaultValue={0}
control={form.control}
render={({ field }) => (
<TimeSelector
id="oAuthDeviceCodeLifespan"
data-testid="oAuthDeviceCodeLifespan"
value={field.value || 0}
onChange={field.onChange}
units={["minute", "hour", "day"]}
/>
)}
/>
)}
/>
</FormGroup>
<FormGroup
label={t("oAuthDevicePollingInterval")}
fieldId="oAuthDevicePollingInterval"
labelIcon={
<HelpItem
helpText={t("oAuthDevicePollingIntervalHelp")}
fieldLabelId="oAuthDevicePollingInterval"
/>
}
>
<Controller
name="oauth2DevicePollingInterval"
defaultValue={0}
control={form.control}
render={({ field }) => (
<NumberInput
id="oAuthDevicePollingInterval"
value={field.value}
min={0}
onPlus={() => field.onChange(field.value || 0 + 1)}
onMinus={() => field.onChange(field.value || 0 - 1)}
onChange={(event) => {
const newValue = Number(event.currentTarget.value);
field.onChange(!isNaN(newValue) ? newValue : 0);
}}
placeholder={t("oAuthDevicePollingInterval")}
</FormGroup>
<FormGroup
label={t("oAuthDevicePollingInterval")}
fieldId="oAuthDevicePollingInterval"
labelIcon={
<HelpItem
helpText={t("oAuthDevicePollingIntervalHelp")}
fieldLabelId="oAuthDevicePollingInterval"
/>
}
>
<Controller
name="oauth2DevicePollingInterval"
defaultValue={0}
control={form.control}
render={({ field }) => (
<NumberInput
id="oAuthDevicePollingInterval"
value={field.value}
min={0}
onPlus={() => field.onChange(field.value || 0 + 1)}
onMinus={() => field.onChange(field.value || 0 - 1)}
onChange={(event) => {
const newValue = Number(event.currentTarget.value);
field.onChange(!isNaN(newValue) ? newValue : 0);
}}
placeholder={t("oAuthDevicePollingInterval")}
/>
)}
/>
)}
/>
</FormGroup>
<FormGroup
label={t("shortVerificationUri")}
fieldId="shortVerificationUri"
labelIcon={
<HelpItem
helpText={t("shortVerificationUriTooltipHelp")}
fieldLabelId="shortVerificationUri"
/>
}
>
<KeycloakTextInput
id="shortVerificationUri"
placeholder={t("shortVerificationUri")}
{...form.register("attributes.shortVerificationUri")}
/>
</FormGroup>
</FormGroup>
<FormGroup
label={t("shortVerificationUri")}
fieldId="shortVerificationUri"
labelIcon={
<HelpItem
helpText={t("shortVerificationUriTooltipHelp")}
fieldLabelId="shortVerificationUri"
/>
}
>
<KeycloakTextInput
id="shortVerificationUri"
placeholder={t("shortVerificationUri")}
{...form.register("attributes.shortVerificationUri")}
/>
</FormGroup>
</>
)}
</FormAccess>
</FormPanel>
<FormPanel

View file

@ -7,6 +7,7 @@ export enum Feature {
Kerberos = "KERBEROS",
DynamicScopes = "DYNAMIC_SCOPES",
DPoP = "DPOP",
DeviceFlow = "DEVICE_FLOW",
}
export default function useIsFeatureEnabled() {

View file

@ -47,17 +47,17 @@ Feature:
--features <feature> Enables a set of one or more features. Possible values are: account-api,
account2, account3, admin-api, admin-fine-grained-authz, admin2,
authorization, ciba, client-policies, client-secret-rotation,
declarative-user-profile, docker, dpop, dynamic-scopes, fips, impersonation,
js-adapter, kerberos, linkedin-oauth, map-storage, par, preview,
recovery-codes, scripts, step-up-authentication, token-exchange,
declarative-user-profile, device-flow, docker, dpop, dynamic-scopes, fips,
impersonation, js-adapter, kerberos, linkedin-oauth, map-storage, par,
preview, recovery-codes, scripts, step-up-authentication, token-exchange,
update-email, web-authn.
--features-disabled <feature>
Disables a set of one or more features. Possible values are: account-api,
account2, account3, admin-api, admin-fine-grained-authz, admin2,
authorization, ciba, client-policies, client-secret-rotation,
declarative-user-profile, docker, dpop, dynamic-scopes, fips, impersonation,
js-adapter, kerberos, linkedin-oauth, map-storage, par, preview,
recovery-codes, scripts, step-up-authentication, token-exchange,
declarative-user-profile, device-flow, docker, dpop, dynamic-scopes, fips,
impersonation, js-adapter, kerberos, linkedin-oauth, map-storage, par,
preview, recovery-codes, scripts, step-up-authentication, token-exchange,
update-email, web-authn.
HTTP/TLS:

View file

@ -47,17 +47,17 @@ Feature:
--features <feature> Enables a set of one or more features. Possible values are: account-api,
account2, account3, admin-api, admin-fine-grained-authz, admin2,
authorization, ciba, client-policies, client-secret-rotation,
declarative-user-profile, docker, dpop, dynamic-scopes, fips, impersonation,
js-adapter, kerberos, linkedin-oauth, map-storage, par, preview,
recovery-codes, scripts, step-up-authentication, token-exchange,
declarative-user-profile, device-flow, docker, dpop, dynamic-scopes, fips,
impersonation, js-adapter, kerberos, linkedin-oauth, map-storage, par,
preview, recovery-codes, scripts, step-up-authentication, token-exchange,
update-email, web-authn.
--features-disabled <feature>
Disables a set of one or more features. Possible values are: account-api,
account2, account3, admin-api, admin-fine-grained-authz, admin2,
authorization, ciba, client-policies, client-secret-rotation,
declarative-user-profile, docker, dpop, dynamic-scopes, fips, impersonation,
js-adapter, kerberos, linkedin-oauth, map-storage, par, preview,
recovery-codes, scripts, step-up-authentication, token-exchange,
declarative-user-profile, device-flow, docker, dpop, dynamic-scopes, fips,
impersonation, js-adapter, kerberos, linkedin-oauth, map-storage, par,
preview, recovery-codes, scripts, step-up-authentication, token-exchange,
update-email, web-authn.
HTTP/TLS:

View file

@ -58,17 +58,17 @@ Feature:
--features <feature> Enables a set of one or more features. Possible values are: account-api,
account2, account3, admin-api, admin-fine-grained-authz, admin2,
authorization, ciba, client-policies, client-secret-rotation,
declarative-user-profile, docker, dpop, dynamic-scopes, fips, impersonation,
js-adapter, kerberos, linkedin-oauth, map-storage, par, preview,
recovery-codes, scripts, step-up-authentication, token-exchange,
declarative-user-profile, device-flow, docker, dpop, dynamic-scopes, fips,
impersonation, js-adapter, kerberos, linkedin-oauth, map-storage, par,
preview, recovery-codes, scripts, step-up-authentication, token-exchange,
update-email, web-authn.
--features-disabled <feature>
Disables a set of one or more features. Possible values are: account-api,
account2, account3, admin-api, admin-fine-grained-authz, admin2,
authorization, ciba, client-policies, client-secret-rotation,
declarative-user-profile, docker, dpop, dynamic-scopes, fips, impersonation,
js-adapter, kerberos, linkedin-oauth, map-storage, par, preview,
recovery-codes, scripts, step-up-authentication, token-exchange,
declarative-user-profile, device-flow, docker, dpop, dynamic-scopes, fips,
impersonation, js-adapter, kerberos, linkedin-oauth, map-storage, par,
preview, recovery-codes, scripts, step-up-authentication, token-exchange,
update-email, web-authn.
Config:

View file

@ -121,17 +121,17 @@ Feature:
--features <feature> Enables a set of one or more features. Possible values are: account-api,
account2, account3, admin-api, admin-fine-grained-authz, admin2,
authorization, ciba, client-policies, client-secret-rotation,
declarative-user-profile, docker, dpop, dynamic-scopes, fips, impersonation,
js-adapter, kerberos, linkedin-oauth, map-storage, par, preview,
recovery-codes, scripts, step-up-authentication, token-exchange,
declarative-user-profile, device-flow, docker, dpop, dynamic-scopes, fips,
impersonation, js-adapter, kerberos, linkedin-oauth, map-storage, par,
preview, recovery-codes, scripts, step-up-authentication, token-exchange,
update-email, web-authn.
--features-disabled <feature>
Disables a set of one or more features. Possible values are: account-api,
account2, account3, admin-api, admin-fine-grained-authz, admin2,
authorization, ciba, client-policies, client-secret-rotation,
declarative-user-profile, docker, dpop, dynamic-scopes, fips, impersonation,
js-adapter, kerberos, linkedin-oauth, map-storage, par, preview,
recovery-codes, scripts, step-up-authentication, token-exchange,
declarative-user-profile, device-flow, docker, dpop, dynamic-scopes, fips,
impersonation, js-adapter, kerberos, linkedin-oauth, map-storage, par,
preview, recovery-codes, scripts, step-up-authentication, token-exchange,
update-email, web-authn.
Config:

View file

@ -58,17 +58,17 @@ Feature:
--features <feature> Enables a set of one or more features. Possible values are: account-api,
account2, account3, admin-api, admin-fine-grained-authz, admin2,
authorization, ciba, client-policies, client-secret-rotation,
declarative-user-profile, docker, dpop, dynamic-scopes, fips, impersonation,
js-adapter, kerberos, linkedin-oauth, map-storage, par, preview,
recovery-codes, scripts, step-up-authentication, token-exchange,
declarative-user-profile, device-flow, docker, dpop, dynamic-scopes, fips,
impersonation, js-adapter, kerberos, linkedin-oauth, map-storage, par,
preview, recovery-codes, scripts, step-up-authentication, token-exchange,
update-email, web-authn.
--features-disabled <feature>
Disables a set of one or more features. Possible values are: account-api,
account2, account3, admin-api, admin-fine-grained-authz, admin2,
authorization, ciba, client-policies, client-secret-rotation,
declarative-user-profile, docker, dpop, dynamic-scopes, fips, impersonation,
js-adapter, kerberos, linkedin-oauth, map-storage, par, preview,
recovery-codes, scripts, step-up-authentication, token-exchange,
declarative-user-profile, device-flow, docker, dpop, dynamic-scopes, fips,
impersonation, js-adapter, kerberos, linkedin-oauth, map-storage, par,
preview, recovery-codes, scripts, step-up-authentication, token-exchange,
update-email, web-authn.
Config:

View file

@ -121,17 +121,17 @@ Feature:
--features <feature> Enables a set of one or more features. Possible values are: account-api,
account2, account3, admin-api, admin-fine-grained-authz, admin2,
authorization, ciba, client-policies, client-secret-rotation,
declarative-user-profile, docker, dpop, dynamic-scopes, fips, impersonation,
js-adapter, kerberos, linkedin-oauth, map-storage, par, preview,
recovery-codes, scripts, step-up-authentication, token-exchange,
declarative-user-profile, device-flow, docker, dpop, dynamic-scopes, fips,
impersonation, js-adapter, kerberos, linkedin-oauth, map-storage, par,
preview, recovery-codes, scripts, step-up-authentication, token-exchange,
update-email, web-authn.
--features-disabled <feature>
Disables a set of one or more features. Possible values are: account-api,
account2, account3, admin-api, admin-fine-grained-authz, admin2,
authorization, ciba, client-policies, client-secret-rotation,
declarative-user-profile, docker, dpop, dynamic-scopes, fips, impersonation,
js-adapter, kerberos, linkedin-oauth, map-storage, par, preview,
recovery-codes, scripts, step-up-authentication, token-exchange,
declarative-user-profile, device-flow, docker, dpop, dynamic-scopes, fips,
impersonation, js-adapter, kerberos, linkedin-oauth, map-storage, par,
preview, recovery-codes, scripts, step-up-authentication, token-exchange,
update-email, web-authn.
Config:

View file

@ -74,17 +74,17 @@ Feature:
--features <feature> Enables a set of one or more features. Possible values are: account-api,
account2, account3, admin-api, admin-fine-grained-authz, admin2,
authorization, ciba, client-policies, client-secret-rotation,
declarative-user-profile, docker, dpop, dynamic-scopes, fips, impersonation,
js-adapter, kerberos, linkedin-oauth, map-storage, par, preview,
recovery-codes, scripts, step-up-authentication, token-exchange,
declarative-user-profile, device-flow, docker, dpop, dynamic-scopes, fips,
impersonation, js-adapter, kerberos, linkedin-oauth, map-storage, par,
preview, recovery-codes, scripts, step-up-authentication, token-exchange,
update-email, web-authn.
--features-disabled <feature>
Disables a set of one or more features. Possible values are: account-api,
account2, account3, admin-api, admin-fine-grained-authz, admin2,
authorization, ciba, client-policies, client-secret-rotation,
declarative-user-profile, docker, dpop, dynamic-scopes, fips, impersonation,
js-adapter, kerberos, linkedin-oauth, map-storage, par, preview,
recovery-codes, scripts, step-up-authentication, token-exchange,
declarative-user-profile, device-flow, docker, dpop, dynamic-scopes, fips,
impersonation, js-adapter, kerberos, linkedin-oauth, map-storage, par,
preview, recovery-codes, scripts, step-up-authentication, token-exchange,
update-email, web-authn.
Hostname:

View file

@ -72,17 +72,17 @@ Feature:
--features <feature> Enables a set of one or more features. Possible values are: account-api,
account2, account3, admin-api, admin-fine-grained-authz, admin2,
authorization, ciba, client-policies, client-secret-rotation,
declarative-user-profile, docker, dpop, dynamic-scopes, fips, impersonation,
js-adapter, kerberos, linkedin-oauth, map-storage, par, preview,
recovery-codes, scripts, step-up-authentication, token-exchange,
declarative-user-profile, device-flow, docker, dpop, dynamic-scopes, fips,
impersonation, js-adapter, kerberos, linkedin-oauth, map-storage, par,
preview, recovery-codes, scripts, step-up-authentication, token-exchange,
update-email, web-authn.
--features-disabled <feature>
Disables a set of one or more features. Possible values are: account-api,
account2, account3, admin-api, admin-fine-grained-authz, admin2,
authorization, ciba, client-policies, client-secret-rotation,
declarative-user-profile, docker, dpop, dynamic-scopes, fips, impersonation,
js-adapter, kerberos, linkedin-oauth, map-storage, par, preview,
recovery-codes, scripts, step-up-authentication, token-exchange,
declarative-user-profile, device-flow, docker, dpop, dynamic-scopes, fips,
impersonation, js-adapter, kerberos, linkedin-oauth, map-storage, par,
preview, recovery-codes, scripts, step-up-authentication, token-exchange,
update-email, web-authn.
Hostname:

View file

@ -137,17 +137,17 @@ Feature:
--features <feature> Enables a set of one or more features. Possible values are: account-api,
account2, account3, admin-api, admin-fine-grained-authz, admin2,
authorization, ciba, client-policies, client-secret-rotation,
declarative-user-profile, docker, dpop, dynamic-scopes, fips, impersonation,
js-adapter, kerberos, linkedin-oauth, map-storage, par, preview,
recovery-codes, scripts, step-up-authentication, token-exchange,
declarative-user-profile, device-flow, docker, dpop, dynamic-scopes, fips,
impersonation, js-adapter, kerberos, linkedin-oauth, map-storage, par,
preview, recovery-codes, scripts, step-up-authentication, token-exchange,
update-email, web-authn.
--features-disabled <feature>
Disables a set of one or more features. Possible values are: account-api,
account2, account3, admin-api, admin-fine-grained-authz, admin2,
authorization, ciba, client-policies, client-secret-rotation,
declarative-user-profile, docker, dpop, dynamic-scopes, fips, impersonation,
js-adapter, kerberos, linkedin-oauth, map-storage, par, preview,
recovery-codes, scripts, step-up-authentication, token-exchange,
declarative-user-profile, device-flow, docker, dpop, dynamic-scopes, fips,
impersonation, js-adapter, kerberos, linkedin-oauth, map-storage, par,
preview, recovery-codes, scripts, step-up-authentication, token-exchange,
update-email, web-authn.
Hostname:

View file

@ -135,17 +135,17 @@ Feature:
--features <feature> Enables a set of one or more features. Possible values are: account-api,
account2, account3, admin-api, admin-fine-grained-authz, admin2,
authorization, ciba, client-policies, client-secret-rotation,
declarative-user-profile, docker, dpop, dynamic-scopes, fips, impersonation,
js-adapter, kerberos, linkedin-oauth, map-storage, par, preview,
recovery-codes, scripts, step-up-authentication, token-exchange,
declarative-user-profile, device-flow, docker, dpop, dynamic-scopes, fips,
impersonation, js-adapter, kerberos, linkedin-oauth, map-storage, par,
preview, recovery-codes, scripts, step-up-authentication, token-exchange,
update-email, web-authn.
--features-disabled <feature>
Disables a set of one or more features. Possible values are: account-api,
account2, account3, admin-api, admin-fine-grained-authz, admin2,
authorization, ciba, client-policies, client-secret-rotation,
declarative-user-profile, docker, dpop, dynamic-scopes, fips, impersonation,
js-adapter, kerberos, linkedin-oauth, map-storage, par, preview,
recovery-codes, scripts, step-up-authentication, token-exchange,
declarative-user-profile, device-flow, docker, dpop, dynamic-scopes, fips,
impersonation, js-adapter, kerberos, linkedin-oauth, map-storage, par,
preview, recovery-codes, scripts, step-up-authentication, token-exchange,
update-email, web-authn.
Hostname:

View file

@ -75,17 +75,17 @@ Feature:
--features <feature> Enables a set of one or more features. Possible values are: account-api,
account2, account3, admin-api, admin-fine-grained-authz, admin2,
authorization, ciba, client-policies, client-secret-rotation,
declarative-user-profile, docker, dpop, dynamic-scopes, fips, impersonation,
js-adapter, kerberos, linkedin-oauth, map-storage, par, preview,
recovery-codes, scripts, step-up-authentication, token-exchange,
declarative-user-profile, device-flow, docker, dpop, dynamic-scopes, fips,
impersonation, js-adapter, kerberos, linkedin-oauth, map-storage, par,
preview, recovery-codes, scripts, step-up-authentication, token-exchange,
update-email, web-authn.
--features-disabled <feature>
Disables a set of one or more features. Possible values are: account-api,
account2, account3, admin-api, admin-fine-grained-authz, admin2,
authorization, ciba, client-policies, client-secret-rotation,
declarative-user-profile, docker, dpop, dynamic-scopes, fips, impersonation,
js-adapter, kerberos, linkedin-oauth, map-storage, par, preview,
recovery-codes, scripts, step-up-authentication, token-exchange,
declarative-user-profile, device-flow, docker, dpop, dynamic-scopes, fips,
impersonation, js-adapter, kerberos, linkedin-oauth, map-storage, par,
preview, recovery-codes, scripts, step-up-authentication, token-exchange,
update-email, web-authn.
Hostname:

View file

@ -73,17 +73,17 @@ Feature:
--features <feature> Enables a set of one or more features. Possible values are: account-api,
account2, account3, admin-api, admin-fine-grained-authz, admin2,
authorization, ciba, client-policies, client-secret-rotation,
declarative-user-profile, docker, dpop, dynamic-scopes, fips, impersonation,
js-adapter, kerberos, linkedin-oauth, map-storage, par, preview,
recovery-codes, scripts, step-up-authentication, token-exchange,
declarative-user-profile, device-flow, docker, dpop, dynamic-scopes, fips,
impersonation, js-adapter, kerberos, linkedin-oauth, map-storage, par,
preview, recovery-codes, scripts, step-up-authentication, token-exchange,
update-email, web-authn.
--features-disabled <feature>
Disables a set of one or more features. Possible values are: account-api,
account2, account3, admin-api, admin-fine-grained-authz, admin2,
authorization, ciba, client-policies, client-secret-rotation,
declarative-user-profile, docker, dpop, dynamic-scopes, fips, impersonation,
js-adapter, kerberos, linkedin-oauth, map-storage, par, preview,
recovery-codes, scripts, step-up-authentication, token-exchange,
declarative-user-profile, device-flow, docker, dpop, dynamic-scopes, fips,
impersonation, js-adapter, kerberos, linkedin-oauth, map-storage, par,
preview, recovery-codes, scripts, step-up-authentication, token-exchange,
update-email, web-authn.
Hostname:

View file

@ -138,17 +138,17 @@ Feature:
--features <feature> Enables a set of one or more features. Possible values are: account-api,
account2, account3, admin-api, admin-fine-grained-authz, admin2,
authorization, ciba, client-policies, client-secret-rotation,
declarative-user-profile, docker, dpop, dynamic-scopes, fips, impersonation,
js-adapter, kerberos, linkedin-oauth, map-storage, par, preview,
recovery-codes, scripts, step-up-authentication, token-exchange,
declarative-user-profile, device-flow, docker, dpop, dynamic-scopes, fips,
impersonation, js-adapter, kerberos, linkedin-oauth, map-storage, par,
preview, recovery-codes, scripts, step-up-authentication, token-exchange,
update-email, web-authn.
--features-disabled <feature>
Disables a set of one or more features. Possible values are: account-api,
account2, account3, admin-api, admin-fine-grained-authz, admin2,
authorization, ciba, client-policies, client-secret-rotation,
declarative-user-profile, docker, dpop, dynamic-scopes, fips, impersonation,
js-adapter, kerberos, linkedin-oauth, map-storage, par, preview,
recovery-codes, scripts, step-up-authentication, token-exchange,
declarative-user-profile, device-flow, docker, dpop, dynamic-scopes, fips,
impersonation, js-adapter, kerberos, linkedin-oauth, map-storage, par,
preview, recovery-codes, scripts, step-up-authentication, token-exchange,
update-email, web-authn.
Hostname:

View file

@ -136,17 +136,17 @@ Feature:
--features <feature> Enables a set of one or more features. Possible values are: account-api,
account2, account3, admin-api, admin-fine-grained-authz, admin2,
authorization, ciba, client-policies, client-secret-rotation,
declarative-user-profile, docker, dpop, dynamic-scopes, fips, impersonation,
js-adapter, kerberos, linkedin-oauth, map-storage, par, preview,
recovery-codes, scripts, step-up-authentication, token-exchange,
declarative-user-profile, device-flow, docker, dpop, dynamic-scopes, fips,
impersonation, js-adapter, kerberos, linkedin-oauth, map-storage, par,
preview, recovery-codes, scripts, step-up-authentication, token-exchange,
update-email, web-authn.
--features-disabled <feature>
Disables a set of one or more features. Possible values are: account-api,
account2, account3, admin-api, admin-fine-grained-authz, admin2,
authorization, ciba, client-policies, client-secret-rotation,
declarative-user-profile, docker, dpop, dynamic-scopes, fips, impersonation,
js-adapter, kerberos, linkedin-oauth, map-storage, par, preview,
recovery-codes, scripts, step-up-authentication, token-exchange,
declarative-user-profile, device-flow, docker, dpop, dynamic-scopes, fips,
impersonation, js-adapter, kerberos, linkedin-oauth, map-storage, par,
preview, recovery-codes, scripts, step-up-authentication, token-exchange,
update-email, web-authn.
Hostname:

View file

@ -100,11 +100,15 @@ public class OIDCWellKnownProvider implements WellKnownProvider {
public OIDCWellKnownProvider(KeycloakSession session, Map<String, Object> openidConfigOverride, boolean includeClientScopes) {
DEFAULT_GRANT_TYPES_SUPPORTED = Stream.of(OAuth2Constants.AUTHORIZATION_CODE,
OAuth2Constants.IMPLICIT, OAuth2Constants.REFRESH_TOKEN, OAuth2Constants.PASSWORD, OAuth2Constants.CLIENT_CREDENTIALS,
OAuth2Constants.DEVICE_CODE_GRANT_TYPE,
OAuth2Constants.CIBA_GRANT_TYPE).collect(Collectors.toList());
if (Profile.isFeatureEnabled(Profile.Feature.TOKEN_EXCHANGE)) {
DEFAULT_GRANT_TYPES_SUPPORTED.add(OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE);
}
if (Profile.isFeatureEnabled(Profile.Feature.DEVICE_FLOW)) {
DEFAULT_GRANT_TYPES_SUPPORTED.add(OAuth2Constants.DEVICE_CODE_GRANT_TYPE);
}
this.session = session;
this.openidConfigOverride = openidConfigOverride;
this.includeClientScopes = includeClientScopes;
@ -127,9 +131,11 @@ public class OIDCWellKnownProvider implements WellKnownProvider {
config.setIntrospectionEndpoint(backendUriBuilder.clone().path(OIDCLoginProtocolService.class, "token").path(TokenEndpoint.class, "introspect").build(realm.getName(), OIDCLoginProtocol.LOGIN_PROTOCOL).toString());
config.setUserinfoEndpoint(backendUriBuilder.clone().path(OIDCLoginProtocolService.class, "issueUserInfo").build(realm.getName(), OIDCLoginProtocol.LOGIN_PROTOCOL).toString());
config.setLogoutEndpoint(frontendUriBuilder.clone().path(OIDCLoginProtocolService.class, "logout").build(realm.getName(), OIDCLoginProtocol.LOGIN_PROTOCOL).toString());
config.setDeviceAuthorizationEndpoint(frontendUriBuilder.clone().path(OIDCLoginProtocolService.class, "auth")
.path(AuthorizationEndpoint.class, "authorizeDevice").path(DeviceEndpoint.class, "handleDeviceRequest")
.build(realm.getName(), OIDCLoginProtocol.LOGIN_PROTOCOL).toString());
if (Profile.isFeatureEnabled(Profile.Feature.DEVICE_FLOW)) {
config.setDeviceAuthorizationEndpoint(frontendUriBuilder.clone().path(OIDCLoginProtocolService.class, "auth")
.path(AuthorizationEndpoint.class, "authorizeDevice").path(DeviceEndpoint.class, "handleDeviceRequest")
.build(realm.getName(), OIDCLoginProtocol.LOGIN_PROTOCOL).toString());
}
URI jwksUri = backendUriBuilder.clone().path(OIDCLoginProtocolService.class, "certs").build(realm.getName(),
OIDCLoginProtocol.LOGIN_PROTOCOL);
@ -301,7 +307,9 @@ public class OIDCWellKnownProvider implements WellKnownProvider {
mtls_endpoints.setTokenEndpoint(config.getTokenEndpoint());
mtls_endpoints.setRevocationEndpoint(config.getRevocationEndpoint());
mtls_endpoints.setIntrospectionEndpoint(config.getIntrospectionEndpoint());
mtls_endpoints.setDeviceAuthorizationEndpoint(config.getDeviceAuthorizationEndpoint());
if (Profile.isFeatureEnabled(Profile.Feature.DEVICE_FLOW)) {
mtls_endpoints.setDeviceAuthorizationEndpoint(config.getDeviceAuthorizationEndpoint());
}
mtls_endpoints.setRegistrationEndpoint(config.getRegistrationEndpoint());
mtls_endpoints.setUserInfoEndpoint(config.getUserinfoEndpoint());
mtls_endpoints.setBackchannelAuthenticationEndpoint(config.getBackchannelAuthenticationEndpoint());

View file

@ -125,6 +125,9 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
*/
@Path("device")
public Object authorizeDevice() {
if (!Profile.isFeatureEnabled(Profile.Feature.DEVICE_FLOW)) {
return null;
}
return new DeviceEndpoint(session, event);
}

View file

@ -309,19 +309,26 @@ public class TokenEndpoint {
event.event(EventType.PERMISSION_TOKEN);
action = Action.PERMISSION;
} else if (grantType.equals(OAuth2Constants.DEVICE_CODE_GRANT_TYPE)) {
if (!Profile.isFeatureEnabled(Profile.Feature.DEVICE_FLOW)) {
throw newUnsupportedGrantTypeException();
}
event.event(EventType.OAUTH2_DEVICE_CODE_TO_TOKEN);
action = Action.OAUTH2_DEVICE_CODE;
} else if (grantType.equals(OAuth2Constants.CIBA_GRANT_TYPE)) {
event.event(EventType.AUTHREQID_TO_TOKEN);
action = Action.CIBA;
} else {
throw new CorsErrorResponseException(cors, OAuthErrorException.UNSUPPORTED_GRANT_TYPE,
"Unsupported " + OIDCLoginProtocol.GRANT_TYPE_PARAM, Response.Status.BAD_REQUEST);
throw newUnsupportedGrantTypeException();
}
event.detail(Details.GRANT_TYPE, grantType);
}
private CorsErrorResponseException newUnsupportedGrantTypeException() {
return new CorsErrorResponseException(cors, OAuthErrorException.UNSUPPORTED_GRANT_TYPE,
"Unsupported " + OIDCLoginProtocol.GRANT_TYPE_PARAM, Status.BAD_REQUEST);
}
private void checkParameterDuplicated() {
for (String key : formParams.keySet()) {
if (formParams.get(key).size() != 1) {

View file

@ -20,18 +20,20 @@
package org.keycloak.protocol.oidc.grants.device.endpoints;
import org.keycloak.Config;
import org.keycloak.common.Profile;
import org.keycloak.events.EventBuilder;
import org.keycloak.models.KeycloakContext;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.RealmModel;
import org.keycloak.provider.EnvironmentDependentProviderFactory;
import org.keycloak.services.resource.RealmResourceProvider;
import org.keycloak.services.resource.RealmResourceProviderFactory;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public class DeviceEndpointFactory implements RealmResourceProviderFactory {
public class DeviceEndpointFactory implements RealmResourceProviderFactory, EnvironmentDependentProviderFactory {
@Override
public RealmResourceProvider create(KeycloakSession session) {
@ -60,4 +62,9 @@ public class DeviceEndpointFactory implements RealmResourceProviderFactory {
public String getId() {
return "device";
}
@Override
public boolean isSupported() {
return Profile.isFeatureEnabled(Profile.Feature.DEVICE_FLOW);
}
}

View file

@ -113,8 +113,11 @@ import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Collections;
@ -124,6 +127,7 @@ import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.UUID;
@ -881,6 +885,19 @@ public class TestingResourceProvider implements RealmResourceProvider {
}
private void setFeatureInProfileFile(File file, Profile.Feature featureProfile, String newState) {
doWithProperties(file, props -> {
props.setProperty("feature." + featureProfile.toString().toLowerCase(), newState);
});
}
private void unsetFeatureInProfileFile(File file, Profile.Feature featureProfile) {
doWithProperties(file, props -> {
props.remove("feature." + featureProfile.toString().toLowerCase());
});
}
private void doWithProperties(File file, Consumer<Properties> callback) {
Properties properties = new Properties();
if (file.isFile() && file.exists()) {
try (FileInputStream fis = new FileInputStream(file)) {
@ -890,7 +907,11 @@ public class TestingResourceProvider implements RealmResourceProvider {
}
}
properties.setProperty("feature." + featureProfile.toString().toLowerCase(), newState);
callback.accept(properties);
if (file.isFile() && !file.getParentFile().exists()) {
file.getParentFile().mkdirs();
}
try (FileOutputStream fos = new FileOutputStream(file)) {
properties.store(fos, null);
@ -922,6 +943,30 @@ public class TestingResourceProvider implements RealmResourceProvider {
return updateFeature(feature, false);
}
@POST
@Path("/reset-feature/{feature}")
@Consumes(MediaType.APPLICATION_JSON)
public void resetFeature(@PathParam("feature") String featureKey) {
Profile.Feature feature;
try {
feature = Profile.Feature.valueOf(featureKey);
} catch (IllegalArgumentException e) {
System.err.printf("Feature '%s' doesn't exist!!\n", featureKey);
throw new BadRequestException();
}
FeatureDeployerUtil.initBeforeChangeFeature(feature);
String jbossServerConfigDir = System.getProperty("jboss.server.config.dir");
// If we are in jboss-based container, we need to write profile.properties file, otherwise the change in system property will disappear after restart
if (jbossServerConfigDir != null) {
File file = new File(jbossServerConfigDir, "profile.properties");
unsetFeatureInProfileFile(file, feature);
}
}
private Set<Profile.Feature> updateFeature(String featureKey, boolean shouldEnable) {
Profile.Feature feature;

View file

@ -79,9 +79,9 @@ public class KeycloakTestingClient implements AutoCloseable {
}
public void enableFeature(Profile.Feature feature) {
Set<Profile.Feature> disabledFeatures = testing().enableFeature(feature.toString());
Assert.assertFalse(disabledFeatures.contains(feature));
ProfileAssume.updateDisabledFeatures(disabledFeatures);
Set<Profile.Feature> enabledFeatures = testing().enableFeature(feature.toString());
Assert.assertFalse(enabledFeatures.contains(feature));
ProfileAssume.updateDisabledFeatures(enabledFeatures);
}
public void disableFeature(Profile.Feature feature) {
@ -90,6 +90,15 @@ public class KeycloakTestingClient implements AutoCloseable {
ProfileAssume.updateDisabledFeatures(disabledFeatures);
}
/**
* Resets the feature to it's default setting.
*
* @param feature
*/
public void resetFeature(Profile.Feature feature) {
testing().resetFeature(feature.toString());
}
public TestApplicationResource testApp() { return target.proxy(TestApplicationResource.class); }
public TestSamlApplicationResource testSamlApp() { return target.proxy(TestSamlApplicationResource.class); }

View file

@ -343,6 +343,16 @@ public interface TestingResource {
@Produces(MediaType.APPLICATION_JSON)
Set<Profile.Feature> disableFeature(@PathParam("feature") String feature);
/**
* Resets the given feature to it's default state.
*
* @param feature
*/
@POST
@Path("/reset-feature/{feature}")
@Consumes(MediaType.APPLICATION_JSON)
void resetFeature(@PathParam("feature") String feature);
/**
* If property-value is null, the system property will be unset (removed) on the server
*/

View file

@ -22,19 +22,23 @@ import static org.junit.Assert.assertNull;
import static org.keycloak.models.OAuth2DeviceConfig.DEFAULT_OAUTH2_DEVICE_CODE_LIFESPAN;
import static org.keycloak.models.OAuth2DeviceConfig.DEFAULT_OAUTH2_DEVICE_POLLING_INTERVAL;
import org.apache.http.client.methods.HttpGet;
import org.jboss.arquillian.graphene.page.Page;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.OAuth2Constants;
import org.keycloak.OAuthErrorException;
import org.keycloak.admin.client.resource.ClientResource;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.common.Profile;
import org.keycloak.events.Errors;
import org.keycloak.models.ClientScopeModel;
import org.keycloak.models.OAuth2DeviceConfig;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.oidc.OIDCConfigAttributes;
import org.keycloak.protocol.oidc.grants.device.endpoints.DeviceEndpoint;
import org.keycloak.protocol.oidc.representations.OIDCConfigurationRepresentation;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.UserInfo;
import org.keycloak.representations.idm.ClientRepresentation;
@ -49,6 +53,7 @@ import org.keycloak.testsuite.pages.ErrorPage;
import org.keycloak.testsuite.pages.OAuth2DeviceVerificationPage;
import org.keycloak.testsuite.pages.OAuthGrantPage;
import org.keycloak.testsuite.util.ClientBuilder;
import org.keycloak.testsuite.util.ContainerAssume;
import org.keycloak.testsuite.util.OAuthClient;
import org.keycloak.testsuite.util.RealmBuilder;
import org.keycloak.testsuite.util.UserBuilder;
@ -62,11 +67,11 @@ import org.apache.http.impl.client.CloseableHttpClient;
import org.keycloak.util.BasicAuthHelper;
import org.openqa.selenium.Cookie;
import java.io.IOException;
import java.util.List;
import java.util.LinkedList;
import java.io.UnsupportedEncodingException;
import java.util.concurrent.TimeUnit;
/**
* @author <a href="mailto:h2-wada@nri.co.jp">Hiroyuki Wada</a>
@ -992,6 +997,43 @@ public class OAuth2DeviceAuthorizationGrantTest extends AbstractKeycloakTest {
}
}
@Test
public void ensureDeviceFlowConfigPresentWhenDeviceFlowIsEnabled() {
OIDCConfigurationRepresentation oidcConfigRep = oauth.doWellKnownRequest(REALM_NAME);
Assert.assertNotNull("deviceAuthorizationEndpoint should be not null", oidcConfigRep.getDeviceAuthorizationEndpoint());
Assert.assertNotNull("mtlsEndpointAliases.deviceAuthorizationEndpoint should be not null", oidcConfigRep.getMtlsEndpointAliases().getDeviceAuthorizationEndpoint());
}
@Test
// @DisableFeature(value = Profile.Feature.DEVICE_FLOW, executeAsLast = false, skipRestart = true)
public void ensureDeviceFlowConfigNotPresentWhenDeviceFlowIsDisabled() throws Exception {
// this test currently does not work with -Pauth-server-quarkus
ContainerAssume.assumeAuthServerUndertow();
testingClient.disableFeature(Profile.Feature.DEVICE_FLOW);
try {
OIDCConfigurationRepresentation oidcConfigRep = oauth.doWellKnownRequest(REALM_NAME);
Assert.assertNull("deviceAuthorizationEndpoint should be null", oidcConfigRep.getDeviceAuthorizationEndpoint());
Assert.assertNull("mtlsEndpointAliases.deviceAuthorizationEndpoint should be null", oidcConfigRep.getMtlsEndpointAliases().getDeviceAuthorizationEndpoint());
try (var httpClient = oauth.getHttpClient().get()) {
Assert.assertEquals("Should return not found for device auth endpoint"
, (long) 404
, (long) httpClient.execute(new HttpGet(oauth.getDeviceAuthorizationUrl()), r -> r.getStatusLine().getStatusCode()));
}
oauth.realm(REALM_NAME);
oauth.clientId(DEVICE_APP_PUBLIC);
OAuthClient.AccessTokenResponse tokenResponse = oauth.doDeviceTokenRequest(DEVICE_APP_PUBLIC, null, "dummy");
Assert.assertEquals(OAuthErrorException.UNSUPPORTED_GRANT_TYPE, tokenResponse.getError());
} finally {
testingClient.resetFeature(Profile.Feature.DEVICE_FLOW);
}
}
private void openVerificationPage(String verificationUri) {
driver.navigate().to(verificationUri);
}