Skip to content
Open
Show file tree
Hide file tree
Changes from 7 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
Original file line number Diff line number Diff line change
Expand Up @@ -2844,6 +2844,9 @@
{% endfor %}
{% endif %}
},
"pushDeviceManagement": {
"maxDeviceLimitUpperBound": {{ push_device_management.max_device_limit_upper_bound }}
},
Comment thread
coderabbitai[bot] marked this conversation as resolved.
"routes": {
"organizationEnabledRoutes": {
{% if console.ui.routes.organizationEnabledRoutes.items() is defined %}
Expand Down
3 changes: 3 additions & 0 deletions apps/console/src/public/deployment.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -2244,6 +2244,9 @@
"productVersionConfig": {
"productVersion": "7.2.0"
},
"pushDeviceManagement": {
"maxDeviceLimitUpperBound": 10
},
"routes": {
"organizationEnabledRoutes": {
"accountSecurity": "v1.0.0",
Expand Down
38 changes: 38 additions & 0 deletions apps/myaccount/src/api/multi-factor-push.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ import { AsgardeoSPAClient, HttpClientInstance, HttpRequestConfig } from "@asgar
import { AxiosError, AxiosResponse } from "axios";
import { HttpErrorResponseDataInterface } from "@wso2is/core/models";
import { HttpMethods } from "../models";
import {
ConfigPreferenceResponseInterface,
ConfigPreferenceRequestInterface
} from "../models/push-authenticator";
import { store } from "../store";

/**
Expand Down Expand Up @@ -75,6 +79,40 @@ const getPushEnabledDevices = () => {
});
};

/**
* Fetch push device management configuration preferences for the tenant.
*
* @param data - Resource types, names and properties to fetch.
* @returns Promise resolving to the config preference response.
*/
export const getConfigPreferences = (
data: ConfigPreferenceRequestInterface[]
): Promise<ConfigPreferenceResponseInterface[]> => {
const requestConfig: HttpRequestConfig = {
data,
headers: {
"Accept": "application/json",
"Content-Type": "application/json"
},
method: HttpMethods.POST,
url: store.getState().config.endpoints.pushDeviceMgtConfigs
};

return httpClient(requestConfig)
.then((response: AxiosResponse) => {
if (response.status !== 200) {
return Promise.reject(
new Error(`Failed to fetch config preferences. Status: ${response.status}`)
);
}

return Promise.resolve(response?.data as ConfigPreferenceResponseInterface[]);
})
.catch((error: AxiosError<HttpErrorResponseDataInterface>) => {
return Promise.reject(error);
});
};

export const deletePushAuthRegisteredDevice = (deviceId: string) => {
const requestConfig: HttpRequestConfig = {
headers: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,13 @@ export const PushAuthenticator: React.FunctionComponent<PushAuthenticatorProps>

const {
deleteRegisteredDevice,
deviceLimit,
handlePushAuthenticatorInitCancel,
handlePushAuthenticatorSetupSubmit,
initPushAuthenticatorRegFlow,
isConfigPushAuthenticatorModalOpen,
isDeviceLimitReached,
isPushDeviceMgtConfigLoading,
isRegisteredDeviceListLoading,
qrCode,
registeredDeviceList,
Expand Down Expand Up @@ -93,7 +96,7 @@ export const PushAuthenticator: React.FunctionComponent<PushAuthenticatorProps>
<Modal.Content data-componentId={ `${ componentId }-modal-content` } scrolling>
{ renderPushAuthenticatorWizardContent() }
</Modal.Content>
{ registeredDeviceList?.length > 0 && (
{ (
<Modal.Actions
data-componentId={ `${ componentId }-view-modal-actions` }
className ="actions"
Expand All @@ -111,7 +114,7 @@ export const PushAuthenticator: React.FunctionComponent<PushAuthenticatorProps>
* @returns Modal content
*/
const renderPushAuthenticatorWizardContent = (): React.ReactElement => {
if (!registeredDeviceList || registeredDeviceList?.length === 0) {
if (qrCode) {
return (
<Segment basic>
<h5 className=" text-center"> { t(translateKey + "modals.scan.heading") }</h5>
Expand Down Expand Up @@ -195,7 +198,7 @@ export const PushAuthenticator: React.FunctionComponent<PushAuthenticatorProps>
* @returns Modal actions
*/
const renderPushAuthenticatorWizardActions = (): React.ReactElement => {
if (registeredDeviceList?.length > 0) {
if (!qrCode) {
return (
<Button
primary
Expand Down Expand Up @@ -279,22 +282,31 @@ export const PushAuthenticator: React.FunctionComponent<PushAuthenticatorProps>
</List.Content>
</Grid.Column>
<Grid.Column width={ 3 } className="last-column">
{ (!registeredDeviceList || registeredDeviceList?.length === 0) && (
{(
<List.Content floated="right">
<Popup
trigger={
(<Icon
link={ true }
onClick={ initPushAuthenticatorRegFlow }
link={ !isDeviceLimitReached }
onClick={ isDeviceLimitReached ? undefined : initPushAuthenticatorRegFlow }
className="list-icon padded-icon"
size="small"
color="grey"
name="plus"
disabled={ isRegisteredDeviceListLoading }
disabled={
isRegisteredDeviceListLoading
|| isPushDeviceMgtConfigLoading
|| isDeviceLimitReached
}
data-componentId={ `${componentId}-view-button` }
/>)
}
content={ t(translateKey + "addHint") }
content={
isDeviceLimitReached
? t(translateKey + "deviceLimitReachedHint",
{ limit: deviceLimit })
: t(translateKey + "addHint")
}
inverted
/>
</List.Content>
Expand Down
1 change: 1 addition & 0 deletions apps/myaccount/src/configs/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ export class Config {
preference: `${this.getDeploymentConfig()?.serverHost}/api/server/v1/identity-governance/preferences`,
profileSchemas: `${this.getDeploymentConfig()?.serverHost}/scim2/Schemas`,
push: `${this.getDeploymentConfig()?.serverHost}/api/users/v1/me/push`,
pushDeviceMgtConfigs: `${this.getDeploymentConfig()?.serverHost}/api/server/v1/configs/preferences`,
revoke: `${this.getDeploymentConfig()?.serverHost}/oauth2/revoke`,
sessions: `${this.getDeploymentConfig()?.serverHost}/api/users/v1/me/sessions`,
token: `${this.getDeploymentConfig()?.serverHost}/oauth2/token`,
Expand Down
11 changes: 11 additions & 0 deletions apps/myaccount/src/constants/mfa-constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,17 @@
* under the License.
*/

/**
* Class containing push authenticator specific constants.
*/
export class PushAuthenticatorConstants {

public static readonly RESOURCE_TYPE: string = "DEVICE_MANAGEMENT";
public static readonly DEVICE_MGT_RESOURCE_NAME: string = "PUSH_DEVICE_MANAGEMENT";
public static readonly PROPERTY_ENABLE_MULTIPLE_DEVICE_ENROLLMENT: string = "enableMultipleDeviceEnrollment";
public static readonly PROPERTY_MAXIMUM_DEVICE_LIMIT: string = "maximumDeviceLimit";
}

/**
* Class containing multi factor authentication constants.
*/
Expand Down
83 changes: 78 additions & 5 deletions apps/myaccount/src/hooks/use-push-authenticator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,18 @@ import { useTranslation } from "react-i18next";
import { useDispatch } from "react-redux";
import { Dispatch } from "redux";
import useGetPushAuthRegisteredDevices from "./use-get-push-auth-registered-devices";
import { deletePushAuthRegisteredDevice, initPushAuthenticatorQRCode } from "../api/multi-factor-push";
import {
deletePushAuthRegisteredDevice,
getConfigPreferences,
initPushAuthenticatorQRCode
} from "../api/multi-factor-push";
import { AlertLevels } from "../models/alert";
import { HttpResponse } from "../models/api";
import { PushAuthenticatorConstants } from "../constants/mfa-constants";
import {
ConfigPreferenceResponseInterface,
PushDeviceMgtConfigInterface
} from "../models/push-authenticator";
import { addAlert } from "../store/actions/global";

/**
Expand All @@ -42,11 +51,67 @@ const usePushAuthenticator = () => {
const [ isLoading, setIsLoading ] = useState<boolean>(false);
const [ isConfigPushAuthenticatorModalOpen, setIsConfigPushAuthenticatorModalOpen ] = useState<boolean>(false);
const [ qrCode, setQrCode ] = useState<string>(null);
const [ isPushDeviceMgtConfigLoading, setIsPushDeviceMgtConfigLoading ] = useState<boolean>(false);
const [ pushDeviceMgtConfig, setPushDeviceMgtConfig ] = useState<PushDeviceMgtConfigInterface>(null);

const { t } = useTranslation();

const translateKey: string = "myAccount:components.mfa.pushAuthenticatorApp.";

const isMultipleDeviceEnrollmentEnabled: boolean =
pushDeviceMgtConfig?.enableMultipleDeviceEnrollment ?? false;

const deviceLimit: number = isMultipleDeviceEnrollmentEnabled
? (pushDeviceMgtConfig?.maximumDeviceLimit ?? 1)
: 1;

const isDeviceLimitReached: boolean = (registeredDeviceList?.length ?? 0) >= deviceLimit;

useEffect(() => {
setIsPushDeviceMgtConfigLoading(true);

getConfigPreferences([
{
attributeNames: [
PushAuthenticatorConstants.PROPERTY_ENABLE_MULTIPLE_DEVICE_ENROLLMENT,
PushAuthenticatorConstants.PROPERTY_MAXIMUM_DEVICE_LIMIT
],
resourceName: PushAuthenticatorConstants.DEVICE_MGT_RESOURCE_NAME,
resourceType: PushAuthenticatorConstants.RESOURCE_TYPE
}
])
.then((response: ConfigPreferenceResponseInterface[]) => {
const resource: ConfigPreferenceResponseInterface | undefined = response?.find(
(item: ConfigPreferenceResponseInterface) =>
item.resourceType === PushAuthenticatorConstants.RESOURCE_TYPE &&
item.resourceName === PushAuthenticatorConstants.DEVICE_MGT_RESOURCE_NAME
);

if (resource) {
const getValue = (name: string): string | undefined =>
resource.attributeNames.find((p) => p.name === name)?.value;

setPushDeviceMgtConfig({
enableMultipleDeviceEnrollment:
getValue(PushAuthenticatorConstants.PROPERTY_ENABLE_MULTIPLE_DEVICE_ENROLLMENT) === "true",
maximumDeviceLimit: parseInt(
getValue(PushAuthenticatorConstants.PROPERTY_MAXIMUM_DEVICE_LIMIT) ?? "1", 10
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
});
}
})
.catch(() => {
dispatch(addAlert({
description: t(translateKey + "notifications.configFetchError.genericError.description"),
level: AlertLevels.ERROR,
message: t(translateKey + "notifications.configFetchError.genericError.message")
}));
})
.finally(() => {
setIsPushDeviceMgtConfigLoading(false);
});
}, []);

useEffect(() => {
if (registeredDeviceListFetchError && !isRegisteredDeviceListLoading) {
dispatch(addAlert({
Expand All @@ -62,7 +127,11 @@ const usePushAuthenticator = () => {
/**
* Initiate the push authenticator configuration flow.
*/
const initPushAuthenticatorRegFlow = () => {
const initPushAuthenticatorRegFlow = (): void => {
if (isDeviceLimitReached) {
return;
}

setIsLoading(true);

initPushAuthenticatorQRCode()
Expand Down Expand Up @@ -101,7 +170,7 @@ const usePushAuthenticator = () => {
const handlePushAuthenticatorSetupSubmit = (event: React.FormEvent<HTMLFormElement>): void => {
event.preventDefault();
updateRegisteredDeviceList();
// setIsConfigPushAuthenticatorModalOpen(false);
setQrCode(null);
};

/**
Expand All @@ -122,9 +191,9 @@ const usePushAuthenticator = () => {
}
).catch((_err: any) => {
dispatch(addAlert({
description: t(translateKey + "notifications.deleteError.genericError.description"),
description: t(translateKey + "notifications.delete.genericError.description"),
level: AlertLevels.ERROR,
message: t(translateKey + "notifications.deleteError.genericError.message")
message: t(translateKey + "notifications.delete.genericError.message")
}));
}).finally(() => {
setIsLoading(false);
Expand All @@ -133,11 +202,15 @@ const usePushAuthenticator = () => {

return {
deleteRegisteredDevice,
deviceLimit,
handlePushAuthenticatorInitCancel,
handlePushAuthenticatorSetupSubmit,
initPushAuthenticatorRegFlow,
isConfigPushAuthenticatorModalOpen,
isDeviceLimitReached,
isLoading,
isPushDeviceMgtConfigLoading,
isMultipleDeviceEnrollmentEnabled,
isRegisteredDeviceListLoading,
qrCode,
registeredDeviceList,
Expand Down
1 change: 1 addition & 0 deletions apps/myaccount/src/models/app-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ export interface ServiceResourceEndpointsInterface {
preference: string;
profileSchemas: string;
push: string;
pushDeviceMgtConfigs: string;
sessions: string;
otpCodeValidate: string;
token: string;
Expand Down
35 changes: 35 additions & 0 deletions apps/myaccount/src/models/push-authenticator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,38 @@
name: string;
provider: string;
}

/**
* Resolved push device management configuration used internally by the UI.
*/
export interface PushDeviceMgtConfigInterface {
enableMultipleDeviceEnrollment: boolean;
maximumDeviceLimit: number;
}

/**
* A single property entry in a ConfigPreferenceResponse.
* Values are always returned as strings by the preferences API.
*/
export interface ConfigPreferencePropertyInterface {

Check failure on line 38 in apps/myaccount/src/models/push-authenticator.ts

View workflow job for this annotation

GitHub Actions / ✂️ Knip (DEAD CODE) (20.x, 10.33.0)

✂️ Knip / Unused exported types

ConfigPreferencePropertyInterface in apps/myaccount/src/models/push-authenticator.ts
name: string;
value: string;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}

/**
* One resource block in the POST /configs/preferences response.
*/
export interface ConfigPreferenceResponseInterface {
resourceType: string;
resourceName: string;
attributeNames: ConfigPreferencePropertyInterface[];
}

/**
* One item in the POST /configs/preferences request body.
*/
export interface ConfigPreferenceRequestInterface {
resourceType: string;
resourceName: string;
attributeNames: string[];
}
3 changes: 2 additions & 1 deletion features/admin.connections.v1/configs/endpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export const getConnectionResourceEndpoints = (serverHost: string): ConnectionRe
localAuthenticators: `${ serverHost }/api/server/v1/configs/authenticators`,
multiFactorAuthenticators: `${ serverHost }/api/server/v1/identity-governance/${
CommonAuthenticatorConstants.MFA_CONNECTOR_CATEGORY_ID
}`
}`,
pushDeviceMgtConfigs: `${ serverHost }/api/server/v1/configs/push-device-mgt`
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -181,11 +181,17 @@ export class ConnectionUIConstants {
RESEND_INTERVAL_MAX_VALUE: number;
RESEND_INTERVAL_MIN_LENGTH: number;
RESEND_INTERVAL_MIN_VALUE: number;
MAXIMUM_DEVICE_LIMIT_MIN_LENGTH: number;
MAXIMUM_DEVICE_LIMIT_MIN_VALUE: number;
MAXIMUM_DEVICE_LIMIT_MAX_VALUE: number;
} = {
ALLOWED_RESEND_ATTEMPT_COUNT_MAX_LENGTH: 2,
ALLOWED_RESEND_ATTEMPT_COUNT_MAX_VALUE: 10,
ALLOWED_RESEND_ATTEMPT_COUNT_MIN_LENGTH: 1,
ALLOWED_RESEND_ATTEMPT_COUNT_MIN_VALUE: 0,
MAXIMUM_DEVICE_LIMIT_MAX_VALUE: 109,
MAXIMUM_DEVICE_LIMIT_MIN_LENGTH: 1,
MAXIMUM_DEVICE_LIMIT_MIN_VALUE: 2,
RESEND_INTERVAL_MAX_LENGTH: 2,
RESEND_INTERVAL_MAX_VALUE: 10,
RESEND_INTERVAL_MIN_LENGTH: 1,
Expand Down
1 change: 1 addition & 0 deletions features/admin.connections.v1/models/endpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,5 @@ export interface ConnectionResourceEndpointsInterface {
identityProviders: string;
localAuthenticators: string;
multiFactorAuthenticators: string;
pushDeviceMgtConfigs: string;
}
Loading
Loading