Complete Guide to Setting Up Custom Authentication
Learn how to set up Custom authentication.
To create a custom authentication scheme:
- On the Workspace home page, click the App Builder icon.
- Select an application.
- On the Application home page, click Shared Components.
- The Shared Components page appears.
Under Security, select Authentication Schemes.
On the Authentication Schemes page, click Create.
Select Based on a pre-configured scheme from the gallery and click Next.
Under Name:
Name – Enter the name used to reference the authentication scheme by other application developers.
Scheme Type – Select Custom.
Fill in the appropriate fields.
To learn more about a specific field, see field-level Help.
Click Create Authentication Scheme.
Creating a custom authentication scheme alone is not sufficient for building a fully tailored authentication system. There are many additional, actionable, and important considerations involved.
In this article, we’ll dive deep into this complex subject and provide you with practical, step-by-step guidance to help you implement a complete custom authentication solution in your Oracle APEX application.
Here are the key topics we’ll cover to help you gain a thorough understanding of the subject.
Create a custom authentication scheme – covered above.
Create Custom Logic – a PL/SQL code
Password Security
Register a User
Authenticate User
Using Username and Password
Using Username and OTP (optional)
Password Reset
Role Base Access Control – RBAC
Writing a separate article on RBAC.
Lets start writing our custom logic for our Oracle APEX Application’s custom authentication.
Login to your APEX instance as developer.
Navigation:
Home > SQL Workshop > Object Browser
On the left panel, click on + sign and choose Package to create
Give a name – LAMCX_AUTH
Enter the name of your choice for the Package to be created. The name must conform to Oracle naming conventions and cannot contain spaces, or start with a number or underscore.
Click on Create Package
Now refresh page and check in left panel, you will find your newly created package.
Navigate to Package Specification
Create function – HASH_PASSWORD
FUNCTION hash_password (
p_user_name IN VARCHAR2,
p_password IN VARCHAR2
) RETURN VARCHAR2;
Create procedure – SIGNUP
PROCEDURE signup (
p_first_name IN VARCHAR2,
p_last_name IN VARCHAR2,
p_user_name IN VARCHAR2,
p_email IN VARCHAR2,
p_app_id NUMBER,
p_company_id NUMBER,
p_referral varchar2,
p_password IN VARCHAR2 DEFAULT NULL
);
Create procedure – AUTHENTICATE_USER
FUNCTION authenticate_user (
p_username IN VARCHAR2,
p_password IN VARCHAR2 DEFAULT NULL,
p_app_id IN NUMBER DEFAULT NULL,
p_by_otp IN VARCHAR2 DEFAULT 'N',
p_otp IN NUMBER DEFAULT NULL
) RETURN BOOLEAN;
Create procedure – RESET_PASSWORD
PROCEDURE reset_password (
p_user_name IN VARCHAR2,
p_password IN VARCHAR2,
p_password_new IN VARCHAR2
);
Create function – IS_ACTIVE
FUNCTION is_active(
p_username varchar2 default null
) return boolean;
Click on button –Â Save and Compile
You must check and confirm, if package is compiled successfully
Create database table – LAMCX_USERS, to store user data.
CREATE TABLE "LAMCX_USERS"
( "ID" NUMBER GENERATED BY DEFAULT ON NULL AS IDENTITY MINVALUE 1 MAXVALUE 9999999999999999999999999999 INCREMENT BY 1 START WITH 1 CACHE 20 NOORDER NOCYCLE NOKEEP NOSCALE NOT NULL ENABLE,
"FIRST_NAME" VARCHAR2(2000 CHAR),
"LAST_NAME" VARCHAR2(2000 CHAR),
"USER_NAME" VARCHAR2(100),
"START_DATE" DATE,
"END_DATE" DATE,
"STATUS" VARCHAR2(30 CHAR),
"REMARKS" VARCHAR2(4000 CHAR),
"ATTRIBUTE1" VARCHAR2(150 CHAR),
"ATTRIBUTE2" VARCHAR2(150 CHAR),
"ATTRIBUTE3" VARCHAR2(150 CHAR),
"ATTRIBUTE4" VARCHAR2(150 CHAR),
"ATTRIBUTE5" VARCHAR2(150 CHAR),
"PASSWORD_HASH" VARCHAR2(2000 CHAR),
"PASSWORD_RAW" VARCHAR2(30 CHAR),
"MOBILE" VARCHAR2(30 CHAR),
"EMAIL" VARCHAR2(100 CHAR),
"SOURCE" VARCHAR2(30 CHAR),
"USER_TYPE" VARCHAR2(30 CHAR),
"PWD_UPDATE_STATUS" VARCHAR2(10 CHAR),
"PWD_UPDATE_DATE" DATE,
"PWD_UPDATED_BY" NUMBER,
"PWD_OTP" VARCHAR2(10 CHAR),
"PWD_OTP_REQ_START" DATE,
"PWD_OTP_REQ_END" DATE,
"EMP_IMG" BLOB,
"COMPANY_ID" NUMBER,
"CREATED" DATE,
"CREATED_BY" VARCHAR2(255 CHAR),
"UPDATED" DATE,
"UPDATED_BY" VARCHAR2(255 CHAR),
"ACCESS_ROLES" VARCHAR2(500),
"FULL_NAME" VARCHAR2(150),
"APPLICATION_ID" NUMBER,
"BUYER" VARCHAR2(1) DEFAULT 'N',
"AGENT" VARCHAR2(1) DEFAULT 'N',
"LINKEDIN" VARCHAR2(200),
"LAST_LOGGED_METHOD" VARCHAR2(30),
"LAST_LOGGED" DATE,
"DISPLAY_IMAGE" VARCHAR2(4000),
"EMAIL_VERIFIED" VARCHAR2(10) DEFAULT 'N',
"COUNTRY" VARCHAR2(100),
"STATE" VARCHAR2(100),
"CITY" VARCHAR2(100),
"ADDRESS_1" VARCHAR2(100),
"ADDRESS_2" VARCHAR2(100),
"POSTAL_CODE" VARCHAR2(15),
"ORG_TAX_ID" VARCHAR2(20),
"ORG_INCOME_ID" VARCHAR2(20),
"CONNECT_REQ" VARCHAR2(10) DEFAULT 'N',
"REQUESTED_BY" NUMBER,
"REQUEST_DATE" DATE,
"ISD" NUMBER,
"REQUESTED_BY_COMP" NUMBER,
"ACCOUNT_TYPE" VARCHAR2(20),
"RELEASE_REQUEST" VARCHAR2(10) DEFAULT 'N',
"RELEASE_REQUEST_DATE" DATE,
"RELEASE_REQUEST_BY" NUMBER,
"RELEASE_REQUEST_REM" VARCHAR2(1000),
"WF_ID" NUMBER,
"ACCEPT_PRIVACY_POLICY" VARCHAR2(10) DEFAULT 'N',
"ACCEPT_PRIVACY_POLICY_DATE" DATE,
"REFERRAL_USER" NUMBER,
"OTP_LOGIN" NUMBER,
"OTP_LOGIN_DATE" DATE,
"PARSE_COUNT" NUMBER DEFAULT 0,
"PARSE_COUNT_MAX" NUMBER DEFAULT 3,
CONSTRAINT "LAMCX_USERS_ID_PK" PRIMARY KEY ("ID")
USING INDEX ENABLE
) ;
Create an Index on the table
CREATE OR REPLACE EDITIONABLE TRIGGER "LAMCX_USERS_BIU" BEFORE
INSERT OR UPDATE ON LAMCX_USERS
FOR EACH ROW
BEGIN
IF inserting THEN
-- Make sure to save the full name
-- Make sure to save the username in upper case
:new.user_name := upper(:new.user_name);
-- Hash the password so we are not saving clear text
-- :new.password_hash := lams_auth.hash_password(upper(:new.user_name), :new.user_name||'@123');
:new.password_hash := lamcx_auth.hash_password(upper(:new.user_name), nvl(:new.password_raw,:new.user_name||'@123'));
:new.created := sysdate;
:new.created_by := e(coalesce(sys_context('APEX$SESSION', 'APP_USER'), user));
END IF;
:new.first_name := initcap(substr(:new.first_name,1,100));
:new.last_name := initcap(substr(:new.last_name,1,100));
:new.full_name := trim(:new.first_name||' '||:new.last_name);
:new.updated := sysdate;
:new.updated_by := e(coalesce(sys_context('APEX$SESSION', 'APP_USER'), user));
END LAMCX_USERS_biu;
CREATE TABLE "LAMCX_APEX_WORKSPACE_SESSIONS"
( "WORKSPACE_ID" NUMBER NOT NULL ENABLE,
"WORKSPACE_NAME" VARCHAR2(255) NOT NULL ENABLE,
"WORKSPACE_DISPLAY_NAME" VARCHAR2(4000),
"APEX_SESSION_ID" NUMBER,
"USER_NAME" VARCHAR2(255),
"REMOTE_ADDR" VARCHAR2(255),
"SESSION_CREATED" DATE NOT NULL ENABLE,
"SESSION_IDLE_TIMEOUT_ON" DATE NOT NULL ENABLE,
"SESSION_LIFE_TIMEOUT_ON" DATE NOT NULL ENABLE,
"SESSION_MAX_IDLE_SEC" NUMBER(5,0) NOT NULL ENABLE,
"SESSION_TIME_ZONE" VARCHAR2(255),
"SESSION_LANG" VARCHAR2(5),
"SESSION_TERRITORY" VARCHAR2(255),
"SESSION_DEBUG_LEVEL" NUMBER(1,0),
"SESSION_TRACE_MODE" VARCHAR2(5),
"SESSION_AUTHENTICATION_ID" NUMBER,
"SESSION_TENANT_ID" VARCHAR2(32),
"SESSION_AUTHENTICATION_NAME" VARCHAR2(255),
"APPLICATION_ID" NUMBER,
"SESSION_END" DATE
) ;
As now we have created our package specification, ready with our database table, now we proceed to create our package body.
Inside package, select Body tab and start writing your actual logic for the process.
Function – HASH_PASSWORD
FUNCTION hash_password (
p_user_name IN VARCHAR2,
p_password IN VARCHAR2
) RETURN VARCHAR2 IS
l_user lamcx_users.user_name%TYPE := upper(p_user_name);
l_password VARCHAR2(1000);
BEGIN
--
-- The following encrypts the provided password and returns the encrypted string.
-- This is a one-way encryption using SHA512
--
SELECT
standard_hash(p_user_name || p_password, 'SHA512')
INTO l_password
FROM
dual;
RETURN l_password;
END hash_password;
Procedure – SIGNUP
PROCEDURE signup (
p_first_name IN VARCHAR2,
p_last_name IN VARCHAR2,
p_user_name IN VARCHAR2,
p_email IN VARCHAR2,
p_app_id NUMBER,
p_company_id NUMBER,
p_referral varchar2,
p_password IN VARCHAR2 DEFAULT NULL
) IS
l_status VARCHAR2(1000) := NULL;
l_status2 VARCHAR2(1000) := NULL;
l_id NUMBER;
x NUMBER := 0;
cnt NUMBER := 0;
-- e_oldpass EXCEPTION;
-- PRAGMA exception_init ( e_oldpass, -20001 );
BEGIN
BEGIN
SELECT
COUNT(1)
INTO x
FROM
lamcx_users
WHERE
user_name = p_user_name
AND (company_id is null or company_id = p_company_id);
IF x > 0 THEN
cnt := cnt + 1;
l_status := cnt || '. Invalid User Name !';
END IF;
EXCEPTION
WHEN OTHERS THEN
NULL;
END;
x := 0;
BEGIN
SELECT
COUNT(1)
INTO x
FROM
lamcx_users
WHERE
d(email) = p_email
AND company_id = nvl(p_company_id,company_id)
and rownum=1;
IF x > 0 THEN
cnt := cnt + 1;
l_status2 := cnt || '. Email Exists, Provide a unique email !';
END IF;
EXCEPTION
WHEN OTHERS THEN
NULL;
END;
IF cnt = 0 THEN
INSERT INTO lamcx_users (
first_name,
last_name,
user_name,
email,
source,
status,
start_date,
application_id,
company_id,
password_raw,REFERRAL_USER
) VALUES (
e(p_first_name),
e(p_last_name),
p_user_name,
e(p_email),
'SIGNUP',
'Y',
sysdate,
p_app_id,
p_company_id,
nvl(p_password, p_user_name || '@xxxxx'),LAMCX_UTL.ENCODE(p_referral,'DECODE')
) RETURNING id INTO l_id;
ELSE
raise_application_error(-20001, l_status
|| chr(13)
|| chr(10)
|| l_status2);
END IF;
EXCEPTION
WHEN OTHERS THEN
RAISE;
END signup;
Procedure – AUTHENTICATE_USER
FUNCTION authenticate_user (
p_username IN VARCHAR2,
p_password IN VARCHAR2 DEFAULT NULL,
p_app_id IN NUMBER DEFAULT NULL,
p_by_otp IN VARCHAR2 DEFAULT 'N',
p_otp IN NUMBER DEFAULT NULL
) RETURN BOOLEAN IS
l_user_name lamcx_users.user_name%TYPE := upper(p_username);
l_password lamcx_users.password_hash%TYPE;
l_hashed_password VARCHAR2(1000);
l_roles VARCHAR2(1000);
l_name VARCHAR2(1000);
l_curr VARCHAR2(10);
l_count NUMBER;
l_otp NUMBER;
l_otp_date DATE;
l_id lamcx_users.id%TYPE;
l_comp_id lamcx_users.company_id%TYPE;
BEGIN
-- Codes I normally use for AUTHENTICATION_RESULTS
-- These get logged to the APEX Login Access Log.
--
-- 0 Normal, successful authentication
-- 1 Unknown User Name
-- 2 Account Locked
-- 3 Account Expired
-- 4 Incorrect Password
-- 5 Password First Use
-- 6 Maximum Login Attempts Exceeded
-- 7 Unknown Internal Error
--
-- First, check to see if the user exists
BEGIN
SELECT
id,
company_id, trim(d(FIRST_NAME)||' '||d(last_name)), OTP_LOGIN,OTP_LOGIN_DATE
INTO
l_id,
l_comp_id,l_name,l_otp,l_otp_date
FROM
lamcx_users
WHERE
upper(user_name) = l_user_name
-- AND application_id = NVL(p_app_id,v('APP_ID'))
AND status='Y'
AND ROWNUM = 1;
EXCEPTION
WHEN OTHERS THEN
l_id := 0;
END;
IF l_id > 0 THEN
BEGIN
SELECT
currency_code
INTO l_curr
FROM
lamcx_company_profiles
WHERE
company_id = l_comp_id;
EXCEPTION
WHEN OTHERS THEN
NULL;
END;
-- update lamcx_users set ADDRESS_2=l_otp||'.'||p_otp||'-'||l_otp_date where id=l_id;
IF p_by_otp='Y' THEN
IF l_otp_date < sysdate THEN
-- OTP expired.
apex_util.set_authentication_result(3);
RETURN FALSE;
ELSIF l_otp = p_otp THEN
-- Good result.
apex_util.set_authentication_result(0);
apex_util.set_session_state(p_name => 'USER_ID', p_value => l_id);
apex_util.set_session_state(p_name => 'USER_ROLE', p_value => l_roles);
apex_util.set_session_state(p_name => 'P_NAME', p_value => l_name);
apex_util.set_session_state(p_name => 'COMPANY_ID', p_value => l_comp_id);
apex_util.set_session_state(p_name => 'CURRENCY_CODE', p_value => l_curr);
apex_util.set_session_state(p_name => 'SYSTEM_DATE', p_value => to_char(sysdate, 'DD-MON-RRRR'));
apex_util.set_session_state(p_name => 'APP_YEAR', p_value => to_char(sysdate, 'RRRR'));
BEGIN
INSERT INTO LAMCX_APEX_WORKSPACE_SESSIONS (WORKSPACE_ID,WORKSPACE_NAME,APEX_SESSION_ID,
USER_NAME,REMOTE_ADDR,SESSION_CREATED,SESSION_IDLE_TIMEOUT_ON,SESSION_LIFE_TIMEOUT_ON,
SESSION_MAX_IDLE_SEC,SESSION_TIME_ZONE,SESSION_AUTHENTICATION_ID,SESSION_AUTHENTICATION_NAME,APPLICATION_ID)
SELECT WORKSPACE_ID,WORKSPACE_NAME,APEX_SESSION_ID,
l_user_name,REMOTE_ADDR,SESSION_CREATED,SESSION_IDLE_TIMEOUT_ON,SESSION_LIFE_TIMEOUT_ON,
SESSION_MAX_IDLE_SEC,SESSION_TIME_ZONE,SESSION_AUTHENTICATION_ID,NVL(SESSION_AUTHENTICATION_NAME,'EMAIL'),v('APP_ID')
FROM APEX_WORKSPACE_SESSIONS
WHERE APEX_SESSION_ID=APEX_CUSTOM_AUTH.GET_SESSION_ID;
EXCEPTION
WHEN OTHERS THEN NULL;
END;
RETURN TRUE;
ELSE
-- Good Invalid OTP.
apex_util.set_authentication_result(4);
RETURN FALSE;
END IF;
ELSE-- if login by password
-- Hash the password provided
l_hashed_password := hash_password(l_user_name, p_password);
-- Get the stored password
SELECT
password_hash
INTO l_password
FROM
lamcx_users
WHERE
-- user_name = l_user_name;
id=l_id;
-- Compare the two, and if there is a match, return TRUE
IF l_hashed_password = l_password THEN
-- Good result.
apex_util.set_authentication_result(0);
apex_util.set_session_state(p_name => 'USER_ID', p_value => l_id);
apex_util.set_session_state(p_name => 'USER_ROLE', p_value => l_roles);
apex_util.set_session_state(p_name => 'P_NAME', p_value => l_name);
apex_util.set_session_state(p_name => 'COMPANY_ID', p_value => l_comp_id);
apex_util.set_session_state(p_name => 'CURRENCY_CODE', p_value => l_curr);
apex_util.set_session_state(p_name => 'SYSTEM_DATE', p_value => to_char(sysdate, 'DD-MON-RRRR'));
apex_util.set_session_state(p_name => 'APP_YEAR', p_value => to_char(sysdate, 'RRRR'));
BEGIN
INSERT INTO LAMCX_APEX_WORKSPACE_SESSIONS (WORKSPACE_ID,WORKSPACE_NAME,APEX_SESSION_ID,
USER_NAME,REMOTE_ADDR,SESSION_CREATED,SESSION_IDLE_TIMEOUT_ON,SESSION_LIFE_TIMEOUT_ON,
SESSION_MAX_IDLE_SEC,SESSION_TIME_ZONE,SESSION_AUTHENTICATION_ID,SESSION_AUTHENTICATION_NAME,APPLICATION_ID)
SELECT WORKSPACE_ID,WORKSPACE_NAME,APEX_SESSION_ID,
l_user_name,REMOTE_ADDR,SESSION_CREATED,SESSION_IDLE_TIMEOUT_ON,SESSION_LIFE_TIMEOUT_ON,
SESSION_MAX_IDLE_SEC,SESSION_TIME_ZONE,SESSION_AUTHENTICATION_ID,NVL(SESSION_AUTHENTICATION_NAME,'EMAIL'),v('APP_ID')
FROM APEX_WORKSPACE_SESSIONS
WHERE APEX_SESSION_ID=APEX_CUSTOM_AUTH.GET_SESSION_ID;
EXCEPTION
WHEN OTHERS THEN NULL;
END;
RETURN TRUE;
ELSE
-- Incorrect Password
-- raise_application_error(-20004, 'Error while LOGIN!' || p_username||', '||p_password);
apex_util.set_authentication_result(4);
RETURN FALSE;
END IF;
END IF;--END p_by_otp
ELSE
-- The username does not exist
apex_util.set_authentication_result(1);
RETURN FALSE;
END IF;
--
RETURN TRUE;
EXCEPTION
WHEN OTHERS THEN
raise_application_error(-20003, 'Error while LOGIN!' || sqlerrm);
-- We don't know what happened so log an unknown internal error
apex_util.set_authentication_result(7);
apex_util.set_custom_auth_status(sqlerrm);
RETURN FALSE;
END authenticate_user;
Procedure – RESET_PASSWORD
PROCEDURE reset_password (
p_user_name IN VARCHAR2,
p_password IN VARCHAR2,
p_password_new IN VARCHAR2
) IS
l_user_name lamcx_users.user_name%TYPE := upper(p_user_name);
l_password VARCHAR2(255);
l_hashed_password VARCHAR2(1000);
l_count NUMBER;
l_id lamcx_users.id%TYPE;
l_comp_id lamcx_users.company_id%TYPE;
BEGIN
-- First, check to see if the user exists
BEGIN
SELECT
id,
company_id
INTO
l_id,
l_comp_id
FROM
lamcx_users
WHERE
user_name = l_user_name
AND password_hash = hash_password(l_user_name, p_password)
-- AND application_id = v('APP_ID')
AND ROWNUM = 1;
EXCEPTION
WHEN OTHERS THEN
l_id := 0;
END;
IF l_id > 0 THEN
-- Hash the password provided
l_hashed_password := hash_password(l_user_name, p_password_new);
-- Set the hashed password
UPDATE lamcx_users
SET
password_hash = l_hashed_password,
password_raw = p_password_new
WHERE
user_name = l_user_name;
ELSE
raise_application_error(-20002, 'Invalid user/password - ' || p_user_name);
RETURN;
END IF;
EXCEPTION
WHEN OTHERS THEN
raise_application_error(-20003, 'Error while updating password!' || sqlerrm);
-- We don't know what happened so log an unknown internal error
END reset_password;
FUNCTION is_active(p_username varchar2 default null)
return boolean
is
x number := 0;
begin
select COUNT(1) INTO X from APEX_WORKSPACE_SESSIONS
where 1=1
and UPPER(user_name)=nvl(p_username,v('APP_USER'))
and sysdate between SESSION_CREATED and SESSION_LIFE_TIMEOUT_ON;
IF x=0 then
return false;
else
return true;
end if;
EXCEPTION
WHEN OTHERS THEN
RETURN false;
end is_active;
Save and Compile
Check and confirm, package must be compiled successfully.
Now we are ready with our package ‘LAMCX_AUTH’, lets configure it in our Authenticate Scheme. Navigate back to ‘create a custom authentication scheme’ section in this blog post, and see how we have mapped this package in point no. 7 sub-section 2, image 2.
Now we are done with our custom authentication scheme creation with appropriate custom logic.
You can create Registration and Password reset page and use these procedures, accordingly.
Your existing Login page will work with this custom auth.