Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
174 changes: 12 additions & 162 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,176 +3,22 @@
# Instructions to use the application

1. Register an organization with Asgardeo.
2. Create [custom attributes](https://wso2.com/asgardeo/docs/guides/users/attributes/manage-attributes/) named `accountType` and `businessName`. Add the accountType and country attributes to the profile scope.
2. Create [custom attributes](https://wso2.com/asgardeo/docs/guides/users/attributes/manage-attributes/) named `accountType` and `businessName`. Add the businessName, accountType and country attributes to the profile scope.
3. Create another [custom attribute](https://wso2.com/asgardeo/docs/guides/users/attributes/manage-attributes/) with the name `isFirstLogin`.
4. Enable the [Attribute Update Verification](https://wso2.com/asgardeo/docs/guides/users/attributes/user-attribute-change-verification/) for user email.
5. Create a SPA application.
* Enable the `Code` and `Refresh Grant` types
* Navigate to the "Shared Access" tab and share the application with all organizations.
* Enable the `Code`, `Refresh Grant` and `Organization Switch` types.
* Note that the organization switch grant type is available only after shared access is enabled.
* Add authorize redirect URL: `http://localhost:5173` and allowed origin: `http://localhost:5173`
* Add the `country` and `accountType` to Profile scope navigating to `User Attributes & Stores` -> `Attributes` -> `OpenId Connect` -> `Scopes` -> `Profile` -> `New Attribute`.
* Add the `mobile`, `country`, `email` and `accountType` to Profile scope navigating to `User Attributes & Stores` -> `Attributes` -> `OpenId Connect` -> `Scopes` -> `Profile` -> `New Attribute`.
* Enable the following scopes and attributes within the client application created.
* `Profile - Country, First Name, Last Name, Username, Birth Date, AccountType; Email - email; Phone - telephone; Address - country.`
* `Profile - Country, First Name, Last Name, Username, Birth Date, AccountType, Business Name, Email; Email - email; Phone - telephone; Address - country.`
6. Enable the following authenticators within the client application:
* `Identifier First` - First Step
* `Username and Password`, `Passkey` - Second Step
* `Totp` and `Email OTP` - Third Step
7. Configure the following conditional authentication script (Replace the `<NODE_SERVER_BASE_PATH>` with server URL):
```js
var moneyTransferThres = 10000;
var riskEndpoint = "<NODE_SERVER_BASE_PATH>/risk"

var onLoginRequest = function(context) {

var isMoneyTransfer = context.request.params.action && context.request.params.action[0] === "money-transfer";

if (isMoneyTransfer) {
Log.info("Custom param:" + context.request.params.action[0]);
Log.info("Custom param:" + context.request.params.transfer_amount[0]);
var amount = parseInt(context.request.params.transfer_amount[0] || -1);

executeStep(1);
if (amount > moneyTransferThres) {
executeStep(4, {
stepOptions: {
forceAuth: 'true'
}
}, {});
}

} else {

executeStep(1, {
onSuccess: function(context) {
var user = context.steps[1].subject;
var accountType = user.localClaims["http://wso2.org/claims/accountType"];
var country = user.localClaims["http://wso2.org/claims/country"];
Log.info("Account Type: " + accountType);
Log.info("Country: " + country);
var ipAddress = context.request.ip;
Log.info("IP Address: " + ipAddress);
var requestPayload = {
ip: ipAddress,
country: country,
};
if (accountType === "Personal") {
httpPost(riskEndpoint, requestPayload, {
"Accept": "application/json"
}, {
onSuccess: function(context, data) {
Log.info("Successfully invoked the external API.");
Log.info("Logging data for country risk: " + data.hasRisk);

if (data.hasRisk === false) {
executeStep(2, {
authenticationOptions: [{
authenticator: 'FIDOAuthenticator'
}, {
authenticator: 'BasicAuthenticator'
}]
}, {
onSuccess: function(context) {
var user = context.currentKnownSubject;
var sessions = getUserSessions(user);
Log.info(sessions);
if (sessions.length > 0) {
executeStep(3, {
authenticationOptions: [{
authenticator: 'email-otp-authenticator'
}]
}, {});
}
}
});
} else {
executeStep(2, {
authenticationOptions: [{
authenticator: 'FIDOAuthenticator'
}, {
authenticator: 'BasicAuthenticator'
}],
}, {});
Log.info("In 2nd step for Personal Accounts");

executeStep(3, {
authenticationOptions: [{
authenticator: 'email-otp-authenticator'
}]
}, {});
}
},
onFail: function(context, data) {
Log.error("Failed to invoke risk API");
fail();
}
});
} else if (accountType === "Business") {
Log.info("In second step for Business");

executeStep(2, {
authenticationOptions: [{
authenticator: 'BasicAuthenticator'
}]
}, {});
var preferredClaimURI = "http://wso2.org/claims/identity/preferredMFAOption";
var preferredClaim = user.localClaims[preferredClaimURI];

if (preferredClaim != null) {
Log.info("Preferred Claim Available");

var jsonObj = JSON.parse(preferredClaim);
var authenticationOption = jsonObj.authenticationOption;
Log.info("preferredClaim authenticationOption " + authenticationOption);
executeStep(3, {
authenticationOptions: [{
authenticator: authenticationOption
}],
}, {});
} else {
Log.info("Preferred claim not available and in 3rd step");
executeStep(3, {
authenticationOptions: [{
authenticator: 'totp'
}, {
authenticator: 'email-otp-authenticator'
}]
}, {
onSuccess: function(context) {
var preferredClaimURI = "http://wso2.org/claims/identity/preferredMFAOption";
Log.info("3rd step successful");
var user = context.steps[3].subject;
var isFirstLogin = user.localClaims["http://wso2.org/claims/isFirstLogin"];
Log.info("User isFirstLogin claim:" + isFirstLogin);
if (isFirstLogin === "false") {
var authenticatorName = context.steps[3].authenticator;
var preferredMFA = {
authenticationOption: authenticatorName
};
user.localClaims[preferredClaimURI] = JSON.stringify(preferredMFA);
Log.info("Preferred MFA set from second login for user" + user.username + " as " + user.localClaims[preferredClaimURI]);
} else {
user.localClaims["http://wso2.org/claims/isFirstLogin"] = false;
Log.info("User logged in for the first time. Setting isFirstLogin to false");
}
}
});
}
}
},
onFail: function(context) {
Log.info('User not found');
var parameterMap = {
'errorCode': 'login_failed',
'errorMessage': 'login could not be completed',
"errorURI": 'https://localhost:9443/authenticationendpoint/login.jsp'
};
fail(parameterMap);

}
});
}
};

```
7. Configure the conditional authentication script (Replace the `<NODE_SERVER_BASE_PATH>` with server URL) with the one found at conditional-auth-script.js.
8. Create a standard web application.
9. Navigate to the "Shared Access" tab and share the application with all organizations.
10. Enable the following grant types:
Expand All @@ -191,6 +37,10 @@ redirect url: `https://localhost:5003`, allowed origin: `https://localhost:5003
```
internal_organization_create internal_organization_view internal_organization_update internal_organization_delete
```
- OAuth2 Introspection API
```
internal_oauth2_introspect
```
- Organization APIs:
- SCIM2 Users API with the scopes:
```
Expand All @@ -201,7 +51,7 @@ redirect url: `https://localhost:5003`, allowed origin: `https://localhost:5003
internal_org_user_mgt_view internal_org_role_mgt_delete internal_org_role_mgt_create internal_org_role_mgt_update internal_org_role_mgt_view
```

13. Navigate to the Roles tab and create an application role named `Business Administrator` with the permissions for the SCIM2 Users and SCIM2 Roles organization APIs.
13. Navigate to the Roles tab and create an application role named `Business Administrator` with the permissions for the SCIM2 Users and SCIM2 Roles organization APIs. Also, create roles `Manager`, `Auditor` and `Member`.
14. Navigate to Connections -> Passkey Setup -> Add the Trusted Origins: `http://localhost:5173` and enable `Allow Passkey usernameless authentication` option.

15. Configure [Onfido identity verification](https://wso2.com/asgardeo/docs/guides/identity-verification/add-identity-verification-with-onfido/) for your organization.
Expand Down
1 change: 1 addition & 0 deletions app/global.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ interface Window {
APP_BASE_URL: string;
ASGARDEO_BASE_URL: string;
APP_CLIENT_ID: string;
APP_NAME: string;
DISABLED_FEATURES: string[];
TRANSFER_THRESHOLD: number;
IDENTITY_VERIFICATION_PROVIDER_ID: string;
Expand Down
3 changes: 3 additions & 0 deletions app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,17 @@
"@fortawesome/free-regular-svg-icons": "^6.7.2",
"@fortawesome/free-solid-svg-icons": "^6.7.2",
"@fortawesome/react-fontawesome": "^0.2.2",
"@mui/icons-material": "^6.5.0",
"@mui/material": "^6.4.5",
"@mui/x-data-grid": "^8.11.1",
"@vitejs/plugin-react": "^4.3.4",
"axios": "^1.8.1",
"notistack": "^3.0.2",
"onfido-sdk-ui": "^14.43.0",
"prop-types": "^15.8.1",
"qrcode.react": "^4.2.0",
"react": "^19.0.0",
"react-bootstrap": "^2.10.10",
"react-dom": "^19.0.0",
"react-router": "^7.3.0",
"vite": "^6.1.1",
Expand Down
1 change: 1 addition & 0 deletions app/public/config.example.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ window.config = {
ASGARDEO_BASE_URL: "",
ORGANIZATION_NAME: "",
APP_CLIENT_ID: "",
APP_NAME: "",
DISABLED_FEATURES: [],
TRANSFER_THRESHOLD: 10000,
IDENTITY_VERIFICATION_PROVIDER_ID: "",
Expand Down
40 changes: 34 additions & 6 deletions app/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
* under the License.
*/

import { useAsgardeo, SignedIn, SignOutButton, SignedOut, SignInButton } from "@asgardeo/react";
import { useAsgardeo, SignedIn, SignOutButton, SignedOut, SignInButton, useUser } from "@asgardeo/react";
import { lazy, Suspense, useState } from "react";
import {
BrowserRouter as Router,
Expand All @@ -26,7 +26,7 @@ import {
NavLink
} from "react-router";
import { SnackbarProvider } from "notistack";
import { ROUTES, SITE_SECTIONS } from "./constants/app-constants";
import { ACCOUNT_TYPES, ROUTES, SITE_SECTIONS } from "./constants/app-constants";
import PersonalBankingPage from "./pages/personal-banking";
import RegisterAccountPage from "./pages/register-account";
import UserProfilePage from "./pages/user-profile";
Expand All @@ -37,10 +37,13 @@ import "./assets/css/style.scss";
import { BankAccountProvider } from "./context/bank-account-provider";
import IdentityVerificationPage from "./pages/identity-verification";
import { IdentityVerificationProvider } from "./context/identity-verification-provider";
import { Dropdown, DropdownButton } from "react-bootstrap";
import BusinessProfilePage from "./pages/business-profile";

const App = () => {
const { isSignedIn, signIn, signOut } = useAsgardeo();
const { isSignedIn } = useAsgardeo();
const [ siteSection, setSiteSection ] = useState("");
const { profile} = useUser();

const TransferFundsPage = lazy(() => import("./pages/transfer-funds"));
const TransferFundsVerifyPage = lazy(() => import("./pages/transfer-funds-verify"));
Expand Down Expand Up @@ -70,7 +73,7 @@ const App = () => {
</span>
<span>
<SignedIn>
<SignOutButton className="login_link">Logout</SignOutButton>
<SignOutButton className="logout_link">Logout</SignOutButton>
</SignedIn>

<SignedOut>
Expand All @@ -79,7 +82,23 @@ const App = () => {
Open an account
</span>
</Link>
<SignInButton className="login_link">Login</SignInButton>
<span className="divider">|</span>
<DropdownButton
id="dropdown-custom-components"
title="Login"
autoClose={false}
>
<Dropdown.Item as="div" className="dropdown-item-custom dropdown-item-custom-top">
<SignInButton className="login_link" signInOptions={{ loginType: 'Personal' }}>
Personal Login
</SignInButton>
</Dropdown.Item>
<Dropdown.Item as="div" className="dropdown-item-custom">
<SignInButton className="login_link" signInOptions={{ loginType: 'Business' }}>
Business Login
</SignInButton>
</Dropdown.Item>
</DropdownButton>
</SignedOut>
</span>
</div>
Expand Down Expand Up @@ -166,11 +185,20 @@ const App = () => {
{ isSignedIn &&
<Route path={ ROUTES.USER_PROFILE } element={ <UserProfilePage setSiteSection={ setSiteSection } /> } />
}
{ isSignedIn &&
<Route path={ ROUTES.BUSINESS_PROFILE } element={ <BusinessProfilePage setSiteSection={ setSiteSection } /> } />
}
{/* <Route path="/" element={ <Navigate to={ ROUTES.PERSONAL_BANKING } setSiteSection={ setSiteSection } /> } /> */}
<Route path="/" element={
isSignedIn ?
(
<UserProfilePage setSiteSection={ setSiteSection } />
<>
{profile && profile["urn:scim:schemas:extension:custom:User"].accountType === ACCOUNT_TYPES.BUSINESS ? (
<BusinessProfilePage setSiteSection={ setSiteSection } />
) : (
<UserProfilePage setSiteSection={ setSiteSection } />
)}
</>
) : (
<PersonalBankingPage setSiteSection={ setSiteSection } />
)
Expand Down
4 changes: 4 additions & 0 deletions app/src/api/profile.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ export const closeAccount = (token) => {
})
};

export const closeBusinessAccount = (businessName) => {
return axiosClient.delete(`/close-business-account?businessName=${businessName}`);
};

export const resetPassword = (username, currentPassword, newPassword) => {
// In case the password contains non-ascii characters, converting to valid ascii format.
const encoder = new TextEncoder();
Expand Down
Loading
Loading