Use Azure PIM with groups in ASP.NET Core

Standing administrator access is one of the easiest things to get wrong in an Azure environment. When a person holds permanent membership in a privileged group, that membership becomes a target the moment their account is compromised. Azure Privileged Identity Management (PIM) for Groups removes that standing access and replaces it with time bound, justified activation. This article walks through combining PIM for Groups with a Conditional Access authentication context to force phishing resistant sign in during elevation, applied to an ASP.NET Core application.

The setup covers an ASP.NET Core app that is reachable only by members of a specific Azure AD security group. That group is managed through PIM, so nobody sits in it permanently. Anyone requesting access has to activate their membership, supply a reason, and authenticate with a phishing resistant method before the elevation is granted.

Creating the Conditional Access authentication context

The first piece is a Conditional Access authentication context, a tag attached to specific operations rather than an entire application. Azure AD allows a fixed number of these contexts, usually labelled c1 through c25, each given a friendly name. Here the context represents the act of activating membership in the PIM group, not just the act of signing in to the app itself.

Once the context exists, it gets referenced in a Conditional Access policy that targets it and requires phishing resistant authentication, for example a FIDO2 security key or Windows Hello for Business. A password plus an SMS code does not satisfy this policy. This is what stops someone from elevating their own access with a weak second factor, even if their everyday sign in relies on something less strong.

Conditional Access authentication context configured for PIM activation
Conditional Access authentication context configured for PIM activation

Onboarding the security group to PIM

With the authentication context in place, the Azure AD security group gets onboarded into PIM for Groups. This turns a normal group into one where membership and ownership are managed as eligible assignments instead of permanent ones. The authentication context created earlier is attached to the activation settings for the group, so anyone activating membership is routed through the phishing resistant Conditional Access policy.

A common mistake happens right here. Teams onboard the group to PIM but forget to wire the authentication context into the activation settings, which leaves the elevation step protected by nothing more than the user’s regular sign in method. The point of layering PIM with Conditional Access is lost if that link is skipped, so it is worth checking the activation settings tab directly rather than assuming the context carried over automatically.

Security group onboarded to PIM for Groups with the authentication context applied
Security group onboarded to PIM for Groups with the authentication context applied

Adding eligible user assignments

Users are added to the group as eligible members rather than active ones. Each assignment can carry a justification requirement, so when someone activates, they have to type a reason, and that reason is included in the notification email sent to the group owners. This gives an audit trail without needing a separate logging system bolted on afterward.

It is worth setting a maximum activation duration that matches how long the work actually takes, rather than leaving the default in place. A four hour window is usually enough for a maintenance task, and a shorter window reduces the time an attacker has if a session token is somehow captured after activation.

Eligible user assignment added to the PIM managed group with a justification requirement
Eligible user assignment added to the PIM managed group with a justification requirement

Locking the Enterprise Application to assigned users only

The Enterprise Application backing the ASP.NET Core app needs its Assignment Required setting turned on. Without this, anyone in the tenant with a valid account can sign in to the app regardless of PIM group membership, which defeats the purpose of everything configured so far. Assignment Required forces Azure AD to check that the signed in user, or a group they belong to, is explicitly assigned to the application before it issues a token.

Enterprise Application configured with Assignment Required set to Yes
Enterprise Application configured with Assignment Required set to Yes

The PIM managed security group is then added under Users and groups on the Enterprise Application. Because membership in that group is only ever active during a PIM elevation window, a user shows up as assigned to the application only while their elevation is live. Once the activation expires, Azure AD stops treating them as assigned, and their next sign in attempt is rejected.

PIM managed security group added under Users and groups on the Enterprise Application
PIM managed security group added under Users and groups on the Enterprise Application

Configuring the ASP.NET Core app as a confidential client

The application authenticates using an App Registration with a client secret, which makes it a confidential client rather than a public one. This distinction matters for anything tied to administrative access. A public client, such as a single page app relying on the authorization code flow with PKCE alone, cannot reliably prove its own identity to Azure AD, and using one for an admin surface would weaken the guarantees PIM and Conditional Access are meant to provide.

Microsoft.Identity.Web handles the OpenID Connect code flow with PKCE on top of this confidential client setup. The registration below wires up authentication, enables token acquisition for calling a downstream API, and adds a distributed token cache so tokens survive across instances if the app scales out.

builder.Services.AddAuthentication(
        OpenIdConnectDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApp(
        builder.Configuration.GetSection("AzureAd"))
    .EnableTokenAcquisitionToCallDownstreamApi()
    .AddDistributedTokenCaches();
 
builder.Services.AddRazorPages()
    .AddMicrosoftIdentityUI();

AddMicrosoftIdentityWebApp reads the AzureAd section from configuration, which should include the tenant ID, client ID, and client secret, ideally pulled from a key vault rather than checked into source control. EnableTokenAcquisitionToCallDownstreamApi is needed only if the app calls Microsoft Graph or another protected API on the user’s behalf after sign in, so skip it if the app just needs authentication with no follow up API calls. AddDistributedTokenCaches without a configured backing store, such as Redis or SQL Server, falls back to in memory caching, which does not survive an app restart and breaks token refresh in a multi instance deployment, so that backing store needs to be configured separately in production.

What the user actually experiences

When an administrator assigns someone to the group, Azure AD sends that person an email notification. From there, the user opens the application, gets redirected into the PIM activation flow if their membership is not currently active, provides a justification, completes the phishing resistant authentication challenge, and lands back in the application with access for the configured duration.

Email notification sent to the user once assigned to the PIM managed group
Email notification sent to the user once assigned to the PIM managed group

Nothing about this flow requires custom code in the ASP.NET Core app itself. The enforcement happens entirely in Azure AD and Conditional Access before a token is ever issued, which keeps the application code simple and keeps the security logic in a place where it can be audited and changed centrally instead of scattered across app configuration.

Licensing, trade-offs, and when to use roles instead

PIM requires an Azure AD Premium P2 license for every user in scope, which is worth budgeting for before committing to this pattern at scale. It is not something that can be quietly enabled for a handful of admins sitting on lower tier licenses.

PIM also supports Azure AD built in and custom roles directly, not just groups, so it is fair to ask why this setup uses a group at all. Groups work better when the same set of people needs coordinated, time bound access to multiple resources at once, since one activation event covers everything the group is tied to. Roles are the better fit when managing a single, well defined administrative permission, such as Global Administrator or a custom RBAC role scoped to one resource, because the built in role activation flow already handles that case without needing a wrapper group.

The broader lesson here is to follow the KISS principle rather than turning on every available feature at once. Conditional Access authentication contexts, PIM for groups, phishing resistant policies, and Assignment Required can all be combined, but each piece added has to be maintained and understood by whoever inherits this configuration later. Enable what the threat model actually calls for, and skip the rest.

Leave a Reply

Discover more from Behind the Stack

Subscribe now to keep reading and get access to the full archive.

Continue reading