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

Add Custom Authenticator UI #7363

Merged
merged 26 commits into from
Jan 27, 2025
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
3697192
Add external auth API integration and edit page content
Shenali-SJ Jan 22, 2025
7a18cd3
Update external auth endpoint configs
Shenali-SJ Jan 22, 2025
4ae97b3
Add custom local auth create API
Shenali-SJ Jan 22, 2025
bf649cb
Merge upstream
Shenali-SJ Jan 24, 2025
e1563db
Merge origin branch for validation changes
Shenali-SJ Jan 24, 2025
a3a9d06
Merge Shenali-SJ:custom-auth-create-wizard-update
Shenali-SJ Jan 24, 2025
36d613e
Temp commit
Shenali-SJ Jan 24, 2025
3fedea5
Temp comment
Shenali-SJ Jan 25, 2025
8788fbb
Improve edit section
Shenali-SJ Jan 26, 2025
dc99ee7
Improve edit section
Shenali-SJ Jan 26, 2025
424df62
Add a type guard for custom local authenticators
Shenali-SJ Jan 27, 2025
7a3c599
Improve settings tab in custom authenticators
Shenali-SJ Jan 27, 2025
53cffce
Improve settings tab
Shenali-SJ Jan 27, 2025
027ca2e
Clean the code
Shenali-SJ Jan 27, 2025
c3430b9
Merge upstream
Shenali-SJ Jan 27, 2025
1be964b
minor improvements
Shenali-SJ Jan 27, 2025
2d81e06
Revamp settings tab using action components
Shenali-SJ Jan 27, 2025
22e15d7
Update local custom auth image
Shenali-SJ Jan 27, 2025
276229c
Merge upstream
Shenali-SJ Jan 27, 2025
a1cdaa3
Remove feature flag
Shenali-SJ Jan 27, 2025
ad32e99
Remove unwanted files
Shenali-SJ Jan 27, 2025
b4f965e
Add changeset
Shenali-SJ Jan 27, 2025
26350ee
Fix lint issues
Shenali-SJ Jan 27, 2025
2fb99c4
Update pnpm lock with actions
Shenali-SJ Jan 27, 2025
5a14274
Address comments
Shenali-SJ Jan 27, 2025
bf5cedb
Fix lint issues
Shenali-SJ Jan 27, 2025
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
32 changes: 32 additions & 0 deletions features/admin.connections.v1/api/connections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
ConnectionListResponseInterface,
ConnectionRolesInterface,
ConnectionTemplateInterface,
CustomAuthenticatorInterface,
FederatedAuthenticatorListItemInterface,
FederatedAuthenticatorListResponseInterface,
FederatedAuthenticatorMetaInterface,
Expand Down Expand Up @@ -86,6 +87,37 @@ export const createConnection = (
});
};

/**
* Function to create a custom authentication
*
* @param connection - Connection settings data.
*/
export const createCustomAuthentication = (
connection: CustomAuthenticatorInterface
): Promise<AxiosResponse<CustomAuthenticatorInterface>> => {

const requestConfig: AxiosRequestConfig = {
data: connection,
headers: {
"Accept": "application/json",
"Content-Type": "application/json"
},
method: HttpMethods.POST,
url: store.getState().config.endpoints.customAuthenticators
};

return httpClient(requestConfig)
.then((response: AxiosResponse) => {
if ((response.status !== 201)) {
return Promise.reject(new Error("Failed to create the application."));
}

return Promise.resolve(response);
}).catch((error: AxiosError) => {
return Promise.reject(error);
});
};

/**
* Hook to get the connection list with limit and offset.
*
Expand Down

Large diffs are not rendered by default.

19 changes: 15 additions & 4 deletions features/admin.connections.v1/components/edit/connection-edit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ export const EditConnection: FunctionComponent<EditConnectionPropsInterface> = (
*/
const [ isTrustedTokenIssuer, setIsTrustedTokenIssuer ] = useState<boolean>(false);
const [ isExpertMode, setIsExpertMode ] = useState<boolean>(false);
const [ isCustomAuthenticator, setIsCustomAuthenticator ] = useState<boolean>(false);

const hasApplicationReadPermissions: boolean = useRequiredScopes(featureConfig?.applications?.scopes?.read);

Expand Down Expand Up @@ -224,6 +225,7 @@ export const EditConnection: FunctionComponent<EditConnectionPropsInterface> = (
templateType={ type }
isSaml={ isSaml }
isOidc={ isOidc }
isCustomAuthenticator= { isCustomAuthenticator }
editingIDP={ identityProvider }
isLoading={ isLoading }
onDelete={ onDelete }
Expand Down Expand Up @@ -357,6 +359,11 @@ export const EditConnection: FunctionComponent<EditConnectionPropsInterface> = (
setIsTrustedTokenIssuer(type === CommonAuthenticatorConstants
.CONNECTION_TEMPLATE_IDS.TRUSTED_TOKEN_ISSUER);
setIsExpertMode(type === CommonAuthenticatorConstants.CONNECTION_TEMPLATE_IDS.EXPERT_MODE);
setIsCustomAuthenticator(
type === CommonAuthenticatorConstants.CONNECTION_TEMPLATE_IDS.EXTERNAL_CUSTOM_AUTHENTICATION ||
type === CommonAuthenticatorConstants.CONNECTION_TEMPLATE_IDS.INTERNAL_CUSTOM_AUTHENTICATION ||
type === CommonAuthenticatorConstants.CONNECTION_TEMPLATE_IDS.TWO_FACTOR_CUSTOM_AUTHENTICATION
);
}, [ type ]);

useEffect(() => {
Expand Down Expand Up @@ -446,6 +453,7 @@ export const EditConnection: FunctionComponent<EditConnectionPropsInterface> = (
// Evaluate whether to Show/Hide `Attributes`.
if (shouldShowTab(type, ConnectionTabTypes.USER_ATTRIBUTES)
&& !isOrganizationEnterpriseAuthenticator
&& !isCustomAuthenticator
&& (type !== CommonAuthenticatorConstants
.CONNECTION_TEMPLATE_IDS.OIDC || isAttributesEnabledForOIDC)
&& (type !== CommonAuthenticatorConstants
Expand All @@ -467,7 +475,8 @@ export const EditConnection: FunctionComponent<EditConnectionPropsInterface> = (

if (shouldShowTab(type, ConnectionTabTypes.IDENTITY_PROVIDER_GROUPS) &&
featureConfig?.identityProviderGroups?.enabled &&
!isOrganizationEnterpriseAuthenticator) {
!isOrganizationEnterpriseAuthenticator
&& !isCustomAuthenticator) {
panes.push({
"data-tabid": ConnectionUIConstants.TabIds.IDENTITY_PROVIDER_GROUPS,
menuItem: "Groups",
Expand All @@ -477,7 +486,8 @@ export const EditConnection: FunctionComponent<EditConnectionPropsInterface> = (

if (shouldShowTab(type, ConnectionTabTypes.OUTBOUND_PROVISIONING) &&
identityProviderConfig.editIdentityProvider.showOutboundProvisioning &&
!isOrganizationEnterpriseAuthenticator) {
!isOrganizationEnterpriseAuthenticator
&& !isCustomAuthenticator) {
panes.push({
"data-tabid": ConnectionUIConstants.TabIds.OUTBOUND_PROVISIONING,
menuItem: "Outbound Provisioning",
Expand All @@ -497,7 +507,8 @@ export const EditConnection: FunctionComponent<EditConnectionPropsInterface> = (

if (shouldShowTab(type, ConnectionTabTypes.ADVANCED) &&
identityProviderConfig.editIdentityProvider.showAdvancedSettings &&
!isOrganizationEnterpriseAuthenticator) {
!isOrganizationEnterpriseAuthenticator
&& !isCustomAuthenticator) {
panes.push({
"data-tabid": ConnectionUIConstants.TabIds.ADVANCED,
menuItem: "Advanced",
Expand Down Expand Up @@ -528,7 +539,7 @@ export const EditConnection: FunctionComponent<EditConnectionPropsInterface> = (

if (!identityProvider || isLoading ||
((!isOrganizationEnterpriseAuthenticator && !isTrustedTokenIssuer
&& !isEnterpriseConnection && !isExpertMode) && !tabPaneExtensions)) {
&& !isEnterpriseConnection && !isExpertMode && !isCustomAuthenticator) && !tabPaneExtensions)) {

return <Loader />;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
/**
* Copyright (c) 2025, 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 { AppState, ConfigReducerStateInterface } from "@wso2is/admin.core.v1";

Check warning on line 19 in features/admin.connections.v1/components/edit/forms/custom-auth-general-details-form.tsx

View workflow job for this annotation

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

'AppState' is defined but never used. Allowed unused vars must match /^_/u

Check warning on line 19 in features/admin.connections.v1/components/edit/forms/custom-auth-general-details-form.tsx

View workflow job for this annotation

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

'ConfigReducerStateInterface' is defined but never used. Allowed unused vars must match /^_/u
import { IdentifiableComponentInterface } from "@wso2is/core/models";
import { Field, Form } from "@wso2is/form";
import { EmphasizedSegment } from "@wso2is/react-components";
import { FormValidation } from "@wso2is/validation";
import React, { FunctionComponent, ReactElement } from "react";
import { useTranslation } from "react-i18next";
import { useSelector } from "react-redux";

Check warning on line 26 in features/admin.connections.v1/components/edit/forms/custom-auth-general-details-form.tsx

View workflow job for this annotation

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

'useSelector' is defined but never used. Allowed unused vars must match /^_/u
import { ConnectionUIConstants } from "../../../constants/connection-ui-constants";
import {
ConnectionInterface,
ConnectionListResponseInterface,
CustomAuthGeneralDetailsFormValuesInterface,
StrictConnectionInterface

Check warning on line 32 in features/admin.connections.v1/components/edit/forms/custom-auth-general-details-form.tsx

View workflow job for this annotation

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

'StrictConnectionInterface' is defined but never used. Allowed unused vars must match /^_/u
} from "../../../models/connection";

/**
* Proptypes for the identity provider general details form component.
*/
interface CustomAuthGeneralDetailsFormPopsInterface extends IdentifiableComponentInterface {
/**
* Currently editing IDP.
*/
editingIDP?: ConnectionInterface;
/**
* Mark identity provider as primary.
*/
isPrimary?: boolean;
/**
* On submit callback.
*/
onSubmit: (values: any) => void;
/**
* Callback to update the idp details.
*/
onUpdate?: (id: string) => void;
/**
* Externally trigger form submission.
*/
triggerSubmit?: boolean;
/**
* Optimize for the creation wizard.
*/
enableWizardMode?: boolean;
/**
* List of available Idps.
*/
idpList?: ConnectionListResponseInterface;
/**
* Why? to hide or show the IdP logo edit input field.
* Introduced this for SAML and OIDC enterprise protocols.
* By default the icon/logo for this is readonly from
* extensions.
*/
hideIdPLogoEditField?: boolean;
/**
* Specifies if the component should only be read-only.
*/
isReadOnly: boolean;
/**
* Type of the template.
*/
templateType?: string;
/**
* Specifies if the form is submitting.
*/
isSubmitting?: boolean;
}

const FORM_ID: string = "idp-custom-auth-general-details-form";

/**
* Form to edit general details of the custom authenticator.
*
* @param props - Props injected to the component.
* @returns Functional component.
*/
export const CustomAuthGeneralDetailsForm: FunctionComponent<CustomAuthGeneralDetailsFormPopsInterface> = ({
onSubmit,
editingIDP,
idpList,

Check warning on line 99 in features/admin.connections.v1/components/edit/forms/custom-auth-general-details-form.tsx

View workflow job for this annotation

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

'idpList' is defined but never used. Allowed unused args must match /^_/u
hideIdPLogoEditField,

Check warning on line 100 in features/admin.connections.v1/components/edit/forms/custom-auth-general-details-form.tsx

View workflow job for this annotation

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

'hideIdPLogoEditField' is defined but never used. Allowed unused args must match /^_/u
isReadOnly,
templateType,
isSubmitting,
"data-componentid": _componentId = "idp-edit-custom-auth-general-settings-form"
}: CustomAuthGeneralDetailsFormPopsInterface): ReactElement => {

const { t } = useTranslation();

/**
* Prepare form values for submitting.
*
* @param values - Form values.
* @returns Sanitized form values.
*/
const updateConfigurations = (values: CustomAuthGeneralDetailsFormValuesInterface): void => {
onSubmit({
description: values.description?.toString(),
image: values.image?.toString(),
isPrimary: !!values.isPrimary,
name: values.name?.toString()
});
};

/**
* Decode the encoded string.
*
* @param encodedStr - Encoded string.
* @returns Decoded string.
*/
const decodeString = (encodedStr: string): string => {
try {
return atob(encodedStr);
} catch (error) {
return "";
}
};

return (
<React.Fragment>
<EmphasizedSegment padded="very">
<Form
id={ FORM_ID }
uncontrolledForm={ false }
onSubmit={ (values: CustomAuthGeneralDetailsFormValuesInterface): void => {
updateConfigurations(values);
} }
data-componentid={ _componentId }
validate={ (values: CustomAuthGeneralDetailsFormValuesInterface) => {
const errors: Partial<Record<keyof CustomAuthGeneralDetailsFormValuesInterface, string>> = {
image: undefined
};

if (!FormValidation.isValidResourceName(values.name)) {
errors.name = t(
"customAuthentication:fields.createWizard.generalSettingsStep." +
"displayName.validations.invalid"
);
}


Shenali-SJ marked this conversation as resolved.
Show resolved Hide resolved
return errors;
} }
>
<Field.Input
ariaLabel="identifier"
inputType="identifier"
name="identifier"
value={ decodeString(editingIDP.federatedAuthenticators.defaultAuthenticatorId) }
label={ t("customAuthentication:fields.createWizard.generalSettingsStep.identifier.label") }
placeholder={ t("customAuthentication:fields.createWizard.generalSettingsStep." +
"identifier.placeholder") }
maxLength={ 100 }
minLength={ 3 }
data-componentid={ `${_componentId}-form-wizard-identifier` }
width={ 15 }
hint={ t(
"customAuthentication:fields.createWizard.generalSettingsStep.helpPanel." +
"identifier.description"
) }
readOnly={ true }
/>
<Field.Input
ariaLabel="name"
inputType="resource_name"
name="name"
value={ editingIDP.name }
label={ t("customAuthentication:fields.createWizard.generalSettingsStep.displayName.label") }
placeholder={ t(
"customAuthentication:fields.createWizard.generalSettingsStep.displayName.placeholder"
) }
required={ true }
maxLength={ 100 }
minLength={ 3 }
data-componentid={ `${_componentId}-form-wizard-display-name` }
width={ 15 }
/>
<Field.Input
name="image"
ariaLabel="image"
inputType="url"
label={ "Icon URL" }
required={ false }
placeholder={ t("authenticationProvider:" +
"forms.generalDetails.image." +
"placeholder") }
value={ editingIDP.image }
data-componentid={ `${ _componentId }-idp-image` }
maxLength={
ConnectionUIConstants
.GENERAL_FORM_CONSTRAINTS.IMAGE_URL_MAX_LENGTH as number
Shenali-SJ marked this conversation as resolved.
Show resolved Hide resolved
}
minLength={
ConnectionUIConstants
.GENERAL_FORM_CONSTRAINTS.IMAGE_URL_MIN_LENGTH as number
}
hint="Logo to display in login pages."
Shenali-SJ marked this conversation as resolved.
Show resolved Hide resolved
readOnly={ isReadOnly }
/>
<Field.Textarea
name="description"
ariaLabel="description"
label={ t("authenticationProvider:forms." +
"generalDetails.description.label") }
required={ false }
placeholder={ t("authenticationProvider:forms." +
"generalDetails.description.placeholder") }
value={ editingIDP.description }
data-componentid={ `${ _componentId }-idp-description` }
maxLength={ ConnectionUIConstants.IDP_NAME_LENGTH.max }
minLength={ ConnectionUIConstants.IDP_NAME_LENGTH.min }
hint="A text description of the authenticator."
Shenali-SJ marked this conversation as resolved.
Show resolved Hide resolved
readOnly={ isReadOnly }
/>
{ !isReadOnly && (
<Field.Button
form={ FORM_ID }
ariaLabel="Update General Details"
size="small"
buttonType="primary_btn"
label={ t("common:update") }
name="submit"
disabled={ isSubmitting }
loading={ isSubmitting }
/>
) }
</Form>
</EmphasizedSegment>
</React.Fragment>
);
};
Loading
Loading