EvolitBlogContact

Entra ID App Registration Secrets Expiry: How to Stop Getting Caught Off Guard

Client secrets on App Registrations expire silently — no email alert is sent by default. When the deadline passes, every integration fails immediately with AADSTS7000215. This article covers a PowerShell audit script, safe rotation without a maintenance window, and credential lifecycle policies.

Entra ID App Registration Secrets Expiry: How to Stop Getting Caught Off Guard

TL;DR: Client secrets and certificates on Entra ID App Registrations expire silently — no email notification is sent by default. When the deadline passes, every integration using that credential fails immediately with AADSTS7000215. This article walks through a PowerShell script to audit expiring credentials across your entire tenant, how to safely rotate secrets without a maintenance window, and how to enforce credential lifecycle policies for new applications.

The Problem

It is Monday morning. Users are calling. Your monitoring dashboard is red. You pull up Entra sign-in logs and find AADSTS7000215: Invalid client secret provided repeated hundreds of times. The application has worked fine for 18 months. What happened? A client secret on an App Registration expired over the weekend.

In Microsoft 365 environments, App Registrations are how applications prove their identity to Entra ID so they can access Microsoft Graph, Exchange Online, SharePoint, Teams, or any other Microsoft service. Each registration can hold credentials in two forms: client secrets (strings, similar to passwords) or certificates (asymmetric X.509 keys). Both have expiration dates. Neither triggers a notification email by default.

The number of App Registrations in a tenant grows faster than anyone plans for. Every Graph API automation script, every Logic App connector, every third-party integration using OAuth, every Azure Function calling SharePoint — all of them may need their own registration with credentials. A tenant with a few hundred users can easily have 40-80 registrations. MSPs managing multiple client tenants can have hundreds.

This problem is compounded by history. For years, the Azure portal offered a "Never Expire" option for client secrets. Technically, it set the expiration date to December 31, 2299. It looked safe. It was not — those credentials were never rotated, potentially leaked and never revoked, and now sitting in tenants waiting for someone to audit them.

Why This Happens

The History of Secret Lifetime Limits

Microsoft policy on credential lifetimes has changed several times:

  • Before April 2021: The portal offered a "Never Expire" option. Technically set expiry to year 2299.
  • April 2021: Microsoft removed the "Never Expire" option from the Azure portal. New secrets required a concrete expiration date.
  • February 2022: Official announcement that client secrets are capped at a 2-year maximum. The portal enforces this. The Microsoft Graph API and PowerShell technically still allowed longer lifetimes at that point, but Microsoft signaled this loophole would close.

Organizations that built integrations before 2021 may have secrets in their tenants with expiry dates showing 12/31/2299. These credentials still work. But they are unrotated, possibly years-old, full-access credentials — exactly the security risk Microsoft aimed to eliminate.

No Native Email Alerting

Microsoft does not send any email when an App Registration credential is about to expire. The only built-in mechanism is the Recommendations tab in the Entra admin center (entra.microsoft.com > Overview > Recommendations), which surfaces the applicationCredentialExpiry recommendation — but:

  1. It only warns 30 days before expiration.
  2. It requires someone to actively check the portal — no push notification, no email.
  3. It covers App Registrations but not all SAML certificates on Enterprise Applications.
  4. Full API access to the recommendation data requires a Microsoft Entra Workload ID license.

Thirty days is often not enough time. If credential rotation requires contacting the application owner, going through a change request, running regression tests, and deploying to production — you need 60-90 days minimum. By the time anyone notices the Recommendations panel, the margin is already too thin.

Solution Step by Step

Step 1: Install Microsoft Graph PowerShell

Install-Module Microsoft.Graph -Scope CurrentUser -Force
# Verify installation
Get-InstalledModule Microsoft.Graph | Select-Object Name, Version

Step 2: Audit All Expiring Credentials Across App Registrations

The script below retrieves all App Registrations and reports credentials expiring within a configurable threshold. Critical detail: the -Property parameter is mandatory. Without explicitly requesting passwordCredentials and keyCredentials, the cmdlet returns empty arrays — your script will report no issues even if everything expires tomorrow.

Connect-MgGraph -Scopes "Application.Read.All"

$thresholdDays = 60
$threshold = (Get-Date).AddDays($thresholdDays)
$today = Get-Date

$apps = Get-MgApplication -All -Property "id,displayName,appId,passwordCredentials,keyCredentials"

$report = foreach ($app in $apps) {
    foreach ($secret in $app.PasswordCredentials) {
        if ($secret.EndDateTime -and $secret.EndDateTime -lt $threshold) {
            [PSCustomObject]@{
                AppName    = $app.DisplayName
                AppId      = $app.AppId
                Type       = "ClientSecret"
                SecretName = $secret.DisplayName
                ExpiryDate = $secret.EndDateTime.ToString("yyyy-MM-dd")
                DaysLeft   = [math]::Round(($secret.EndDateTime - $today).TotalDays)
                Status     = if ($secret.EndDateTime -lt $today) { "EXPIRED" } else { "EXPIRING" }
            }
        }
    }
    foreach ($cert in $app.KeyCredentials) {
        if ($cert.EndDateTime -and $cert.EndDateTime -lt $threshold) {
            [PSCustomObject]@{
                AppName    = $app.DisplayName
                AppId      = $app.AppId
                Type       = "Certificate"
                SecretName = $cert.DisplayName
                ExpiryDate = $cert.EndDateTime.ToString("yyyy-MM-dd")
                DaysLeft   = [math]::Round(($cert.EndDateTime - $today).TotalDays)
                Status     = if ($cert.EndDateTime -lt $today) { "EXPIRED" } else { "EXPIRING" }
            }
        }
    }
}

if ($report) {
    $report | Sort-Object DaysLeft | Format-Table -AutoSize
    Write-Host "Total: $($report.Count) expiring credentials found"
} else {
    Write-Host "No credentials expiring within $thresholdDays days."
}

Step 3: Audit SAML Certificates on Enterprise Applications

App Registrations are only half the picture. Enterprise Applications — particularly SAML integrations with HR systems, identity providers, or SaaS platforms — store their certificates as Service Principal objects in Entra ID, separate from App Registrations. You will not find them with Get-MgApplication.

Connect-MgGraph -Scopes "Application.Read.All"

$threshold = (Get-Date).AddDays(60)
$today = Get-Date

$sps = Get-MgServicePrincipal -All `
    -Property "id,displayName,appId,keyCredentials,passwordCredentials" `
    -Filter "servicePrincipalType eq 'Application'"

$report = foreach ($sp in $sps) {
    foreach ($cert in $sp.KeyCredentials) {
        if ($cert.EndDateTime -and $cert.EndDateTime -lt $threshold) {
            [PSCustomObject]@{
                AppName    = $sp.DisplayName
                AppId      = $sp.AppId
                Type       = "ServicePrincipal-Cert"
                ExpiryDate = $cert.EndDateTime.ToString("yyyy-MM-dd")
                DaysLeft   = [math]::Round(($cert.EndDateTime - $today).TotalDays)
                Status     = if ($cert.EndDateTime -lt $today) { "EXPIRED" } else { "EXPIRING" }
            }
        }
    }
}

$report | Sort-Object DaysLeft | Format-Table -AutoSize

Step 4: Safe Secret Rotation Without Downtime

The rotation sequence matters. Reversing steps 2 and 3 causes application downtime.

Connect-MgGraph -Scopes "Application.ReadWrite.All"

$appId = "YOUR-APP-ID-HERE"  # Use appId, not objectId

$app = Get-MgApplication -Filter "appId eq '$appId'"
Write-Host "App: $($app.DisplayName) | Current secrets: $($app.PasswordCredentials.Count)/48"

# Step 1: Create the new secret
$endDate = (Get-Date).AddMonths(6)
$newSecret = Add-MgApplicationPassword -ApplicationId $app.Id `
    -PasswordCredential @{
        DisplayName = "Rotated-$(Get-Date -Format 'yyyy-MM-dd')"
        EndDateTime = $endDate
    }

# CRITICAL: The secret value is only visible once — save it immediately
Write-Host "=== SAVE THIS NOW ==="
Write-Host "New secret value: $($newSecret.SecretText)"
Write-Host "Key ID: $($newSecret.KeyId)"
Write-Host "Expires: $($newSecret.EndDateTime)"
Write-Host "====================="

# Step 2: Update your application configuration with the new secret
# Step 3: Verify the application works (check Entra sign-in logs for new Key ID)
# Step 4: Wait at least 30 minutes
# Step 5: Remove the old secret:
# Remove-MgApplicationPassword -ApplicationId $app.Id -KeyId "OLD-KEY-ID"

Step 5: Enforce Credential Policies for New Applications

Application Management Policy restricts credential types and lifetimes for App Registrations created after a specified date:

Connect-MgGraph -Scopes 'Policy.ReadWrite.All'
Import-Module Microsoft.Graph.Identity.SignIns

$params = @{
    isEnabled = $true
    applicationRestrictions = @{
        keyCredentials = @(
            @{
                restrictionType = "asymmetricKeyLifetime"
                state           = "enabled"
                maxLifetime     = "P180D"  # ISO 8601 duration: 180 days
                restrictForAppsCreatedAfterDateTime = [DateTime]::Parse("2025-01-01T00:00:00Z")
            }
        )
    }
}

Update-MgPolicyDefaultAppManagementPolicy -BodyParameter $params
Write-Host "Policy applied. New App Registrations after 2025-01-01 cannot create certificates with lifetime > 180 days."

Step 6: Automate Notifications via Logic Apps

For environments where weekly manual checks are insufficient, a Logic App with a Recurrence trigger (every Monday at 8 AM) can query Graph API and send a consolidated email report:

https://graph.microsoft.com/v1.0/applications?$select=id,appId,displayName,passwordCredentials

Key implementation details:

  • Create a dedicated App Registration with Application.Read.All (application permission, not delegated)
  • Enable pagination in the HTTP action — the default Graph API page size is 100-999 records. Tenants with 1000+ App Registrations require @odata.nextLink handling
  • Filter endDateTime < today + 60 days in the For Each loop
  • Send one consolidated email with a table instead of one email per expiring secret

Common Pitfalls and Edge Cases

Pitfall 1: Year 2299 Secrets Still Lurking in Old Tenants

In tenants created before 2021, you will find App Registrations with expiry dates showing 12/31/2299. These work fine today. The problem: they are credentials that have never been rotated, possibly created by developers who left years ago, used by applications no one documented. Find them:

Connect-MgGraph -Scopes "Application.Read.All"
$legacyDate = Get-Date "2200-01-01"

Get-MgApplication -All -Property "displayName,appId,passwordCredentials" |
    ForEach-Object {
        $app = $_
        $app.PasswordCredentials | Where-Object { $_.EndDateTime -gt $legacyDate } |
        ForEach-Object {
            [PSCustomObject]@{
                App        = $app.DisplayName
                AppId      = $app.AppId
                SecretName = $_.DisplayName
                FakeExpiry = $_.EndDateTime
            }
        }
    }

For each result: check if the application is active in sign-in logs, then either plan rotation or schedule the registration for decommissioning.

Pitfall 2: The 48-Secret Limit Hits Without Warning

Each App Registration supports a maximum of 48 password credentials. If you add a new secret each rotation cycle but leave old ones in place, you will eventually hit this wall. The next rotation attempt fails with InvalidKeyCredentialsCount. There is no warning as you approach the limit — you find out when the next rotation fails.

The script in Step 4 displays the current count ($app.PasswordCredentials.Count/48). Every rotation cycle must end with removing the old credential.

Pitfall 3: Secret Value Visible Only Once — Store It Immediately

A client secret value is readable only at creation time — both via portal and API. Subsequent reads return null. This creates a common failure pattern: "I cannot find where the old secret is stored, I will create a new one" — which does not solve the problem and counts toward the 48 limit.

The only safe approach is Azure Key Vault. Your rotation script should save the new value to Key Vault before the session ends:

# Immediately after Add-MgApplicationPassword — push to Key Vault
$kvName     = "your-keyvault-name"
$secretName = "$($app.DisplayName)-appsecret"
$secureVal  = ConvertTo-SecureString $newSecret.SecretText -AsPlainText -Force
Set-AzKeyVaultSecret -VaultName $kvName -Name $secretName -SecretValue $secureVal
Write-Host "Secret stored in Key Vault: $kvName/$secretName"

Pitfall 4: Propagation Delay Creates a Rotation Window

After adding a new secret and updating your application configuration, there is a propagation delay — Microsoft documents up to 15 minutes before new credentials are fully active across all datacenters. If you delete the old secret too quickly after adding the new one, you can hit a window where neither credential works — an outage during a planned no-downtime rotation.

Safe strategy: after verifying the new secret is working by confirming the new Key ID appears in Entra sign-in logs, keep both secrets active for at least 30-60 minutes before removing the old one.

Pitfall 5: Application Management Policy Is Prospective, Not Retroactive

The restrictForAppsCreatedAfterDateTime parameter in Application Management Policy means exactly what it says — the policy applies only to applications created after that date. Existing App Registrations created before that date can still add new credentials without any restrictions. The policy does not retroactively enforce limits on historical registrations.

This means the policy builds good habits going forward but does not replace the audit work of inventorying existing registrations.

Pitfall 6: Multi-Tenant Applications Add Complexity

If your App Registration is configured for multi-tenant sign-in (AzureADMultipleOrgs), Service Principals representing your application may exist in external tenants. Credentials attached to those external service principals expire independently from your home tenant App Registration credentials. You can only monitor and manage credentials in your own tenant — external tenants are out of scope for your audit scripts.

The Long-Term Solution: Managed Identities

The best credential management story is no credential management at all. For applications running on Azure infrastructure — Azure Functions, App Service, Automation Runbooks, Virtual Machines, Logic Apps — Managed Identity eliminates the problem entirely. The application authenticates to Graph API and other Azure resources without any secret or certificate. Microsoft handles credential rotation automatically in the background, with no administrator involvement.

If you have App Registrations being used by Azure-hosted workloads, migrating to Managed Identity should be on your roadmap. For applications running outside Azure (on-premises servers, other cloud providers, third-party SaaS), client secrets or certificates remain the only option — that is where the PowerShell audit from Step 2 is non-negotiable.

Summary

  • Entra ID sends no email when a credential expires. Entra Recommendations warns 30 days in advance only if you actively check the portal — too short a lead time for proper change management.
  • Get-MgApplication requires explicit -Property passwordCredentials,keyCredentials — without it, the fields are empty and your script reports no issues even if everything expires tomorrow.
  • Service Principals are separate — SAML certificates and Enterprise Application credentials are audited via Get-MgServicePrincipal, not Get-MgApplication.
  • Rotation has a mandatory sequence: create new secret → update app config → verify in sign-in logs → wait 30-60 min → delete old.
  • The 48-secret limit has no warning — clean up old credentials after every rotation cycle.
  • Application Management Policy is forward-looking only — it restricts new apps, not existing ones, and does not replace a historical credential audit.
  • Target state: Managed Identity for Azure-hosted workloads eliminates the problem entirely.