Three Things Nobody Audited When a Coding Agent Made a Client Secret at 2 a.m.

AI coding assistants are auto-generating Entra app registrations with client secrets at 2 a.m. Here's how to audit what already exists and why most admins have a blind spot.

A junior dev asks Copilot to “set up Azure AD auth for this service.” Ninety seconds later there’s a working app registration, a client secret, and a .env file that will be committed by lunch. Nobody approved it. Nobody’s tracking it. And it authenticates against your tenant exactly as well as anything you did approve.

That’s the pattern Arturo Lucatero, PM for Entra Agent ID, laid out in an interview on Entra.news this week. The tools doing it aren’t exotic — GitHub Copilot, Cursor, and the rest of the coding-agent crowd. Ask them to wire up authentication and they reach for the client-credentials flow because it’s the one that works without a human clicking a consent screen. A secret is the path of least resistance, so a secret is what you get.

Let me pre-empt the colleague already drafting the angry reply.

“This is a non-story. These are dev/test apps. Half of them never leave localhost.”

Mostly true, and I’m not going to pretend an unused scaffold app is a breach. But a client secret doesn’t know it’s dev/test. It’s a bearer credential sitting in your production directory with whatever Graph scopes the agent cheerfully requested — sometimes more than the task needed, because the agent optimised for “make it work,” not least privilege. When it ends up in Git history, a leaked laptop, or a CI log, the blast radius is a function of the permissions, not the developer’s intent.

“Fine. But I’d see it. Risky app registrations show up in reports.”

Would you? Nothing here trips a sign-in risk alert. It’s a legitimate app, created by a legitimate user with the default ability to register applications, doing a legitimate OAuth flow. The default Users can register applications setting means any member can do this without a Global Admin ever seeing it. That’s the blind spot — not malice, just volume moving faster than governance.

So before the architecture debate, do the inventory. Here’s a read-only pass for apps created in the last 30 days that carry a secret. It paginates, handles throttling, and changes nothing:

audit.ps1PowerShell
Connect-MgGraph -Scopes 'Application.Read.All','AuditLog.Read.All' -NoWelcome
​
$cutoff = (Get-Date).AddDays(-30).ToString('yyyy-MM-ddTHH:mm:ssZ')
$results = [System.Collections.Generic.List[object]]::new()
​
try {
    $apps = Get-MgApplication -All -Filter "createdDateTime ge $cutoff" `
        -Property Id,AppId,DisplayName,CreatedDateTime,PasswordCredentials,SignInAudience `
        -ErrorAction Stop
​
    foreach ($app in $apps) {
        if ($app.PasswordCredentials.Count -gt 0) {
            $results.Add([pscustomobject]@{
                DisplayName = $app.DisplayName
                AppId       = $app.AppId
                Created     = $app.CreatedDateTime
                SecretCount = $app.PasswordCredentials.Count
                SecretsExpire = ($app.PasswordCredentials.EndDateTime -join '; ')
                Audience    = $app.SignInAudience
            })
        }
    }
}
catch {
    Write-Error "Graph query failed: $($_.Exception.Message)"
}
​
$results | Sort-Object Created -Descending | Format-Table -AutoSize
$results | Export-Csv .\recent-apps-with-secrets.csv -NoTypeInformation

Widen the window to 90 days by changing AddDays(-30). Cross-reference the CSV with your known app owners — Get-MgApplicationOwner tells you who to go ask. Review the list by hand. Do not pipe it straight into a removal loop; orphaned-looking apps have a way of being load-bearing in some pipeline you’ve never heard of.

Only once you’ve reviewed it should anything change state. Remove a stray secret with a dry run first:

audit.ps1PowerShell
# Operates on ONE reviewed AppId. -WhatIf by default.
$target = Get-MgApplication -Filter "appId eq '00000000-0000-0000-0000-000000000000'"
foreach ($cred in $target.PasswordCredentials) {
    Remove-MgApplicationPassword -ApplicationId $target.Id `
        -KeyId $cred.KeyId -WhatIf
}

Drop -WhatIf only after the output matches what you expect. Rotate what’s genuinely in use; delete what’s genuinely dead.

The real fix is upstream. Stop handing out long-lived secrets at all. Workload Identity Federation lets an app trust an external token issuer — GitHub Actions, a Kubernetes service account — so CI authenticates with a short-lived federated token and there’s no secret to leak. For agents acting on their own, Entra Agent ID gives them first-class, trackable identities instead of a human’s app registration with a password stapled on. Neither is a magic wand, but both move the credential out of the .env file and into something you can actually see.

And tighten the tap: set Users can register applications to No in Entra admin center → Identity → Users → User settings, route registrations through a request flow, and consider a Conditional Access policy or app-management policy that caps or bans password credentials on new apps.

The agent isn’t the problem. The two-year secret it leaves behind — the one you’ll find during an incident, not an audit — is.