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 SMS based password recovery support #6418

Merged
merged 29 commits into from
Jun 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
5eef44c
Revert "Revert "Update recovery portal to support SMS OTP based passw…
RushanNanayakkara May 21, 2024
9b87c22
Revert "revertinng 5808"
RushanNanayakkara May 21, 2024
b900af4
Add sms recovery to Account recovery section
RushanNanayakkara May 22, 2024
271435b
Add sms recovery in myaccount
RushanNanayakkara May 24, 2024
6167995
Add choose other option button to sms otp page
RushanNanayakkara May 27, 2024
f884dc5
Add otp resend success message
RushanNanayakkara May 27, 2024
5d09f52
bug fixes
RushanNanayakkara May 30, 2024
533f221
rename beta tag config
RushanNanayakkara Jun 5, 2024
79dd320
bug fixes
RushanNanayakkara Jun 7, 2024
92391d2
Addressing review comments
RushanNanayakkara Jun 7, 2024
e222834
remove unnecessary encoding
RushanNanayakkara Jun 11, 2024
3d893f2
Add change set
RushanNanayakkara Jun 12, 2024
0dd77c8
Addressing comments
RushanNanayakkara Jun 12, 2024
460602d
remove account-recovery connector from auto enable list
RushanNanayakkara Jun 13, 2024
7cb4d54
Addressing review comments
RushanNanayakkara Jun 16, 2024
1e19da2
Add unit tests
RushanNanayakkara Jun 17, 2024
c7cf2af
Addressing review comments
RushanNanayakkara Jun 18, 2024
a62056b
Keep only unit tests for testing
RushanNanayakkara Jun 18, 2024
545a5d5
undo temp PR builder changes
RushanNanayakkara Jun 18, 2024
4c2a8f9
Add component id to error message tip
RushanNanayakkara Jun 19, 2024
8f2a798
Add unit tests
RushanNanayakkara Jun 19, 2024
9029033
Addressing comments
RushanNanayakkara Jun 19, 2024
f078ab4
Addressing comments
RushanNanayakkara Jun 19, 2024
cc994db
Fix style issues
RushanNanayakkara Jun 20, 2024
65768c4
Add missing i18n Strings
RushanNanayakkara Jun 24, 2024
0382f56
add sms-otp to branding preference screens
RushanNanayakkara Jun 24, 2024
eb41f6f
revert pnpm-lock.yaml and package.json changes
RushanNanayakkara Jun 24, 2024
34b1b04
addressing comments
RushanNanayakkara Jun 24, 2024
1e59bc5
Addressing comments
RushanNanayakkara Jun 25, 2024
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
11 changes: 11 additions & 0 deletions .changeset/wicked-fireants-punch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"@wso2is/identity-apps-core": minor
"@wso2is/console": minor
"@wso2is/admin.server-configurations.v1": patch
"@wso2is/admin.branding.v1": patch
"@wso2is/admin.core.v1": patch
"@wso2is/myaccount": patch
"@wso2is/i18n": patch
---

Introduce sms otp for password recovery
18 changes: 16 additions & 2 deletions apps/console/src/extensions/i18n/models/extensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3056,10 +3056,24 @@ export interface Extensions {
form: {
fields: {
enable: FormAttributes;
enableSMSBasedRecovery: FormAttributes;
enableEmailBasedRecovery: FormAttributes;
expiryTime: FormAttributes;
notifySuccess: FormAttributes;
};
};
maxResendCount: FormAttributes;
maxFailedAttemptCount: FormAttributes;
smsOtpExpiryTime: FormAttributes;
passwordRecoveryOtpUseUppercase: FormAttributes;
passwordRecoveryOtpUseLowercase: FormAttributes;
passwordRecoveryOtpUseNumeric: FormAttributes;
passwordRecoveryOtpLength: FormAttributes;
};
};
recoveryOptionSubHeadingEmailLink: string;
recoveryOptionSubHeadingSMS: string;
recoveryOptionHeading: string;
otpConfigHeading: string;
failedAttemptConfigHeading: string;
connectorDescription: string;
heading: string;
notification: {
Expand Down
78 changes: 76 additions & 2 deletions apps/console/src/extensions/i18n/resources/en-US/extensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3576,6 +3576,10 @@ export const extensions: Extensions = {
hint: "Enabling this will let the users reset their password using an email.",
label: "Enable"
},
enableSMSBasedRecovery: {
hint: "This specifies whether to send an SMS OTP to the mobile.",
label: "Enable SMS based recovery"
},
expiryTime: {
hint: "Password recovery link expiry time in minutes.",
label: "Recovery link expiry time",
Expand All @@ -3595,9 +3599,80 @@ export const extensions: Extensions = {
"This specifies whether to notify the user via an email when password " +
"recovery is successful.",
label: "Notify on successful recovery"
},
maxResendCount: {
hint: "Password recovery maximum resend count.",
label: "Maximum resend attempts count",
placeholder: "Enter max resend count",
validations: {
invalid: "Password recovery OTP resend count should be an integer.",
empty: "Password recovery OTP resend count cannot be empty.",
range:
"Password recovery OTP resend count should be between 1 & 5.",
maxLengthReached:
"Password recovery OTP resend count should be a number with 1 digit."
}
},
maxFailedAttemptCount: {
hint: "Password recovery maximum failed attempt count.",
label: "Max failed attempts count",
placeholder: "Enter max failed attempts",
validations: {
invalid: "Password recovery max failed attempts count should be an integer.",
empty: "Password recovery max failed attempts count cannot be empty.",
range:
"Password recovery max failed attempts count should be between 1 & 10.",
maxLengthReached:
"Password recovery max failed attempts count should be a number with less than 3 digits."
}
},
smsOtpExpiryTime: {
hint: "Password recovery OTP expiry time in minutes.",
label: "Password recovery OTP expiry time",
placeholder: "Enter expiry time",
validations: {
invalid: "Password recovery OTP expiry time should be an integer.",
empty: "Password recovery OTP expiry time cannot be empty.",
range:
"Password recovery OTP expiry time should be between 1 minute & 1440 minutes " +
"(1 day).",
maxLengthReached:
"Password recovery OTP expiry time should be a number with 4 or less digits."
}
},
passwordRecoveryOtpUseUppercase: {
hint: "This specifies whether to use upper case characters in the password recovery otp code.",
label: "Include upper case letters"
},
passwordRecoveryOtpUseLowercase: {
hint: "This specifies whether to use lower case characters in the password recovery otp code.",
label: "Include lower case letters"
},
passwordRecoveryOtpUseNumeric: {
hint: "This specifies whether to use numeric characters in the password recovery otp code.",
label: "Include numeric characters"
},
passwordRecoveryOtpLength: {
hint: "Password recovery OTP length in characters",
label: "Password recovery OTP code length",
placeholder: "Enter OTP code length",
validations: {
empty: "Password recovery OTP length cannot be empty.",
maxLengthReached:
"Password recovery OTP length should be between 6 and 10 characters."
}
},
enableEmailBasedRecovery: {
hint: "This specifies whether to send an recovery link to the email address.",
label: "Enable email link based recovery"
}
}
},
recoveryOptionSubHeadingEmailLink: "Email Link",
recoveryOptionSubHeadingSMS: "SMS OTP",
recoveryOptionHeading: "Recovery Option Selection",
otpConfigHeading: "OTP Code Configuration",
failedAttemptConfigHeading: "Recovery Attempts Limitation",
connectorDescription: "Enable self-service password recovery for users " + "on the login page.",
heading: "Password Recovery",
notification: {
Expand All @@ -3611,8 +3686,7 @@ export const extensions: Extensions = {
}
},
subHeading:
"Enable self-service password recovery for users " +
"on the login page.\nThe user will receive a password reset link via email upon request."
"Enable self-service password recovery for users on the login page."
},
subHeading: "Account Recovery related settings."
},
Expand Down
1 change: 1 addition & 0 deletions apps/console/src/public/deployment.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -1181,6 +1181,7 @@
},
"selfAppIdentifier": "Console",
"showAppSwitchButton": true,
"showSmsOtpPwdRecoveryFeatureStatusChip": false,
"showStatusLabelForNewAuthzRuntimeFeatures": false,
"systemAppsIdentifiers": [
"Console",
Expand Down
75 changes: 49 additions & 26 deletions apps/myaccount/src/components/account-recovery/account-recovery.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2019-2023, WSO2 LLC. (https://www.wso2.com).
* Copyright (c) 2019-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
Expand All @@ -16,15 +16,15 @@
* under the License.
*/

import { hasRequiredScopes, isFeatureEnabled } from "@wso2is/core/helpers";
import { useRequiredScopes } from "@wso2is/access-control";
import { isFeatureEnabled } from "@wso2is/core/helpers";
import { SBACInterface, TestableComponentInterface } from "@wso2is/core/models";
import { EmphasizedSegment } from "@wso2is/react-components";
import { AxiosError } from "axios";
import React, { FunctionComponent, ReactElement, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useSelector } from "react-redux";
import { List, Placeholder } from "semantic-ui-react";
import { EmailRecovery, SecurityQuestionsComponent } from "./options";
import { EmailRecovery, SMSRecovery, SecurityQuestionsComponent } from "./options";
import { getPreference } from "../../api";
import { AppConstants } from "../../constants";
import {
Expand All @@ -35,7 +35,6 @@ import {
PreferenceProperty,
PreferenceRequest
} from "../../models";
import { AppState } from "../../store";
import { SettingsSection } from "../shared";

/**
Expand All @@ -58,15 +57,20 @@ export const AccountRecoveryComponent: FunctionComponent<AccountRecoveryProps> =
const { onAlertFired, featureConfig, ["data-testid"]: testId } = props;

const { t } = useTranslation();
const allowedScopes: string = useSelector((state: AppState) => state?.authenticationInformation?.scope);
const RECOVERY_CONNECTOR: string = "account-recovery";
const RECOVERY_PASSWORD_QUESTION: string = "Recovery.Question.Password.Enable";
const RECOVERY_PASSWORD_NOTIFICATION: string = "Recovery.Notification.Password.Enable";
const RECOVERY_PASSWORD_NOTIFICATION_EMAIL_LINK: string = "Recovery.Notification.Password.emailLink.Enable";
const RECOVERY_PASSWORD_NOTIFICATION_SMS_OTP: string = "Recovery.Notification.Password.smsOtp.Enable";
const RECOVERY_USERNAME_NOTIFICATION: string = "Recovery.Notification.Username.Enable";
const [ isQsRecoveryEnabled, setIsQsRecoveryEnabled ] = useState<boolean>(false);
const [ isNotificationRecoveryEnabled, setIsNotificationRecoveryEnabled ] = useState<boolean>(false);
const [ isNotificationRecoveryEmailLinkEnabled, setIsNotificationRecoveryEmailLinkEnabled ] =
useState<boolean>(false);
const [ isNotificationRecoverySMSOTPEnabled, setIsNotificationRecoverySMSOTPEnabled ] = useState<boolean>(false);
const [ isUsernameRecoveryEnabled, setIsUsernameRecoveryEnabled ] = useState<boolean>(false);
const [ isAccountRecoveryDetailsLoading, setIsAccountRecoveryDetailsLoading ] = useState<boolean>(false);
const hasAccountSecurityConfigReadPermissions: boolean = useRequiredScopes( featureConfig?.security?.scopes?.read );

/**
* The following method gets the preference for account recovery.
Expand All @@ -80,7 +84,9 @@ export const AccountRecoveryComponent: FunctionComponent<AccountRecoveryProps> =
properties: [
RECOVERY_PASSWORD_QUESTION,
RECOVERY_PASSWORD_NOTIFICATION,
RECOVERY_USERNAME_NOTIFICATION
RECOVERY_USERNAME_NOTIFICATION,
RECOVERY_PASSWORD_NOTIFICATION_EMAIL_LINK,
RECOVERY_PASSWORD_NOTIFICATION_SMS_OTP
]
}
];
Expand All @@ -92,14 +98,30 @@ export const AccountRecoveryComponent: FunctionComponent<AccountRecoveryProps> =
const responseProperties: PreferenceProperty[] = passwordRecoveryOptions[0].properties;

responseProperties.forEach((prop: PreferenceProperty) => {
if (prop.name === RECOVERY_PASSWORD_QUESTION) {
setIsQsRecoveryEnabled(prop.value.toLowerCase() == "true" ? true : false);
}
if (prop.name === RECOVERY_PASSWORD_NOTIFICATION) {
setIsNotificationRecoveryEnabled(prop.value.toLowerCase() == "true" ? true : false);
}
if (prop.name === RECOVERY_USERNAME_NOTIFICATION) {
setIsUsernameRecoveryEnabled(prop.value.toLowerCase() == "true" ? true : false);
switch (prop.name) {
case RECOVERY_PASSWORD_QUESTION:
setIsQsRecoveryEnabled(prop.value.toLowerCase() === "true");

break;
case RECOVERY_PASSWORD_NOTIFICATION:
setIsNotificationRecoveryEnabled(prop.value.toLowerCase() === "true");

break;
case RECOVERY_PASSWORD_NOTIFICATION_EMAIL_LINK:
setIsNotificationRecoveryEmailLinkEnabled(prop.value.toLowerCase() === "true");

break;
case RECOVERY_PASSWORD_NOTIFICATION_SMS_OTP:
setIsNotificationRecoverySMSOTPEnabled(prop.value.toLowerCase() === "true");

break;
case RECOVERY_USERNAME_NOTIFICATION:
setIsUsernameRecoveryEnabled(prop.value.toLowerCase() === "true");

break;
default:
// Cases where prop.name doesn't match any of the above cases are not handled.
break;
}
});
} else {
Expand Down Expand Up @@ -161,11 +183,7 @@ export const AccountRecoveryComponent: FunctionComponent<AccountRecoveryProps> =
{ !isAccountRecoveryDetailsLoading ? (
<List divided={ true } verticalAlign="middle" className="main-content-inner">
<List.Item className="inner-list-item">
{ hasRequiredScopes(
featureConfig?.security,
featureConfig?.security?.scopes?.read,
allowedScopes
) &&
{ hasAccountSecurityConfigReadPermissions &&
isFeatureEnabled(
featureConfig?.security,
AppConstants.FEATURE_DICTIONARY.get("SECURITY_ACCOUNT_RECOVERY_CHALLENGE_QUESTIONS")
Expand All @@ -178,22 +196,27 @@ export const AccountRecoveryComponent: FunctionComponent<AccountRecoveryProps> =
) : null }
</List.Item>
<List.Item className="inner-list-item">
{ hasRequiredScopes(
featureConfig?.security,
featureConfig?.security?.scopes?.read,
allowedScopes
) &&
{ hasAccountSecurityConfigReadPermissions &&
isFeatureEnabled(
featureConfig?.security,
AppConstants.FEATURE_DICTIONARY.get("SECURITY_ACCOUNT_RECOVERY_EMAIL_RECOVERY")
) &&
(isNotificationRecoveryEnabled || isUsernameRecoveryEnabled) ? (
(isNotificationRecoveryEmailLinkEnabled || isUsernameRecoveryEnabled) ? (
<EmailRecovery
onAlertFired={ onAlertFired }
data-testid={ `${testId}-settings-section-email-recovery` }
/>
) : null }
</List.Item>
<List.Item className="inner-list-item">
{ hasAccountSecurityConfigReadPermissions &&
isNotificationRecoverySMSOTPEnabled ? (
<SMSRecovery
onAlertFired={ onAlertFired }
data-componentid={ `${testId}-settings-section-sms-recovery` }
/>
) : null }
</List.Item>
</List>
) : (
<EmphasizedSegment className="placeholder-container">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2019, WSO2 LLC. (https://www.wso2.com). All Rights Reserved.
* Copyright (c) 2019-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
Expand All @@ -18,3 +18,4 @@

export * from "./email-recovery";
export * from "./security-questions-recovery";
export * from "./sms-recovery/sms-recovery";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we are moving away from index files right ?
Then shall we let this new "sms-recovery" file to be directly imported instead of using index file ?

cc: @JayaShakthi97

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this index file is already here, wouldn't it make sense to use it instead of using direct imports for this file only?

Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* 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.
*/

.mobile-field {
width: 200px !important;
}

.small-description{
margin-top: 10px !important;
font-size: 12px;
}

.description {
margin-top: 7px;
}

.inline.fields {
margin-top: 8px;
}
Loading
Loading