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
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 DATABASE
cloud_demo;
Users
CREATE TABLE(
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,
id
PRIMARY KEY ()
) 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.
In the Event section check the Registration events.
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
Create a CRUD service and expose it as an API
Click on the 'Create' button above Component Listing and choose Service.
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);
}
}`
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.
API Service is public and it will be visible in the developer portal.
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
});
}
}`
Git-hub repository https://github.com/lpastor74/asgrd-hook
Once your component is created you can publish them and set a configuration per environment.
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.
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
On login, the user will be redirected to Asgardeo and SignIn form (please, observe I branded the form ;)) will have the option to Register.
The user will populate the required field, and if he wants, some of the optional ones.
Upon successful account creation, we can observe runtime logs in Choreo indicating that the event was triggered.
And in DB we have a new record with the user email, status 0, and endSub date set 5 days in the future.
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.
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)