{"id":8086,"date":"2026-10-02T19:21:22","date_gmt":"2026-10-02T17:21:22","guid":{"rendered":"https:\/\/blog.redbaronofazure.com\/?p=8086"},"modified":"2026-10-02T19:30:51","modified_gmt":"2026-10-02T17:30:51","slug":"step-up-mfa-with-entra-authentication-context","status":"publish","type":"post","link":"https:\/\/blog.redbaronofazure.com\/?p=8086","title":{"rendered":"Step-up MFA with Entra Authentication Context"},"content":{"rendered":"\n<p>If your app does many things that have a normal security level but does one thing that is very sensitive, like a payment transfer, then the simplest option is to require MFA when you sign into the app. However, if the sensitive operation is only carried out by some users, and also infrequently, the high security bar for everyone all the time isn&#8217;t going to make you the popular guy at the company&#8217;s X-mas party.<\/p>\n\n\n\n<p>There is an option to get your dance card full at the X-mas party, and that is to use <strong>Entra Authentication Context<\/strong> together with Conditional Access policies. It will be a just-in-time step-up MFA when the sensitive operation is carried out. Using Authentication context requires that the code base is built for it. It is not something you just can enforce by configuring it in the Entra portal.<\/p>\n\n\n\n<h2>Authentication Context sequence diagram<\/h2>\n\n\n\n<p>Below is a sequence diagram describing what is happening when Authentication Context comes into play. Everything is normal until step 4 where the webapp makes a call to the webapi for the sensitive operation. In step 5, the webapi checks if conditions for executing the sensitive operation is met, and when it is not, the webapi responds with a 401 Unauthorized. But it doesn&#8217;t stop there, because it also tells the webapp why it is unauthorized and what the webapp has to do to resolve the situation as it responds with &#8220;you are missing the value cX in the acr claim&#8221;. This &#8220;unauthorized dance&#8221; is OIDC protocol standard and in the Entra case, acquiring the cX value in the acr claim is performed by a Conditional Access policy. The dance continues between step 5 through 14 until the webapp has an access token that is acceptable to perform the sensitive operation, which is carried out in step 15.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full is-resized\"><img loading=\"lazy\" src=\"https:\/\/blog.redbaronofazure.com\/wp-content\/uploads\/2026\/10\/entra_authentication_context_flow_small.jpg\" alt=\"\" class=\"wp-image-8087\" width=\"560\" height=\"504\" srcset=\"https:\/\/blog.redbaronofazure.com\/wp-content\/uploads\/2026\/10\/entra_authentication_context_flow_small.jpg 747w, https:\/\/blog.redbaronofazure.com\/wp-content\/uploads\/2026\/10\/entra_authentication_context_flow_small-300x270.jpg 300w\" sizes=\"(max-width: 560px) 100vw, 560px\" \/><\/figure>\n\n\n\n<h2>Configuring Authentication Context and a Conditional Access policy to act on it<\/h2>\n\n\n\n<p>Creating an Authentication context is pretty unexciting. You give it a name (which has no real meaning more than distinguishing it in the portal) and select an available number between c1 and c99. This means that there can only be 99 different auth context in your enterprise, but that number is unlikely to run out, because how many different ways can you ask a user to perform MFA?<\/p>\n\n\n\n<figure class=\"wp-block-image size-full is-resized\"><img loading=\"lazy\" src=\"https:\/\/blog.redbaronofazure.com\/wp-content\/uploads\/2026\/10\/Screenshot-2026-10-02-170514.png\" alt=\"\" class=\"wp-image-8088\" width=\"419\" height=\"209\" srcset=\"https:\/\/blog.redbaronofazure.com\/wp-content\/uploads\/2026\/10\/Screenshot-2026-10-02-170514.png 559w, https:\/\/blog.redbaronofazure.com\/wp-content\/uploads\/2026\/10\/Screenshot-2026-10-02-170514-300x149.png 300w\" sizes=\"(max-width: 419px) 100vw, 419px\" \/><\/figure>\n\n\n\n<p>The auth context object in itself requires a Conditional Access policy using it to be meaningful, because it is the CA policy that will enforce the MFA required. When you create a new CA policy, in the Target section, select <em>Authentication context<\/em> to what it applies to and check your auth context. In the <em>Grant<\/em> section, you can configure your requirements, like phishing resistant MFA and a compliant device.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full is-resized\"><img loading=\"lazy\" src=\"https:\/\/blog.redbaronofazure.com\/wp-content\/uploads\/2026\/10\/Screenshot-2026-10-02-171008.png\" alt=\"\" class=\"wp-image-8089\" width=\"458\" height=\"243\" srcset=\"https:\/\/blog.redbaronofazure.com\/wp-content\/uploads\/2026\/10\/Screenshot-2026-10-02-171008.png 610w, https:\/\/blog.redbaronofazure.com\/wp-content\/uploads\/2026\/10\/Screenshot-2026-10-02-171008-300x159.png 300w\" sizes=\"(max-width: 458px) 100vw, 458px\" \/><\/figure>\n\n\n\n<h2>WebApi code implementation<\/h2>\n\n\n\n<p>The endpoint for the sensitive operation in the webapi needs to check that the authentication context requirement is met and return a 401 Unauthorized with a challange if not. In code, this looks like below.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"csharp\" class=\"language-csharp\">    \nprotected bool IsUserToken() {\r\n    if (HttpContext!.User == null) return false;\r\n    var scopeClaim = HttpContext.User.FindFirst(c => c.Type == ClaimConstants.Scope || c.Type == ClaimConstants.Scp);\r\n    if (scopeClaim == null) return false;\r\n    return true;\r\n}\r\nprotected bool HasRequiredAuthContext() {\r\n    return User.FindFirst(\"acrs\")?.Value == requiredAuthContextId;\r\n}\r\n\n[HttpGet(\"\/api\/sensitiveoperation\")]\r\n[Authorize(Policy = \"ScopeOrRolePolicy.Read\")]\r\n[Produces(\"application\/json\")]\r\npublic async Task&lt;ActionResult&lt;string>> Get() {\r\n    if ( IsUserToken() &amp;&amp; !HasRequiredAuthContext() ) {\r\n        return ReturnUnauthorizedAuthContext();\r\n    }\r\n    var resp = new { Message = \"Sensitive operation completed successfully.\", Timestamp = DateTime.UtcNow.ToString(\"o\") };\r\n    string content = JsonSerializer.Serialize(resp, CompactJsonOptions);\r\n    return new ContentResult { ContentType = \"application\/json\", Content = content, StatusCode = (int)HttpStatusCode.OK };\r\n}\r\n<\/code><\/pre>\n\n\n\n<p>If your API endpoint accepts calls from both user and app tokens, you need to make sure you just enforce the auth context requirement when a user calls your API. Then if the selected value between c1 and c99 is not available in the acrs claim, the API needs to return a authentication challenge to the caller. The <em>requiredAuthContextId<\/em> would be a string with value like &#8220;c13&#8221;. The challange is returned in the HTTP response header with the well-known key of <em>WWW-Authenticate<\/em>. The value of that header key is base64 encoded JSON string saying that &#8220;the access_token must have the acr(s) claim with the value of c13 &#8211; and that is essential&#8221;.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"cpp\" class=\"language-cpp\">protected UnauthorizedObjectResult ReturnUnauthorizedAuthContext() {\r\n    var claimsChallenge = new {\r\n        access_token = new {\r\n            acrs = new {\r\n                essential = true,\r\n                value = requiredAuthContextId\r\n            }\r\n        }\r\n    };\r\n    string base64Challenge = Convert.ToBase64String(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(claimsChallenge)));\r\n    Response.Headers.Append(\"WWW-Authenticate\", $\"Bearer error=\\\"insufficient_claims\\\", claims=\\\"{base64Challenge}\\\"\");\r\n    return Unauthorized(\"Step-up authentication is required for this action.\");\r\n}\r\n<\/code><\/pre>\n\n\n\n<h2>WebApp code implementation<\/h2>\n\n\n\n<p>The webapp needs to be able to look at the WWW-Authenticate header when it gets a 401 Unauthorized response, otherwise there is no joy. In the MicrosoftIdentityWeb world, it does that for us and throws a specific exception that we can act upon. The exception has the very long name of MicrosoftIdentityWebChallageUserException, but the name is very correct &#8211; it is a user challange exception. The exception message will contain AADSTS50076 as the error code indicating that &#8220;you must use multi-factor authentication to access &lt;appId>&#8221;. In the aspnet Razor Pages world, all you have to do then is to call ChallangeAsync with the claims passed in the exception. <\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"csharp\" class=\"language-csharp\">try {\r\n    ApiResponseDTO resp = await _downstreamApi!.GetForUserAsync&lt;ApiResponseDTO>(\"DownstreamAPI\",\r\n        options => { options.RelativePath = \"api\/sensitiveoperation\"; });\r\n} catch (MicrosoftIdentityWebChallengeUserException miwcue) {\r\n    await HandleUserChallangeException(miwcue);\r\n} catch (Exception ex) {\r\n    _log.LogError($\"Exception: {ex.Message}\");\r\n}\n\n\nprivate async Task HandleUserChallangeException(MicrosoftIdentityWebChallengeUserException miwcue ) {\r\n    var properties = new AuthenticationProperties();\r\n    if (   null != miwcue.MsalUiRequiredException \r\n        &amp;&amp; miwcue.MsalUiRequiredException.Message.Contains(\"AADSTS50076:\")\r\n        &amp;&amp; null != miwcue.MsalUiRequiredException.Claims ) {\r\n        properties.Items.Add(\"claims\", miwcue.MsalUiRequiredException.Claims);\r\n    }\r\n    await HttpContext.ChallengeAsync(OpenIdConnectDefaults.AuthenticationScheme, properties);\r\n}\r\n\r\n<\/code><\/pre>\n\n\n\n<p>The claims in the exception will be something like, where the guid is the id of your CA policy that targets auth context &#8216;C13&#8217;. Passing this back to Entra tells it to execute the CA policy that will acquire the acr claim for you.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"json\" class=\"language-json\">{\"access_token\":{\"capolids\":{\"essential\":true,\"values\":[\"f9028060-9ed6-4a55-a9d5-025e7b9aafa7\"]}}}<\/code><\/pre>\n\n\n\n<h2>Network trace &#8211; what is really happening on the wire?<\/h2>\n\n\n\n<ol><li>The wepapp calls the webapi with the access token without the required auth context<\/li><\/ol>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"bash\" class=\"language-bash\">GET https:\/\/localhost:5011\/api\/sensitiveoperation\n\nHTTP\/1.1 401 Unauthorized\r\nContent-Type: application\/json; charset=utf-8\r\nDate: Fri, 02 Oct 2026 09:09:18 GMT\r\nServer: Kestrel\r\nTransfer-Encoding: chunked\r\nWWW-Authenticate: Bearer error=\"insufficient_claims\", claims=\"eyJhY2Nlc3NfdG9rZW4iOnsiYWNycyI6eyJlc3NlbnRpYWwiOnRydWUsInZhbHVlIjoiYzEzIn19fQ==\"\r<\/code><\/pre>\n\n\n\n<p>The claims property is as explain above a base 64 encoded string that contains <em>{&#8220;access_token&#8221;:{&#8220;acrs&#8221;:{&#8220;essential&#8221;:true,&#8221;value&#8221;:&#8221;c13&#8243;}}}<\/em>, indicating that &#8220;you are missing c13 in your acr claim.<\/p>\n\n\n\n<p>2. The webapp calls Entra to step in and acquire the required claim. It passes the decoded base64 string and the refresh token in order to acquire a new access token.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"bash\" class=\"language-bash\">POST https:\/\/login.microsoftonline.com\/9885457a-2026-4e2c-a47e-32ff52ea0b8d\/oauth2\/v2.0\/token\n\n...&amp;claims=%7B%22access_token%22%3A%7B%22acrs%22%3A%7B%22essential%22%3Atrue%2C%22value%22%3A%22c13%22%7D%7D%7D&amp;grant_type=refresh_token&amp;refresh_token=...<\/code><\/pre>\n\n\n\n<p>Entra sees the request and responds with a redirect to the UI that performs the MFA. Once that is completed, and the upgraded access token is acquired, the aspnet middleware repeats the API call to the downstream webapi.<\/p>\n\n\n\n<h2>This is cool and I want to use it<\/h2>\n\n\n\n<p>Yes, it works really well and can help you with step-up MFA just-in-time. Just be aware of:<\/p>\n\n\n\n<ol><li>Your webapp and webapi both needs to be developed for using Authentication Context. You can&#8217;t add it as an afterthought.<\/li><li>If you develop a SaaS app, using &#8216;c13&#8217; for your auth context shouldn&#8217;t be hard coded. The individual enterprise using your app should be able to choose between c1 and c99.<\/li><li>Given the statement above, both the webapp and the webapi needs to agree that it is &#8216;c13&#8217; they are using. If webapi is asking for &#8216;c87&#8217; while webapp is using &#8216;c91&#8217;, it&#8217;s not going to work.<\/li><li>If you have a webapi that is asking for &#8216;c13&#8217; and you can&#8217;t modify your webapp (maybe because it&#8217;s a SaaS app), you can use a CA policy and enforce sign-in to the app to acquire &#8216;c13&#8217; at login time and keep the webapi happy. It won&#8217;t be step-up, but atleast you can make it work (but remember that your dance card will not be as full at the X-mas party).<\/li><\/ol>\n","protected":false},"excerpt":{"rendered":"<p>If your app does many things that have a normal security level but does one thing that is very sensitive, like a payment transfer, then the simplest option is to require MFA when you sign into the app. However, if the sensitive operation is only carried out by some users, and also infrequently, the high [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":[],"categories":[467,453],"tags":[],"_links":{"self":[{"href":"https:\/\/blog.redbaronofazure.com\/index.php?rest_route=\/wp\/v2\/posts\/8086"}],"collection":[{"href":"https:\/\/blog.redbaronofazure.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/blog.redbaronofazure.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/blog.redbaronofazure.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/blog.redbaronofazure.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=8086"}],"version-history":[{"count":4,"href":"https:\/\/blog.redbaronofazure.com\/index.php?rest_route=\/wp\/v2\/posts\/8086\/revisions"}],"predecessor-version":[{"id":8094,"href":"https:\/\/blog.redbaronofazure.com\/index.php?rest_route=\/wp\/v2\/posts\/8086\/revisions\/8094"}],"wp:attachment":[{"href":"https:\/\/blog.redbaronofazure.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=8086"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blog.redbaronofazure.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=8086"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blog.redbaronofazure.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=8086"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}