Sitemap

How to secure APEX REST Service?

9 min readSep 6, 2026

--

If you’ve built RESTful services with Oracle APEX and/or ORDS, you’ve probably run into the same wall every developer eventually hits: you built something powerful, and now anyone with the URL can call it.

  1. Why bother securing REST services at all?
  2. Credentials creation and configuration.
  3. Usage examples

1. Why bother securing REST services at all?

By default, when you create a RESTful Service Module in APEX (SQL Workshop → RESTful Services), the endpoints are public. Anyone who knows — or guesses — the URI template can hit your handler and execute your PL/SQL, pull your data, or trigger your logic.

That’s fine for a demo. It’s a liability in production, and it’s worth being concrete about why.

Your REST module is a direct line to your database. Unlike a typical web app where a browser sits between the user and your data, an ORDS REST endpoint often is the data layer. There’s no login page standing in the way, no session cookie required — just an HTTP request. If a module exposes /sales/orders and returns customer names, order totals, and shipping addresses, that data is one curl command away from anyone who finds the URL, whether they found it in your PL/SQL source, a leaked Postman collection, a misconfigured Swagger doc, or simply by guessing a predictable pattern like /employees/{id}.

URL obscurity is not security. It’s tempting to think “nobody will find this endpoint, it’s not linked anywhere.” But URI templates get discovered constantly — through browser dev tools, referrer headers, log files, GitHub commits, or automated scanners that crawl for common ORDS/APEX paths. Security researchers and attackers alike routinely scan for exposed /ords/ paths precisely because so many of them turn out to be unprotected.

GET isn’t automatically “safe.” Even a read-only endpoint can leak PII, pricing, internal IDs, or business logic that competitors or bad actors shouldn’t see. And if your module also exposes POST, PUT, or DELETE handlers — which is common once you start building real CRUD APIs — an unsecured endpoint doesn’t just leak data, it lets anyone modify or destroy it. Someone could insert bogus records, overwrite existing rows, or delete data outright, all without ever touching your application’s front end.

PL/SQL handlers can do more than query data. A REST handler might kick off a batch job, send an email, call another web service, or write to an audit table. If that handler is unauthenticated, you’re not just exposing data — you’re exposing actions, and actions are often far more damaging than a data leak.

Compliance and audit requirements assume access control exists. If your organization deals with GDPR, HIPAA, PCI-DSS, or SOC 2, auditors will specifically ask “who can call this API, and how do you know?” An unsecured endpoint has no good answer to that question — anyone can call it, and you have no way to tie a request back to an identity, which also means no meaningful audit trail when something goes wrong.

It’s cheap for attackers, expensive for you. Automated bots constantly probe for open /ords/* paths the same way they probe for open S3 buckets or exposed .env files — it costs them nothing to try. The asymmetry matters: it takes an attacker one lucky guess to find an open endpoint, but it takes you a full incident response cycle to contain the damage once they do.

Unsecured APIs erode trust with integration partners. If you’re exposing REST services for partners, vendors, or a mobile app team to consume, the first question any competent engineer on the other side will ask is “how do we authenticate?” Shipping an API with no auth story signals that security wasn’t part of the design — and that’s a hard first impression to walk back.

The upside is that closing this gap in ORDS doesn’t require a security team, a WAF, or a rewrite. As you’ll see next, it’s a native, standards-based feature that’s already sitting in your toolbox — you just have to turn it on.

2. Credentials creation and configuration.

ORDS solves this with a layered model:

  1. Privilege — the “lock.” A named permission that protects one or more modules/URI patterns.
  2. Role — the “key type.” A role is what a privilege actually checks for.
  3. Client — the “key holder.” An OAuth2 client (with a client_id and client_secret) is granted a role, which in turn satisfies a privilege.

When a request comes in without a valid OAuth2 access token — or with a token belonging to a client that lacks the required role — ORDS rejects it before your PL/SQL ever executes. No custom code, no risk of forgetting to check auth in some obscure branch of your handler.

Everything you need for configuration is available as PL/SQL procedures and displays on UI. Easy and intuitive.

Press enter or click to view image in full size

Let’s go step by step with credentials configuration. For this example I have simple REST endpoint that could return rates or sales. Both will receive different privileges as it supposed to be.

Press enter or click to view image in full size

Step 1: Create a Role

A role is just a label that groups clients together by what they’re allowed to do. Think sales_api_users or partner_readonly. There are pre-defined roles available by default. Use them on your convenience, but do not rely on them in production.

You can create this through APEX’s UI (SQL Workshop → RESTful Services → Security → Roles)

Press enter or click to view image in full size

Or straight from PL/SQL

BEGIN
ORDS.CREATE_ROLE(
p_role_name => 'Read Rates'
);
END;
/

I’ve added two new roles for each GET endpoint accordingly.

Press enter or click to view image in full size

Step 2: Create a Privilege and Attach It to Your Module

Now define the privilege and tell it which role can satisfy it, and which module/pattern it protects. You can add several roles, patterns and modules to the privilege. Check the documentation for more info.

To create the privilege, use button Create Privilege on UI

Press enter or click to view image in full size

Or simply invoke PL/SQL package.

DECLARE
l_roles owa.vc_arr;
l_paterns owa.vc_arr;
l_modules owa.vc_arr;
BEGIN
l_roles(1) := 'Read Sales';
l_paterns(1) := '/demo/sales';
/*l_modules(1) := 'demo';*/

ORDS.DEFINE_PRIVILEGE(
p_privilege_name => 'sales.api.privilege',
p_roles => l_roles,
p_patterns => l_paterns,
p_modules => l_modules,
p_label => 'Sales API Access',
p_description => 'Grants access to the Sales REST API module'
);
END;
/

When module specified in privileges, then pattern ignored and whole module covered by specified privilege.

To simply check if everything works fine, back to module page and find the Resource Templates section. There will be Protected by Privilege column.

Press enter or click to view image in full size

When you see green check mark in Protected by Privilege column, your endpoint is secure. Now every request to this endpoint requires authorization. All the unauthorized requests will fail with status 401.

Press enter or click to view image in full size

Step 3: Create an OAuth2 Client (the actual “Credential”)

Get Yurii Slobodianiuk’s stories in your inbox

Join Medium for free to get updates from this writer.

When endpoint is secure from unauthorized requests is a good idea to finally grant the access. Otherwise, no-one can use you API:)

This is the piece most people mean when they say “ORDS Credentials” — the OAuth2 client registration that generates a client_id and client_secret, exactly like you'd get from Google, GitHub, or any modern API provider.

There is OATH PL/SQL package that handles all the credentials processes like: create, update, delete etc. To create credentials use CREATE_CLIENT procedure.

Press enter or click to view image in full size

Credentials itself is a record in database, so execution of this procedure will create new record in user_ords_clients table. After procedure execution you should query this table and save client_id and client_secret somewhere securely.

BEGIN
OAUTH.CREATE_CLIENT(
p_name => 'Test Credentials',
p_grant_type => 'client_credentials',
p_description => 'Test credentials for demo',
p_support_email => 'test@mail.com',
p_privilege_names => NULL,
p_token_duration => 3600,
p_refresh_duration => NULL,
p_code_duration => NULL
);
END;
/
Press enter or click to view image in full size

Default generated client_secret is not important, you can easily change it using UPDATE_CLIENT_SECRET procedure. But remember, reseting the client_secret will delete all the existing client sessions.

When Client created, you have to assign the roles, to grant the actuall access for the REST modules.

BEGIN
OAUTH.GRANT_CLIENT_ROLE(
p_client_name => 'Test Credentials',
p_role_name => 'Read Sales');
END;
/

If you have any questions or something does not works as expected, please refer to Oracle documentation.

That’s it. Roles created and assigned to privileges, endpoint are securely closed from unauthorized access, credentials created and ready for usage.

3. Usage examples

It doesn’t matter which endpoint you are going to call, firstly, you have to receive the OAuth token and this procedure is the same across the workspace (ORDS Schema).

Procedure is simple and same as any OAuth2 authentication: Authenticate using client_id and client_secret → Receive Baerer Token → Call API with this token

For Authentication use standard endpoint: {YOUR_HOST_NAME}/ords/{SCHEMA_NAME}/oauth/

Mandatory parameter to specify:
grant_type=client_credentials

For authorization use:
Basic Auth

Mandatory request type:
POST

Press enter or click to view image in full size

Response to this authentication call is a JSON with Bearer token. By default, it’s valid for 3600 seconds (1 hour). This value specified during credentials creation.

With the client_credentials grant, there’s actually no refresh token in the OAuth2 spec. Refresh tokens exist for flows involving a user (authorization code grant), where you don’t want to keep asking the user to log in. Client credentials is machine-to-machine, so when the token expires, you just request a new one the same way you got the first one — using the client_id/client_secret again.

And finally, API call

Press enter or click to view image in full size

What Makes This “Easy”?

Compare this to the alternative: writing custom token validation logic inside every PL/SQL handler, managing your own credentials table, hand-rolling expiry checks, and hoping nobody forgets to call your check_auth() procedure in a new endpoint six months from now.

With ORDS Credentials:

  • Enforcement happens at the platform level, not in your code — so it can’t be forgotten or bypassed.
  • Standards-based — it’s real OAuth2 client credentials flow, so any HTTP client, API gateway, or integration tool already knows how to talk to it.
  • Fine-grained — you can protect an entire module or a single URI pattern, and issue different clients for different partners, each with only the roles they need.
  • No extra infrastructure — no separate identity server, no API gateway required. It’s all native to the database tier ORDS already sits on.

A Few Practical Tips

  • One client per consumer, not per environment. Give each partner or application its own client so you can revoke access individually without affecting anyone else.
  • Rotate secrets periodically. ORDS.CREATE_CLIENT again with the same name, or useORDS.ROTATE_CLIENT_SECRET
  • Use HTTPS everywhere. OAuth2 bearer tokens are bearer tokens — anyone who intercepts one over plain HTTP can use it until it expires.
  • Keep privileges narrow. Resist the temptation to create one giant “api_access” privilege for your whole schema. Model privileges around actual business capabilities (sales.read, sales.write, inventory.read), and compose roles from there.
  • Revoke, don’t delete, when in doubt. ORDS.DELETE_CLIENT is permanent. If you just need to cut off access temporarily, remove the role grant instead.

Conclusion

Securing APEX REST services doesn’t have to mean adopting a whole new security stack. ORDS already gives you a production-grade, standards-compliant OAuth2 implementation baked into the same tool you’re using to build the APIs in the first place. Define a Role, attach a Privilege to your module, issue a Client, and you’ve got secured, revocable, auditable API access in minutes — not days.

If you’re still shipping public APEX REST modules, this is genuinely one of the highest-leverage, lowest-effort improvements you can make to your application’s security posture today.

--

--

Written by Yurii Slobodianiuk

Devone Software CEO, Oracle Expert