DEV Community

Cover image for Identity events and conditional authentication
Laslo Pastor
Laslo Pastor

Posted on

Identity events and conditional authentication

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

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 is used as IDP and Choreo as an Integration platform.


Database setup

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

CREATE DATABASEcloud_demo;
CREATE TABLE
Users(
idint NOT NULL AUTO_INCREMENT,
userNamevarchar(55) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
allowAccessint NOT NULL,
endSubdate 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

Image description
In the Event section check the Registration events.

Image description

Image description
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

Create a CRUD service and expose it as an API

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

Image description
Note that you will need a GitHub repository with your code. I have used Ballerina, 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);
}
Enter fullscreen mode Exit fullscreen mode

}`

Git-hub repository https://github.com/lpastor74/db_crud

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

Image description

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

Image description

Image description

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.

Create a webhook

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
         });
  }    
}`
Enter fullscreen mode Exit fullscreen mode

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

Image description
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

Image description

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);
            }
        });
    }
});
Enter fullscreen mode Exit fullscreen mode

};`


Test everything

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

Image description
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
The user will populate the required field, and if he wants, some of the optional ones.

Image description

Image description
Upon successful account creation, we can observe runtime logs in Choreo indicating that the event was triggered.

Image description
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
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
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

Top comments (0)