Private data in public web application — Microsoft EntraId, GitHub Pages, OCI API Gateway 1k418gJmkuNaco3LMdzRpgQ

Private data in public web application — Microsoft EntraId, GitHub Pages, OCI API Gateway

What is the easiest, cheapest and fastest way to make a static web application available to a group of end users? Probably a public GitHub repository and GitHub Pages. Second question: how do I make the private data available that this application works with only to end users that are my colleagues at Conclusion? This question is what concerns me in this article.

This image shows what I am after:

Private data in public web application — Microsoft EntraId, GitHub Pages, OCI API Gateway 1k418gJmkuNaco3LMdzRpgQ
User can load public web application from GitHub but should only be able to fetch the associated data if they are authenticated with the designated Microsoft EntraId tenant

The user access the web application through the URL that is directed at https://lucasjellema.github.io/<application>. The browser loads the static web resources — HTML, CSS and JavaScript as well as images and other supporting assets. The JavaScript is initialized. It needs to download a JSON document with the data for the application. However, that data is not public. So it cannot be loaded from GitHub Pages.

What I want to achieve is:

  • the web application runs a login flow to authenticate the user with my company’s EntraId tenancy to get hold of a JWT token that proves the user’s authentication status and identity
  • when the authentication is achieved, then the JSON document can be retrieved; from a location that only supports retrieval by authenticated users with an identity asserted by [my company’s] Microsoft EntraId

The JSON document needs to be protected. This can be done in many ways. I choose one that is simple for me — and does not involve any back end coding. The document is uploaded to a Bucket on Oracle Cloud Infrastructure’s Object Storage service. I have created a Pre Authenticated Request — a URL (with a long, unguessable string) that allows holders of the URL to directly access the document.

Private data in public web application — Microsoft EntraId, GitHub Pages, OCI API Gateway 1k9XhqHXTHjiSLl QTMNemQ
JSON document on OCI Object Storage, accessible through a read (only ) Pre Authenticated Request that is used as the backend for an HTTP route in an API Deployment on OCI API Gateway

This PAR URL is private — it is only to be used in a Route defined in an API Deployment on OCI API Gateway. This route can be invoked by the public web application to get hold of the data. But the route can only be executed successfully if API Gateway has determined that a valid token from the correct issuer and for the expected audience is included and therefore only authenticated users will be able to access the data.

The end to end picture is a little bit more complex:

Private data in public web application — Microsoft EntraId, GitHub Pages, OCI API Gateway 1Lr8Ij 2EcLGxQPv rr7FTw
1. User accesses web application from GitHub Page 2. Application runs login / authentication flow to have user authenticate with Microsoft EntraId using their own credentials; a JWT token is handed to the browser; 3. the web application calls an endpoint on OCI API Gateway to fetch the JSON document; the token is included in the request. 4. API Gateway receives the request, checks if a token is present ; 5. it retrieves the public key from the designated Microsoft location and checks the signature (and validity) of the token; the issuer and audience are verified. 6. Upon successful token validation, the HTTP backend of the route is invoked: a GET request that leverages the Pre Authenticated Request to fetch the JSON document from OCI Object Storage and 7. return the data to the browser

Here we see the entire flow:

  1. User accesses web application from GitHub Page
  2. Application runs login / authentication flow to have user authenticate with Microsoft EntraId using their own credentials; a JWT token is handed to the browser;
  3. the web application calls an endpoint on OCI API Gateway to fetch the JSON document; the token is included in the request.
  4. API Gateway receives the request, checks if a token is present ;
  5. it retrieves the public key from the designated Microsoft location and checks the signature (and validity) of the token; the issuer and audience are verified.
  6. Upon successful token validation, the HTTP backend of the route is invoked: a GET request that leverages the Pre Authenticated Request to fetch the JSON document from OCI Object Storage and
  7. return the data to the browser

Is there anything special in this story? Well, a few things that took me quite some time:

  • only one Azure App Id Registration is required; one to establish authentication and provide a token to the Static Web Application running in the browser, loaded from GitHub Pages
  • use an Id Token — and not an Access Token — as Bearer token in the call to API Gateway. Hints were (I understood much too late): “Signature verification failed” when inspecting the token at jwt.io and “token could not be verified” in the API Gateway logs.
  • in order to facilitate the development phase: make sure to include localhost in the redirect URL in the App Registration and in the CORS Allowed Origins in the API Gateway
  • take good care to exactly duplicate values from the App Registration in the Web Application and in the API Gateway deployment configuration; as an example I stared for a bvery long time at the closing / in the GitHub Pages url when configurating the CORS Allowed Origin before realizing that it should not be there. However, the port number (for localhost) should be included. It needs to be exactly correct.
  • inspect the use of the msal library — and do not just replicate code samples or accept whatever AI proposes (I guess these two options boil down to the exact same thing).
  • Using a single (Web) App Registration in the manner described in this article apparently is not very common. It is more common to register to Apps: the web app and then also the API is needs to invoke, even if that is not implemented in Azure. The web app gets permission to invoke the API app registration, the web app requests a token for calling the API based on that second app registration and it is that registration that the API Gateway uses for token validation. However, I do not believe it adds value in this scenario. I am perfectly fine with granting access to the API to all colleagues. And I want to keep things as simple as possible for my colleagues at the Service Desk: create a single App Registration without any frills or advanced setup.

Baring these in mind, it has become rather straightforward to implement this scenario.

Three main steps:

  • create a Azure App Registration
  • configure the API Gateway, the Deployment and the specific route on OCI
  • implement the authentication flow in the web application and make the call to fetch the protected JSON document through the API Gateway endpoint

I am assuming the JSON document already lives on OCI Object Storage and a Pre Authenticated Request has been defined to get to the document. This URL will be used when setting up the Route. I also assume a Log Group has been set up on OCI — to be associated with the log output from API Gateway (I needed logging for debugging when things did not work and the Token could not be retried). The application is currently live at GitHub Pages. Of course you can only use it if you can authenticate with our Microsoft EntraId Tenancy. The source code is publicly available at the GitHub Repository.

1. Create a Azure App Registration

Go to portal.azure.com. Go to Entra ID → App registrations → New registration. (also see instructions in https://learn.microsoft.com/en-us/entra/identity-platform/quickstart-register-app)

Private data in public web application — Microsoft EntraId, GitHub Pages, OCI API Gateway 1s CI5d828K8Dpg6LyhoQAg
Click on New registration to create a new App registration

Provide the name. Under Supported account types, specify who can use the application. Usually you will go with Accounts in this organizational directory only for most applications, as did I. Select Register to complete the app registration.

Private data in public web application — Microsoft EntraId, GitHub Pages, OCI API Gateway 1vF1Zzdu49ReC5Pbcehjikw
Define Registration Name and Account Type

I have defined a Redirect URI: http://localhost:5501, where the application runs on my laptop during development.

The register app looks as shown next. The most important property that has been assigned: Application Id (aka Client Id). This uniquely identifies your registered (web) application and is used in your application’s code.

Private data in public web application — Microsoft EntraId, GitHub Pages, OCI API Gateway 1y5UoGCE33A9sunzPwi8R7Q
Overview of new App registration

Go to the Authentication node under Manage. Now define all relevant Redirect URIs — such as http://localhost:port for the application in development and https://lucasjellema.github.io for the production application running on GitHub Pages.

Check the boxes under Implicit grant and hybrid flows, for ID tokens (used for implicit and hybrid flows) and for Access tokens (used for implicit flows) (although I only think the former is essential for my purpose.)

Private data in public web application — Microsoft EntraId, GitHub Pages, OCI API Gateway
Configure Authentication for App registration: specify redirect URIs and the tokens to be issued.

Check on API Permissions. It only requires the default permission to User.Read the user’s profile.

Private data in public web application — Microsoft EntraId, GitHub Pages, OCI API Gateway 191ojX2U2bb850mLBSrVb0g
Default settings on the API permissions pane

This should complete the configuration of the App registration. However, we need a piece of information. Click on Endpoints at the top of the App registration overview page. This brings up a panel with Endpoints. Locate the one marked OpenID Connect metadata document.

Private data in public web application — Microsoft EntraId, GitHub Pages, OCI API Gateway 1OZYTekHSIFZE7FB31 dHBg
List Endpoints for App registration

Copy this URL. Paste it in a browser and load the associated document.

Locate the property jwks_uri. Copy its value and store it somewhere for later use (in the API Gateway deployment definition).

Private data in public web application — Microsoft EntraId, GitHub Pages, OCI API Gateway
Find property jwks_uri in Open ID Connect metadata document

Getting a little bit ahead of ourselves: once the web application is set up with the authentication flow, it will retrieve a token from EntraId. That token (a base64 encoded data) when inspected using sites like jwt.io and jwt.ms looks like this:

Private data in public web application — Microsoft EntraId, GitHub Pages, OCI API Gateway 1DVVSfw3XTmK9rKBqRqSWRQ
Inspecting the Id Token that the web application will retrieve from EntraId as part of the authentication flow. The values in the aud and iss claims are directly relevant to validation by OCI API Gateway — as is the fact that the token can be decrypted with the Microsoft published public key that is retrieved from the JWKS URI and is valid (not expired).

The token contains the claims aud and iss that have the Client Id of the App Registration and the token issuer (that includes the EntraId Tenancy Id) as values.

2. OCI API Gateway

The configuration of the OCI API Gateway is done in three steps:

  • create the API Gateway itself (not described in this article; see OCI Documentation for details)
  • create a Deployment — with CORS and authentication set up
  • define the Route in the Deployment for the specific JSON data document

Deployment

Create a new deployment. Provide a name and a path prefix — such as /conclusion-proxy.

The edit the CORS properties to handle the cross origin request. The web application running at GitHub Page (production) or localhost:port (development) needs to make a call to the API Gateway. And we need to configure the appropriate CORS settings to make that possible. Include the localhost (including http:// and port number) in the allowed origins and also include the GitHub Page URI, including https:// and ending with .github.io. Not with the specific application (repo) path and not with a / (I tried that, it did not work). Ensure that the Authorization header is allowed. I was lazy and allowed all HTTP methods (wildcard *); optionally you can specify exactly which methods are allowed (make sure to include HEAD and OPTIONS along with GET).

I am not sure if Enable allow credentials needs to be checked — but things are working like this so it seems like a good idea. Enabling logging also is a good idea — when debugging turns out to be required. Frankly, the logging does not contain as much details for debugging as would be useful, but we make do with the little we at least can get.

Private data in public web application — Microsoft EntraId, GitHub Pages, OCI API Gateway 13OXi7ZbPKPgg80ut2bjvoQ
Configure basic settings — including CORS — for the Deployment

Press Next. The Authentication tab. Here we need the values from the App registration in EntraId as shown in the next picture:

Private data in public web application — Microsoft EntraId, GitHub Pages, OCI API Gateway 1gK9l0bdBok4eIjVUWULEyQ
Mapping values — from App registration via OpenID Connect metadata and token to API Gateway deployment authentication configuration

Click on Single Authentication. Select OAuth 2.0/OpenID Connect as the Authentication Type. Select Header as the token location. The name of the header that contains the token is Authorization and the Authentication Scheme is set to Bearer. As is shown in the screenshot.

Set the Validation Type to Remote JWKS. The JWKS URI field is set to the value found in the OpenID Connect metadata document.

Under additional JWT Validations, define Allowed Issuers. set as value: “https://login.microsoftonline.com/<tenancy id>/discovery/v2.0/keys” and replace of course <tenancy id> with the value for your tenancy.

Also set the allowed audience. The value set here is the Application Id or Client Id from the Azure App Registration.

Private data in public web application — Microsoft EntraId, GitHub Pages, OCI API Gateway
Configuring the Authentication for the Deployment

Click on Next. Time to define a Route, in the tab shown below. The path for this route (part of the overall endpoint as accessed from the application that also includes the API Gateway’s endpoint and the path prefix of the deployment) is defined here as /speakerpool-data. It can be accessed with GET, HEAD and OPTIONS methods. It has a single backend, of type HTTP. And the URL for the backend is the Pre Authenticated Request URL that leads to the JSON data document.

Private data in public web application — Microsoft EntraId, GitHub Pages, OCI API Gateway 1TCSFMvsQF wf8YYmwzZXzQ
Define the Route — with an HTTP Backend that refers to the Pre Authenticated Request URL for the JSON data document on OCI Object Storage

Click on Next. Review the definition of the deployment. Click on Save to create the deployment. This may take several dozens of seconds before the deployment is active on the Gateway.

Private data in public web application — Microsoft EntraId, GitHub Pages, OCI API Gateway
API Gateway and the new deployment that holds the route to the JSON data document

Once it is ready, you can make a test call to the API Gateway’s endpoint:

Private data in public web application — Microsoft EntraId, GitHub Pages, OCI API Gateway 1dws9aYPdSPoX4TKuMGjnGw
1. is the endpoint of the API Gateway, 2. is the path prefix for the Deployment and 3. is the path for the route conected to the PAR URL for the JSON Data document. The expected reply is the 401 error code: the request does not contain a token and is therefore unauthorized

If we had a valid token, we should be able to make a successful call with it. It would look like this:

Private data in public web application — Microsoft EntraId, GitHub Pages, OCI API Gateway 1hftgI q6Jk2BoZT5SItZ7Q
Curl GET Call to the endpoint of the route for the JSON document including the Authorization header with a valid Bearer token retrieved from EntraId

3. Implement Authentication and Call with Token to fetch protected data document

The third step to tie all this together is implementation in the Web Application of authenticating the user and using the IdToken that is the result of the successful authentication in the call to API Gateway to fetch the JSON data.

The code for making that call to API Gateway — including the Bearer token in the Authentication header — is pretty straightforward:

export async function getDataWithToken(endpoint, idToken) {
try {
if (!idToken) {
console.error('No ID token available. User might not be authenticated.');
throw new Error('Authentication required. Please sign in.');
}

const options = {
method: 'GET',
headers: {
'Authorization': `Bearer ${idToken}`
}
};

const response = await fetch(endpoint, options);
if (response.status === 401) {
console.error('Authentication failed. Token might be invalid or expired.');
}

return response;

The parameters endpoint and idToken have values like “https://odzno3q.apigateway.eu-amsterdam-1.oci.customer-oci.com/conclusion-proxy/speakerpool-data” and “eyJ0eXAiObGVtY” (though much longer obviously).

The next screenshot shows all requests from the web application — first for authentication, then for fetching the JSON document. These last two correspond to the previous code snippet.

Private data in public web application — Microsoft EntraId, GitHub Pages, OCI API Gateway 1IjKJCPBTGuTy6V0c0hEa7Q
Network calls from the browser — three for authenticaton with Microsoft EntraId and two for retrieving the JSON document through OCI API Gateway

The missing piece of code is relatively standard, found in many samples and happily provided by my Windsurf/Cascade/SWE1 AI assistant (though the time or quality gained in this case was very limited).

The JS module authConfig.js provided the configuration details — taken from the App registration on EntraId:

/**
* Configuration object to be passed to MSAL instance on creation.
* For a full list of MSAL.js configuration parameters, visit:
* https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/configuration.md
*/
export const msalConfig = {
auth: {
// 'Application (client) ID' of app registration in Azure portal - this value is a GUID
clientId: "0de52ab5-0a69-46b7-e",
// Full directory URL, in the form of https://login.microsoftonline.com/<tenant-id>
authority: "https://login.microsoftonline.com/bf104ffd-a095-49bd",
// Full redirect URL, in form of http://localhost:3000 or window.location.origin
redirectUri: window.location.origin,
},
cache: {
cacheLocation: "sessionStorage", // This configures where your cache will be stored
storeAuthStateInCookie: false, // Set this to "true" if you are having issues on IE11 or Edge
},
system: {
loggerOptions: {
loggerCallback: (level, message, containsPii) => {
if (containsPii) {
return;
}
switch (level) {
case msal.LogLevel.Error:
console.error(message);
return;
case msal.LogLevel.Info:
console.info(message);
return;
case msal.LogLevel.Verbose:
console.debug(message);
return;
case msal.LogLevel.Warning:
console.warn(message);
return;
}
}
}
}
};

/**
* Scopes you add here will be prompted for user consent during sign-in.
* By default, MSAL.js will add OIDC scopes (openid, profile, email) to any login request.
* For more information about OIDC scopes, visit:
* https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-permissions-and-consent#openid-connect-scopes
*/
export const loginRequest = {
scopes: ["User.Read","openid","profile"]
}

The code that does the actual authentication is in authPopup.js:

import { msalConfig, loginRequest } from './authConfig.js';

// Create the main msalInstance instance
// configuration parameters are located at authConfig.js
export const msalInstance = new msal.PublicClientApplication(msalConfig);
let idToken
let idTokenClaims
let username = "";

// Add event listener for successful login
msalInstance.addEventCallback((message) => {
console.log('MSAL Event:', message.eventType);

if (message.eventType === 'msal:loginSuccess' || message.eventType === 'msal:acquireTokenSuccess' ) {
console.log('Login successful:', message);
idToken = message.payload.idToken;
idTokenClaims = message.payload.idTokenClaims;
// This custom event that signals successful login is consumed in main.js and is used for updating the UI with user details
// and loading the protected JSON document with data
const event = new CustomEvent('msalLoginSuccess', { detail: message });
window.dispatchEvent(event);
// Update UI if needed
if (message.account) {
showWelcomeMessage(message.account.username);
}
}
});

export function signIn() {

/**
* You can pass a custom request object below. This will override the initial configuration. For more information, visit:
* https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/request-response-object.md#request
*/

msalInstance.loginPopup(loginRequest)
.then(handleResponse)
.catch(error => {
console.error(error);
});
}

function selectAccount() {
const currentAccounts = msalInstance.getAllAccounts();
if (currentAccounts.length === 0) {
return;
} else if (currentAccounts.length > 1) {
// Add choose account code here
console.warn("Multiple accounts detected.");
} else if (currentAccounts.length === 1) {
username = currentAccounts[0].username;
showWelcomeMessage(username);
}
}

function handleResponse(response) {
if (response !== null) {
username = response.account.username;
showWelcomeMessage(username);
} else {
selectAccount();
}
}

/**
* Displays account details in the console
* @param {string} username - The username of the logged-in account
*/
function showWelcomeMessage(username) {
const accounts = msalInstance.getAllAccounts();
const account = accounts.find(acc => acc.username === username);

if (account) {
console.group('Account Details');
console.log('👤 Username:', account.username);
console.log('🏠 Home Account ID:', account.homeAccountId);
console.log('🏢 Tenant ID:', account.tenantId);
console.log('🔐 Local Account ID:', account.localAccountId);

// Log additional claims if available
if (account.idTokenClaims) {
console.group('ID Token Claims');
Object.entries(account.idTokenClaims).forEach(([key, value]) => {
// Skip standard claims that are already logged
if (!['iss', 'sub', 'aud', 'exp', 'iat', 'nbf', 'aio'].includes(key)) {
console.log(`🔹 ${key}:`, value);
}
});
console.groupEnd();
}

console.log('🔑 Scopes:', loginRequest.scopes);
console.groupEnd();
} else {
console.warn('No account found for username:', username);
}
}

The function that makes the end user authenticate is signIn(). It is invoked indirectly from main.js during the initialization of the web application.

Logging in the browser console looks like this and gives you some idea of what goes on:

Private data in public web application — Microsoft EntraId, GitHub Pages, OCI API Gateway 1hFHDjAeSTdhRtifTQL7W6w
Logging in browser console conveying a sense of what happens during authentication data fetch

Conclusion

It took me a while to wrap my head around this. I was chasing the wrong token, confused by the one or two App registrations required, mixing several properties that to my uninitiated mind seemed very similar and interchangeable. Taking a step back at some point probably would have helped save time and confusion.

I have now reached a point where I have a good understanding of how I can combine a public web application with all resources available to anyone and globally distributed by GitHub Pages (for free) with a data set that should only be available to known, authenticated users. This is a pattern I intend to apply many times more. It is (now) quite simple, very cheap, required no backend coding and does not require me to have resource permanently running specifically for me — saving on money and energy.

Resources

Microsoft Tutorial Quickstart: Sign in users in a single-page app (SPA) and call the Microsoft Graph API — https://learn.microsoft.com/en-us/entra/identity-platform/quickstart-single-page-app-sign-in?tabs=javascript-workforce%2Cjavascript-external&pivots=workforce

Microsoft Learning Module — Register apps using Microsoft Entra ID — https://learn.microsoft.com/en-us/training/modules/register-apps-use-microsoft-entra-id/?source=recommendations

Microsoft EntraId Learn — Single-page application tutorial with JavaScript code for SPA authentication

Tutorial on JWT Token validation with OCI API Gateway — slightly too complex for my purpose: https://docs.oracle.com/en/learn/apigw-ms-entraid/index.html#task-1-configure-microsoft-entra-id-as-an-oauth-identity-provider-id p

OCI API Gateway documentation on JWT Token validation — https://docs.oracle.com/en-us/iaas/Content/APIGateway/Tasks/apigatewayusingjwttokens.htm

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.