Identity events and conditional authentication
Tags: ciam, wso2, idp, iam
Published: 2024-04-10 02:03:15

In today's fast-paced digital landscape, application developers face a hard job: meeting high business demands while ensuring a seamless and secure login experience. The task becomes even more challenging when considering the need for resilience, ease of implementation, scalability, and adherence to tight timeframes. 
Modern Identity and Access Management (IAM) solutions are designed to accommodate evolving business needs while remaining developer-friendly.In essence, the quest for an ideal login experience is neverending, but with the right IAM solution, developers can perform modern application development with confidence and ease.

Recently, I had a task to implement a solution supporting a self-sign-up process and providing users with limited-time access to applications until they've subscribed for full access. 

## Solution proposed 

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3xs1cb0g5ok2t51boq97.png)

The application redirects users to the Identity Provider (IDP) during the login process (configuration needs to be provided). The IDP will facilitate self-registration, creating a user profile in an external persistent store, such as a MySQL database, with a trial period. Subsequently, an external payment system will alter the user's status in the database upon successful payment confirmation.

During the login flow, upon successful user identification, the application will verify the user's status in the external database. If the user attempts to log in during the trial period, access will be granted. However, if the user tries to log in after the trial period and payment confirmation is pending, access will be denied. 

The WSO2 [Asgardeo](https://wso2.com/asgardeo/) is used as IDP and [Choreo](https://choreo.dev/) as an Integration platform.

---

## Database setup

I have used the publicly available MySQL database and created a simple table with the following DDL.

`CREATE DATABASE `cloud_demo`;
CREATE TABLE `Users` (
  `id` int NOT NULL AUTO_INCREMENT,
  `userName` varchar(55) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
  `allowAccess` int NOT NULL,
  `endSub` date NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=56 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;`


## IDP setup

In Asgardeo created an organization YDNC.
In Login&Registation, click on Self Registration and enable it.
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/n866mvqboyp2hxw0e065.png)

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gdu6r7ikzwfgs6srzrkb.png)
In the Event section check the Registration events.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8r17irk51zh0h2tpbgzu.png)

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ye1momw3asuvz9lwz9az.png)
From here if you can click on 'Go to Choreo', note that you need to have a valid subscription and organization with the same name.

## Choreo setup

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/c30as1w65noicvevk1fn.png)

<u>Create a CRUD service and expose it as an API</u>

Click on the 'Create' button above Component Listing and choose Service. 

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lqs61ro6v24z1h3tvmrj.png)
Note that you will need a GitHub repository with your code. I have used  [Ballerina](https://ballerina.io/), a cloud-native language optimized for integration.
Creating the REST API service to connect with DB to support the CRUD command is straightforward. 

`service /company on new http:Listener(8090) {

    isolated resource function post user(@http:Payload User payload) returns error? {
        sql:ParameterizedQuery insertQuery = `INSERT INTO Users (userName,allowAccess,endSub) VALUES (${payload.userName}, ${payload.allowAccess}, DATE_ADD(now(),interval 5 day))`;
        sql:ExecutionResult _ = check mysqlClient->execute(insertQuery);
    }

    isolated resource function put user(@http:Payload User payload) returns error? {
        sql:ParameterizedQuery updateQuery = `UPDATE  Users SET allowAccess=${payload.allowAccess},
                                             endSub=${payload?.endSub} where
                                             userName= ${payload.userName}`;
        sql:ExecutionResult _ = check mysqlClient->execute(updateQuery);
    }
    
    isolated resource function get users(string userName) returns json|error {
        sql:ParameterizedQuery selectQuery = `select userName,allowAccess,endSub from Users where userName=${userName}`;
        stream <User, sql:Error?> resultStream = mysqlClient->query(selectQuery);
        User[] users = [];

        check from User item in resultStream
            do {
                users.push(item);
            };
        return users;
    }

    isolated resource function post risk(@http:Payload RiskRequest payload) returns User|error? {
       
        sql:ParameterizedQuery selectQuery = `select userName,allowAccess,endSub from Users where userName=${payload.userName}`;
        
        stream <User, sql:Error?> resultStream =  mysqlClient->query(selectQuery);
        User[] users = [];

        check from User item in resultStream
            do {
                users.push(item);
            };
        return users[0];
    }

    isolated resource function delete user(string userName) returns error? {
        sql:ParameterizedQuery deleteQuery = `DELETE FROM Users WHERE userName = ${userName}`;
        sql:ExecutionResult _ = check mysqlClient->execute(deleteQuery);
    }
}`

Git-hub repository [https://github.com/lpastor74/db_crud](https://github.com/lpastor74/db_crud)

Once your component is created you can publish it and set up a configuration per environment.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2x5iodfp4dn4ek8ppcwa.png)

API Service is public and it will be visible in the developer portal.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9p0m4tpi48ytsursynfm.png)

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yura0kgvtmnzy4q9iwk5.png)

Create an application and subscribe to API. In the credential section note the ClientID, Secret, Token Endpoint, and API service URL because you will need it to finish the setup of the webhook component.

<u>Create a webhook</u>

Click on the 'Create' button above Component Listing and choose Webhook. The code to handle the 'add user' event checks if the method is 'SELF_SIGNUP' and connects to API service and in DB creates the record with the user name and sets up the allowAccess value to 0 as forbidden.

`remote function onAddUser(asgardeo:AddUserEvent event ) returns error? {
      log:printInfo(event.toJsonString());

      json __user = event.toJson();
      
      string method  = check __user.eventData.userOnboardMethod;
      if(method=="SELF_SIGNUP")
      { 
           string userNameStr  = check __user.eventData.userName;
           log:printInfo(userNameStr);
           http:Client http_Client = check new (_endUrl, 
             auth = {
                     tokenUrl: _tokenUrl,
                     clientId: _clientId,
                     clientSecret: _clientSecret
             });
             log:printInfo("...sending...");
             anydata|http:ClientError unionResult = check http_Client->/user.post({
                 userName: userNameStr,
                 allowAccess: 0
             });
      }    
    }`

Git-hub repository [https://github.com/lpastor74/asgrd-hook](https://github.com/lpastor74/asgrd-hook)
Once your component is created you can publish them and set a configuration per environment.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fy4u075f08m2q79ads8f.png)
Note that the configuration of the webhook is populated automatically and you are only responsible for filling up the configuration to allow connecting to your REST API created in the previous section

## Create an application in IDP
 

I chose a single-page application and the Sample React application provided in the QuickStart tab to run it locally, to simulate the login flow.
Don't forget to update the app with your configuration (ClientID, Base URL, Redirect URL, and Scope). All details are in the Quickstart section. 

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/y6ux0cgchna5ckm7kd87.png)

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mvhoy8cfxeql4ote7n5l.png)

in Login Flow enable Conditional Authentication and put the following code

`var errorPageParameters = {
    'status': 'Unauthorized',
    'statusMsg': 'You need have valid subsription.'
};

var errorPage = '';

var onLoginRequest = function(context) {
    executeStep(1, {
        onSuccess: function(context) {
            var today = new Date();
            var user = context.currentKnownSubject;
            Log.debug('UserName of ' + context.currentKnownSubject.uniqueId + ' is : ' + user.username);

            var connectionMetadata = {
                "url": "https://URLtoPublicAPIservice/risk",
                "consumerKey": "xxxxxx",
                "consumerSecret": "xxxxx",
                "asgardeoTokenEndpoint": "https://api.asgardeo.io/t/ydnc/oauth2/token"
            };

            var requestPayload = {
                "userName": user.username
            };

            callChoreo(connectionMetadata, requestPayload, {
                onSuccess: function(context, data) {
                    Log.info("Successfully invoked the Choreo API.");
                    Log.info(data.userName);
                    Log.info(data.allowAccess);

                    if (data.allowAccess == 0) and (today > data.endSub){
                        Log.debug('User ' + context.currentKnownSubject.uniqueId + ' dont have valid subscription.');
                        sendError(errorPage, errorPageParameters);
                    }

                },
                onFail: function(context, data) {
                    Log.info("Error occurred while invoking the Choreo API.");
                    Log.info(data);
                },
                onTimeout: function(context, data) {
                    Log.info("Invoking Choreo API timed out.");
                    Log.info(data);
                }
            });
        }
    });
};` 

---

## Test everything

Locally, start the React application on https://localhost:3000

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2o3g8r4t9lde32ujwotu.png)
On login, the user will be redirected to Asgardeo and SignIn form (please, observe I branded the form ;)) will have the option to Register.
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/oncdlqxbyknoa1w75bli.png)
The user will populate the required field, and if he wants, some of the optional ones.  

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mboyx8jmzbzma6wko7oq.png)

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2318t1qy34uly57lws5e.png)
Upon successful account creation, we can observe runtime logs in Choreo indicating that the event was triggered.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ufuxuvegx4iox3j1ok50.png)
And in DB we have a new record with the user email, status 0, and endSub date set 5 days in the future.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/aeg9kf2j0or46a3k5idh.png)
If the user tries to access during these 5 days login will be successful but after that (if the status is not set to 1) he will get the following message.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/v7le4wq7ctneep0l6sih.png)
Note that I have not covered the part where, after valid payment, the user status is changed in DB from 0 to 1.
Integrating with the publicly exposed API will be straightforward. You just need to make a PUT call to the /user method and include the following payload.
`{
  "userName": correctUserName,
  "allowAccess": 1
} `

**Final note**
All functionality presented here is available out of the box and with a free subscription.
Good luck and happy coding