diff --git a/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties b/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties index 30bb956c520..a5b0a83e682 100644 --- a/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties +++ b/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties @@ -2848,6 +2848,7 @@ searchScope=Search scope dateFrom=Date(from) importAdded_one=One record added. clientAccessType=It uses the client's access type (confidential, public, bearer-only) to determine whether the policy is applied. Condition is checked during most of OpenID Connect requests (Authorization requests, token requests, introspection endpoint request, etc.). Confidential client has enabled client authentication when public client has disabled client authentication. Bearer-only is a deprecated client type. +identityProviderAlias=Condition that checks the Identity Provider that is involved in the client request. Only applies to operations in which an IdP is involved (for example JWT Authorization grant). firstName=First name emptySecondaryAction=Configure a new mapper defaultGroupAdded_one=New group added to the default groups diff --git a/js/apps/admin-ui/src/clients/add/CapabilityConfig.tsx b/js/apps/admin-ui/src/clients/add/CapabilityConfig.tsx index 47331f44c83..306e4dd55e4 100644 --- a/js/apps/admin-ui/src/clients/add/CapabilityConfig.tsx +++ b/js/apps/admin-ui/src/clients/add/CapabilityConfig.tsx @@ -1,9 +1,5 @@ import type ClientRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientRepresentation"; -import { - HelpItem, - SelectControl, - useFetch, -} from "@keycloak/keycloak-ui-shared"; +import { HelpItem, SelectControl } from "@keycloak/keycloak-ui-shared"; import { Checkbox, FormGroup, @@ -20,13 +16,8 @@ import { FormAccess } from "../../components/form/FormAccess"; import { convertAttributeNameToForm } from "../../util"; import useIsFeatureEnabled, { Feature } from "../../utils/useIsFeatureEnabled"; import { FormFields } from "../ClientDetails"; -import { MultiValuedListComponent } from "../../components/dynamic/MultivaluedListComponent"; -import IdentityProviderRepresentation, { - IdentityProviderType, -} from "@keycloak/keycloak-admin-client/lib/defs/identityProviderRepresentation"; -import { useAdminClient } from "../../admin-client"; -import { useState } from "react"; -import { IdentityProvidersQuery } from "@keycloak/keycloak-admin-client/lib/resources/identityProviders"; +import { IdentityProviderSelect } from "../../components/identity-provider/IdentityProviderSelect"; +import { IdentityProviderType } from "@keycloak/keycloak-admin-client/lib/defs/identityProviderRepresentation"; import { useAccess } from "../../context/access/Access"; type CapabilityConfigProps = { @@ -38,7 +29,6 @@ export const CapabilityConfig = ({ unWrap, protocol: type, }: CapabilityConfigProps) => { - const { adminClient } = useAdminClient(); const { t } = useTranslation(); const { control, watch, setValue } = useFormContext(); const protocol = type || watch("protocol"); @@ -51,28 +41,8 @@ export const CapabilityConfig = ({ false, ); const isFeatureEnabled = useIsFeatureEnabled(); - const [idps, setIdps] = useState([]); - const [search, setSearch] = useState(""); const { hasSomeAccess } = useAccess(); const showIdentityProviders = hasSomeAccess("view-identity-providers"); - useFetch( - async () => { - if (!showIdentityProviders) { - return []; - } - const params: IdentityProvidersQuery = { - max: 20, - realmOnly: true, - }; - if (search) { - params.search = search; - } - params.type = IdentityProviderType.JWT_AUTHORIZATION_GRANT; - return await adminClient.identityProviders.find(params); - }, - setIdps, - [search], - ); return ( ( "attributes.oauth2.jwt.authorization.grant.idp", )} label={t("jwtAuthorizationGrantIdp")} helpText={t("jwtAuthorizationGrantIdpHelp")} convertToName={convertAttributeNameToForm} - stringify + identityProviderType={ + IdentityProviderType.JWT_AUTHORIZATION_GRANT + } isDisabled={clientAuthentication} - options={idps.map(({ alias }) => alias ?? "")} - onSearch={setSearch} + realmOnly + stringify /> )} {isFeatureEnabled(Feature.DPoP) && ( diff --git a/js/apps/admin-ui/src/components/dynamic/IdentityProviderMultiSelectComponent.tsx b/js/apps/admin-ui/src/components/dynamic/IdentityProviderMultiSelectComponent.tsx new file mode 100644 index 00000000000..f1ecbf8e409 --- /dev/null +++ b/js/apps/admin-ui/src/components/dynamic/IdentityProviderMultiSelectComponent.tsx @@ -0,0 +1,10 @@ +import type { ComponentProps } from "./components"; +import { IdentityProviderSelect } from "../identity-provider/IdentityProviderSelect"; + +export const IdentityProviderMultiSelectComponent = (props: ComponentProps) => ( + +); diff --git a/js/apps/admin-ui/src/components/dynamic/MultivaluedListComponent.tsx b/js/apps/admin-ui/src/components/dynamic/MultivaluedListComponent.tsx index f0586bc9c9e..a75e863d947 100644 --- a/js/apps/admin-ui/src/components/dynamic/MultivaluedListComponent.tsx +++ b/js/apps/admin-ui/src/components/dynamic/MultivaluedListComponent.tsx @@ -1,4 +1,5 @@ import { + FormErrorText, HelpItem, KeycloakSelect, SelectVariant, @@ -17,6 +18,10 @@ function toStringValue(formValue: string[]): string { return formValue.join("##"); } +type MultiValuedListComponentProps = ComponentProps & { + variant?: `${SelectVariant}`; +}; + export const MultiValuedListComponent = ({ name, label, @@ -28,9 +33,13 @@ export const MultiValuedListComponent = ({ required, convertToName, onSearch, -}: ComponentProps) => { + variant = SelectVariant.typeaheadMulti, +}: MultiValuedListComponentProps) => { const { t } = useTranslation(); - const { control } = useFormContext(); + const { + control, + formState: { errors }, + } = useFormContext(); const [open, setOpen] = useState(false); function setSearch(value: string) { @@ -39,6 +48,14 @@ export const MultiValuedListComponent = ({ } } + const convertedName = convertToName(name!); + + const getError = () => { + return convertedName + .split(".") + .reduce((record: any, key) => record?.[key], errors); + }; + return ( ( - setOpen(isOpen)} - selections={ - stringify ? stringToMultiline(field.value) : field.value - } - onSelect={(v) => { - const option = v.toString(); - const values = stringify - ? stringToMultiline(field.value) - : field.value; - let newValue; - if (values.includes(option)) { - newValue = values.filter((item: string) => item !== option); - } else { - newValue = [...values, option]; + <> + { - field.onChange(stringify ? "" : []); - }} - onFilter={(value) => setSearch(value)} - isOpen={open} - aria-label={t(label!)} - > - {options?.map((option) => ( - - {option} - - ))} - + onSelect={(v) => { + const option = v.toString(); + if (variant === SelectVariant.typeaheadMulti) { + const values = stringify + ? stringToMultiline(field.value) + : field.value; + let newValue; + if (values.includes(option)) { + newValue = values.filter((item: string) => item !== option); + } else if (option !== "") { + newValue = [...values, option]; + } else { + newValue = values; + } + field.onChange( + stringify && variant === SelectVariant.typeaheadMulti + ? toStringValue(newValue) + : newValue, + ); + } else { + field.onChange(option); + } + }} + onClear={() => { + field.onChange( + stringify || variant !== SelectVariant.typeaheadMulti + ? "" + : [], + ); + }} + onFilter={setSearch} + isOpen={open} + > + {options?.map((option) => ( + + {option} + + ))} + + {getError() && } + )} /> diff --git a/js/apps/admin-ui/src/components/dynamic/components.ts b/js/apps/admin-ui/src/components/dynamic/components.ts index a217572a304..1df06b8e200 100644 --- a/js/apps/admin-ui/src/components/dynamic/components.ts +++ b/js/apps/admin-ui/src/components/dynamic/components.ts @@ -3,6 +3,7 @@ import { FunctionComponent } from "react"; import { BooleanComponent } from "./BooleanComponent"; import { ClientSelectComponent } from "./ClientSelectComponent"; +import { IdentityProviderMultiSelectComponent } from "./IdentityProviderMultiSelectComponent"; import { FileComponent } from "./FileComponent"; import { GroupComponent } from "./GroupComponent"; import { ListComponent } from "./ListComponent"; @@ -45,6 +46,7 @@ type ComponentType = | "Group" | "MultivaluedList" | "ClientList" + | "IdentityProviderMultiList" | "UserProfileAttributeList" | "MultivaluedString" | "File" @@ -65,6 +67,7 @@ export const COMPONENTS: { Map: MapComponent, Group: GroupComponent, ClientList: ClientSelectComponent, + IdentityProviderMultiList: IdentityProviderMultiSelectComponent, UserProfileAttributeList: UserProfileAttributeListComponent, MultivaluedList: MultiValuedListComponent, MultivaluedString: MultiValuedStringComponent, diff --git a/js/apps/admin-ui/src/components/identity-provider/IdentityProviderSelect.tsx b/js/apps/admin-ui/src/components/identity-provider/IdentityProviderSelect.tsx new file mode 100644 index 00000000000..bf11274e27e --- /dev/null +++ b/js/apps/admin-ui/src/components/identity-provider/IdentityProviderSelect.tsx @@ -0,0 +1,51 @@ +import type IdentityProviderRepresentation from "@keycloak/keycloak-admin-client/lib/defs/identityProviderRepresentation"; +import { IdentityProviderType } from "@keycloak/keycloak-admin-client/lib/defs/identityProviderRepresentation"; +import type { IdentityProvidersQuery } from "@keycloak/keycloak-admin-client/lib/resources/identityProviders"; +import { SelectVariant, useFetch } from "@keycloak/keycloak-ui-shared"; +import { useState } from "react"; +import { useAdminClient } from "../../admin-client"; +import type { ComponentProps } from "../dynamic/components"; +import { MultiValuedListComponent } from "../dynamic/MultivaluedListComponent"; + +type IdentityProviderSelectProps = ComponentProps & { + variant?: `${SelectVariant}`; + identityProviderType?: IdentityProviderType; + realmOnly?: boolean; +}; + +export const IdentityProviderSelect = ({ + identityProviderType = IdentityProviderType.ANY, + realmOnly = false, + ...props +}: IdentityProviderSelectProps) => { + const { adminClient } = useAdminClient(); + + const [identityProviders, setIdentityProviders] = useState< + IdentityProviderRepresentation[] + >([]); + const [search, setSearch] = useState(""); + + useFetch( + () => { + const params: IdentityProvidersQuery = { + max: 20, + type: identityProviderType, + realmOnly: realmOnly, + }; + if (search) { + params.search = search; + } + return adminClient.identityProviders.find(params); + }, + (identityProviders) => setIdentityProviders(identityProviders), + [search], + ); + + return ( + alias!)} + /> + ); +}; diff --git a/server-spi/src/main/java/org/keycloak/provider/ProviderConfigProperty.java b/server-spi/src/main/java/org/keycloak/provider/ProviderConfigProperty.java index a301fb580db..2bfd963d0e4 100755 --- a/server-spi/src/main/java/org/keycloak/provider/ProviderConfigProperty.java +++ b/server-spi/src/main/java/org/keycloak/provider/ProviderConfigProperty.java @@ -84,6 +84,8 @@ public class ProviderConfigProperty { */ public static final String URL_TYPE ="Url"; + public static final String IDENTITY_PROVIDER_MULTI_LIST_TYPE="IdentityProviderMultiList"; // only in admin console, not in themes + protected String name; protected String label; protected String helpText; diff --git a/services/src/main/java/org/keycloak/services/clientpolicy/condition/IdentityProviderCondition.java b/services/src/main/java/org/keycloak/services/clientpolicy/condition/IdentityProviderCondition.java new file mode 100644 index 00000000000..7c4d68a66a7 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/clientpolicy/condition/IdentityProviderCondition.java @@ -0,0 +1,78 @@ +/* + * Copyright 2025 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.services.clientpolicy.condition; + +import java.util.List; + +import org.keycloak.models.KeycloakSession; +import org.keycloak.representations.idm.ClientPolicyConditionConfigurationRepresentation; +import org.keycloak.services.clientpolicy.ClientPolicyContext; +import org.keycloak.services.clientpolicy.ClientPolicyException; +import org.keycloak.services.clientpolicy.ClientPolicyVote; +import org.keycloak.services.clientpolicy.context.JWTAuthorizationGrantContext; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * + * @author rmartinc + */ +public class IdentityProviderCondition extends AbstractClientPolicyConditionProvider { + + public static class Configuration extends ClientPolicyConditionConfigurationRepresentation { + + @JsonProperty(IdentityProviderConditionFactory.IDENTITY_PROVIDERS_ALIASES) + protected List identityProviderAliases; + + public List getIdentityProviderAliases() { + return identityProviderAliases; + } + + public void setIdentityProviderAliases(List identityProviderAliases) { + this.identityProviderAliases = identityProviderAliases; + } + } + + public IdentityProviderCondition(KeycloakSession session) { + super(session); + } + + @Override + public Class getConditionConfigurationClass() { + return Configuration.class; + } + + @Override + public String getProviderId() { + return IdentityProviderConditionFactory.PROVIDER_ID; + } + + @Override + public ClientPolicyVote applyPolicy(ClientPolicyContext context) throws ClientPolicyException { + return switch (context.getEvent()) { + case JWT_AUTHORIZATION_GRANT -> isIdentityProvider(((JWTAuthorizationGrantContext) context).getIdentityProvider().getAlias()) + ? ClientPolicyVote.YES + : ClientPolicyVote.NO; + default -> ClientPolicyVote.ABSTAIN; + }; + } + + private boolean isIdentityProvider(String identityProviderAlias) throws ClientPolicyException { + return configuration.identityProviderAliases.contains(identityProviderAlias); + } +} diff --git a/services/src/main/java/org/keycloak/services/clientpolicy/condition/IdentityProviderConditionFactory.java b/services/src/main/java/org/keycloak/services/clientpolicy/condition/IdentityProviderConditionFactory.java new file mode 100644 index 00000000000..fe537d8066d --- /dev/null +++ b/services/src/main/java/org/keycloak/services/clientpolicy/condition/IdentityProviderConditionFactory.java @@ -0,0 +1,70 @@ +/* + * Copyright 2025 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.services.clientpolicy.condition; + +import java.util.List; + +import org.keycloak.models.KeycloakSession; +import org.keycloak.provider.ProviderConfigProperty; +import org.keycloak.provider.ProviderConfigurationBuilder; + +/** + *

Condition that defines a list of Identity Provider aliases and checks if the + * alias in the client policy context is (or is not) part of that list.

+ * + * @author rmartinc + */ +public class IdentityProviderConditionFactory extends AbstractClientPolicyConditionProviderFactory { + + public static final String PROVIDER_ID = "identity-provider-alias"; + public static final String IDENTITY_PROVIDERS_ALIASES = "identity_provider_aliases"; + + @Override + public ClientPolicyConditionProvider create(KeycloakSession session) { + return new IdentityProviderCondition(session); + } + + @Override + public String getId() { + return PROVIDER_ID; + } + + @Override + public String getHelpText() { + return """ + Condition that checks the Identity Provider that is involved in the client request. + Only applies to operations in which an IdP is involved (for example JWT Authorization grant). + """; + } + + @Override + public List getConfigProperties() { + List properties = ProviderConfigurationBuilder.create() + .property() + .name(IDENTITY_PROVIDERS_ALIASES) + .type(ProviderConfigProperty.IDENTITY_PROVIDER_MULTI_LIST_TYPE) + .label("Identity provider aliases") + .helpText("List of Identity Provider aliases to take into consideration for the condition.") + .required(Boolean.TRUE) + .add() + .build(); + addCommonConfigProperties(properties); + return properties; + } + +} diff --git a/services/src/main/resources/META-INF/services/org.keycloak.services.clientpolicy.condition.ClientPolicyConditionProviderFactory b/services/src/main/resources/META-INF/services/org.keycloak.services.clientpolicy.condition.ClientPolicyConditionProviderFactory index 6c4b5a6ac6f..e7cfa1bbdca 100644 --- a/services/src/main/resources/META-INF/services/org.keycloak.services.clientpolicy.condition.ClientPolicyConditionProviderFactory +++ b/services/src/main/resources/META-INF/services/org.keycloak.services.clientpolicy.condition.ClientPolicyConditionProviderFactory @@ -10,3 +10,4 @@ org.keycloak.services.clientpolicy.condition.ClientProtocolConditionFactory org.keycloak.services.clientpolicy.condition.ClientAttributesConditionFactory org.keycloak.services.clientpolicy.condition.AcrConditionFactory org.keycloak.services.clientpolicy.condition.GrantTypeConditionFactory +org.keycloak.services.clientpolicy.condition.IdentityProviderConditionFactory diff --git a/test-framework/core/src/main/java/org/keycloak/testframework/realm/ClientPolicyBuilder.java b/test-framework/core/src/main/java/org/keycloak/testframework/realm/ClientPolicyBuilder.java index d71384512bd..fcf358a40f7 100644 --- a/test-framework/core/src/main/java/org/keycloak/testframework/realm/ClientPolicyBuilder.java +++ b/test-framework/core/src/main/java/org/keycloak/testframework/realm/ClientPolicyBuilder.java @@ -8,6 +8,7 @@ import org.keycloak.representations.idm.ClientPolicyConditionConfigurationRepres import org.keycloak.representations.idm.ClientPolicyConditionRepresentation; import org.keycloak.representations.idm.ClientPolicyRepresentation; import org.keycloak.services.clientpolicy.condition.GrantTypeCondition; +import org.keycloak.services.clientpolicy.condition.IdentityProviderCondition; import org.keycloak.util.JsonSerialization; import com.fasterxml.jackson.databind.JsonNode; @@ -30,14 +31,24 @@ public class ClientPolicyBuilder { return new ClientPolicyBuilder(rep); } - public static GrantTypeCondition.Configuration grantTypeConditionConfiguration(String... types) { + public static GrantTypeCondition.Configuration grantTypeConditionConfiguration(boolean negativeLogic, String... types) { GrantTypeCondition.Configuration config = new GrantTypeCondition.Configuration(); + config.setNegativeLogic(negativeLogic); if (types != null && types.length > 0) { config.setGrantTypes(List.of(types)); } return config; } + public static IdentityProviderCondition.Configuration identityProviderConditionConfiguration(boolean negativeLogic, String... aliases) { + IdentityProviderCondition.Configuration config = new IdentityProviderCondition.Configuration(); + config.setNegativeLogic(negativeLogic); + if (aliases != null && aliases.length > 0) { + config.setIdentityProviderAliases(List.of(aliases)); + } + return config; + } + public static ClientPolicyBuilder update(ClientPolicyRepresentation rep) { return new ClientPolicyBuilder(rep); } diff --git a/tests/base/src/test/java/org/keycloak/tests/oauth/JWTAuthorizationGrantDownscopeClientPoliciesTest.java b/tests/base/src/test/java/org/keycloak/tests/oauth/JWTAuthorizationGrantDownscopeClientPoliciesTest.java index f3889cb9464..f45c51b554a 100644 --- a/tests/base/src/test/java/org/keycloak/tests/oauth/JWTAuthorizationGrantDownscopeClientPoliciesTest.java +++ b/tests/base/src/test/java/org/keycloak/tests/oauth/JWTAuthorizationGrantDownscopeClientPoliciesTest.java @@ -75,7 +75,7 @@ public class JWTAuthorizationGrantDownscopeClientPoliciesTest extends BaseAbstra .name("policy") .description("description of policy") .condition(GrantTypeConditionFactory.PROVIDER_ID, ClientPolicyBuilder.grantTypeConditionConfiguration( - OAuth2Constants.JWT_AUTHORIZATION_GRANT)) + false, OAuth2Constants.JWT_AUTHORIZATION_GRANT)) .profile("executor") .build()); diff --git a/tests/base/src/test/java/org/keycloak/tests/oauth/JWTIdentityProviderConditionDownscopeClientPoliciesTest.java b/tests/base/src/test/java/org/keycloak/tests/oauth/JWTIdentityProviderConditionDownscopeClientPoliciesTest.java new file mode 100644 index 00000000000..d213908aecb --- /dev/null +++ b/tests/base/src/test/java/org/keycloak/tests/oauth/JWTIdentityProviderConditionDownscopeClientPoliciesTest.java @@ -0,0 +1,62 @@ +/* + * Copyright 2025 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.tests.oauth; + +import org.keycloak.services.clientpolicy.condition.IdentityProviderConditionFactory; +import org.keycloak.services.clientpolicy.executor.DownscopeAssertionGrantEnforcerExecutorFactory; +import org.keycloak.testframework.annotations.InjectRealm; +import org.keycloak.testframework.annotations.KeycloakIntegrationTest; +import org.keycloak.testframework.realm.ClientPolicyBuilder; +import org.keycloak.testframework.realm.ClientProfileBuilder; +import org.keycloak.testframework.realm.ManagedRealm; +import org.keycloak.testframework.realm.RealmConfigBuilder; + +/** + * + * @author rmartinc + */ +@KeycloakIntegrationTest(config = JWTAuthorizationGrantTest.JWTAuthorizationGrantServerConfig.class) +public class JWTIdentityProviderConditionDownscopeClientPoliciesTest extends JWTAuthorizationGrantDownscopeClientPoliciesTest { + + @InjectRealm(config = JWTAuthorizationGranthRealmConfig.class) + protected ManagedRealm realm; + + public static class JWTAuthorizationGranthRealmConfig extends OIDCIdentityProviderJWTAuthorizationGrantTest.JWTAuthorizationGrantRealmConfig { + + @Override + public RealmConfigBuilder configure(RealmConfigBuilder realm) { + super.configure(realm); + + realm.clientProfile(ClientProfileBuilder.create() + .name("executor") + .description("executor description") + .executor(DownscopeAssertionGrantEnforcerExecutorFactory.PROVIDER_ID, null) + .build()); + + realm.clientPolicy(ClientPolicyBuilder.create() + .name("policy") + .description("description of policy") + .condition(IdentityProviderConditionFactory.PROVIDER_ID, ClientPolicyBuilder.identityProviderConditionConfiguration( + false, "other", IDP_ALIAS)) + .profile("executor") + .build()); + + return realm; + } + } +} diff --git a/tests/base/src/test/java/org/keycloak/tests/oauth/JWTNegativeIdentityProviderConditionDownscopeClientPoliciesTest.java b/tests/base/src/test/java/org/keycloak/tests/oauth/JWTNegativeIdentityProviderConditionDownscopeClientPoliciesTest.java new file mode 100644 index 00000000000..9e7e3b49aff --- /dev/null +++ b/tests/base/src/test/java/org/keycloak/tests/oauth/JWTNegativeIdentityProviderConditionDownscopeClientPoliciesTest.java @@ -0,0 +1,62 @@ +/* + * Copyright 2025 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.tests.oauth; + +import org.keycloak.services.clientpolicy.condition.IdentityProviderConditionFactory; +import org.keycloak.services.clientpolicy.executor.DownscopeAssertionGrantEnforcerExecutorFactory; +import org.keycloak.testframework.annotations.InjectRealm; +import org.keycloak.testframework.annotations.KeycloakIntegrationTest; +import org.keycloak.testframework.realm.ClientPolicyBuilder; +import org.keycloak.testframework.realm.ClientProfileBuilder; +import org.keycloak.testframework.realm.ManagedRealm; +import org.keycloak.testframework.realm.RealmConfigBuilder; + +/** + * + * @author rmartinc + */ +@KeycloakIntegrationTest(config = JWTAuthorizationGrantTest.JWTAuthorizationGrantServerConfig.class) +public class JWTNegativeIdentityProviderConditionDownscopeClientPoliciesTest extends JWTAuthorizationGrantDownscopeClientPoliciesTest { + + @InjectRealm(config = JWTAuthorizationGranthRealmConfig.class) + protected ManagedRealm realm; + + public static class JWTAuthorizationGranthRealmConfig extends OIDCIdentityProviderJWTAuthorizationGrantTest.JWTAuthorizationGrantRealmConfig { + + @Override + public RealmConfigBuilder configure(RealmConfigBuilder realm) { + super.configure(realm); + + realm.clientProfile(ClientProfileBuilder.create() + .name("executor") + .description("executor description") + .executor(DownscopeAssertionGrantEnforcerExecutorFactory.PROVIDER_ID, null) + .build()); + + realm.clientPolicy(ClientPolicyBuilder.create() + .name("policy") + .description("description of policy") + .condition(IdentityProviderConditionFactory.PROVIDER_ID, ClientPolicyBuilder.identityProviderConditionConfiguration( + true, "other")) + .profile("executor") + .build()); + + return realm; + } + } +}