diff --git a/.changeset/rich-laws-change.md b/.changeset/rich-laws-change.md new file mode 100644 index 00000000000..fbb39f43ff2 --- /dev/null +++ b/.changeset/rich-laws-change.md @@ -0,0 +1,6 @@ +--- +"@wso2is/admin.applications.v1": major +"@wso2is/i18n": major +--- + +Add Access Token Attributes diff --git a/features/admin.applications.v1/components/access-token-attribute-option.scss b/features/admin.applications.v1/components/access-token-attribute-option.scss new file mode 100644 index 00000000000..2c52b7b21c0 --- /dev/null +++ b/features/admin.applications.v1/components/access-token-attribute-option.scss @@ -0,0 +1,21 @@ +/** + * Copyright (c) 2024, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you 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. + */ + + .access-token-attribute-option-checkbox { + padding: 4px; +} diff --git a/features/admin.applications.v1/components/access-token-attribute-option.tsx b/features/admin.applications.v1/components/access-token-attribute-option.tsx new file mode 100644 index 00000000000..450cc96e6b9 --- /dev/null +++ b/features/admin.applications.v1/components/access-token-attribute-option.tsx @@ -0,0 +1,76 @@ +/** + * Copyright (c) 2024, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you 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. + */ + +import Checkbox from "@oxygen-ui/react/Checkbox"; +import Grid from "@oxygen-ui/react/Grid"; +import ListItemText from "@oxygen-ui/react/ListItemText"; +import { IdentifiableComponentInterface } from "@wso2is/core/models"; +import { Code } from "@wso2is/react-components"; +import React, { + FunctionComponent, + HTMLAttributes, + ReactElement +} from "react"; +import "./access-token-attribute-option.scss"; + +interface AccessTokenAttributeOptionPropsInterface extends IdentifiableComponentInterface { + /** + * Is the option selected. + */ + selected?: boolean; + /** + * The display name of the option. + */ + displayName: string; + /** + * The claim URI of the option. + */ + claimURI: string; + /** + * The props passed to the option. + */ + renderOptionProps: HTMLAttributes +} + +export const AccessTokenAttributeOption: FunctionComponent = ( + props: AccessTokenAttributeOptionPropsInterface +): ReactElement => { + + const { + selected, + displayName, + claimURI, + renderOptionProps + } = props; + + return ( +
  • + + + + + + + + { claimURI } + + + +
  • + ); +}; diff --git a/features/admin.applications.v1/components/forms/inbound-oidc-form.scss b/features/admin.applications.v1/components/forms/inbound-oidc-form.scss index 4d7397971c9..1ad162e6302 100644 --- a/features/admin.applications.v1/components/forms/inbound-oidc-form.scss +++ b/features/admin.applications.v1/components/forms/inbound-oidc-form.scss @@ -20,3 +20,25 @@ margin-top: 0 !important; margin-right: 1rem !important; } + +.access-token-attributes-feature-banner { + padding-top: 1rem !important; +} + +.access-token-attributes-dropdown { + padding-top: 1rem !important; +} + +.access-token-attributes-dropdown-input { + input { + border: none !important; + width: auto !important; + } + .MuiInputBase-root { + margin-top: .5em !important; + } + + .MuiFormLabel-root { + font-size: .92857143em !important; + } +} diff --git a/features/admin.applications.v1/components/forms/inbound-oidc-form.tsx b/features/admin.applications.v1/components/forms/inbound-oidc-form.tsx index 2193b5c171c..cc9a7d74157 100644 --- a/features/admin.applications.v1/components/forms/inbound-oidc-form.tsx +++ b/features/admin.applications.v1/components/forms/inbound-oidc-form.tsx @@ -16,8 +16,15 @@ * under the License. */ +import Alert from "@oxygen-ui/react/Alert"; +import Autocomplete, { + AutocompleteRenderGetTagProps, + AutocompleteRenderInputParams +} from "@oxygen-ui/react/Autocomplete"; import Box from "@oxygen-ui/react/Box"; import Chip from "@oxygen-ui/react/Chip"; +import TextField from "@oxygen-ui/react/TextField"; +import { getAllExternalClaims, getAllLocalClaims } from "@wso2is/admin.claims.v1/api"; import { AppState, ConfigReducerStateInterface } from "@wso2is/admin.core.v1"; import useGlobalVariables from "@wso2is/admin.core.v1/hooks/use-global-variables"; import { applicationConfig } from "@wso2is/admin.extensions.v1"; @@ -30,6 +37,8 @@ import { IdentityAppsApiException } from "@wso2is/core/exceptions"; import { isFeatureEnabled } from "@wso2is/core/helpers"; import { AlertLevels, + Claim, + ExternalClaim, FeatureAccessConfigInterface, IdentifiableComponentInterface, TestableComponentInterface @@ -62,9 +71,11 @@ import React, { ChangeEvent, Fragment, FunctionComponent, + HTMLAttributes, MouseEvent, MutableRefObject, ReactElement, + SyntheticEvent, useEffect, useRef, useState @@ -73,6 +84,7 @@ import { Trans, useTranslation } from "react-i18next"; import { useDispatch, useSelector } from "react-redux"; import { Dispatch } from "redux"; import { Button, Container, Divider, DropdownProps, Form, Grid, Label, List, Table } from "semantic-ui-react"; +import { OIDCScopesManagementConstants } from "../../../admin.oidc-scopes.v1/constants"; import { getGeneralIcons } from "../../configs/ui"; import { ApplicationManagementConstants } from "../../constants"; import CustomApplicationTemplate from @@ -102,7 +114,9 @@ import { additionalSpProperty } from "../../models"; import { ApplicationManagementUtils } from "../../utils/application-management-utils"; +import { AccessTokenAttributeOption } from "../access-token-attribute-option"; import { ApplicationCertificateWrapper } from "../settings/certificate"; +import "./inbound-oidc-form.scss"; /** * Proptypes for the inbound OIDC form component. @@ -248,6 +262,12 @@ export const InboundOIDCForm: FunctionComponent = isRefreshTokenWithoutAllowedGrantType, setRefreshTokenWithoutAlllowdGrantType ] = useState(false); + const [ activeOption, setActiveOption ] = useState(undefined); + const [ claims, setClaims ] = useState([]); + const [ externalClaims, setExternalClaims ] = useState([]); + const [ selectedAccessTokenAttributes, setSelectedAccessTokenAttributes ] = useState(undefined); + const [ accessTokenAttributes, setAccessTokenAttributes ] = useState([]); + const [ accessTokenAttributesEnabled, setAccessTokenAttributesEnabled ] = useState(false); const [ isSubjectTokenEnabled, setIsSubjectTokenEnabled ] = useState(false); const [ isSubjectTokenFeatureAvailable, setIsSubjectTokenFeatureAvailable ] = useState(false); const config: ConfigReducerStateInterface = useSelector((state: AppState) => state.config); @@ -435,6 +455,75 @@ export const InboundOIDCForm: FunctionComponent = ); }, [ application ]); + const fetchLocalClaims = () => { + getAllLocalClaims(null) + .then((response: Claim[]) => { + setClaims(response); + }) + .catch(() => { + dispatch(addAlert({ + description: t("claims:local.notifications.fetchLocalClaims.genericError.description"), + level: AlertLevels.ERROR, + message: t("claims:local.notifications.fetchLocalClaims.genericError.message") + })); + }); + }; + + const fetchExternalClaims = () => { + getAllExternalClaims(OIDCScopesManagementConstants.OIDC_ATTRIBUTE_ID, null) + .then((response: ExternalClaim[]) => { + setExternalClaims(response); + }) + .catch(() => { + dispatch(addAlert({ + description: t("claims:external.notifications.fetchExternalClaims.genericError.description"), + level: AlertLevels.ERROR, + message: t("claims:external.notifications.fetchExternalClaims.genericError.message") + })); + }); + }; + + useEffect(() => { + fetchLocalClaims(); + fetchExternalClaims(); + }, []); + + + useEffect(() => { + if (claims?.length > 0 && externalClaims?.length > 0) { + const updatedAttributes : ExternalClaim[] = externalClaims.map((externalClaim : ExternalClaim) => { + const matchedLocalClaim: Claim = claims.find((localClaim: Claim) => + localClaim.claimURI === externalClaim.mappedLocalClaimURI + ); + + if (matchedLocalClaim?.displayName) { + return { + ...externalClaim, + localClaimDisplayName: matchedLocalClaim.displayName + }; + } + + return externalClaim; + }); + + setAccessTokenAttributes(updatedAttributes); + } + }, [ claims, externalClaims ]); + + useEffect(() => { + if (!initialValues.accessToken.accessTokenAttributes) { + return; + } + + const selectedAttributes: ExternalClaim[] = initialValues.accessToken.accessTokenAttributes + .map((claim: string) => accessTokenAttributes + .find((claimObj: ExternalClaim) => claimObj.claimURI === claim)) + .filter((claimObj: ExternalClaim | undefined) => claimObj !== undefined); + + setSelectedAccessTokenAttributes(selectedAttributes); + setAccessTokenAttributesEnabled(initialValues.accessToken.accessTokenAttributesEnabled); + }, [ accessTokenAttributes ]); + useEffect(() => { const isSharedWithAll: additionalSpProperty[] = application?.advancedConfigurations ?.additionalSpProperties?.filter((property: additionalSpProperty) => @@ -1210,6 +1299,8 @@ export const InboundOIDCForm: FunctionComponent = if (!isSystemApplication && !isDefaultApplication) { let inboundConfigFormValues: any = { accessToken: { + accessTokenAttributes: selectedAccessTokenAttributes?.map((claim: ExternalClaim) => claim.claimURI), + accessTokenAttributesEnabled: accessTokenAttributesEnabled, applicationAccessTokenExpiryInSeconds: values.get("applicationAccessTokenExpiryInSeconds") ? Number(values.get("applicationAccessTokenExpiryInSeconds")) : Number(metadata?.defaultApplicationAccessTokenExpiryTime), @@ -2603,6 +2694,157 @@ export const InboundOIDCForm: FunctionComponent = readOnly={ readOnly } data-testid={ `${ testId }-access-token-type-radio-group` } /> + { isJWTAccessTokenTypeSelected && + isFeatureEnabled(applicationFeatureConfig, "applications.accessTokenAttributes") ? ( + + { !initialValues?.accessToken?.accessTokenAttributesEnabled && ( + + + + Previously, all attributes marked as + requested in the application's + User Attributes section (referred + to as requested attributes) were automatically included in the + access token. With the latest update, admins can now choose + which attributes to include in the access token. To enable + this feature, select + Enable Access Token Attributes. + To ensure a smooth transition from the old behavior, selecting + it for the first time will populate the + Access Token Attributes + section with all the requested attributes. Admins can then + remove any unwanted attributes. After saving the changes, + only the selected attributes will be included in the access + token. Moving forward, all requested attributes will appear + in a dropdown for admins to manage as needed. + + Important: Once updated, requested attributes are no longer + automatically included in the access token and this change is + irreversible. Admin-selected attributes will be included in + the access token even without requiring the relevant OIDC + scopes. + Proceed with caution.. + + + ): void => { + const accessTokenAttributesEnabled: boolean = + values.get("accessTokenAttributesEnabledConfig") + .includes("accessTokenAttributesEnabled"); + + setAccessTokenAttributesEnabled(accessTokenAttributesEnabled); + } } + value={ + initialValues?.accessToken?.accessTokenAttributesEnabled + ? [ "accessTokenAttributesEnabled" ] + : [] + } + children={ [ + { + label: t("applications:forms.inboundOIDC.sections" + + ".accessToken.fields.accessTokenAttributes.enable.label"), + value: "accessTokenAttributesEnabled" + } + ] } + readOnly={ readOnly } + data-testid={ + `${ testId }-access-token-attributes-enabled-checkbox` + } + /> + + ) } + + claim.claimURI + } + renderInput={ (params: AutocompleteRenderInputParams) => ( + + ) } + onChange={ (event: SyntheticEvent, claims: ExternalClaim[]) => { + setIsFormStale(true); + setSelectedAccessTokenAttributes(claims); + } } + isOptionEqualToValue={ + (option: ExternalClaim, value: ExternalClaim) => + option.id === value.id + } + renderTags={ ( + value: ExternalClaim[], + getTagProps: AutocompleteRenderGetTagProps + ) => value.map((option: ExternalClaim, index: number) => ( + claim.id === option.id + ) + ? "solid" + : "outlined" + } + /> + )) } + renderOption={ ( + props: HTMLAttributes, + option: ExternalClaim, + { selected }: { selected: boolean } + ) => ( + + ) } + /> + + + Select the attributes that should be included in + the access_token. + + + + + ) : null } ) diff --git a/features/admin.applications.v1/components/settings/access-configuration.tsx b/features/admin.applications.v1/components/settings/access-configuration.tsx index efcadc478b6..501a69118bb 100644 --- a/features/admin.applications.v1/components/settings/access-configuration.tsx +++ b/features/admin.applications.v1/components/settings/access-configuration.tsx @@ -16,6 +16,7 @@ * under the License. */ +import { useRequiredScopes } from "@wso2is/access-control"; import { AppState, AuthenticatorAccordion, @@ -24,7 +25,6 @@ import { store } from "@wso2is/admin.core.v1"; import { applicationConfig } from "@wso2is/admin.extensions.v1"; -import { hasRequiredScopes } from "@wso2is/core/helpers"; import { AlertLevels, IdentifiableComponentInterface, SBACInterface } from "@wso2is/core/models"; import { addAlert } from "@wso2is/core/store"; import { FormValue } from "@wso2is/forms"; @@ -216,6 +216,7 @@ export const AccessConfiguration: FunctionComponent state.application.meta.protocolMeta); - const allowedScopes: string = useSelector((state: AppState) => state?.auth?.allowedScopes); const tenantName: string = store.getState().config.deployment.tenant; const allowMultipleProtocol: boolean = useSelector( (state: AppState) => state.config.deployment.allowMultipleAppProtocols); - const organizationType: string = useSelector((state: AppState) => state?.organization?.organizationType); const [ selectedProtocol, setSelectedProtocol ] = useState(undefined); const [ inboundProtocolList, setInboundProtocolList ] = useState([]); @@ -260,6 +259,8 @@ export const AccessConfiguration: FunctionComponent = useRef(null); const [ accordionActiveIndexes, setAccordionActiveIndexes ] = useState([]); + const hasApplicationUpdatePermissions: boolean = useRequiredScopes(featureConfig?.applications?.scopes?.update); + /** * Handles the inbound config delete action. * @@ -377,7 +378,7 @@ export const AccessConfiguration: FunctionComponent { - onUpdate(appId); + onProtocolUpdate(); if (!updateError) { createSAMLApplication(); } @@ -791,12 +792,7 @@ export const AccessConfiguration: FunctionComponentaccess_token.", + label: "Access Token Attributes", + placeholder: "Search by attribute name", + enable: { + hint : "Previously, all attributes marked as requested in the application's " + + "User Attributes section (referred to as requested attributes) were " + + "automatically included in the access token. With the latest update, " + + "admins can now choose which attributes to include in the access token. " + + "To enable this feature, select Enable Access Token Attributes. To ensure " + + "a smooth transition from the old behavior, selecting it for the first time " + + "will populate the Access Token Attributes section with all the requested " + + "attributes. Admins can then remove any unwanted attributes. After saving the " + + "changes, only the selected attributes will be included in the access token. " + + "Moving forward, all requested attributes will appear in a dropdown for admins " + + "to manage as needed. Important: Once updated, requested attributes are no " + + "longer automatically included in the access token and this change is " + + "irreversible. Admin-selected attributes will be included in the access token " + + "even without requiring the relevant OIDC scopes. Proceed with caution.", + label: "Enable Access Token Attributes" + } } }, heading: "Access Token",