0

I have an Angular SPA and an ASP.NET Core Web API backend. I have an Azure application registration which the SPA is configured to use, and I can make Graph API calls from the SPA directly using @azure/msal-angular and @microsoft/microsoft-graph-client.

What I want to do now is have the SPA call the Web API, and for the Web API to make the Graph API calls on behalf of the logged in user.

I've waded through reams of documentation but I've not been able to find a complete tutorial for this scenario or find a definitive answer to a key question.

Which is, do I need a separate app registration for the SPA and the ASP.NET Core Web API, or do I need a single application registration with two platforms configured?

UPDATE

With the help of the responses in this thread I have this working.

One difference from the configuration proposed by @Rukmini is that in the configuration of the SPA app registration I do not have the web api added to API Permissions (it is not listed under My APIs). Instead in the configuration of the web API app registration, under Expose an API -> Authorized client applications I have the SPA app registration selected there. (I don't know what the difference is between these two approaches but it is working).

enter image description here

In the ASP web api my appsettings.json is trivial

    "AzureAd": {
      "Instance": "https://login.microsoftonline.com/",
      "ClientId": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
      "TenantId": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
      "ClientSecret": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
      "Scopes": "Api.Access"
    }

No section is required to configure the Graph API, the defaults work.

Program.cs is also straightforward

    // Using Microsoft.Identity.Web.MicrosoftGraph 3.5.0
    builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
       .AddMicrosoftIdentityWebApi(builder.Configuration)
       .EnableTokenAcquisitionToCallDownstreamApi()
       .AddMicrosoftGraph()
       .AddDistributedTokenCaches();

Default configuration all works.

Controller is as follows.

    [Route("api/[controller]")]
    [ApiController]
    [Authorize]
    public class GraphTestController : ControllerBase
    {
       public readonly GraphServiceClient _graphClient;
 
       public GraphTestController(GraphServiceClient graphClient)
       {
          _graphClient = graphClient;
       }
 
       [HttpGet("invite-user")]
       public async Task<ActionResult> InviteUser(string email, string displayName)
       {
          var invite = new Invitation()
          {
             InvitedUserDisplayName = displayName,
             InvitedUserEmailAddress = email,
             InviteRedirectUrl = "https://www.google.com",
             SendInvitationMessage = true
          };
          Invitation response = await _graphClient.Invitations.PostAsync(invite);
          return Ok(response);
       }
    }

To send invitations the User.Invite.All delegated scope is added to both SPA and Web API app registrations, and the user must also have sufficient permissions.

Thanks everyone for the help!

4
  • I use a separate app registration and a client secret for this. In my case I'm using Graph API to manage Azure AD B2C users, so maybe not exactly what you need. Commented Jan 15 at 21:31
  • Seems like Jack's comment is on the right track here. I imagine you'll need one app reg for the Web API, if you are only making calls from Web API to Graph API. If you also need to make calls from SPA to Graph API, then you'll need 2 app reg's. I would lean in favor of managed identity if you can host your backend in such a way to leverage that, and if not, then a client secret as Jack is doing in his comment. The good thing about hosted client secret (if you do not need to make calls directly to Graph from SPA) is that it's easier to keep it hidden from any attackers. Commented Jan 15 at 21:49
  • I ran out of space to address the "on behalf of" issue. I would just craft the call from SPA to your Web API exactly the same as it would be crafted to hit Graph API (the only differences will be the endpoints and required credentials, etc). Then just make that same call to Graph API from your Web API. Perhaps I'm simplifying but in the end all you need are the right claims, endpoints, app reg secrets, etc, and Bob's your uncle. Commented Jan 15 at 21:52
  • You need two separate app registrations: one for the Angular SPA and one for the ASP.NET Core Web API. The SPA authenticates the user and passes the access token to the Web API, which then uses the On-Behalf-Of (OBO) flow to acquire a Graph API token and make calls to Microsoft Graph on behalf of the user. Commented Jan 16 at 7:30

2 Answers 2

1

I agree with @Jack A, You need two separate app registrations in Microsoft Entra ID, one for the Angular SPA (client) and one for the ASP.NET Core Web API (backend).

  • SPA App Registration: This is for the Angular application, which handles user authentication and retrieves an access token (via MSAL) to interact with the Web API. The SPA uses this access token to authenticate its requests to the Web API.
  • Web API App Registration: This is for the backend (ASP.NET Core Web API). It verifies the incoming requests from the SPA, and once the user is authenticated, the Web API can use the granted permissions to call the Microsoft Graph API on behalf of the user.

User Authentication in SPA -> SPA Calls the Web API -> Web API Validates the Token -> Web API Calls Graph API

Create a ASP.NET Core Web API application and expose an API add scope:

enter image description here

And add Microsoft Graph API permissions in ASP.NET Core Web API application:

enter image description here

In Angular SPA Microsoft Entra ID application, grant API permission of the ASP.NET Core Web API:

enter image description here

For sample, I generated access token using Angular SPA:

https://login.microsoftonline.com/tenantId/oauth2/v2.0/token

grant_type:authorization_code
client_id:ClientSPAappId
scope: api://xxxxxxxx/webapi.access
code: xxx
code_verifier:S256
redirect_uri: https://jwt.ms

enter image description here

The above generated access token can be used to call web API.

Now generate on-behalf-of flow access token to call Microsoft Graph:

https://login.microsoftonline.com/tenantID/oauth2/v2.0/token

client_id:WebAPIappID
client_secret:WebAPIappSecret
scope: https://graph.microsoft.com/.default
grant_type: urn:ietf:params:oauth:grant-type:jwt-bearer
assertion:<paste_token_from_above_step>
requested_token_use:on_behalf_of

enter image description here

You can use this access token to call Microsoft Graph API:

https://graph.microsoft.com/v1.0/users

enter image description here

For sample, I generated tokens via Rest API this can be done via code too.

Reference:

Microsoft identity platform and OAuth2.0 On-Behalf-Of flow - | Microsoft

Sign up to request clarification or add additional context in comments.

Comments

1

have the SPA call the Web API, and for the Web API to make the Graph API calls on behalf of the logged in user

Using on-behalf-of flow could help you solve your issue. In your scenario, there might be 2 approaches.

Firstly, if your web api requires authorization, then you have to make it be protected by Azure AD, you can follow this tutorial to configure your web api. You will have to expose an Api permission in your Azure AD app, you can use the same app which is used for SPA, then add Api permission. If you choose to create another Azure AD app, then pls add Api permission to your SPA AAD app. In your SPA you can take a look at this sample or this document to see how to generate access token, but here pls remember that the scope should be the one you exposed, the format should be similar to api://aad_client_id_you_exposed_api/api_scope_name.

Then you can use this token to call the web api. In your Web api, you can add Graph SDK at the same time like codes below, so that you could injection GraphServiceClient dependency into Controller. If the token received by API is valid, GraphServiceClient will be authenticated automatically and you can use it to call Graph API.

In another scenario, if your web api doesn't require authorization, I mean the API called by your SPA is a public endpoint and you want this API to help call Graph API on behalf of the signed in user. You can get help from Microsoft Identity Platform SDK to help you authenticate GraphServiceClient, you can obtain the access token from http request and use the token to do the authentication.

Here's a complete sample codes for web api parts. I've test it and it worked well.

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"))
    .EnableTokenAcquisitionToCallDownstreamApi()
    .AddMicrosoftGraph(builder.Configuration.GetSection("DownstreamApi"))//require Microsoft.Identity.Web.GraphServiceClient package
    .AddInMemoryTokenCaches();
    
...
    
app.UseAuthorization();


[Authorize]
[ApiController]
[Route("[controller]")]
[RequiredScope(RequiredScopesConfigurationKey = "AzureAd:Scopes")]
public class WeatherForecastController : ControllerBase
{
    private GraphServiceClient _graphServiceClient;

    public WeatherForecastController(GraphServiceClient graphServiceClient)
    {
        _graphServiceClient = graphServiceClient;
    }

    [HttpGet(Name = "GetWeatherForecast")]
    public async Task<string> GetAsync()
    {
        var me = await _graphServiceClient.Me.GetAsync();
        return "success";
    }
}

[Route("api/[controller]")]
[ApiController]
public class HelloController : ControllerBase
{

    [HttpGet]
    public async Task<string> Hello() {
        StringValues authorizationToken;
        HttpContext.Request.Headers.TryGetValue("Authorization", out authorizationToken);
        var token = authorizationToken.ToString().Replace("Bearer ", "");
        var scopes = new[] { "User.Read.All" };
        var tenantId = "tenantId";
        var clientId = "client_id";
        var clientSecret = "client_secret";
        var onBehalfOfCredential = new OnBehalfOfCredential(tenantId, clientId, clientSecret, token);
        var graphClient = new GraphServiceClient(onBehalfOfCredential, scopes);
        var me = await graphClient.Me.GetAsync();
        return "hello";
    }
}

"AzureAd": {
  "Instance": "https://login.microsoftonline.com/",
  "ClientId": "client_id_of_AAD_app_exposed_api_permission",
  "ClientSecret": "client_secret",
  "Domain": "tenant_id",
  "TenantId": "tenant_id",

  "Scopes": "Tiny.Read",//api permission name
  "CallbackPath": "/signin-oidc"
},

<ItemGroup>
  <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.11" NoWarn="NU1605" />
  <PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="8.0.11" NoWarn="NU1605" />
  <PackageReference Include="Microsoft.Identity.Web" Version="3.2.0" />
  <PackageReference Include="Microsoft.Identity.Web.DownstreamApi" Version="3.2.0" />
  <PackageReference Include="Microsoft.Identity.Web.GraphServiceClient" Version="3.5.0" />
  <PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
</ItemGroup>

1 Comment

Your scenario can be summarized as API A requires to call another API B which is secured by Azure AD, A is your custom API, B can be your custom API too but here it's Graph API. This scenario requires to use on-behalf-of flow indeed. @Rukmini already showed how this worked, and what I shared is how we implement it in .net codes. In short, we need the SPA to generate an access token, you can get help from the second and third link I shared above for this topic.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.