4

In my program.cs in .NET 6, this is the code I use for Azure AD authentication.

string[] initialScopes = builder.Configuration.GetValue<string>("DownstreamApi:Scopes")?.Split(' ');

    builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApp(options => {
        builder.Configuration.GetSection("AzureAd").Bind(options);

        options.NonceCookie.SecurePolicy = CookieSecurePolicy.Always;
        options.CorrelationCookie.SecurePolicy = CookieSecurePolicy.Always;
    });

How to get the access token after authentication using the Microsoft Identity platform authentication flow? I need the access token in subsequent requests to access protected resources without needing to use JavaScript to obtain a new access token each time.

I already have the clientId, tenantId and clientSecret.

These code are from https://learn.microsoft.com/en-us/azure/active-directory/develop/scenario-web-api-call-api-acquire-token?tabs=aspnetcore

[Authorize]
public class MyApiController : Controller
{
    /// <summary>
    /// The web API will accept only tokens 1) for users, 2) that have the `access_as_user` scope for
    /// this API.
    /// </summary>
    static readonly string[] scopeRequiredByApi = new string[] { "access_as_user" };

     static readonly string[] scopesToAccessDownstreamApi = new string[] { "api://MyTodolistService/access_as_user" };

    private readonly ITokenAcquisition _tokenAcquisition;

    public MyApiController(ITokenAcquisition tokenAcquisition)
    {
        _tokenAcquisition = tokenAcquisition;
    }

    public IActionResult Index()
    {
        HttpContext.VerifyUserHasAnyAcceptedScope(scopeRequiredByApi);

        string accessToken = _tokenAcquisition.GetAccessTokenForUserAsync(scopesToAccessDownstreamApi);
        return await callTodoListService(accessToken);
    }
}

How do I get scopeRequiredByApi and scopesToAccessDownstreamApi?

Tiny Wang
  • 10,423
  • 1
  • 11
  • 29
Steve
  • 2,963
  • 15
  • 61
  • 133

2 Answers2

3

Not sure if it's good to start from here. For Azure AD authorization(generating access token), there're several flows, most common used is Auth code flow -- let user sign in, then can used the credential to generate access token with delegated api permission, token will have scp claim. Client credential flow -- no need to sign in, generate token with application api permission on behalf the app itself, token will contain roles claim). On-behalf-of flow -- used when an API requires to call another API, both the APIs required an access token).

About the API permission type, for example Microsoft graph create calendar API, using delegated API permission can update resource for user him/herself. But using application API, application can modify any user's calendar event who are in the target tenant. Since we can set Azure AD to protect our own API, so we can also create API permissions in Azure AD, choosing exposing delegated API permission or exposing application API permission.

According to the link you shared in the question, you are trying to use On-behalf-of flow because the document is a tutorial for one API calling another API. Then please allow me to make an assumption here. You have a client APP which integrated MSAL so that you can let users sign in and obtain an access token, you used that token to call your own API, your API requires to call another API to get some other information.

Just like what I said, Azure AD can protect our own WEB API. So we can go to Azure AD -> App Registrations -> create an Azure AD app or choose an existing one -> Expose an API -> after create the API, add a scope and name it like Steve_Allowed, since the assumption is user call API so I create a scope for exposing delegated API. Then in the same Azure AD application -> API permissions -> Add a permission -> My APIs -> choose the AAD app -> Delegated permissions -> choose Steve_Allowed and click Add permissions -> click grant admin consent button beside add api permission button. Next you also need to create a client secret in Certificates & secrets blade if you don't have it.

Now we already finished the configurations in Azure AD, let's go to the API application. Just like you know, when you want to call an API protected by Azure AD, you need to add a request header like Authorization: Bearer xxxx. In our web API we need to integrated Azure AD to validate the request so that it can give you a correct response or a 401 error. So in the API project, add code like below but maybe you already done.

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Identity.Web;

...
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
   .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"))
   .EnableTokenAcquisitionToCallDownstreamApi()//this 2 lines for using _tokenAcquisition to generate another access token
   .AddInMemoryTokenCaches();
builder.Services.AddHttpClient();

...
app.UseAuthentication();
app.UseAuthorization();


 "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "ClientId": "azure_ad_app_id",
    "ClientSecret": "client_secret",
    "Domain": "tenant_id",
    "TenantId": "tenant_id",
    "Audience": "api://client_id"
  },

Assume it contained an API which added [Authorize] attribute with URL is https://localhost:7072/WeatherForecast, then add the request header. In your API code, you can use code below to get the token in the request header.

StringValues authorizationToken;
HttpContext.Request.Headers.TryGetValue("Authorization", out authorizationToken);
string incomingToken = authorizationToken.ToString().Replace("Bearer ", "");

And here the incoming Token contained the scopeRequiredByApi you need. But it already validated by the middle, so you don't need to write a method VerifyUserHasAnyAcceptedScope to validate the token.

Now we have successfully accessed the API and then we need to use on_behalf_of flow to access another API. Since the middleware already did the validation for us, we can directly using _tokenAcquisition in our code to generate another access token and use it to call another API. So in your API you can have code like below. Please note, since you are trying to call another API, if that API is also managed by yourself, you may need to expose another API, then you can get a new scope. If this API is managed by others, for example the create calendar API, then you need to go to the API document to get the scope Calendars.ReadWrite. The API provider should give you the scope, and this scope is what you want scopesToAccessDownstreamApi.

private readonly ITokenAcquisition _tokenAcquisition;
private readonly IHttpClientFactory _httpClientFactory;

public WeatherForecastController(ITokenAcquisition tokenAcquisition, IHttpClientFactory httpClientFactory)
{
    _tokenAcquisition = tokenAcquisition;
    _httpClientFactory = httpClientFactory;
}

[HttpGet(Name = "GetWeatherForecast")]
public async Task<string> GetAsync()
{
    var accessToken = await _tokenAcquisition.GetAccessTokenForUserAsync(new string[] { "api://client_id/scopeName" });

    var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "https://localhost:7212/WeatherForecast")
    {
        Headers =
        {
            { HeaderNames.Authorization, "Bearer "+ accessToken}
        }
    };

    var httpClient = _httpClientFactory.CreateClient();
    var response = await httpClient.SendAsync(httpRequestMessage);

    var res = "";
    if (response.StatusCode == HttpStatusCode.OK)
    {
        res = await response.Content.ReadAsStringAsync();
    }
    return "hello";
}

===========================Summary==========================

What you want is one API calling another API, if both the APIs are managed by yourself, you'd better to create 2 Azure AD applications and expose API permission in both of them. When you want to call any of the API, you need to set a scope to generate access token for the API. For example you exposed API permission for API 1 which scope should look like api://client_id1/scope_name_for_API1. Then use it in the request header and call API 1, in API 1 you want to call API 2, then you need to use _tokenAcquisition to generate with api://client_id2/scope_name_for_API2 for API 2 and call it.

Tiny Wang
  • 10,423
  • 1
  • 11
  • 29
  • [This answer](https://stackoverflow.com/a/68799358/14574199) contains a sample for client app generate access token to call Azure AD protected API. [This answer](https://stackoverflow.com/a/74313765/14574199) have a explanation for OBO flow. [This answer](https://stackoverflow.com/a/74980244/14574199) contains a sample using Graph SDK for the scenario that the second API is Graph API. – Tiny Wang Apr 14 '23 at 12:43
  • If you're getting an MsalUiRequiredException was thrown due to a challenge for the user. You need to clear Cache or open the site in Incognito tab – SurenSaluka Jul 13 '23 at 13:58
1

In your startup, add .EnableTokenAcquisitionToCallDownstreamApi() line, and choose a token cache implementation, like .AddInMemoryTokenCaches().

builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApp(options => {
        builder.Configuration.GetSection("AzureAd").Bind(options);

        options.NonceCookie.SecurePolicy = CookieSecurePolicy.Always;
        options.CorrelationCookie.SecurePolicy = CookieSecurePolicy.Always;
    })
    .EnableTokenAcquisitionToCallDownstreamApi()
    .AddDownstreamWebApi("MyApi", Configuration.GetSection("MyApiConfig"))
    .AddInMemoryTokenCaches();

Inject ITokenAcquisition interface in your controller/service. To get access token call:

string token = await _tokenAcquisition.GetAccessTokenForUserAsync(scopes);

From Microsoft reference:

enter image description here

EDIT: for subsequent question added later

How do I get scopeRequiredByApi and scopesToAccessDownstreamApi?

In Azure portal, go to App Registrations and open your API app registration. Go to Expose an API section. You should find your scopes over there.

YK1
  • 7,327
  • 1
  • 21
  • 28
  • where do I get the `scopes` value from? I have updated my post – Steve Apr 10 '23 at 22:27
  • it will be documented in the API you are trying to access – YK1 Apr 11 '23 at 00:08
  • I already see this line in your code: `string[] initialScopes = builder.Configuration.GetValue("DownstreamApi:Scopes")?.Split(' ');` – YK1 Apr 11 '23 at 00:12
  • I am a bit confused by the `scopes`. Yes, I have `initialScopes` and the value I put the "user.read" which I believe it is the `scopeRequiredByApi `. But what about `scopesToAccessDownstreamApi`? How do I get this? – Steve Apr 11 '23 at 00:31
  • In Azure portal, go to `App Registrations` and open your API app registration. Go to `Expose an API`. You should find your scope there. – YK1 Apr 11 '23 at 01:33