Skip to content
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

Introduce JWT Access Token Attributes #6730

Open
wants to merge 24 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
shashimalcse marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/**
* 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";

interface AccessTokenAttributeOption 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<HTMLLIElement>
}

export const AccessTokenAttributeOption: FunctionComponent<AccessTokenAttributeOption> = (
props: AccessTokenAttributeOption
): ReactElement => {

const {
selected,
displayName,
claimURI,
renderOptionProps
} = props;

return (
<li { ...renderOptionProps }>
<Grid container justifyContent="space-between" alignItems="center" xs={ 12 }>
<Grid container alignItems="center" xs={ 8 }>
<Grid>
{
typeof selected === "boolean" && (
shashimalcse marked this conversation as resolved.
Show resolved Hide resolved
<Checkbox checked={ selected } />
)
}
</Grid>
<Grid xs={ 5 }>
<ListItemText primary={ displayName } />
<Code>{ claimURI }</Code>
</Grid>
</Grid>
</Grid>
</li>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,10 @@
margin-top: 0 !important;
margin-right: 1rem !important;
}

.jwt-attributes-dropdown-input {
input {
border: none !important;
width: auto !important;
}
}
211 changes: 211 additions & 0 deletions features/admin.applications.v1/components/forms/inbound-oidc-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,15 @@
* under the License.
*/

import Autocomplete, {
AutocompleteRenderGetTagProps,
AutocompleteRenderInputParams
} from "@oxygen-ui/react/Autocomplete";
import Box from "@oxygen-ui/react/Box";
import Chip from "@oxygen-ui/react/Chip";
import InputLabel from "@oxygen-ui/react/InputLabel";
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";
Expand All @@ -30,6 +37,8 @@ import { IdentityAppsApiException } from "@wso2is/core/exceptions";
import { isFeatureEnabled } from "@wso2is/core/helpers";
import {
AlertLevels,
Claim,
ExternalClaim,
FeatureAccessConfigInterface,
IdentifiableComponentInterface,
TestableComponentInterface
Expand Down Expand Up @@ -62,9 +71,12 @@ import React, {
ChangeEvent,
Fragment,
FunctionComponent,
HTMLAttributes,
MouseEvent,
MutableRefObject,
ReactElement,
SyntheticEvent,
useCallback,
useEffect,
useRef,
useState
Expand All @@ -73,6 +85,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
Expand Down Expand Up @@ -102,7 +115,9 @@ import {
additionalSpProperty
} from "../../models";
import { ApplicationManagementUtils } from "../../utils/application-management-utils";
import { AccessTokenAttributeOption } from "../components/access-token-attribute-option";
import { ApplicationCertificateWrapper } from "../settings/certificate";
import "./inbound-oidc-form.scss";

/**
* Proptypes for the inbound OIDC form component.
Expand Down Expand Up @@ -248,6 +263,12 @@ export const InboundOIDCForm: FunctionComponent<InboundOIDCFormPropsInterface> =
isRefreshTokenWithoutAllowedGrantType,
setRefreshTokenWithoutAlllowdGrantType
] = useState<boolean>(false);
const [ activeOption, setActiveOption ] = useState<ExternalClaim>(undefined);
const [ claims, setClaims ] = useState<Claim[]>([]);
const [ externalClaims, setExternalClaims ] = useState<ExternalClaim[]>([]);
const [ selectedAccessTokenAttributes, setSelectedAccessTokenAttributes ] = useState<ExternalClaim[]>(undefined);
const [ accessTokenAttributes, setAccessTokenAttributes ] = useState<ExternalClaim[]>([]);
const [ accessTokenAttributesEnabled, setAccessTokenAttributesEnabled ] = useState<boolean>(false);
const [ isSubjectTokenEnabled, setIsSubjectTokenEnabled ] = useState<boolean>(false);
const [ isSubjectTokenFeatureAvailable, setIsSubjectTokenFeatureAvailable ] = useState<boolean>(false);
const config: ConfigReducerStateInterface = useSelector((state: AppState) => state.config);
Expand Down Expand Up @@ -288,6 +309,7 @@ export const InboundOIDCForm: FunctionComponent<InboundOIDCFormPropsInterface> =
const requestObjectSigningAlg: MutableRefObject<HTMLElement> = useRef<HTMLElement>();
const requestObjectEncryptionAlgorithm: MutableRefObject<HTMLElement> = useRef<HTMLElement>();
const requestObjectEncryptionMethod: MutableRefObject<HTMLElement> = useRef<HTMLElement>();
const accessTokenAttributesEnabledConfig: MutableRefObject<HTMLElement> = useRef<HTMLElement>();
const subjectToken: MutableRefObject<HTMLElement> = useRef<HTMLElement>();
const applicationSubjectTokenExpiryInSeconds: MutableRefObject<HTMLElement> = useRef<HTMLElement>();

Expand Down Expand Up @@ -435,6 +457,74 @@ export const InboundOIDCForm: FunctionComponent<InboundOIDCFormPropsInterface> =
);
}, [ application ]);

const fetchLocalClaims: () => void = useCallback(() => {
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: () => void = useCallback(() => {
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")
}));
});
}, []);
shashimalcse marked this conversation as resolved.
Show resolved Hide resolved

useEffect(() => {
fetchLocalClaims();
fetchExternalClaims();
}, []);


useEffect(() => {
if (claims.length && externalClaims.length) {
shashimalcse marked this conversation as resolved.
Show resolved Hide resolved
const updatedAttributes : ExternalClaim[] = externalClaims.map((externalClaim : ExternalClaim) => {
const matchedLocalClaim: Claim = claims.find((localClaim: Claim) =>
localClaim.claimURI === externalClaim.mappedLocalClaimURI
);

if (matchedLocalClaim && matchedLocalClaim.displayName) {
shashimalcse marked this conversation as resolved.
Show resolved Hide resolved
return {
...externalClaim,
localClaimDisplayName: matchedLocalClaim.displayName
};
}

return externalClaim;
});

setAccessTokenAttributes(updatedAttributes);
}
}, [ claims, externalClaims ]);

useEffect(() => {
if (!initialValues.accessToken.accessTokenAttributes) {
return;
}
const selectedAttributes: ExternalClaim[] = initialValues.accessToken.accessTokenAttributes
shashimalcse marked this conversation as resolved.
Show resolved Hide resolved
.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) =>
Expand Down Expand Up @@ -1210,6 +1300,8 @@ export const InboundOIDCForm: FunctionComponent<InboundOIDCFormPropsInterface> =
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),
Expand Down Expand Up @@ -2603,6 +2695,125 @@ export const InboundOIDCForm: FunctionComponent<InboundOIDCFormPropsInterface> =
readOnly={ readOnly }
data-testid={ `${ testId }-access-token-type-radio-group` }
/>
{ isJWTAccessTokenTypeSelected ? (
<Grid.Row>
<Grid.Column width={ 8 }>
<Autocomplete
disablePortal
multiple
disableCloseOnSelect
loading={ isLoading }
options={ accessTokenAttributes }
value={ selectedAccessTokenAttributes ?? [] }
disabled = { !initialValues?.accessToken?.accessTokenAttributesEnabled }
data-componentid={ `${ componentId }-assigned-jwt-attribute-list` }
getOptionLabel={
(claim: ExternalClaim) => claim.claimURI
}
renderInput={ (params: AutocompleteRenderInputParams) => (
<>
<InputLabel htmlFor="tags-filled" disableAnimation shrink={ false }>
{ t(
"applications:forms.inboundOIDC.sections" +
".accessToken.fields.accessTokenAttributes.label"
) }
</InputLabel>
<TextField
className="jwt-attributes-dropdown-input"
{ ...params }
placeholder={ !false &&
t("applications:forms.inboundOIDC.sections" +
".accessToken.fields.accessTokenAttributes.placeholder") }
shashimalcse marked this conversation as resolved.
Show resolved Hide resolved
/>
</>
) }
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) => (
<Chip
{ ...getTagProps({ index }) }
key={ index }
label={ option.claimURI }
activeOption={ activeOption }
setActiveOption={ setActiveOption }
variant={
accessTokenAttributes?.find(
(claim: ExternalClaim) => claim.id === option.id
)
? "solid"
: "outlined"
}
/>
)) }
renderOption={ (
props: HTMLAttributes<HTMLLIElement>,
option: ExternalClaim,
{ selected }: { selected: boolean }
) => (
<AccessTokenAttributeOption
selected={ selected }
displayName={ option.localClaimDisplayName }
claimURI={ option.claimURI }
renderOptionProps={ props }
/>
) }
/>
<Hint>
<Trans
values={ { productName: config.ui.productName } }
i18nKey={
"applications:forms.inboundOIDC.sections." +
"accessTokenAttributes.hint"
}
>
Select the attributes that should be included in
the <Code withBackground>access token</Code>
</Trans>
</Hint>
</Grid.Column>
{ !initialValues?.accessToken?.accessTokenAttributesEnabled && (
<Grid.Column>
<Field
ref={ accessTokenAttributesEnabledConfig }
name="accessTokenAttributesEnabledConfig"
label=""
required={ false }
type="checkbox"
listen={ (values: Map<string, FormValue>): 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` }
/>
</Grid.Column>
) }
</Grid.Row>
) : null }
</Grid.Column>
</Grid.Row>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
store
} from "@wso2is/admin.core.v1";
import { applicationConfig } from "@wso2is/admin.extensions.v1";
import { hasRequiredScopes } from "@wso2is/core/helpers";

Check failure on line 27 in features/admin.applications.v1/components/settings/access-configuration.tsx

View workflow job for this annotation

GitHub Actions / ⬣ ESLint (STATIC ANALYSIS) (lts/*, 8.7.4)

'hasRequiredScopes' import from '@wso2is/core/helpers' is restricted. Please use "import { useRequiredScopes } from '@wso2is/access-control'" instead. Refer documentation: https://github.com/wso2/identity-apps/blob/master/docs/write-code/PERFORMANCE.md#use-userequiredscopes-hook-instead-of-hasrequiredscopes-function
import { AlertLevels, IdentifiableComponentInterface, SBACInterface } from "@wso2is/core/models";
import { addAlert } from "@wso2is/core/store";
import { FormValue } from "@wso2is/forms";
Expand Down Expand Up @@ -216,6 +216,7 @@
isLoading,
setIsLoading,
onUpdate,
onProtocolUpdate,
allowedOriginList,
onAllowedOriginsUpdate,
onApplicationSecretRegenerate,
Expand Down Expand Up @@ -377,7 +378,7 @@
".genericError.message")
}));
}).finally(() => {
onUpdate(appId);
onProtocolUpdate();
if (!updateError) {
createSAMLApplication();
}
Expand Down
Loading
Loading