{"id":8113,"date":"2026-10-14T10:26:03","date_gmt":"2026-10-14T08:26:03","guid":{"rendered":"https:\/\/blog.redbaronofazure.com\/?p=8113"},"modified":"2026-10-14T15:45:04","modified_gmt":"2026-10-14T13:45:04","slug":"managed-identity-and-scopes-and-approles","status":"publish","type":"post","link":"https:\/\/blog.redbaronofazure.com\/?p=8113","title":{"rendered":"Managed Identity, scopes and appRoles"},"content":{"rendered":"\n<p>If you have an API that needs to work with both user and app access tokens, and you have deployed the API so it can use Entra Managed Identity, then you encounter the problem that Managed Identity can&#8217;t expose scopes. This means the scopes in the user tokens needs another solution. The solution is to let the API use Managed Identity when there is an app access token calling it and use the on-behalf-of flow (<a href=\"https:\/\/learn.microsoft.com\/en-us\/entra\/identity-platform\/v2-oauth2-on-behalf-of-flow\" target=\"_blank\" rel=\"noreferrer noopener\">OBO<\/a>) when it is a user who is calling the API.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" width=\"865\" height=\"289\" src=\"https:\/\/blog.redbaronofazure.com\/wp-content\/uploads\/2026\/10\/ManagedIdentity-scopes-approles-1.png\" alt=\"\" class=\"wp-image-8126\" srcset=\"https:\/\/blog.redbaronofazure.com\/wp-content\/uploads\/2026\/10\/ManagedIdentity-scopes-approles-1.png 865w, https:\/\/blog.redbaronofazure.com\/wp-content\/uploads\/2026\/10\/ManagedIdentity-scopes-approles-1-300x100.png 300w, https:\/\/blog.redbaronofazure.com\/wp-content\/uploads\/2026\/10\/ManagedIdentity-scopes-approles-1-768x257.png 768w\" sizes=\"(max-width: 865px) 100vw, 865px\" \/><\/figure>\n\n\n\n<p>The the above diagram has the following components:<\/p>\n\n\n\n<ul><li><strong>Managed Identity<\/strong> service principal has the Graph <em>Organization.Read.All<\/em> granted as an Application permission.<\/li><li><strong>Backend WebApi<\/strong> exposes the appRole <em>Api.Read<\/em> and the scope <em>access_as_user<\/em>, and has Graph <em>Organization.Read.All<\/em> granted as an Delegated permission<\/li><li><strong>Frontend WebApp<\/strong> has the API permission of Delegated <em>access_as_user<\/em> and Application <em>Api.Read<\/em> to the Backend WebApi.<\/li><\/ul>\n\n\n\n<p>Different API calling paths<\/p>\n\n\n\n<ul><li><strong>Api being called with an app token<\/strong> &#8211; Backend WebApi sees the appRole <em>Api.Read<\/em> and makes the Graph call using the Managed Identity<\/li><li><strong>Api being called with a user token<\/strong> &#8211; Backend WebApi sees the scope <em>access_as_user<\/em>, makes an on-behalf-of token exchange (<a rel=\"noreferrer noopener\" href=\"https:\/\/learn.microsoft.com\/en-us\/entra\/identity-platform\/v2-oauth2-on-behalf-of-flow\" target=\"_blank\">OBO-flow<\/a>) and makes the Graph call using the user&#8217;s access token. If the user doesn&#8217;t have the required permission for the Graph object being requested, the OBO-flow will fail and an 401 Unauthorized will be returned. The OBO-flow will also fail if it requests a delegated permission that the Backend WebApi hasn&#8217;t been granted.<\/li><\/ul>\n\n\n\n<p>In order to make the above solution work, the Backend WebApi needs to authenticate the caller and distinguish if it is a user or app token and proceed differently when calling the downstream API (Graph in this example). Let&#8217;s look at how to achieve this.<\/p>\n\n\n\n<p>There is a github repo <a rel=\"noreferrer noopener\" href=\"https:\/\/github.com\/cljung\/WebApiUsingManagedIdentity\" target=\"_blank\">here<\/a> with accompanying sample code and scripts.<\/p>\n\n\n\n<h2>What is the benefit of this solution?<\/h2>\n\n\n\n<p>By using Managed Identity for app tokens and OBO-flow for user tokens, there needs to be no client secret or certificate for the Bankend WebApi. Not having a client secret\/certificate means that there is nothing that can get lost and there is nothing that needs to be rotated periodically.  The solution is less risky.<\/p>\n\n\n\n<h2>Instantiating the GraphServiceClient<\/h2>\n\n\n\n<p>If the WebApi is working with Graph, you are likely to use GraphServiceClient, assuming your working with aspnet and C#. The below code checks the configuration to see if it is configured to use Managed Identity at all <em>and<\/em> if it is an app token calling (see !IsUserToken). If so, it proceeds to use Managed Identity. If not, (else) if uses client credentials. Why client credentials? If the WebApi is configured not to use Managed Identity, it falls back on client credentials. This is what happens if you run a local debugging session.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"csharp\" class=\"language-csharp\">public GraphClientService( IConfiguration configuration, ILogger&lt;GraphClientService&gt; log\n                            , IHttpContextAccessor httpContextAccessor, ITokenAcquisition? tokenAcquisition = null) {\n    _log = log;\n    _configuration = configuration;\n    _httpContextAccessor = httpContextAccessor;\n    _tokenAcquisition = tokenAcquisition;\n    var authMode = configuration[\"Entra:Graph:AuthMode\"];\n    var clientId = configuration[\"Entra:Graph:ClientId\"];\n    if (null != authMode &amp;&amp; authMode!.StartsWith(\"ManagedIdentity\") &amp;&amp; !IsUserToken() ) {\n        \/\/ system-assigned vs user-assigned managed identity\n        if ( authMode.Contains(\"User\", StringComparison.OrdinalIgnoreCase)) {\n            _tokenCredential = new ManagedIdentityCredential(ManagedIdentityId.FromUserAssignedClientId(clientId));\n            _authMode = $\"ManagedIdentity:User:{clientId}\";\n        } else {\n            _tokenCredential = new ManagedIdentityCredential(ManagedIdentityId.SystemAssigned);\n            _authMode = $\"ManagedIdentity\";\n        }\n    } else {\n        _tokenAcquisition = tokenAcquisition ?? throw new InvalidOperationException( \"ITokenAcquisition is required.\");\n        var options = new ClientSecretCredentialOptions { AuthorityHost = AzureAuthorityHosts.AzurePublicCloud };\n        _tokenCredential = new ClientSecretCredential(configuration[\"Entra:TenantId\"], clientId, configuration[\"Entra:Graph:ClientSecret\"], options);\n        _authMode = $\"ClientCreds:{clientId}\";\n    }\n    _graphClient = new GraphServiceClient(_tokenCredential, _scopes);\n}\n<\/code><\/pre>\n\n\n\n<p>Methods that makes Graph API calls then looks something like below where they always make an attempt to do the OBO-flow exchange with the scopes required for the user. If the user doesn&#8217;t have the scopes required, or need to consent to them being used by this app, an exception will be thrown that needs to be returned to the calling app.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"csharp\" class=\"language-csharp\">public async Task&lt;List&lt;Microsoft.Graph.Models.Organization&gt;?&gt; GetOrganizationAsync() {\n    await OboExchangeTokenAsync(new[] { \"Organization.Read.All\" });\n    var response = await _graphClient!.Organization.GetAsync();\n    return response?.Value!;\n}\n<\/code><\/pre>\n\n\n\n<h2>App or User token?<\/h2>\n\n\n\n<p>The OBO-flow exchange only happens if it is a user token calling. A user token will contain the &#8216;scp&#8217; claim. Ap app token will have a &#8216;roles&#8217; claim. If it is not a user token, it does nothing and continues with the Managed Identity setup in the constructor.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"csharp\" class=\"language-csharp\">private bool IsUserToken() {\n    if (_httpContextAccessor.HttpContext!.User == null) return false;\n    var scopeClaim = _httpContextAccessor.HttpContext.User.FindFirst(c =&gt; c.Type == ClaimConstants.Scope || c.Type == ClaimConstants.Scp);\n    if (scopeClaim == null) return false;\n    return true;\n}\nprivate async Task OboExchangeTokenAsync(string[] graphScopes) {\n    if (!IsUserToken()) return;\n    string accessToken = await _tokenAcquisition!.GetAccessTokenForUserAsync(graphScopes);\n    _oboFlowTokenProvider = new BaseBearerTokenAuthenticationProvider(new OboFlowTokenProvider(accessToken));\n    _graphClient = new GraphServiceClient(_oboFlowTokenProvider);\n}\n<\/code><\/pre>\n\n\n\n<h2>Calling the API as an app and as a user<\/h2>\n\n\n\n<p>The result of calling the API with a user and an app token will be like below. The API echos back what token type was received and what authentication mode was used for calling Graph. The API also echos back some organizational details of the tenant retrieved via Graph. <\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" width=\"1024\" height=\"702\" src=\"https:\/\/blog.redbaronofazure.com\/wp-content\/uploads\/2026\/10\/Screenshot-2026-10-13-224648-1024x702.png\" alt=\"\" class=\"wp-image-8121\" srcset=\"https:\/\/blog.redbaronofazure.com\/wp-content\/uploads\/2026\/10\/Screenshot-2026-10-13-224648-1024x702.png 1024w, https:\/\/blog.redbaronofazure.com\/wp-content\/uploads\/2026\/10\/Screenshot-2026-10-13-224648-300x206.png 300w, https:\/\/blog.redbaronofazure.com\/wp-content\/uploads\/2026\/10\/Screenshot-2026-10-13-224648-768x526.png 768w, https:\/\/blog.redbaronofazure.com\/wp-content\/uploads\/2026\/10\/Screenshot-2026-10-13-224648.png 1240w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<h2>Entra Backend WebApi app configuration<\/h2>\n\n\n\n<p>The backend WebApi app registration has a delegated scope for Organization.Read.All so that it&#8217;s users are allowed to request that permission via the OBO-flow.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" width=\"779\" height=\"390\" src=\"https:\/\/blog.redbaronofazure.com\/wp-content\/uploads\/2026\/10\/Screenshot-2026-10-14-080801.png\" alt=\"\" class=\"wp-image-8127\" srcset=\"https:\/\/blog.redbaronofazure.com\/wp-content\/uploads\/2026\/10\/Screenshot-2026-10-14-080801.png 779w, https:\/\/blog.redbaronofazure.com\/wp-content\/uploads\/2026\/10\/Screenshot-2026-10-14-080801-300x150.png 300w, https:\/\/blog.redbaronofazure.com\/wp-content\/uploads\/2026\/10\/Screenshot-2026-10-14-080801-768x384.png 768w\" sizes=\"(max-width: 779px) 100vw, 779px\" \/><\/figure>\n\n\n\n<p>In order for the user to request the correct scope, the app registration has a scope defined as assess_as_user. This scope is what an interactive login request will request.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" width=\"932\" height=\"496\" src=\"https:\/\/blog.redbaronofazure.com\/wp-content\/uploads\/2026\/10\/Screenshot-2026-10-14-081658.png\" alt=\"\" class=\"wp-image-8128\" srcset=\"https:\/\/blog.redbaronofazure.com\/wp-content\/uploads\/2026\/10\/Screenshot-2026-10-14-081658.png 932w, https:\/\/blog.redbaronofazure.com\/wp-content\/uploads\/2026\/10\/Screenshot-2026-10-14-081658-300x160.png 300w, https:\/\/blog.redbaronofazure.com\/wp-content\/uploads\/2026\/10\/Screenshot-2026-10-14-081658-768x409.png 768w\" sizes=\"(max-width: 932px) 100vw, 932px\" \/><\/figure>\n\n\n\n<p>The app will request a \/.default scope which will generate an access token with the app role Api.Read.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" width=\"938\" height=\"500\" src=\"https:\/\/blog.redbaronofazure.com\/wp-content\/uploads\/2026\/10\/Screenshot-2026-10-14-081552.png\" alt=\"\" class=\"wp-image-8129\" srcset=\"https:\/\/blog.redbaronofazure.com\/wp-content\/uploads\/2026\/10\/Screenshot-2026-10-14-081552.png 938w, https:\/\/blog.redbaronofazure.com\/wp-content\/uploads\/2026\/10\/Screenshot-2026-10-14-081552-300x160.png 300w, https:\/\/blog.redbaronofazure.com\/wp-content\/uploads\/2026\/10\/Screenshot-2026-10-14-081552-768x409.png 768w\" sizes=\"(max-width: 938px) 100vw, 938px\" \/><\/figure>\n\n\n\n<h2>Entra Frontend WebApp app configuration<\/h2>\n\n\n\n<p>The frontend WebApp adds the exposed scope and app role as API permissions (that an admin grants). This gives the frontend WebApp the permission to request access_as_user as a delegated (user) permission and Api.Read as an application permission.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" width=\"824\" height=\"444\" src=\"https:\/\/blog.redbaronofazure.com\/wp-content\/uploads\/2026\/10\/Screenshot-2026-10-14-080900.png\" alt=\"\" class=\"wp-image-8130\" srcset=\"https:\/\/blog.redbaronofazure.com\/wp-content\/uploads\/2026\/10\/Screenshot-2026-10-14-080900.png 824w, https:\/\/blog.redbaronofazure.com\/wp-content\/uploads\/2026\/10\/Screenshot-2026-10-14-080900-300x162.png 300w, https:\/\/blog.redbaronofazure.com\/wp-content\/uploads\/2026\/10\/Screenshot-2026-10-14-080900-768x414.png 768w\" sizes=\"(max-width: 824px) 100vw, 824px\" \/><\/figure>\n\n\n\n<h2>Azure App Services and Entra Managed Identity configuration<\/h2>\n\n\n\n<p>In Azure App Services, where the backend WebApi is deployed, you simply enable the use of Managed Identity. This creates a service principal in Entra.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" width=\"723\" height=\"619\" src=\"https:\/\/blog.redbaronofazure.com\/wp-content\/uploads\/2026\/10\/Screenshot-2026-10-14-082408.png\" alt=\"\" class=\"wp-image-8131\" srcset=\"https:\/\/blog.redbaronofazure.com\/wp-content\/uploads\/2026\/10\/Screenshot-2026-10-14-082408.png 723w, https:\/\/blog.redbaronofazure.com\/wp-content\/uploads\/2026\/10\/Screenshot-2026-10-14-082408-300x257.png 300w\" sizes=\"(max-width: 723px) 100vw, 723px\" \/><\/figure>\n\n\n\n<p>The Entra service principal can be viewed in the <em>Enterprise apps<\/em> menu section. However, adding the permissions must be done via powershell. In the below case, the managed identity was granted the rather powerful permission Directory.Reader.All.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" width=\"1024\" height=\"611\" src=\"https:\/\/blog.redbaronofazure.com\/wp-content\/uploads\/2026\/10\/Screenshot-2026-10-14-081016-1024x611.png\" alt=\"\" class=\"wp-image-8133\" srcset=\"https:\/\/blog.redbaronofazure.com\/wp-content\/uploads\/2026\/10\/Screenshot-2026-10-14-081016-1024x611.png 1024w, https:\/\/blog.redbaronofazure.com\/wp-content\/uploads\/2026\/10\/Screenshot-2026-10-14-081016-300x179.png 300w, https:\/\/blog.redbaronofazure.com\/wp-content\/uploads\/2026\/10\/Screenshot-2026-10-14-081016-768x459.png 768w, https:\/\/blog.redbaronofazure.com\/wp-content\/uploads\/2026\/10\/Screenshot-2026-10-14-081016.png 1040w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"powershell\" class=\"language-powershell\">Connect-MgGraph -Scopes \"Directory.Read.All\"\n\n$ManagedIdentityObjectId = \"YOUR_FUNCTION_MANAGED_IDENTITY_OBJECT_ID\"\n$GraphServicePrincipal = Get-MgServicePrincipal -Filter \"AppId eq '00000003-0000-0000-c000-000000000000'\"\n\n$AppRole = $GraphServicePrincipal.AppRoles | Where-Object {$_.Value -eq \"Directory.Read.All\" -and $_.AllowedMemberTypes -contains \"Application\"}\n\nNew-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $ManagedIdentityObjectId `\n\t\t\t\t\t\t\t\t\t\t-PrincipalId $ManagedIdentityObjectId `\n\t\t\t\t\t\t\t\t\t\t-ResourceId $GraphServicePrincipal.Id `\n\t\t\t\t\t\t\t\t\t\t-AppRoleId $AppRole.Id\n<\/code><\/pre>\n","protected":false},"excerpt":{"rendered":"<p>If you have an API that needs to work with both user and app access tokens, and you have deployed the API so it can use Entra Managed Identity, then you encounter the problem that Managed Identity can&#8217;t expose scopes. This means the scopes in the user tokens needs another solution. The solution is to [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":[],"categories":[453,470,472],"tags":[406,471],"_links":{"self":[{"href":"https:\/\/blog.redbaronofazure.com\/index.php?rest_route=\/wp\/v2\/posts\/8113"}],"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=8113"}],"version-history":[{"count":14,"href":"https:\/\/blog.redbaronofazure.com\/index.php?rest_route=\/wp\/v2\/posts\/8113\/revisions"}],"predecessor-version":[{"id":8144,"href":"https:\/\/blog.redbaronofazure.com\/index.php?rest_route=\/wp\/v2\/posts\/8113\/revisions\/8144"}],"wp:attachment":[{"href":"https:\/\/blog.redbaronofazure.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=8113"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blog.redbaronofazure.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=8113"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blog.redbaronofazure.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=8113"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}