
In today’s enterprise landscape, apps that integrate with Microsoft 365 are in high demand – especially when it comes to calendar data. This data can be accessed through the Microsoft Graph API.
But too often, developers over-permission their apps, requesting access to all calendars in a Microsoft 365 tenant. That’s risky, unnecessary, and avoidable.
In this article, I’ll walk you through building an Oracle APEX application that:
- Supports Microsoft Azure AD social sign-in
- Uses delegated Microsoft Graph API permissions to access only the signed-in user's calendar
- Respects the principle of least privilege
Let’s get started.
Why Delegated Permissions Matter
When integrating with Microsoft Graph, there are two permission types: delegated and application.
- Delegated: Your app acts on behalf of a signed-in user and only accesses what the user has access to.
- Application: Your app acts on its own and can access data across the entire tenant.
We’ll be using delegated permissions so users explicitly consent to sharing their data – and your app won't have unrestricted access to the org’s calendars.
Register the App in Azure AD
First, create an Enterprise Application in Azure:
- Go to Azure Portal > Azure Active Directory > App registrations
- Click New registration
- Name your app and set the Redirect URI to your APEX instance: https://<your_apex_domain>/ords/apex_authentication.callback
After registration:
- Save the Application (client) ID and Directory (tenant) ID
- Go to API permissions > Add a permission > Microsoft Graph
- Select Delegated permissions, then add: Calendars.Read
This gives your app permission to read a user’s personal calendar — but only after they sign in and consent.
Configuration of the Application in Azure Portal
Create a set of client credentials and note down:
- "Directory (tenant) ID"
- "Secret Value" (not: the Secret ID; we don't need that at all)
- "Application (client) ID"
Set Up OAuth2 in Oracle APEX
In APEX first of all you have to create Credentials for your newly created OAuth App. For this navigate to Workspace Utilities > Web Credentials >Create. The credentials have to be of type "Basic credentials" with the "Client ID" as the username and the "Secret Value" as the password.
Oracle APEX supports OAuth2 natively through social sign-in. Here's how to configure it:
- Go to Shared Components > Authentication Schemes
- Create a New Scheme > Social Sign-In
- Set the Credential Store to the Credential store created previously.
- Set Authentication Provider to OpenID Connect Provider
- Fill in: Discovery URL: https://login.microsoftonline.com/<TENANT_ID>/.well-known/openid-configuration
- For the Scope it should be enough to just have openid
- In my case I want to have the login/e-mail as the username; best select #upn# to achieve this.
- Finally make sure to enter a post authentication procedure. In my case this is "post_authentication_fct"
Newly setup Authentication Scheme
Enable this scheme and make it the default for your app.
Now, users can log in using their Microsoft 365 credentials, and APEX will manage token storage behind the scenes.
Preserve the REFRESH_TOKEN
After login a Refresh-Token is available within the Post-Login-Procedure. We save that away into an Application Item "REFRESH_TOKEN" as follows:
procedure post_authentication_fct as l_token varchar2(4000); begin -- set token for REST call l_token := trim(apex_json.get_clob('refresh_token')) ; apex_util.set_session_state('REFRESH_TOKEN', l_token); end post_authentication_fct;
For this you have to create the following application items:
- REFRESH_TOKEN
- ACCESS_TOKEN
We can then create an access token based on this application item whenever we need it as follows.
Note: The access_token that I can get in the post_authentication_fct didn't work for unclear reasons for me. But with the REFRESH_TOKEN I was able to proceed:
procedure set_oauth_token as l_access_token varchar2(4000); l_access_token_parsed apex_jwt.t_token; l_refresh_token varchar2(4000); rest_p apex_application_global.vc_arr2; rest_v apex_application_global.vc_arr2; v_response CLOB; begin l_access_token := apex_util.get_session_state('ACCESS_TOKEN'); if l_access_token is not null then l_access_token_parsed := apex_jwt.decode ( p_value => l_access_token ); begin apex_jwt.validate ( p_token => l_access_token_parsed, p_aud => 'https://graph.microsoft.com', p_iss => 'https://sts.windows.net/<TENANT_ID>/', p_leeway_seconds => 120); APEX_WEB_SERVICE.OAUTH_SET_TOKEN(l_access_token); return; exception when VALUE_ERROR then null; -- we try to get a new one later end; end if; l_refresh_token := apex_util.get_session_state('REFRESH_TOKEN'); --Note: You probably want to get ClientID/Secret from the APEX Credentials: rest_p(1) := 'client_id'; rest_v(1) := '<CLIENT_ID>'; rest_p(2) := 'client_secret'; rest_v(2) := '<CLIENT_SECRET>'; rest_p(3) := 'grant_type'; rest_v(3) := 'refresh_token'; rest_p(4) := 'scope'; rest_v(4) := 'https://graph.microsoft.com/.default'; rest_p(5) := 'refresh_token'; rest_v(5) := l_refresh_token; v_response := apex_web_service.make_rest_request( p_url => 'https://login.microsoftonline.com/<TENANT ID>/oauth2/v2.0/token', p_http_method => 'POST', p_parm_name => rest_p, p_parm_value => rest_v ); SELECT JSON_VALUE(v_response, '$.access_token') INTO l_access_token FROM dual; l_access_token_parsed := apex_jwt.decode ( p_value => l_access_token ); apex_jwt.validate ( p_token => l_access_token_parsed, p_aud => 'https://graph.microsoft.com', p_iss => 'https://sts.windows.net/<TENANT_ID>/', p_leeway_seconds => 120); APEX_WEB_SERVICE.OAUTH_SET_TOKEN(l_access_token); apex_util.set_session_state('ACCESS_TOKEN', l_access_token); end set_oauth_token;
You can/should call this procedure before every actual call to the API. The actual call can then be:
declare l_response clob; begin set_oauth_token; l_response := apex_web_service.make_rest_request( p_url => 'https://graph.microsoft.com/v1.0/me/calendars', p_http_method => 'GET', p_scheme => 'OAUTH_CLIENT_CRED', ); -- Use APEX_JSON to parse and display events end;
This call returns the signed-in user’s accessible calendars using their token — no tenant-wide access required. You can get the actual calendar Events with something similar like:
'https://graph.microsoft.com/v1.0/me/calendars/'||p_calendar_id||'/calendarview?startdatetime='||l_start_period || '&enddatetime='||l_end_period;
Want to show a calendar in your app? Parse the JSON response using APEX_JSON and feed it into a Calendar region, Interactive Report, or even a Kanban view.
Wrapping Up
Let’s recap what we built:
- An Oracle APEX app with Microsoft social sign-in
- Delegated permission access to Microsoft 365 calendars
- A secure, least-privilege integration via Microsoft Graph API
This approach offers the best of both worlds: enterprise-grade integration with zero overreach into your organization’s data.
What’s Next?
You can extend this further by:
- Integrating /me/mailFolders for Outlook emails
- Showing availability with /me/calendarView
- Combining calendar data with APEX dashboards or mobile apps
If you’re building internal tools, client portals, or productivity dashboards, this is a powerful – and secure – pattern to adopt.