Search code examples
c#asp.net-coreauthenticationmicrosoft-graph-apigraphserviceclient

Getting Client Credentials and Authorization Code flows - Microsoft Graph Api


Good morning

I have an ASP.net Core app, this app list all the users in the organization of the user who is login.

I made the login using the Authorization Code Flow and ask for delegated permissions to list users, see calendar, etc.

This is the code I used to authenticate the user

var builder = WebApplication.CreateBuilder(args);

// Configure authentication
builder.Services

    // Use OpenId authentication
    .AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)

    // Specify this is a web app and needs auth code flow
    .AddMicrosoftIdentityWebApp(options =>
    {
        builder.Configuration.Bind("AzureAd", options);

        // This causes the signin to prompt the user for which
        // account to use - useful when there are multiple accounts signed
        // into the browser
        options.Prompt = "select_account";

        options.Events.OnTokenValidated = async context =>
        {
            var tokenAcquisition = context.HttpContext.RequestServices
                .GetRequiredService<ITokenAcquisition>();


            var graphClient = new GraphServiceClient(
                new BaseBearerTokenAuthenticationProvider(
                    new TokenAcquisitionTokenProvider(
                        tokenAcquisition,
                        GraphConstants.Scopes,
                        context.Principal)));


            // Get user information from Graph
            var user = await graphClient.Me
                .GetAsync(config =>
                {
                    config.QueryParameters.Select =
                        ["displayName", "mail", "mailboxSettings", "userPrincipalName"];
                });

            context.Principal?.AddUserGraphInfo(user);

        };

        options.Events.OnAuthenticationFailed = context =>
        {
            var error = WebUtility.UrlEncode(context.Exception.Message);
            context.Response
                .Redirect($"/Home/ErrorWithMessage?message=Authentication+error&debug={error}");
            context.HandleResponse();

            return Task.FromResult(0);
        };

        options.Events.OnRemoteFailure = context =>
        {
            if (context.Failure is OpenIdConnectProtocolException)
            {
                var error = WebUtility.UrlEncode(context.Failure.Message);
                context.Response
                    .Redirect($"/Home/ErrorWithMessage?message=Sign+in+error&debug={error}");
                context.HandleResponse();
            }

            return Task.FromResult(0);
        };
    })

    // Add ability to call web API (Graph)
    // and get access tokens
    .EnableTokenAcquisitionToCallDownstreamApi(
        options =>
        {
            builder.Configuration.Bind("AzureAd", options);
        },
        GraphConstants.Scopes)

    // Add a GraphServiceClient via dependency injection
    .AddMicrosoftGraph(options =>
    {
        options.Scopes = GraphConstants.Scopes;
    })

    // Use in-memory token cache
    // See https://github.com/AzureAD/microsoft-identity-web/wiki/token-cache-serialization
    .AddInMemoryTokenCaches();


// Require authentication
builder.Services
    .AddControllersWithViews(options =>
    {
        var policy = new AuthorizationPolicyBuilder()
            .RequireAuthenticatedUser()
            .Build();
        options.Filters.Add(new AuthorizeFilter(policy));
    })

    // Add the Microsoft Identity UI pages for sign in/out
    .AddMicrosoftIdentityUI();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Home/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

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

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

app.Run();

Now I need to implement callings features, for this I need to get access to Applications Permissions which mean I have to create the Client Credentials Flow

There is a way to keep both?

I tried to add this piece of code

// Add the Microsoft Identity Web App services for Client Credentials Flow
builder.Services.AddMicrosoftIdentityWebAppAuthentication(builder.Configuration, "Azure")
    .EnableTokenAcquisitionToCallDownstreamApi(
    options =>
    {
       builder.Configuration.Bind("Azure", options);
    },
    GraphConstants.ScopesApp)
    .AddInMemoryTokenCaches();

but got this error enter image description here

What I think mean I'm trying to authenticate 2 times in the same Schema.

Any Idea to keep both flows?


Solution

  • The exception is due to adding the same scheme name in authentication mechanism. builder.Services.AddMicrosoftIdentityWebAppAuthentication(builder.Configuration, "Azure") is equal to builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme).AddMicrosoftIdentityWebApp() which are both used to integrate microsoft identity platform into the app, using auth code flow. So that you shouldn't add the last piece of code.

    Try to add client credential flow into your app, you can use code below. Please note, using client credential flow means you are trying to generate access token without users sign in, so that this code can be added anywhere you want to generate token or call graph api, it won't have conflict with the feature you already had in your Program.cs.

    using Microsoft.Graph;
    using Azure.Identity;
    
    var scopes = new[] { "https://graph.microsoft.com/.default" };
    var tenantId = "tenant_name.onmicrosoft.com";
    var clientId = "aad_app_id";
    var clientSecret = "client_secret";
    var clientSecretCredential = new ClientSecretCredential(
                    tenantId, clientId, clientSecret);
    var graphClient = new GraphServiceClient(clientSecretCredential, scopes);
    var users = await graphClient.Users.Request().GetAsync();
    

    In your scenario, you already add graph SDK into your code, so that you can inject graphServiceClient into Controller and use delegated permission to call graph api, you can also use my code below to new another graphServiceClient to call graph api with application permission. Let's see a sample which have 2 graphServiceClient together.

    [Authorize]
     public class HomeController : Controller
     {
         private readonly ILogger<HomeController> _logger;
         private readonly GraphServiceClient _graphServiceClient;
    
         public HomeController(ILogger<HomeController> logger, GraphServiceClient graphServiceClient)
         {
             _logger = logger;
             _graphServiceClient = graphServiceClient;
         }
         
         //I'm using Graph SDK V4.x but not V5.x so that I have .Request() in my code
         public async Task<IActionResult> IndexAsync()
        {
            //delegated permission
            var a = await _graphServiceClient.Me.Request().GetAsync();
            //application permission
            var scopes = new[] { "https://graph.microsoft.com/.default" };
            var tenantId = "tenantId";
            var clientId = "clientId";
            var clientSecret = "clientSecret";
            var clientSecretCredential = new ClientSecretCredential(
                            tenantId, clientId, clientSecret);
            var graphClient = new GraphServiceClient(clientSecretCredential, scopes);
            var users = await graphClient.Users.Request().GetAsync();
            return View();
        }
    }