Security & identity

Report expiring Entra app secrets and certificates with Graph PowerShell

Build a Microsoft Graph PowerShell report of every client secret and certificate on Entra app registrations and service principals, find which ones are in use, and rotate them before they expire.

10 min read
On this page

To find expiring Entra app secrets and certificates, connect to Microsoft Graph PowerShell with Application.Read.All, pull every application and service principal with Get-MgApplication -All and Get-MgServicePrincipal -All selecting PasswordCredentials and KeyCredentials, and compare each credential's EndDateTime with today. Export anything that has expired or expires within your rotation window, check the sign-in logs to see which key is actually in use, then add the replacement, update the workload, and remove the old credential.

Who this is for and what you will have

This guide is for Microsoft Entra administrators and platform engineers who own app registrations used by scripts, integrations and line-of-business systems. An expired secret stops a daemon from getting tokens, usually with AADSTS7000222, and a forgotten secret that never expires is a long-lived key an attacker can reuse.

At the end you will have:

  • A CSV of every secret and certificate on application objects and service principals, with days left.
  • A way to see which credential (by key ID) an app actually signs in with.
  • A safe rotation procedure that avoids downtime.
  • A tenant policy that limits how long new secrets and certificates can live.

If you're tightening app security more broadly, pair this with controlling which third-party apps users can approve, covered in stopping illicit consent grants.

Where app credentials live

Microsoft Graph stores credentials in two collections, on two kinds of object.

CollectionGraph typeTypical contentUseful properties
passwordCredentialspasswordCredentialClient secretskeyId, displayName, endDateTime, hint
keyCredentialskeyCredentialCertificates (public key)keyId, displayName, endDateTime, type, usage, customKeyIdentifier

Both collections exist on the application object (what you see under App registrations) and on the service principal (what you see under Enterprise apps). Apps you register in your own tenant normally hold credentials on the application object. SAML signing certificates for enterprise apps live on the service principal, which is why a report that only looks at app registrations misses them.

A few details from the Graph reference that matter for reporting:

  • endDateTime is always in UTC.
  • secretText is only returned when the secret is created. You can't recover it later.
  • hint contains the first three characters of a secret, which helps you match a secret to the value stored in a key vault or config file.
  • For a certificate, customKeyIdentifier defaults to the thumbprint, and the raw key is only returned on single-object requests with $select.

Prerequisites

  • The Microsoft Graph PowerShell SDK (Microsoft.Graph.Applications), plus Microsoft.Graph.Beta.Reports for the sign-in step.
  • Delegated consent for Application.Read.All to read apps and service principals.
  • AuditLog.Read.All to read sign-in logs. Microsoft notes that downloading sign-in logs through Microsoft Graph requires a Microsoft Entra ID P1 or P2 license.
  • Application.ReadWrite.All (or ownership with Application.ReadWrite.OwnedBy for app-only use) when you rotate secrets.

Step 1: Report app registration credentials

The script reads every application with only the properties it needs, flattens each secret and certificate into a row, and calculates days remaining.

Connect-MgGraph -Scopes "Application.Read.All"
 
$windowDays = 60
$nowUtc     = [datetime]::UtcNow
 
function ConvertTo-CredentialRow {
    param($Owner, $ObjectType, $Credential, $Kind)
    $daysLeft = [math]::Floor(($Credential.EndDateTime - $nowUtc).TotalDays)
    [pscustomobject]@{
        ObjectType  = $ObjectType
        DisplayName = $Owner.DisplayName
        AppId       = $Owner.AppId
        ObjectId    = $Owner.Id
        Kind        = $Kind
        Name        = $Credential.DisplayName
        KeyId       = $Credential.KeyId
        EndUtc      = $Credential.EndDateTime
        DaysLeft    = $daysLeft
        Status      = if ($daysLeft -lt 0) { 'Expired' } elseif ($daysLeft -le $windowDays) { 'Expiring' } else { 'OK' }
    }
}
 
$apps = Get-MgApplication -All -Property Id, AppId, DisplayName, PasswordCredentials, KeyCredentials
 
$appRows = foreach ($app in $apps) {
    foreach ($s in $app.PasswordCredentials) { ConvertTo-CredentialRow $app 'Application' $s 'Secret' }
    foreach ($c in $app.KeyCredentials)      { ConvertTo-CredentialRow $app 'Application' $c "Certificate ($($c.Usage))" }
}
 
$appRows | Sort-Object DaysLeft | Format-Table DisplayName, Kind, Name, EndUtc, DaysLeft, Status

Pick a window that matches how long your change process takes. Microsoft's built-in recommendation uses 30 days; 60 leaves room for a change request and a test cycle.

Step 2: Add service principal credentials

Run the same function against service principals, keeping only those that actually hold credentials.

$sps = Get-MgServicePrincipal -All -Property Id, AppId, DisplayName, PasswordCredentials, KeyCredentials |
    Where-Object { $_.PasswordCredentials.Count -gt 0 -or $_.KeyCredentials.Count -gt 0 }
 
$spRows = foreach ($sp in $sps) {
    foreach ($s in $sp.PasswordCredentials) { ConvertTo-CredentialRow $sp 'ServicePrincipal' $s 'Secret' }
    foreach ($c in $sp.KeyCredentials)      { ConvertTo-CredentialRow $sp 'ServicePrincipal' $c "Certificate ($($c.Usage))" }
}
 
$all = @($appRows) + @($spRows)
$all | Where-Object Status -ne 'OK' |
    Sort-Object DaysLeft |
    Export-Csv ".\app-credentials-$(Get-Date -Format yyyyMMdd).csv" -NoTypeInformation

Secrets on service principals are worth a second look even when they aren't expiring. Microsoft recommends changing services so they use credentials on the backing application object rather than the service principal.

When you review the CSV, look for:

  • Expired credentials still present. They no longer authenticate but clutter the inventory. Confirm nothing depends on them and remove them.
  • Apps with several overlapping secrets. Often a sign that rotations happened without the old secret being removed.
  • Secrets that expire far in the future. Anything past 12 months is outside Microsoft's recommendation.
  • Apps with no owner you can identify. Find an owner before the credential expires, not after.

Step 3: Cross-check with Entra recommendations

Microsoft Entra produces two recommendations that cover the same ground:

RecommendationAPI nameWhat triggers it
Renew expiring application credentialsapplicationCredentialExpiryA credential on an application registration expiring within the next 30 days
Renew expiring service principal credentialsservicePrincipalKeyExpiryA credential on a service principal expiring within the next 30 days

Find them in the Microsoft Entra admin center under Entra ID > Overview > Recommendations. Security Reader, Global Reader and Reports Reader can view them; Security Administrator can update them. Credentials that have already lapsed show as completed in the Impacted resources list, so the recommendation is not a substitute for a report that also catches expired items. The sample responses in Microsoft's documentation list microsoftEntraWorkloadId in the requiredLicenses field, so don't depend on the recommendations alone; the script works regardless of licensing.

Step 4: Find out which credential is in use

Before you remove anything, confirm which key the app presents when it signs in. Service principal sign-ins are recorded in the service principal sign-in log, separately from user and managed identity sign-ins. In the beta signIn resource, clientCredentialType shows whether the app used a clientSecret, certificate, federatedIdentityCredential or another type, and servicePrincipalCredentialKeyId and servicePrincipalCredentialThumbprint identify the exact credential.

Connect-MgGraph -Scopes "AuditLog.Read.All"
 
$appId = "<application (client) ID>"
Get-MgBetaAuditLogSignIn -Filter "signInEventTypes/any(t: t eq 'servicePrincipal') and appId eq '$appId'" -Top 50 |
    Select-Object CreatedDateTime, ServicePrincipalName, ClientCredentialType,
                  ServicePrincipalCredentialKeyId, ServicePrincipalCredentialThumbprint |
    Sort-Object CreatedDateTime -Descending

Match the key ID with the KeyId column in your report. A credential with no recent sign-ins is a removal candidate; a credential that is about to expire and shows sign-ins every hour is your priority.

Step 5: Rotate without downtime

The pattern is always add, switch, verify, remove. Entra allows several credentials on one app at the same time, which is what makes overlap possible.

Rotate a client secret

Connect-MgGraph -Scopes "Application.ReadWrite.All"
 
$appObjectId = "<application object ID>"
$newSecret = Add-MgApplicationPassword -ApplicationId $appObjectId -PasswordCredential @{
    displayName = "rotated-$(Get-Date -Format yyyy-MM)"
    endDateTime = (Get-Date).AddMonths(6)
}
 
# SecretText is only available now - store it in your vault immediately
$newSecret | Select-Object KeyId, Hint, EndDateTime

Update the workload's configuration or key vault entry with $newSecret.SecretText, restart or redeploy it, then run the sign-in query from Step 4 and confirm the new KeyId appears. Only then remove the old secret:

Remove-MgApplicationPassword -ApplicationId $appObjectId -KeyId "<old secret keyId>"

Note that -ApplicationId takes the object ID of the application, not the application (client) ID.

Rotate a certificate

In the Microsoft Entra admin center, open the app under App registrations, select Certificates & secrets > Certificates > Upload certificate, and upload the new public key as a .cer, .pem or .crt file. Record the thumbprint, deploy the private key to the workload, confirm servicePrincipalCredentialThumbprint in the sign-in log matches the new certificate, and delete the old one. Microsoft recommends using Azure Key Vault to manage certificate access and lifetime in production.

Rotate a SAML signing certificate

For enterprise apps using SAML, open Single sign-on, edit SAML signing certificate, add a new certificate, make it active, verify the application with the relying party, and remove the inactive certificate.

Step 6: Limit how long new credentials can live

Reporting catches problems; policy prevents them. App management policies can restrict new credentials on applications and service principals. Microsoft's Workload ID FAQ lists application management policies as available in the free edition, not only in Workload ID Premium. In the Microsoft Entra admin center, browse to Entra ID > Enterprise apps > Application policies. The relevant settings are:

Admin center settingUnderlying restrictionEffect
Block password additionpasswordAddition and symmetricKeyAdditionNo new client secrets or symmetric keys
Restrict max password lifetimepasswordLifetime and symmetricKeyLifetimeCaps the lifetime of new secrets
Restrict max certificate lifetimeasymmetricKeyLifetimeCaps the lifetime of new certificates
Block custom passwordscustomPasswordAdditionBlocks client-supplied (non-Entra-generated) secrets

Configuring these needs Security Administrator plus Cloud Application Administrator or Application Administrator, or Global Administrator. Microsoft's tutorial recommends blocking secrets where you can and limiting certificates to 180 days, using restrictForAppsCreatedAfterDateTime to phase restrictions in for newer apps first. Policies don't block token issuance for existing apps; only the create or update operation that violates the policy fails, so you can apply them without breaking running workloads.

Where the workload runs in GitHub Actions, Kubernetes or Azure, a federated identity credential or managed identity removes the secret entirely, which is the best answer to expiry.

Run the report on a schedule

For a weekly job, register a dedicated app with the Application.Read.All application permission and a certificate credential, then connect app-only:

Connect-MgGraph -ClientId "<report app client ID>" -TenantId "<tenant ID>" -CertificateThumbprint "<thumbprint>"

The certificate must be in Cert:\CurrentUser\My or Cert:\LocalMachine\My on the machine running the job. Include the report app's own certificate in the output so the job warns you before it locks itself out.

Troubleshooting

AADSTS7000222: The provided client secret keys are expired. The app is presenting a secret whose endDateTime has passed. Create a new secret, update the workload with its value, and then remove the expired one.

AADSTS7000215: Invalid client secret is provided. The value the app sends doesn't match a valid secret on the app registration, for example because it belongs to a secret that was already removed. Compare its first three characters with the hint property of each secret in your report.

The secret value isn't shown in the portal any more. Microsoft only displays the value when it's created. If it wasn't saved, create a new one.

Adding a secret fails after you enabled an app management policy. That's the policy working. Use a certificate, or grant a scoped exception under Application policies while you migrate the workload.

The report is empty for an enterprise app. Its credentials are on the service principal. Make sure Step 2 ran.

Checklist

  • Weekly report covering both application objects and service principals.
  • Owners identified for every app with an expiring credential.
  • Sign-in log check before removing any credential.
  • Add, switch, verify, remove for every rotation.
  • No secret lifetime over 12 months; certificates preferred.
  • Application policies limiting new secret and certificate lifetimes.
  • Federated credentials or managed identities used wherever the platform supports them.

References

Questions people ask

How long can a client secret on an Entra app registration last?

Microsoft limits client secret lifetime to two years (24 months) or less, and you can't set a custom lifetime longer than 24 months. Microsoft recommends an expiration of less than 12 months, and recommends certificates or federated credentials instead of secrets in production.

Can I read the value of an existing client secret with Graph?

No. The secretText property is only returned in the response to the addPassword request that created the secret. After that, Graph returns only metadata such as keyId, displayName, endDateTime and hint, which holds the first three characters.

Why don't I see the SAML signing certificate on the app registration?

SAML signing certificates for enterprise apps are held on the service principal, not the application object. Microsoft's separate servicePrincipalKeyExpiry recommendation covers them, and you need to query the keyCredentials and passwordCredentials of service principals to report on them.

Does an app management policy break apps that already have long-lived secrets?

No. Microsoft states that neither the tenant default policy nor app management policies block token issuance for existing applications. Only the create or update operation that violates the policy is blocked, so existing credentials keep working until they expire.

Microsoft Entra IDMicrosoft Graph PowerShellApp registrationsWorkload identities
  1. A Microsoft 365 tenant security baseline you can apply in a day

    Harden a new or existing Microsoft 365 tenant in one working day: emergency access, admin roles, MFA and legacy auth, app consent, email protection, external forwarding, audit logging and Secure Score.

  2. AADSTS50076, 50079 and 50158: fix Microsoft Entra MFA sign-in errors

    What AADSTS50076, AADSTS50079 and AADSTS50158 mean, how to find the policy that demanded MFA in the Entra sign-in logs, and how to fix each one for users, scripts and federated domains.

  3. AADSTS53003 blocked by Conditional Access: find the policy and fix it

    Troubleshoot AADSTS53003 in Microsoft Entra ID: trace the correlation ID to the sign-in log, identify the blocking Conditional Access policy, and fix the user, device or policy without weakening security.