Understanding Delegated vs Application Permissions in Microsoft Entra ID: A Security Deep Dive for IT Architects

Introduction

October is Cybersecurity Awareness Month, a reminder that protecting identity and data is everyone’s responsibility, especially in cloud-first environments. As organizations deepen their reliance on Microsoft 365 and Entra ID, understanding how applications gain access to corporate resources has never been more critical.

Access control sits at the heart of Microsoft 365 and Entra ID security. Every permission you grant, whether to a user, application, or service principal defines who can interact with corporate data and how.
For system architects and administrators, understanding the difference between delegated and app-only access isn’t just a technical nuance; it’s essential for preventing data exposure, privilege escalation, and consent-based attacks.

Modern organizations rely heavily on integrations, custom apps, automation scripts, backup agents, or Power Platform connectors – all of which authenticate through Microsoft Entra ID. Knowing how these apps access resources helps you enforce least privilege, evaluate consent requests, and monitor what happens behind the scenes.


Overview

Applications in Microsoft Entra ID can access organizational data in two primary modes:

1. Delegated Access (On Behalf of a User)

Delegated access occurs when an application acts as the signed-in user.
It requires:

  • The user to sign in and grant consent.
  • The application to request delegated permissions (scopes) such as Mail.Read or Files.Read.All.

The app’s access is limited by:

  • The user’s existing rights to the data.
  • The scopes consented to by the user or an admin.

If a Global Administrator signs in, the app temporarily inherits their privileges, creating potential risk if either the account or app is compromised.

Example:
A Teams add-in using delegated Files.Read.All can only read files the signed-in user can access, never tenant-wide content.

OAuth 2.0 Flow (Simplified):

  1. User authenticates interactively via the Microsoft identity platform.
  2. Entra ID displays a consent screen with the requested scopes.
  3. Upon approval, Entra ID issues an access token representing both user and app permissions.
  4. The app uses this token to call APIs such as Microsoft Graph.

2. Application Permissions (Access without a user)

App-only access allows an application to act independently of any user, using its own identity.
The app’s rights come entirely from admin-granted application permissions, like User.Read.All or Mail.Send.

Common in automation, reporting, and backend integrations, this model is powerful but risky: a compromised app credential (client secret or certificate) grants tenant-wide access.

Example:
A compliance automation tool with Files.Read.All app-only permission can access all users’ OneDrive files—even without any user logged in.

OAuth 2.0 Flow (Client Credentials Grant):

  1. The app authenticates using its credentials (client secret or certificate).
  2. Entra ID verifies the identity and issues an app-only access token.
  3. The token represents the service principal, not any specific user.
  4. API calls are logged as service principal activity.

Comparison Table

AspectDelegated PermissionsApplication Permissions
Identity ContextActs on behalf of a signed-in userActs as its own service principal
AuthenticationRequires user sign-in and consent (OAuth 2.0 Authorization Code Flow)Uses client secret, certificate, or managed identity (Client Credentials Flow)
Permission ScopeLimited by user rights and requested scopesDefined by application-level permissions
Risk SurfaceInherits user privileges, risky if user has elevated rolesTenant-wide exposure if app credentials are compromised
Visibility in LogsAppears as user activityAppears as service principal activity (less transparent)
Who Can ConsentUsers (for personal data) or admins (for all users)Admins only
Consent MethodShown interactively at sign-inConfigured statically in app registration
LifespanSession-based; expires with user tokensPersistent; remains active until credentials expire or revoked
Typical Use CaseUser-centric apps (e.g., Outlook add-ins, Teams bots)Background daemons, automation scripts, or data sync agents

Security Risks and Attack Scenarios

Both permission types can be secure when scoped properly but both become dangerous when permissions are broader than required.

Delegated Permissions Access Risks

  • Users unknowingly approve apps with excessive scopes (Mail.ReadWrite.All).
  • Apps inherit Global Admin privileges from highly privileged sign-ins.
  • Attack scenario: A malicious actor launches a consent phishing attack, tricking a user into granting “Send mail as you” permission. The attacker now impersonates the user to perform Business Email Compromise (BEC).

Application Permissions Access Risks

  • Developers grant wide-ranging admin consent (e.g., User.ReadWrite.All) for convenience.
  • Secrets or certificates stored insecurely in code repositories.
  • Attack scenario: A leaked client secret for an app with Directory.ReadWrite.All allows silent privilege escalation—creating shadow accounts or modifying security settings, without triggering MFA or user prompts.

At the core, the danger lies not in the permission model itself, but in granting broader access than necessary.


Best Practices for Securing Entra ID App Access

1. Enforce Least Privilege

  • Assign only the minimum necessary scopes.
    Avoid tenant-wide permissions like *.All unless absolutely required.
  • Use incremental consent for delegated apps requesting additional permissions only when needed.

2. Choose the Right Permission Type

  • Match the application’s use case to the appropriate access model:
    • Delegated: When user context and consent are required.
    • App-Only: When no user is involved (e.g., automation or reporting).
  • Document and review permission mappings for each integration.

3. Control and Review User Consent

  • Configure admin consent workflows in Microsoft Entra ID so that sensitive permissions require approval.
  • Disable or limit user consent for external or unverified applications.
  • Periodically audit app permissions and consents in the Enterprise Applications → Permissions and Consent blade.

4. Secure App Credentials

  • Use certificates or managed identities instead of client secrets whenever possible.
  • Restrict client secret creation using Application policies.
  • Rotate credentials periodically and avoid embedding them in code or configuration files.

5. Monitor and Audit Regularly

  • Review service principals and remove unused app registrations.
  • Monitor Sign-In logs and Audit logs for anomalous activity.
  • Implement alerting for new high-privilege app consents or app registrations.

6. Educate Users and Developers

  • Train users to recognize suspicious consent prompts.
  • Instruct developers to request the narrowest scopes possible during app design.
  • Embed a security review checkpoint into your app registration workflow.

Conclusion

In my years administering Microsoft Entra ID, I’ve learned one universal truth: application vendors rarely understand the full implications of the permissions their apps request.

I’ve seen requests that could make any security admin’s coffee go cold, like an app demanding Mail.ReadWrite and Mail.Send for every mailbox in the tenant. Yes, every single one. Including the CEO’s.

When I questioned the vendor about it, their response was priceless:

“Sure, we ask for those permissions, but it doesn’t mean we’ll actually use them.”

Of course. Because that’s exactly how least privilege works, right? Ask for the kingdom, but promise you’ll only open the gate occasionally.

It took hours of discussion (and a few deep breaths) to walk them through why that approach was a terrible idea.
This experience reinforced a simple but critical lesson: always verify what permissions an application is requesting and why. Blindly granting broad scopes doesn’t just expand functionality, it expands your attack surface.

Doing due diligence before approving access, verifying scopes, questioning necessity, and understanding the real reach of permissions, isn’t just best practice; it’s the difference between secure automation and unintentional data sprawl.

Thank you for stopping by. ✌️

Microsoft 365 Admins: September 2025 Retirements, Security Shifts, and Copilot Usage Insights – Here’s Your Definitive Guide

September 2025 is shaping up to be one of the most impactful months for Microsoft 365 admins this year. From long-awaited retirements (farewell, Classic eDiscovery and Azure AD Graph API) to robust new features (Progressive Alert Scoring, SharePoint smart tagging, and Copilot usage metering), this month is packed with changes that demand action and attention.

Whether you’re in charge of compliance, identity, collaboration, or cloud security, this comprehensive guide gives you what you need to stay one step ahead.

September at a Glance

CategoryCount
🔻 Retirements9
🆕 New Features13
🔧 Enhancements10
🔄 Changes in Functionality4
⚠️ Action Required6

Retirements: Legacy Features You Can Let Go

1. Classic Message Trace (Exchange Online)

Sep 1, 2025 – Legacy UI and cmdlets (Get-MessageTrace, Get-MessageTraceDetail) retired.
Do this: Update to Get-MessageTraceV2 and use the modern EAC interface.
🔗 Ref

2. Client Access Rules (Exchange Online)

Sep 1, 2025 – Deprecated in favor of Conditional Access with Continuous Access Evaluation.
Do this: Migrate access controls to Entra Conditional Access.
🔗 Ref

3. Classic eDiscovery (Premium)

Sep 1, 2025 – Fully removed from Purview.
Do this: Move to Unified eDiscovery for better search and case handling.
🔗 Ref

4. Mobile Devices Page in Outlook Web

Sep 9, 2025 – Removed from OWA and New Outlook.
Do this: Use the My Account portal or native device management tools.
🔗 Ref

5. Cognitive Services & Azure ML in Power BI

Sep 15, 2025 – AI features being pulled in favor of Microsoft Fabric AutoML.
Do this: Transition to Microsoft Fabric for ML workloads.
🔗 Ref

6. Microsoft To Do Ends Support for iOS 16/macOS 12

Mid-Sep 2025 – No updates for outdated devices.
Do this: Update org device baselines to iOS 17 / macOS 13+.
🔗 Ref

7. Defender for Cloud Apps: Sub-Domains Visibility

Sep 22, 2025 – Low-use feature retired.
🔗 Ref

8. Legacy MFA & SSPR Method Management

Sep 30, 2025 – Management of authentication methods moves to the unified policy.
Do this: Transition to converged authentication methods now.
🔗 Ref

9. Azure AD Graph API

Early Sep 2025 – All apps must migrate to Microsoft Graph.
Do this: Use Entra admin center’s Recommendations tab to identify affected apps.
🔗 Ref


New Features: Worth the Hype

1. Progressive Alert Scoring in Insider Risk Management
Assess user activity multiple times per day instead of once every 24 hours. Get near real-time insights into risky behavior.
🔗 Ref

2. High Volume Exchange Email (HVE)
Send large volumes of internal mail for LOB applications and SMTP use cases—beyond standard Exchange Online limits.
🔗 Ref

3. Information Barriers in Microsoft Planner
Prevents users from discovering non-segmented users while sharing Planner plans via web and Teams.
🔗 Ref

4. Silent Test Calls in Teams
Simulate Teams call quality for network diagnostics—now available for Windows and Mac (Teams Premium required).
🔗 Ref

5. Rule-Based App Management in Teams
Set app permissions and availability based on scopes and publisher trust—control Microsoft 365 certified apps centrally.
🔗 Ref

6. New Secure Score Recommendations
Enhance security posture with new recommendations:
• Remove inactive service accounts
• Remove passwords from AD attributes
🔗 Ref

7. Teams + Defender for Office 365 Integration
Manage allow/block lists for external domains using the Tenant Allow/Block List (TABL) in Microsoft Defender.
🔗 Ref

8. New SharePoint Workflows
Power Automate-based automation embedded in SharePoint with natural language, Madlib-style editor, and better UI.
🔗 Ref

9. Retention Based on Last Accessed
Configure Purview retention policies for OneDrive and SharePoint based on when files were last accessed.
🔗 Ref

10. Priority Cleanup in Data Lifecycle Management
Allows deletion of SharePoint/OneDrive items before retention expires. Includes dual admin approval, simulation mode, and logs.
🔗 Ref

11. Copilot Usage Report
Monitor and manage costs from Copilot Chat with detailed reporting on metered message usage by user, agent, and policy.
🔗 Ref

12. Smart Tags for eSigned Docs in SharePoint
Automatically tag signed documents and add metadata columns for signature status and provider.
🔗 Ref

13. Teams Private Channel Expansion
Big lift for Teams limits:
• 1000 private channels per team
• 5000 members per private channel
• Meeting scheduling and DLP support
🔗 Ref


Enhancements: Small but Mighty

  • Auto Work Location Detection in Teams – Based on Wi-Fi or peripherals.
  • Updated SharePoint Page Analytics – Up to 365-day view history, reactions, shares, and Excel exports.
  • Purview Diagnostics Access – Now available to more admin roles.
  • Outlook Mail Merge (Advanced) – Dynamic fields and personalizations now in web and Windows clients.
  • Authenticator Improvements – No more number matching for same-device logins, cleaner FRX setup.
  • SharePoint Smart Tagging – Auto-adds metadata for eSigned documents.
  • Teams Join URL Validation – Ensure rewritten links don’t break meeting joins.
  • License Assignment Path in Admin Center – Easily view direct vs group-based license sources.
  • Streamlined Purview DLP Alert Settings – Sync alert states between portal and PowerShell.
  • Teams Auth Module Updates – New app permissions required: GroupMember.Read.All, RoleManagement.Read.Directory.

Functionality Changes: Take Note

  • Access Review History Limited to 12 Months
    Export old data now using Microsoft Graph or Azure Data Explorer.
    🔗 Ref
  • Defender for Identity Alert Migration to XDR
    Update alert workflows and exclusions for XDR platform.
    🔗 Ref
  • OneDrive: Unlicensed Accounts Enter Read-Only Mode
    Deadline: July 28 → Enforcement: Sep 26, 2025
    🔗 Ref
  • DLP Rule Visibility Fix in Portal
    Now reflects accurate status if disabled via PowerShell.
    🔗 Ref

Action Items: Handle These Before the Alarm Bells Ring

DeadlineTask
Sep 1, 2025Migrate to new Message Trace, Unified eDiscovery, Conditional Access
Sep 2, 2025Create Azure DevOps–specific Conditional Access policy
Sep 14, 2025Update Teams PowerShell app permissions
Sep 15, 2025MFA required for credential management in Entra
Sep 30, 2025Converge legacy MFA and SSPR policies
OngoingMigrate apps from Azure AD Graph to Microsoft Graph

Final Thoughts

September 2025 is a turning point month for Microsoft 365 environments. Between the retirement of major legacy features and a flood of next-gen tools and AI insights, it’s clear that Microsoft is pushing the ecosystem toward tighter security, smarter automation, and more control for admins.

  • Bookmark this guide.
  • Review your tenant configurations.
  • Communicate changes to your teams.
  • Knock out required actions before deadlines bite.

Because in enterprise IT, proactive beats reactive every time.

Thank you for stopping by. ✌️

RBAC vs. ABAC in Azure: Why You Need Both for Cloud Access Control That Actually Works

Let’s cut to the chase, cloud access control isn’t just a checkmark on your compliance list anymore. It’s a daily battlefield. With global teams, hybrid workloads, and rising security risks, who can do what and under what conditions is now a core pillar of IT strategy.

If you’re working in Azure, you’ve likely heard of RBAC (Role-Based Access Control) and ABAC (Attribute-Based Access Control). But what you may not know is that these aren’t mutually exclusive instead they’re better together.

Let’s unpack what each model does, where they shine (and struggle), and how to combine them for airtight, scalable access governance in Azure.

What is Azure Role-Based Access Control (RBAC)?

Azure RBAC helps you control access by assigning roles to security principals (users, groups, service principals, or managed identities) at a specific scope (subscription, resource group, or resource).

Each role is a bundle of permissions, think of them as job descriptions for Azure resources.

Example RBAC Use Cases

  • A user who can manage only virtual machines in the Dev subscription.
  • A group assigned the Reader role at the resource group level.
  • An app given Contributor access to only one storage account.

RBAC works well when your access needs are role-based and relatively straightforward. But as organizations scale and become more dynamic, things can get messy fast.

Where RBAC Falls Short

RBAC starts to creak when:

  • You need to create roles for every unique mix of region, team, and resource.
  • You end up with a Frankenstein monster of roles like:
    • VP - Europe
    • Manager - Asia
    • SalesRep - NorthAmerica - Junior
  • You have hierarchical or multi-tenant data structures that don’t fit RBAC’s flat model.

The result? Role sprawl, administrative pain, and security gaps.

What is Azure Attribute-Based Access Control (ABAC)?

ABAC adds contextual smarts to access control. Instead of relying solely on roles, it factors in attributes of:

  • The user (e.g., department = HR)
  • The resource (e.g., tag = Project:Alpine)
  • The environment (e.g., access during business hours only)

In Azure, ABAC is implemented through role assignment conditions that filter RBAC permissions.

ABAC in Action

  • “Chandra can read blobs only if they’re tagged with Project=Cascade.”
  • “Support engineers can impersonate users only during a help session.”
  • “Users can access data only in their assigned region or cost center.”

This kind of fine-grained access is powerful, flexible, and crucial in multi-tenant, regulated, or fast-moving environments.

RBAC + ABAC: Not a Choice – A Collaboration

Here’s the mindset shift: RBAC and ABAC are not competing models. They’re complementary.

RBAC defines what actions are allowed.
ABAC defines under what conditions those actions are allowed.

By combining the two, you can:

  • Keep your role structure simple and understandable.
  • Layer on access conditions that reflect real-world business rules.

Common Hybrid Patterns

ScenarioRBAC RoleABAC Condition
Multi-tenant appTenant AdminOnly for tenant_id=X
Regional accessSales ManagerRegion = “North America”
Subscription tiersPremium UserAccess feature only if plan=premium
File accessEditorOnly owner=user_id or shared_with=user_id
Support scenariosSupport AgentImpersonation allowed if user_in_session=true

Best Practices for RBAC and ABAC in Azure

Let’s bring it home with the golden rules:

RBAC Best Practices

  • Least Privilege Always: Grant only the permissions needed—nothing more.
  • Limit Subscription Owners: Three max. The fewer, the safer.
  • Use PIM for Just-in-Time Access: With Microsoft Entra PIM, elevate access temporarily.
  • Assign Roles to Groups: Not individuals. Makes scaling and auditing easier.
  • Avoid Wildcards in Custom Roles: Be explicit with Actions and DataActions.
  • Script with Role IDs, Not Names: Avoid breakage from renamed roles.

ABAC Best Practices

  • Tag Strategically: Use meaningful tags like Project, Environment, or Classification to enable ABAC.
  • Use Conditions to Reduce Role Sprawl: Filter access with precision.
  • Start Small: Pilot with blob storage conditions before scaling ABAC elsewhere.
  • Don’t Replace RBAC: Use ABAC as a filter, not a replacement.

Recap: When to Use What

FeatureRBACABACRBAC + ABAC
Simplicity
Contextual Flexibility
Scalability⚠️ (sprawl risk)
Multi-Tenant Scenarios⚠️
Least Privilege Enforcement✅✅

Final Thoughts

RBAC gives you structure. ABAC gives you nuance. In Azure, using both gives you power and precision.

Don’t fall into the “either/or” trap. The real magic happens when you combine the predictability of RBAC with the intelligence of ABAC to build access models that scale with your business.

Thanks for stopping by. ✌

Avoid the Oops: Proactively Monitor Entra Application Secret Expirations with Automation

In the fast-paced world of enterprise IT, even small oversights can lead to major disruptions. One of those easily overlooked, yet critically important tasks is, monitoring Microsoft Entra (formerly Azure AD) application secret expirations.

Imagine this: everything is running smoothly in production until — bam! — an app suddenly fails because its secret expired. No alerts, no heads-up, just user complaints and a flurry of incident reports. Sound familiar?

Why Monitoring Application Secrets Matters

At its core, application secrets are credentials that apps use to authenticate themselves with Microsoft Entra ID. But unlike passwords, secrets come with an expiration date. If they aren’t renewed in time, the app’s authentication fails and with it, the business processes it supports.

For organizations running dozens (or hundreds) of app registrations, staying ahead of these expirations is not optional, it’s essential. Secrets that silently expire can grind systems to a halt, disrupt integrations, and worst of all, trigger security incidents or SLA breaches.

A Common Problem in the Real World

When I first set out to automate monitoring for app secret expirations, I assumed it’d be simple. A few PowerShell lines here, an API call there, maybe some Logic App magic… problem solved, right?

Well, not quite.

Most of the tutorials and blog posts I found focused solely on fetching the expiration date of secrets — they showed how to query the data, but not how to operationalize it. I wanted something that would proactively notify the right people before things went south.

Eventually, I came across a helpful post on Microsoft Tech Community:
Use Azure Logic Apps to Notify of Pending AAD Application Client Secrets and Certificate Expirations

It was a solid foundation. The Logic App would periodically check secrets and send an email notification if one was nearing expiration.

But then… I hit a snag.

If an application had multiple owners which, in the enterprise world, is very common the Logic App would only notify the first listed owner. Everyone else? Left in the dark.

Not ideal. Especially when that one person is on PTO, has left the company, or, let’s be honest, just ignores emails from IT.

So, I decided to roll up my sleeves and build a PowerShell-based solution that:

  • Queries all app registrations
  • Checks for secrets or certificates nearing expiration
  • Looks up all owners (not just the first one)
  • Sends clear, actionable email notifications to each owner

Why Automate This? Let’s Talk Benefits

Here’s why every enterprise IT team should care about automating secret expiration alerts:

  • Proactive Security – Timely notifications help you spot secrets that are about to expire, before they become a security risk or business disruption. It’s the difference between being reactive and being prepared.
  • Reduced Downtime – Missed secret expirations lead to failed authentications, which means broken apps. Proactive alerts buy you time to renew secrets and avoid outages.
  • No More Manual Tracking – Maintaining a spreadsheet of app secrets? Been there. Done that. Automation means less grunt work and fewer mistakes.
  • Smart Notifications – By targeting all app owners, not just the first in line: you’re covering your bases. Even if someone’s on vacation, someone else sees the alert and can take action.

What This Script Does

  • Authenticates to Microsoft Graph with the required permissions
  • Queries all Entra app registrations
  • Identifies app secrets and certificates that are expiring within a defined threshold (e.g., 30 days)
  • Pulls all assigned owners for each app
  • Sends an email notification to each owner with details about the impending expiration
  • Sends an email notification to an administrator email, a distribution group or a shared mailbox containing a list of all secrets that are expiring

Use task scheduler to run this script from your on-premise environment. You can securely store the credentials to be used in the PowerShell script using the SecretManagement module. I’ve covered this in detail in an earlier post here. The ideal and preferred method is to use Azure automation account which is much more easier and secure, which I will cover in a future post.

# Connect to Microsoft Graph
Connect-MgGraph -Scopes "Application.Read.All","User.Read.All","AppRoleAssignment.Read.All"

# Set default value for the number of days until expiration
$DaysUntilExpiration = 30

# Email configuration
$SmtpServer = "smtp.yourdomain.com"
$From = "alerts@yourdomain.com"
$DefaultEmail = "entra_app_cert_notif@yourdomain.com"

# Function to send email
function Send-ExpirationAlert {
    param (
        [string]$To,
        [string]$FirstName,
        [string]$AppName,
        [string]$SecretOrCertName,
        [string]$SecretOrCertId,
        [datetime]$EndDate
    )

    $Subject = "Alert: Secret or Certificate Expiration Notice for $AppName"
    $Body = @"
<html>
<body>
<p>Hello $FirstName,</p>
<p>This is a notification that the secret or certificate named '$SecretOrCertName' for the application '$AppName' will expire on $($EndDate.ToShortDateString()).</p>
<p>Please contact Entra (previously known as Azure AD) Administrators and take the necessary actions to renew or replace the secret or certificate before it expires.</p>
<p>Details to provide the administrators:</p>
<ul>
<li>Application Name: $AppName</li>
<li>Secret or Certificate Name: $SecretOrCertName</li>
<li>Secret or Certificate ID: $SecretOrCertId</li>
<li>Expiration Date: $($EndDate.ToShortDateString())</li>
</ul>
<p><span style="color: red;">Please do not reply to this email, this mailbox is not monitored.</span></p>
<p>Thank you,<br>Your IT Team</p>
</body>
</html>
"@

    Send-MailMessage -SmtpServer $SmtpServer -From $From -To $To -Subject $Subject -Body $Body -BodyAsHtml
}

# Function to send summary email
function Send-SummaryEmail {
    param (
        [string]$To,
        [string]$Body
    )

    $Subject = "Summary: Secrets and Certificates Expiring in the Next 30 Days"
    Send-MailMessage -SmtpServer $SmtpServer -From $From -To $To -Subject $Subject -Body $Body -BodyAsHtml
}

# Get the current date
$Now = Get-Date

# Query all applications
$Applications = Get-MgApplication -All

# Initialize a variable to store the summary of expiring secrets and certificates
$SummaryBody = @"
<html>
<body>
<p>Hello,</p>
<p>The following secrets and certificates are expiring in the next 30 days:</p>
<table border="1">
<tr>
<th>Application Name</th>
<th>Secret or Certificate Name</th>
<th>Secret or Certificate ID</th>
<th>Expiration Date</th>
</tr>
"@

# Process each application
foreach ($App in $Applications) {
    $AppName = $App.DisplayName
    $AppID   = $App.Id
    $ApplID  = $App.AppId

    $AppCreds = Get-MgApplication -ApplicationId $AppID
    $Secrets = $AppCreds.PasswordCredentials
    $Certs   = $AppCreds.KeyCredentials

    foreach ($Secret in $Secrets) {
        $StartDate  = $Secret.StartDateTime
        $EndDate    = $Secret.EndDateTime
        $SecretName = $Secret.DisplayName
        $SecretId   = $Secret.KeyId

        $Owners = Get-MgApplicationOwner -ApplicationId $App.Id

        if ($Owners.Count -eq 0) {
            # No owner information, send to default email
            $FirstName = "Admin"
            Send-ExpirationAlert -To $DefaultEmail -FirstName $FirstName -AppName $AppName -SecretOrCertName $SecretName -SecretOrCertId $SecretId -EndDate $EndDate
        } else {
            foreach ($Owner in $Owners) {
                $Username = $Owner.AdditionalProperties.userPrincipalName
                $OwnerID  = $Owner.Id

                if ($null -eq $Username) {
                    $Username = $Owner.AdditionalProperties.displayName
                    if ($null -eq $Username) {
                        $Username = '**<This is an Application>**'
                    }
                }

                # Extract first name from givenName or user principal name
                $FirstName = $Owner.AdditionalProperties.givenName
                if ($null -eq $FirstName -or $FirstName -eq '') {
                    $FirstName = $Username.Split('@')[0].Split('.')[0]
                }

                $RemainingDaysCount = ($EndDate - $Now).Days

                if ($RemainingDaysCount -le $DaysUntilExpiration -and $RemainingDaysCount -ge 0) {
                    if ($Username -ne '<<No Owner>>') {
                        Send-ExpirationAlert -To $Username -FirstName $FirstName -AppName $AppName -SecretOrCertName $SecretName -SecretOrCertId $SecretId -EndDate $EndDate
                    }
                }
            }
        }

        # Add to summary if expiring in the next 30 days
        if ($RemainingDaysCount -le $DaysUntilExpiration -and $RemainingDaysCount -ge 0) {
            $SummaryBody += @"
<tr>
<td>$AppName</td>
<td>$SecretName</td>
<td>$SecretId</td>
<td>$($EndDate.ToShortDateString())</td>
</tr>
"@
        }
    }

    foreach ($Cert in $Certs) {
        $StartDate  = $Cert.StartDateTime
        $EndDate    = $Cert.EndDateTime
        $CertName   = $Cert.DisplayName
        $CertId     = $Cert.KeyId

        $Owners = Get-MgApplicationOwner -ApplicationId $App.Id

        if ($Owners.Count -eq 0) {
            # No owner information, send to default email
            $FirstName = "Admin"
            Send-ExpirationAlert -To $DefaultEmail -FirstName $FirstName -AppName $AppName -SecretOrCertName $CertName -SecretOrCertId $CertId -EndDate $EndDate
        } else {
            foreach ($Owner in $Owners) {
                $Username = $Owner.AdditionalProperties.userPrincipalName
                $OwnerID  = $Owner.Id

                if ($null -eq $Username) {
                    $Username = $Owner.AdditionalProperties.displayName
                    if ($null -eq $Username) {
                        $Username = '**<This is an Application>**'
                    }
                }

                # Extract first name from givenName or user principal name
                $FirstName = $Owner.AdditionalProperties.givenName
                if ($null -eq $FirstName -or $FirstName -eq '') {
                    $FirstName = $Username.Split('@')[0].Split('.')[0]
                }

                $RemainingDaysCount = ($EndDate - $Now).Days

                if ($RemainingDaysCount -le $DaysUntilExpiration -and $RemainingDaysCount -ge 0) {
                    if ($Username -ne '<<No Owner>>') {
                        Send-ExpirationAlert -To $Username -FirstName $FirstName -AppName $AppName -SecretOrCertName $CertName -SecretOrCertId $CertId -EndDate $EndDate
                    }
                }
            }
        }

        # Add to summary if expiring in the next 30 days
        if ($RemainingDaysCount -le $DaysUntilExpiration -and $RemainingDaysCount -ge 0) {
            $SummaryBody += @"
<tr>
<td>$AppName</td>
<td>$CertName</td>
<td>$CertId</td>
<td>$($EndDate.ToShortDateString())</td>
</tr>
"@
        }
    }
}

# Close the HTML table and body
$SummaryBody += @"
</table>
<p>Thank you,<br>Your IT Team</p>
</body>
</html>
"@

# Send the summary email
Send-SummaryEmail -To $DefaultEmail -Body $SummaryBody

Monitoring Entra application secret expirations may not be the flashiest part of your security strategy, but it’s one of the most crucial. It’s also one of those tasks that’s easy to automate, but costly to ignore.

If you’re currently relying on manual processes or using a Logic App that only pings one owner, consider leveling up your approach. A bit of PowerShell and planning can save you hours of downtime, reduce late-night incident calls, and help keep your environment secure.

Thank you for stopping by. ✌️