In two previous articles I have described how I have implemented my static web application, served from GitHub Pages, with EntraId authentication and data available only to authenticated users through OCI API Gateway (see Private data in public web application — Microsoft EntraId, GitHub Pages, OCI API Gateway and No Code challenge: create file named after user in OCI Bucket through OCI API Gateway).
In this article, I want to add an administrator mode to my application. I want any user who I have designated an admin for the application and its data to be able to edit and save data — not just their own, but all data. I want to implement this without any backend programming or continuously moving pieces. I want to achieve this through a combination of backend configuration (no code) and front end code. What I have implemented and will describe in this article:
- admin mode in web application, triggered by query parameter — making additional functionality available
- Pre Authenticated Request for OCI Object Storage to list, write and read objects in a Bucket — with a certain prefix to restrict operations to a subfolder within the Bucket
- new API Gateway deployment — for admin operations — that checks the JWT token for a specific claim to have one of a set of predefined allowable values (the usernames of the designated admin-users)
- a route within this deployment that accesses resources indicated by the custom request header Asset-Path through the PAR; to that end the URL for the HTTP Backend of this route is defined used the expression ${request.headers[Asset-Path]}
- the Web Application uses this admin-deployment on API Gateway to fetch a list of all delta-files (files created by individual users with changes in their own profile), fetch all individual delta-files and save the merged dataset as a single data set
With this set up, an admin user can read and save changes from the web application — without a need for backend logic implemented in code — as long as they are authenticated and configured as admin user in the API Gateway.
The set up looks as shown here:
Steps:
- define Pre Authenticated Request on OCI Object Storage
- configure API Gateway — deployment and route
- implement logic in front end
1. Define Pre Authenticated Request on OCI Object Storage
In order access objects in folder conclusion-assets/ and all its subfolders, I need the PAR to have a prefix set to “conclusion-assets/” and allow all of read, write and list. As shown in the screenshot.
2. Configure API Gateway — deployment and route
I need to create a new deployment in API Gateway: checking on a specific value in a claim in a JWT token cannot be done on Route level but only on Deployment level.
I create a deployment called conclusion-admin-proxy with a path prefix of the same value. I define the same CORS settings as in the previous article (origins: localhost for development time and github pages for the deployed static web app). I allow all methods. And I allow the headers Authorization, Content-Type and new custom header Asset-Path.
The Authentication definition is largely the same as seen before in the earlier articles — except the validation of the claim value.
First, define OAuth 2.0/Open ID, Header, Authorization and Bearer. Set Validation Type to remote JWKS :
Then define the JWKS URI, specify the Issuer and the Audience. And then the new bit: configure a Claim Validation. I have specified the claim “preferred_username” and a single allowable value: lucas.jell@yqtwtyrt.asl. This means that this deployment will only ever accept requests that have a JWT token that contains the claim preferred_username and that claim has that specific value. Note I can add the preferred_username for colleagues who will also act as administrators.
Next I create the route definition. The path is /speakerpool-admin. It handles any method (which is a bit lazy of me, it should be OPTIONS, PUT and GET). The endpoint is of type HTTP. The URL is defined using the Pre Authenticated Request combined with the expression ${request.headers[Asset-Path]}. This is replaced with whatever value my web application adds to the requests in the custom header Asset-Path.
Create the Deployment.
3. Implement logic in front end
With the backend all set up, it is now time to implement the required calls in the front end application. Fetch a list of all delta files (all objects in folder conclusion-assets/deltas), then all delta files one by one and finally save the combined data set to conclusion-assets/Speakerpool.json.
Retrieve the list of delta objects:
const adminEndpoint = "https://mbxq.apigateway.eu-amsterdam-1.oci.customer-oci.com/conclusion-admin-proxy/speakerpool-admin";
try {
// add timestamp to prevent caching
const listResponse = await fetch(adminEndpoint + `?ts=${Date.now()}`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`,
'Asset-Path': '' // Empty string for listing
}
});
if (!listResponse.ok) {
const errorBody = await listResponse.text();
console.error(`Error fetching delta file list: ${listResponse.status} ${listResponse.statusText}. Body: ${errorBody}`);
return []; // Stop if we can't get the list
}
const listData = await listResponse.json();
The timestamp is added as query parameter to force the browser to not use the cached value from a previous GET request. The variation in the Asset-Path header is not detected by the browser as a change in the GET request so it feels free to reuse a cached response value without actually making the request to the API Gateway.
Retrieving the individual delta-files — using a GET request with the Asset-Path parameter set to the delta-file name including its location:
for (const assetPath of deltaFileNames) {
if (!assetPath || typeof assetPath !== 'string') {
console.warn('Skipping invalid asset path:', assetPath);
continue;
}
console.log(`Admin mode: Fetching delta file with Asset-Path: ${assetPath}`);
try {
const deltaResponse = await fetch(adminEndpoint + `?ts=${Date.now()}`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`,
'Asset-Path': assetPath
}
});
if (deltaResponse.ok) {
const deltaData = await deltaResponse.json();
if (deltaData && (deltaData.id || deltaData.name)) { // Basic validation
allFetchedDeltas.push(deltaData);
The front end code to save all data looks like this:
const adminEndpoint = "https://mbxq.apigateway.eu-amsterdam-1.oci.customer-oci.com/conclusion-admin-proxy/speakerpool-admin";
try {
const response = await fetch(adminEndpoint, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
'Asset-Path': 'conclusion-assets/Sprekerpool.json'
},
body: JSON.stringify(speakerData) // Send the entire current speakerData array
});
if (!response.ok) {
const errorBody = await response.text();
console.error(`Admin save failed: ${response.status} ${response.statusText}. ${errorBody}`);
throw new Error(`Failed to save speaker data as admin: ${response.status} ${response.statusText}. ${errorBody}`);
}
console.log('Admin mode: Speaker data saved successfully via adminEndpoint.');
The important bit — apart from sending the token — is (again) the custom header Asset-Path.
Conclusion
In admin mode, the user can see and do more. For this to work, the API Gateway has to recognize the request as one sent by a real administrator. This is implemented through the Claim Validation for the preferred_username claim. The route connects to a PAR on Object Storage that has permission to list, read and write individual objects. Note: through the PAR we cannot delete objects. We can rewrite them as empty objects though.
The back end set up is no code: only configuration of the PAR (on OCI Object Storage) and deployment and route (OCI API Gateway) is required along with a few simple lines in the front end application.