Fetch API for Oracle APEX - Part 3: Security SIMPLIFIED
Fetch API series:
In my last blog post, I explained how to make the Fetch API work against an APEX session using OAuth. I was quite proud of the solution, but turns out there is a better way.
Instead of using OAuth, we will use First Party Authentication, allowing an APEX Session to securely connect to ORDS using the Fetch API.
Here's my demo app for Part 3. Use login test1
, test2
or test3
with any password. The goal of this app: Show different cards based on :app_user. The top region is a regular classic report and the bottom region is a secured Fetch API call.
How to secure the Fetch API
Prerequisite: DDL for demo
Here's the DDL that I will use for the demo.
Step 1: Create an ORDS endpoint
Create ORDS module, URI template and Resource Handler as follow:
RESTful Service Module: fetch3
Module Base Path: /fetch3/
URI Template: projects
Full URL: https://apex.oracle.com/pls/apex/vmorneau/fetch3/projects
Method: GET
Source Type: Collection Query
Source:
select p.id
, p.project card_title
, p.icon_css_classes card_icon
from projects p
where p.created_by = :current_user -- this is equivalent to :app_user in APEX
order by card_title
Because this query lives in ORDS, it does't have access to :app_user
like we would need. Instead, ORDS offers an implicit parameter :current_user
which will play the same role. See in Step 3 how to make :current_user
have the same value as the APEX session :app_user
.
Step 2: Adding ORDS security
Create an ORDS privilege and have it protect our fetch3
endpoint.
-- Create ORDS privilege to protect module fetch3
declare
l_roles owa.vc_arr;
l_modules owa.vc_arr;
l_patterns owa.vc_arr;
begin
l_modules(1) := 'fetch3';
ords.define_privilege(
p_privilege_name => 'fetch3_priv',
p_roles => l_roles,
p_patterns => l_patterns,
p_modules => l_modules,
p_label => 'fetch3_priv',
p_description => '',
p_comments => null
);
l_roles.delete;
l_modules.delete;
l_patterns.delete;
commit;
end;
/
This secures access to the fetch3
endpoint, so only a valid APEX Session can request access.
Step 3: Add Fetch API JavaScript to your page
To call ORDS and pass the APEX :app_user
we need to set a particular HTTP Header: Apex-Session.
const request = await fetch('https://apex.oracle.com/pls/apex/vmorneau/fetch3/projects', {
method: "GET",
headers: { 'Apex-Session': '&APP_ID.,&SESSION.' }
});
Apex-Session is a header object containing the current APP_ID and SESSION value. In conjunction with the APEX cookie (sent implicitly), together they allow to authenticate to ORDS as First Party Authentication.
Complete JavaScript to your page Function and Global Variable Declaration:
const doFetch = async () => {
// Fetches an ORDS endpoint (GET)
const request = await fetch('https://apex.oracle.com/pls/apex/vmorneau/fetch3/projects', {
method: "GET",
headers: { 'Apex-Session': '&APP_ID.,&SESSION.' }
});
// Turns the request into readable text
const response = await request.text();
// Turns into a JSON object for easier manipulation of the data
const data = JSON.parse(response);
// Design the card template we want to build
const templateBeforeRows = `<ul class="t-Cards u-colors t-Cards--featured t-Cards--block force-fa-lg t-Cards--displayIcons t-Cards--5cols t-Cards--animColorFill t-Cards--hideBody">`;
const template = `
<li class="t-Cards-item #CARD_MODIFIERS#">
<div class="t-Card">
<div class="t-Card-wrap">
<div class="t-Card-icon u-color #CARD_COLOR#"><span class="t-Icon fa #CARD_ICON#"><span class="t-Card-initials" role="presentation">#CARD_INITIALS#</span></span></div>
<div class="t-Card-titleWrap"><h3 class="t-Card-title">#CARD_TITLE#</h3><h4 class="t-Card-subtitle">#CARD_SUBTITLE#</h4></div>
<div class="t-Card-body">
<div class="t-Card-desc">#CARD_TEXT#</div>
<div class="t-Card-info">#CARD_SUBTEXT#</div>
</div>
<span class="t-Card-colorFill u-color #CARD_COLOR#"></span>
</div>
</div>
</li>
`;
const templateAfterRows = `</ul>`;
// Append the Cards wrapper (before and after rows) to a region on our page
apex.jQuery("#RegionFetchAPI").append(templateBeforeRows);
apex.jQuery("#RegionFetchAPI").append(templateAfterRows);
/**
* Turns an object coming from an ORDS request into a usable placeholder for apex.util.applyTemplate
* @param {object} item - Object from an ORDS request
**/
const getPlaceholders = item => {
let placeholders = {};
for (let key in item) {
placeholders[key.toUpperCase()] = item[key];
}
return placeholders;
};
// Append the rows coming from ORDS
data.items.forEach(item =>Β {
let options = { placeholders: getPlaceholders(item) };
let result = apex.util.applyTemplate(template, options);
apex.jQuery("#RegionFetchAPI .t-Cards").append(result);
});
};
And of course, execute the JavaScript function on page load:
doFetch();
How did we get here?
Following [Part 2], Kris Rice replied to my tweet with 1 line of code, destroying my entire approach:
It's one of my favorite things: be offered a free code tip that ends up trimming my code. I enjoy the learning journey so I'm more than happy to be off, but in all fairness Oracle did not document the Apex-Session
HTTP header very well. The only documentation I could find after Kris' tweet was this: https://docs.oracle.com/en/database/oracle/oracle-rest-data-services/20.3/aelig/REST-development-tutorial.html#GUID-FB0CCF4A-FC81-4726-963E-75E0E26C3B8E and it uses an old xhr
API.
I still think there is value in [Part 2] because the OAuth method can be used outside of APEX, so the entire process is more universal. However ORDS and APEX being so integrated with each other, there is a clear winner for APEX development.
Anyway I hope this new way is useful for anyone.