š¹ About
Password-based authentication alone is no longer enough for modern applications.
Adding OTP (One-Time Password) verification improves application security by validating the user through email verification.
In this blog, we will build a complete Email OTP Verification System in Oracle APEX using:
ā
Oracle APEX
ā
PL/SQL
ā
JavaScript
ā
APEX_MAIL
ā
Session State
ā
Dynamic Actions
We will create:
ā Generate OTP button on Login Page
ā OTP table for verification
ā OTP email functionality
ā 6-digit OTP input UI
ā Auto focus OTP boxes
ā OTP resend functionality
ā Countdown timer
ā OTP validation logic
š ļø Tools and Technologies
- Oracle APEX
- Oracle Database
- SQL
- PL/SQL
- JavaScript
- APEX_MAIL
š What We Are Building
š Workflow
User Enters Email & Password
ā
Click Generate OTP
ā
OTP Sent to Email
ā
Redirect to Verify OTP Page
ā
Enter 6 Digit OTP
ā
Validate OTP
ā
Login Successš Step 1: Create OTP Table
Create a table to store OTP details.
CREATE TABLE otp_log (
user_email VARCHAR2(255),
otp_code VARCHAR2(10),
expiry_time TIMESTAMP,
is_verified VARCHAR2(1) DEFAULT 'N'
);š Table Explanation
| Column | Purpose |
|---|---|
| USER_EMAIL | Stores user email |
| OTP_CODE | Generated OTP |
| EXPIRY_TIME | OTP expiry timestamp |
| IS_VERIFIED | Verification status |
š Step 2: Modify Login Button
Change the Login button name to:
Generate OTPš Step 3: Create OTP Generation Process
Create a Page Process on Login Page.
š Process Code
DECLARE
l_otp VARCHAR2(6);
l_recipient VARCHAR2(255);
BEGIN
l_recipient := :P9999_USERNAME;
IF l_recipient IS NULL THEN
apex_error.add_error (
p_message => 'Please enter an email address first.',
p_display_location => apex_error.c_inline_in_notification
);
END IF;
l_otp := LPAD(
TRUNC(DBMS_RANDOM.VALUE(0, 999999)),
6,
'0'
);
INSERT INTO otp_log (
user_email,
otp_code,
expiry_time
)
VALUES (
l_recipient,
l_otp,
SYSTIMESTAMP + INTERVAL '5' MINUTE
);
apex_mail.send(
p_to => l_recipient,
p_from => 'noreply@yourdomain.com',
p_body => 'Your OTP is: ' || l_otp,
p_subj => 'OTP Verification'
);
apex_mail.push_queue;
apex_util.set_session_state(
'GLOBAL_USER_EMAIL',
l_recipient
);
apex_util.redirect_url (
p_url => apex_page.get_url(
p_page => 38
)
);
END;š What This Process Does
ā Generates random 6-digit OTP
ā Saves OTP into database
ā Sends OTP email
ā Stores email in session state
ā Redirects user to OTP verification page
š Step 4: Create Verify OTP Page
Create a new page:
Page Number: 38
Page Name: Verify OTPš Step 5: Create OTP Input UI
Create:
ā 6 Text Fields
ā Verify Button
ā Resend OTP Button
ā Timer Region
š Page Items
P38_C1
P38_C2
P38_C3
P38_C4
P38_C5
P38_C6Each field stores one OTP digit.
š Step 6: Create Input Container Region
Create a Static Content region:
Input ContainerAdd CSS Class:
otp-flex-containerš Step 7: Add CSS Styling
Add below CSS in:
Page ā CSS ā Inlineš Inline CSS
.otp-flex-container {
display: flex !important;
justify-content: center;
align-items: center;
gap: 10px;
margin: 20px 0;
}
.otp-flex-container .t-Form-fieldContainer {
margin: 0 !important;
padding: 0 !important;
flex: 0 1 auto !important;
}
.otp-input-box {
width: 45px !important;
height: 55px !important;
text-align: center;
font-size: 20px !important;
border-radius: 8px !important;
border: 1px solid #ccc !important;
}
#resendBtn {
color: #4CAF50;
font-weight: bold;
text-decoration: none;
}
#resendBtn:hover {
text-decoration: underline;
}
#otpTimer{
font-size: 18px;
font-weight: 700;
text-align: center;
margin-top: 10px;
color: #000;
}š Step 8: Add JavaScript for Auto Focus
Add below code in:
Page ā Execute When Page Loadsš JavaScript Code
$('#P38_C1').focus();
$(document).on('keyup', '.otp-input-box input', function(e) {
let $this = $(this);
let val = $this.val();
if (val.length === 1) {
let nextInput = $('.otp-input-box input')
.eq($('.otp-input-box input').index($this) + 1);
if (nextInput.length) {
nextInput.focus();
}
}
if (e.keyCode === 8 && val.length === 0) {
let prevInput = $('.otp-input-box input')
.eq($('.otp-input-box input').index($this) - 1);
if (prevInput.length) {
prevInput.focus();
}
}
});
$(document).on('input', '.otp-input-box input', function() {
if (this.value.length > 1) {
this.value = this.value.slice(0, 1);
}
});š Features
ā
Auto focus next field
ā
Backspace moves previous field
ā
Prevent multiple characters
š Step 9: Add OTP Timer
š JavaScript Timer Code
window.startOtpTimer = function () {
window.timeLeft = 30;
if (window.timerId) {
clearInterval(window.timerId);
}
$("#resendBtn").hide();
$("#otpTimer").html("Resend OTP in 00:30");
window.timerId = setInterval(function () {
if (window.timeLeft < 0) {
clearInterval(window.timerId);
$("#otpTimer").html("");
$("#resendBtn").show();
} else {
var seconds = window.timeLeft < 10
? '0' + window.timeLeft
: window.timeLeft;
$("#otpTimer").html(
"Resend OTP in 00:" + seconds
);
window.timeLeft--;
}
}, 1000);
};
startOtpTimer();š Features
ā
Countdown timer
ā
Hide resend button initially
ā
Show resend button after 30 seconds
š Step 10: Create Resend OTP Dynamic Action
Create Dynamic Action on:
RESEND_BTNEvent:
clickš True Action 1 ā Execute Server-side Code
š PL/SQL Code
DECLARE
l_otp VARCHAR2(6);
l_recipient VARCHAR2(255);
l_count NUMBER;
BEGIN
l_recipient := :GLOBAL_USER_EMAIL;
SELECT COUNT(*)
INTO l_count
FROM otp_log
WHERE user_email = l_recipient;
IF l_count > 0 THEN
DELETE FROM otp_log
WHERE user_email = l_recipient;
END IF;
l_otp := LPAD(
TRUNC(DBMS_RANDOM.VALUE(0, 999999)),
6,
'0'
);
INSERT INTO otp_log (
user_email,
otp_code,
expiry_time
)
VALUES (
l_recipient,
l_otp,
SYSTIMESTAMP + INTERVAL '5' MINUTE
);
apex_mail.send(
p_to => l_recipient,
p_from => 'noreply@yourdomain.com',
p_body => 'Your OTP is: ' || l_otp,
p_subj => 'OTP Verification'
);
apex_mail.push_queue;
END;š True Action 2 ā Execute JavaScript
window.timeLeft = 30;
clearInterval(window.timerId);
$('#resendBtn').hide();
$('#P38_TIMER').show();
window.timerId = setInterval(
window.countdown,
1000
);
apex.message.showPageSuccess(
"OTP sent successfully!"
);
startOtpTimer();š Step 11: Verify OTP Process
Create Verify Button Process.
š Verification Logic
DECLARE
l_entered_otp VARCHAR2(6);
l_count NUMBER;
BEGIN
l_entered_otp :=
:P38_C1
|| :P38_C2
|| :P38_C3
|| :P38_C4
|| :P38_C5
|| :P38_C6;
SELECT COUNT(*)
INTO l_count
FROM otp_log
WHERE user_email = :GLOBAL_USER_EMAIL
AND otp_code = l_entered_otp
AND expiry_time >= SYSTIMESTAMP;
IF l_count > 0 THEN
UPDATE otp_log
SET is_verified = 'Y'
WHERE user_email = :GLOBAL_USER_EMAIL;
apex_authentication.login(
p_username => :GLOBAL_USER_EMAIL,
p_password => null
);
ELSE
apex_error.add_error (
p_message =>
'Invalid or Expired OTP',
p_display_location =>
apex_error.c_inline_in_notification
);
END IF;
END;š Final Output
Your application now supports:
ā
Secure OTP authentication
ā
Email verification
ā
Auto OTP input navigation
ā
OTP resend functionality
ā
Countdown timer
ā
Modern OTP UI
š Real-Time Use Cases
This type of OTP authentication is useful in:
ā Banking Applications
ā HR Systems
ā Enterprise Portals
ā Approval Applications
ā Customer Login Systems
ā Secure Internal Applications
šÆ Benefits of OTP Verification
ā
Improved security
ā
Reduced password risk
ā
Better authentication flow
ā
Modern login experience
ā
Secure email validation
š Conclusion
Building OTP authentication in Oracle APEX is easier than many developers think.
Using:
ā
APEX_MAIL
ā
PL/SQL
ā
JavaScript
ā
Dynamic Actions
ā
Session State
you can create a secure and professional OTP verification system with minimal code.
This approach provides a modern authentication experience for enterprise Oracle APEX applications.
Hi, Iām Ankur Rai, an Oracle APEX Developer with 6+ years of professional experience in building enterprise applications. I specialize in creating scalable and efficient solutions using Oracle APEX, PL/SQL, and SQL to solve real-world business challenges.
I am a 3X Oracle APEX Professional Certified Developer and also an Oracle ACE Associate Member, actively contributing to the Oracle community by sharing knowledge, insights, and best practices. Through my blogs, I aim to help developers learn, grow, and build better Oracle APEX applications together.



