Secure RESTful Web Service in Oracle APEX with Basic Authentication

Heard they say you should secure your web service?๐Ÿ‘ฎ 

Stick with me in this article, and I’ll show you a professional way to secure your RESTful Web Service with Basic Authentication


 In the previous article, I explained how to CREATE  a RESTful Web Services inside Oracle APEX.

https://lidagholizadeh.blogspot.com/2025/08/all-about-restful-webservice-in-oracle.html

In this Article I will shows you how to build a professional, secure JSON RESTful web service in Oracle APEX that returns employee details from the HR.EMPLOYEES table, protected by Basic Authentication with PBKDF2 password hashing.

Prerequisites

  • Oracle Database 19c or later
  • Oracle APEX 21.1 or newer
  • HR sample schema unlocked and available
  • EXECUTE on DBMS_CRYPTO
  • A workspace with privileges to create RESTful Services
๐Ÿšด Let's GO

Step 1 – Create a Table to Store Web Service Users

CREATE TABLE ws_users (
    username      VARCHAR2(100)  PRIMARY KEY,
    password_hash VARCHAR2(64)   NOT NULL,   -- we store hex PBKDF2-SHA1 (32 bytes → 64 hex chars)
    created_on    DATE           DEFAULT SYSDATE,
    created_by    VARCHAR2(100),
    kommentar     VARCHAR2(1000)
);

Step 2 – Store a Global Salt (recommended: random 16-byte hex string)

-- Create a simple properties table if you don’t have one already
CREATE TABLE app_properties (
    attribute VARCHAR2(100) PRIMARY KEY,
    value     VARCHAR2(4000)
);

INSERT INTO app_properties (attribute, value)
VALUES ('WEBSERVICE_SALT', 'A1B2C3D4E5F678901234567890ABCDEF');  -- 32 hex chars = 16 bytes
COMMIT;

Step 3 – PBKDF2 Hash Function

CREATE OR REPLACE FUNCTION pbkdf2_hash(
    p_password VARCHAR2,
    p_salt     VARCHAR2
) RETURN VARCHAR2 IS
    c_iterations CONSTANT PLS_INTEGER := 10000;
    c_key_len    CONSTANT PLS_INTEGER := 256;   -- bits
    c_hash_len   CONSTANT PLS_INTEGER := 20;    -- SHA1 raw bytes
    l_blocks     PLS_INTEGER;
    l_result     RAW(32767) := NULL;
    l_xorsum     RAW(32767);
    l_last       RAW(32767);
BEGIN
    l_blocks := CEIL(c_key_len / (c_hash_len * 8));
    FOR i IN 1 .. l_blocks LOOP
        l_last := UTL_RAW.CONCAT(
                     UTL_RAW.CAST_TO_RAW(p_salt),
                     UTL_RAW.CAST_FROM_BINARY_INTEGER(i, UTL_RAW.BIG_ENDIAN)
                  );
        l_xorsum := NULL;

        FOR j IN 1 .. c_iterations LOOP
            l_last := DBMS_CRYPTO.MAC(
                        src => l_last,
                        typ => DBMS_CRYPTO.HMAC_SH1,
                        key => UTL_RAW.CAST_TO_RAW(p_password)
                     );
            l_xorsum := CASE WHEN l_xorsum IS NULL
                             THEN l_last
                             ELSE UTL_RAW.BIT_XOR(l_xorsum, l_last)
                        END;
        END LOOP;

        l_result := UTL_RAW.CONCAT(l_result, l_xorsum);
    END LOOP;

    RETURN LOWER(RAWTOHEX(UTL_RAW.SUBSTR(l_result, 1, 32))); -- 32 bytes = 64 hex chars
END;
/

Step 4 – Helper Procedure to Create Users

CREATE OR REPLACE PROCEDURE create_ws_user(
    p_username IN VARCHAR2,
    p_password IN VARCHAR2
) IS
    v_salt  VARCHAR2(32);
    v_hash  VARCHAR2(64);
BEGIN
    SELECT value INTO v_salt
    FROM app_properties
    WHERE attribute = 'WEBSERVICE_SALT';
    v_hash := pbkdf2_hash(p_password, v_salt);
    INSERT INTO ws_users (username, password_hash, created_by, kommentar)
    VALUES (p_username, v_hash, USER, 'Created via tutorial');
    COMMIT;
EXCEPTION
    WHEN DUP_VAL_ON_INDEX THEN
        RAISE_APPLICATION_ERROR(-20001, 'User already exists');
END;
/

Execute once for testing:

BEGIN
    create_ws_user('api_user', 'MyStrongP@ssw0rd2025');
END;
/

Step 5 – Core Package for the Web Service

CREATE OR REPLACE PACKAGE employee_ws AS
    TYPE t_params IS RECORD (
        call_ts      VARCHAR2(30),
        authorization VARCHAR2(4000),
        employee_id  NUMBER
    );

    user_not_authenticated EXCEPTION;
    PRAGMA EXCEPTION_INIT(user_not_authenticated, -20001);

    PROCEDURE check_basic_auth(p_auth IN VARCHAR2);
    PROCEDURE write_json_error(p_msg IN VARCHAR2, p_code IN NUMBER := 500, p_callts IN VARCHAR2 := NULL);
    PROCEDURE get_employee_detail(p_params IN t_params, p_status OUT NUMBER);
END;
/

CREATE OR REPLACE PACKAGE BODY employee_ws AS

    PROCEDURE check_basic_auth(p_auth IN VARCHAR2) IS
        v_salt     VARCHAR2(32);
        v_b64      VARCHAR2(4000);
        v_parts    APEX_T_VARCHAR2;
        v_user     VARCHAR2(100);
        v_pass     VARCHAR2(500);
        v_hash     VARCHAR2(64);
        v_cnt      NUMBER;
    BEGIN
        SELECT value INTO v_salt
        FROM app_properties WHERE attribute = 'WEBSERVICE_SALT';

        IF p_auth IS NULL OR NOT REGEXP_LIKE(p_auth, '^Basic ', 'i') THEN
            RAISE user_not_authenticated;
        END IF;

        v_b64 := REGEXP_REPLACE(p_auth, '^Basic\s+', '', 1, 1, 'i');
        v_parts := APEX_STRING.SPLIT(UTL_RAW.CAST_TO_VARCHAR2(UTL_ENCODE.BASE64_DECODE(v_b64)), ':');

        v_user := v_parts(1);
        v_pass := v_parts(2);

        IF v_user IS NULL OR v_pass IS NULL THEN
            RAISE user_not_authenticated;
        END IF;

        v_hash := pbkdf2_hash(v_pass, v_salt);

        SELECT COUNT(*) INTO v_cnt
        FROM ws_users
        WHERE LOWER(username) = LOWER(v_user)
          AND password_hash = v_hash;

        IF v_cnt = 0 THEN
            RAISE user_not_authenticated;
        END IF;
    END check_basic_auth;
/
    PROCEDURE write_json_error(p_msg    IN VARCHAR2,
                               p_code    IN NUMBER := 500,
                               p_callts  IN VARCHAR2 := NULL) IS
    BEGIN
        OWA_UTIL.mime_header('application/json', FALSE, 'UTF-8');
        HTP.p('Cache-Control: no-cache');
        OWA_UTIL.http_header_close;
        APEX_JSON.open_object;
        APEX_JSON.write('error', p_msg);
        APEX_JSON.write('code', p_code);
        APEX_JSON.write('timestamp', TO_CHAR(SYSTIMESTAMP, 'YYYY-MM-DD"T"HH24:MI:SS.FF3"Z"'));
        IF p_callts IS NOT NULL THEN APEX_JSON.write('call_ts', p_callts); END IF;
        APEX_JSON.close_object;
    END write_json_error;

    PROCEDURE get_employee_detail(p_params IN t_params, 
p_status OUT NUMBER) 
IS
        v_found BOOLEAN := FALSE;
    BEGIN
        check_basic_auth(p_params.authorization);
        OWA_UTIL.mime_header('application/json', FALSE, 'UTF-8');
        HTP.p('Cache-Control: no-cache');
        OWA_UTIL.http_header_close;
        APEX_JSON.open_object;
        APEX_JSON.open_array('items');

        FOR r IN (SELECT employee_id,
                         first_name,
                         last_name,
                         email,
                         phone_number,
                         hire_date,
                         job_id,
                         salary,
                         commission_pct,
                         manager_id,
                         department_id
                  FROM hr.employees
                  WHERE employee_id = p_params.employee_id)
        LOOP
            v_found := TRUE;
            APEX_JSON.open_object;
            APEX_JSON.write('employee_id', r.employee_id);
            APEX_JSON.write('first_name', r.first_name);
            APEX_JSON.write('last_name', r.last_name);
            APEX_JSON.write('email', r.email);
            APEX_JSON.write('phone_number', r.phone_number);
            APEX_JSON.write('hire_date', TO_CHAR(r.hire_date, 'YYYY-MM-DD'));
            APEX_JSON.write('job_id', r.job_id);
            APEX_JSON.write('salary', r.salary);
            APEX_JSON.write('commission_pct', r.commission_pct);
            APEX_JSON.write('manager_id', r.manager_id);
            APEX_JSON.write('department_id', r.department_id);
            APEX_JSON.close_object;
        END LOOP;
        APEX_JSON.close_array;
        APEX_JSON.close_object;
        p_status := 200;
    EXCEPTION
        WHEN user_not_authenticated THEN
            p_status := 401;
            write_json_error('Unauthorized – Invalid credentials', 401, p_params.call_ts);
        WHEN NO_DATA_FOUND THEN
            p_status := 200;  -- empty result is acceptable
            OWA_UTIL.mime_header('application/json', FALSE, 'UTF-8');
            HTP.p('Cache-Control: no-cache');
            OWA_UTIL.http_header_close;
            APEX_JSON.open_object;
            APEX_JSON.open_array('items');
            APEX_JSON.close_array;
            APEX_JSON.close_object;
        WHEN OTHERS THEN
            p_status := 500;
            write_json_error('Internal Server Error', 500, p_params.call_ts);
    END get_employee_detail;
END employee_ws;
/

Step 6 – Create the RESTful Service Handler in APEX๐Ÿ’—

  1. Go to SQL Workshop → RESTful Services
  2. Create Module → Name: EMPLOYEE_API
  3. Create Template → /employee
  4. Create Handler
    • Method: GET
    • Source Type: PL/SQL
    • Requires Authentication: No (we handle it manually)
    • Source:
DECLARE
    v_params   employee_ws.t_params;
    v_status   NUMBER;
    v_callts   VARCHAR2(30) := TO_CHAR(SYSTIMESTAMP, 'yyyymmddhh24missff3');
BEGIN
    v_params.call_ts      := v_callts;
    v_params.authorization := :authorization;          -- APEX binds header
    v_params.employee_id  := :employee_id;             -- bind from URI or query string
    IF v_params.employee_id IS NULL THEN
        :status_code := 400;
        employee_ws.write_json_error('Bad Request – employee_id is required', 400, v_callts);
        RETURN;
    END IF;
    employee_ws.get_employee_detail(v_params, v_status);
    :status_code := v_status;
END;
  1. Parameters:
    • Name: employee_id Source Type: URI Parameter Type: String (or Number) Required: Yes

Step 7 – Test the Web Service

Using curl:

Bash
# Correct credentials
curl -i -u api_user:MyStrongP@ssw0rd2025 \
     "https://your-apex-server.com/ords/your_schema/employee_api/employee/107"

# Wrong password → 401
curl -i -u api_user:wrongpassword \
     "https://your-apex-server.com/ords/your_schema/employee_api/employee/107"

# No credentials → 401
curl -i "https://your-apex-server.com/ords/your_schema/employee_api/employee/107"

Expected successful response (HTTP 200):

MY DEAR JSON ๐Ÿ˜๐Ÿ˜ƒ
{
  "items": [
    {
      "employee_id": 107,
      "first_name": "Diana",
      "last_name": "Lorentz",
      "email": "DIANALORENTZ",
      "phone_number": "590.423.5567",
      "hire_date": "2007-02-07",
      "job_id": "IT_PROG",
      "salary": 4200,
      "commission_pct": null,
      "manager_id": 103,
      "department_id": 60
    }
  ]
}

THANK YOU FOR YOUR READING ๐Ÿ™
I HOPE IT WAS USEFUL FOR YOU

Comments

Post a Comment

Popular posts from this blog

Installation of Oracle Database 23ai and APEX 24.2 and ORDS 24.4 on Windows Step-by-Step

Building a Fully Local AI-Powered Assistant with Oracle APEX and Vector Search #APEXCONN2025 #orclAPEX

Configuration of ORDS: Standalone, Tomcat, and WebLogic on Windows Step-by-Step

✔ RTL Map Region Labels APEX 24.2 & ๐ŸŒGeoCoding

Building an AI-Powered Resume Matching System Using Oracle APEX and 23ai Vector Search๐Ÿ˜Ž

JavaScript In Oracle APEX (Client Side)

Vector Tiles in Oracle Spatial 23

Leveraging ONNX Models for AI Vector Search in Oracle Cloud Database 23ai

My Experience at Oracle CloudWorld Tour in Frankfurt – April 10, 2025

What is SYS.ODCI (SYS.ODCINumberList or SYS.ODCIVarchar2List) and How Can Use it in APEX ?