skip to Main Content

I am writing a REST API with C#/.NET 8 and Minimal API, which is called from a Web-GUI.
I get a JWTSecurityToken, and a list of group ids in the payload of the JWTSecurityToken.
I need the names of the groups.
Seems I have to query Graph to get these names.

In Entra admin center my application has the API permission "Group.Read.All" set, granted by an admin. And the JWTSecurityToken contains this in "scp" in the payload.

I try to create a GraphServiceClient this way (maybe there’s a better way, I tried various others – this one at least yields a GraphServiceClient):

var clientSecretCredential = new ClientSecretCredential(tenantId, clientId, clientSecret);
var graphClient = new GraphServiceClient(clientSecretCredential, scopes);
// tenantId, clientId, clientSecret and scopes read from appsettings.json or secrets.json.

Then I try to get the group name:

string groupId = "<just for test hard coded one existing group id from the token>";
var singleGroup = await graphClient.Groups[groupId].GetAsync();
Console.WriteLine(singleGroup.DisplayName);

This results in different error messages, depending on the value of scopes I use in new GraphServiceClient(clientSecretCredential, scopes):

For the scopes

scopes = ["Group.Read.All"];
scopes = [$"api://{clientId}/Group.Read.All"];

I get
Azure.Identity.AuthenticationFailedException: ClientSecretCredential authentication failed:
AADSTS1002012: The provided value for scope is not valid.
Client credential flows must have a scope value with /.default suffixed to the resource identifier(application ID URI).

For the scopes (having variations of .default)

scopes = ["Group.Read.All", "https://graph.microsoft.com/.default"];
scopes = ["Group.Read.All", "graph.microsoft.com/.default"];
scopes = ["Group.Read.All", ".default"];
scopes = ["graph.microsoft.com/.default"];

I get
Azure.Identity.AuthenticationFailedException: ClientSecretCredential authentication failed:
AADSTS70011: The provided request must include a ‘scope’ input parameter. The provided value for the input parameter ‘scope’ is not valid.
The scope is not valid.

For the scopes

scopes = [".default"];
scopes = ["https://graph.microsoft.com/.default"];

I get
Microsoft.Graph.Models.ODataErrors.ODataError: Insufficient privileges to complete the operation.

Am I using the right Graph package? Examples use Microsoft.Graph or Microsoft.Identity.Web.MicrosoftGraph, which one is correct?

Do I use a correct startup routine?
I have:

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"))
    .EnableTokenAcquisitionToCallDownstreamApi()
    .AddInMemoryTokenCaches();

Most examples have

.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)

and later

.AddMicrosoftGraph(builder.Configuration.GetSection("MicrosoftGraph"))

which leads to a compiler error when I try to add it to my code.

Please, can someone point me to a working example for this? All the examples I found seem to be for older versions of Graph and are not compatible with .NET 8.

Edit: API Permissions, as requested:
API Permissions

2

Answers


  1. You have granted Delegated api permission, and what you have now is an API project, so that we could Azure AD on-behalf-flow to do the authentication and then call Graph API. I test within the minimal API project(created a new minimal API but I choose the Authentication Type as Microsoft Identity Platform using VS template) and here’s my code.

    using Microsoft.AspNetCore.Authentication;
    using Microsoft.AspNetCore.Authentication.JwtBearer;
    using Microsoft.Graph;
    using Microsoft.Identity.Abstractions;
    using Microsoft.Identity.Web;
    using Microsoft.Identity.Web.Resource;
    
    var builder = WebApplication.CreateBuilder(args);
    
    // Add services to the container.
    builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
        .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"))
        .EnableTokenAcquisitionToCallDownstreamApi()
        .AddMicrosoftGraph()
        .AddInMemoryTokenCaches();
    builder.Services.AddAuthorization();
    
    // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
    builder.Services.AddEndpointsApiExplorer();
    builder.Services.AddSwaggerGen();
    
    var app = builder.Build();
    
    // Configure the HTTP request pipeline.
    if (app.Environment.IsDevelopment())
    {
        app.UseSwagger();
        app.UseSwaggerUI();
    }
    
    app.UseHttpsRedirection();
    
    var scope = app.Services.CreateScope();
    var _graphClient = scope.ServiceProvider.GetRequiredService<GraphServiceClient>();
    
    var scopeRequiredByApi = app.Configuration["AzureAd:Scopes"] ?? "";
    var summaries = new[]
    {
        "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
    };
    
    app.MapGet("/weatherforecast", async (HttpContext httpContext) =>
    {
        var groupId = "group_object_id";
        var singleGroup = await _graphClient.Groups[groupId].GetAsync();
    
        httpContext.VerifyUserHasAnyAcceptedScope(scopeRequiredByApi);
    
        var forecast = Enumerable.Range(1, 5).Select(index =>
            new WeatherForecast
            (
                DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
                Random.Shared.Next(-20, 55),
                summaries[Random.Shared.Next(summaries.Length)]
            ))
            .ToArray();
        return forecast;
    })
    .WithName("GetWeatherForecast")
    .WithOpenApi()
    .RequireAuthorization();
    
    app.Run();
    
    internal record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
    {
        public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
    }
    

    enter image description here

    Here’s my nuget packages.

    <ItemGroup>
      <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.8" NoWarn="NU1605" />
      <PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="8.0.8" NoWarn="NU1605" />
      <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.8" />
      <PackageReference Include="Microsoft.Identity.Web" Version="3.1.0" />
      <PackageReference Include="Microsoft.Identity.Web.DownstreamApi" Version="3.1.0" />
      <PackageReference Include="Swashbuckle.AspNetCore" Version="6.7.3" />
     <PackageReference Include="Microsoft.Graph" Version="5.56.0" />
     <PackageReference Include="Microsoft.Graph.Core" Version="3.1.18" />
     <PackageReference Include="Microsoft.Identity.Web.GraphServiceClient" Version="3.1.0" />
    </ItemGroup>
    

    And we need to add configurations in appsettings.json

    "AzureAd": {
      "Instance": "https://login.microsoftonline.com/",
      "Domain": "tenant_id",
      "TenantId": "tenant_id",
      "ClientId": "client_id",
      "ClientSecret": "client_secret",
      "Scopes": "Tiny.Greet",//the api permission name of the api I exposed 
      "CallbackPath": "/signin-oidc"
    },
    

    If you have concerns about how to get the custom API scope, please follow this tutorial to see how to protect our API via Azure AD.

    Login or Signup to reply.
  2. Note that, client credentials flow won’t work with permissions of Delegated type. The correct scope to use with client credentials flow is https://graph.microsoft.com/.default.

    Initially, I too got same error when I tried to retrieve group name by granting Delegated permissions with client credentials flow:

    enter image description here

    To resolve the error, add Group.Read.All permission of Application type and make sure to grant admin consent to it while working with client credentials flow like this:

    enter image description here

    When I ran the code again after granting Application type permission, I got the response successfully with group name in response as below:

    using Azure.Identity;
    using Microsoft.Graph;
    
    
    class Program
    {
        static async Task Main(string[] args)
        {
            var tenantId = configuration["AzureAd:TenantId"];
            var clientId = configuration["AzureAd:ClientId"];
            var clientSecret = configuration["AzureAd:ClientSecret"];
    
            var clientSecretCredential = new ClientSecretCredential(tenantId, clientId, clientSecret);
    
            var graphClient = new GraphServiceClient(clientSecretCredential, new[] { "https://graph.microsoft.com/.default" });
    
            string groupId = "groupId";
    
            try
            {
                var group = await graphClient.Groups[groupId].GetAsync();
                Console.WriteLine($"Group Display Name: {group.DisplayName}");
            }
            catch (ServiceException ex)
            {
                Console.WriteLine($"Error retrieving group info: {ex.Message}");
            }
            catch (Exception ex)
            {
                Console.WriteLine($"An error occurred: {ex.Message}");
            }
        }
    }
    

    Response:

    enter image description here

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search