AzureAD / microsoft-identity-web

Helps creating protected web apps and web APIs with Microsoft identity platform and Azure AD B2C
MIT License
671 stars 208 forks source link

AuthorizeForScopes Not Challenging for GraphServiceClient #2445

Open DaleyKD opened 1 year ago

DaleyKD commented 1 year ago

Microsoft.Identity.Web Library

Microsoft.Identity.Web

Microsoft.Identity.Web version

2.13.3

Web app

Sign-in users and call web APIs

Web API

Not Applicable

Token cache serialization

In-memory caches

Description

When decorating a RazorPage (or MVC Controller Action) with [AuthorizeForScopes(Scopes = new[] { "Mail.Read" })], the GraphServiceClient raises a ServiceException, but not a MsalUiRequiredException. Therefore, incremental consent is never requested.

According to this readme (and others I've seen throughout your repos), it's insinuated that it will all be handled behind the scenes.

Reproduction steps

  1. Use the standard templates.
  2. Ensure that your app registration in Azure AD has delegated Mail.Read
  3. Tweak the HomeController as below.
  4. Run.

Error message

An unhandled exception occurred while processing the request.
ServiceException: Code: ErrorAccessDenied
Message: Access is denied. Check credentials and try again.
ServiceException: Code: ErrorAccessDenied Message: Access is denied. Check credentials and try again. ClientRequestId: {GUID}
Microsoft.Graph.HttpProvider.SendAsync(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationToken cancellationToken)
Microsoft.Graph.BaseRequest.SendRequestAsync(object serializableObject, CancellationToken cancellationToken, HttpCompletionOption completionOption)
Microsoft.Graph.BaseRequest.SendAsync<T>(object serializableObject, CancellationToken cancellationToken, HttpCompletionOption completionOption)
Microsoft.Graph.UserMessagesCollectionRequest.GetAsync(CancellationToken cancellationToken)
Gimmal.Portal.Web.Controllers.HomeController.Privacy() in HomeController.cs
+
               var q = await _graphServiceClient.Me.Messages.Request().Top(10).GetAsync();
Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor+TaskOfIActionResultExecutor.Execute(ActionContext actionContext, IActionResultTypeMapper mapper, ObjectMethodExecutor executor, object controller, object[] arguments)
System.Threading.Tasks.ValueTask<TResult>.get_Result()
Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeActionMethodAsync>g__Logged|12_1(ControllerActionInvoker invoker)
Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeNextActionFilterAsync>g__Awaited|10_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, object state, bool isCompleted)
Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Rethrow(ActionExecutedContextSealed context)
Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(ref State next, ref Scope scope, ref object state, ref bool isCompleted)
Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeInnerFilterAsync>g__Awaited|13_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, object state, bool isCompleted)
Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeNextExceptionFilterAsync>g__Awaited|26_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, object state, bool isCompleted)
Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Rethrow(ExceptionContextSealed context)
Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Next(ref State next, ref Scope scope, ref object state, ref bool isCompleted)
Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeNextResourceFilter>g__Awaited|25_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, object state, bool isCompleted)
Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Rethrow(ResourceExecutedContextSealed context)
Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Next(ref State next, ref Scope scope, ref object state, ref bool isCompleted)
Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeFilterPipelineAsync>g__Awaited|20_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, object state, bool isCompleted)
Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker)
Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker)
Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)
Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context)

Id Web logs

No response

Relevant code snippets

Program.cs

var builder = Microsoft.AspNetCore.Builder.WebApplication.CreateBuilder(args);
builder.Services
    .AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"))
    .EnableTokenAcquisitionToCallDownstreamApi()
    .AddMicrosoftGraph(builder.Configuration.GetSection("MicrosoftGraph"))
    .AddInMemoryTokenCaches();

builder.Services.AddControllersWithViews(options =>
{
    var policy = new AuthorizationPolicyBuilder()
        .RequireAuthenticatedUser()
        .Build();

    options.Filters.Add(new AuthorizeFilter(policy));
});

builder.Services.AddRazorPages().AddMicrosoftIdentityUI();

var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

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

app.UseRouting();

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

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

app.Run();

HomeController.cs

[Authorize]
public class HomeController : Controller
{
    private readonly GraphServiceClient _graphServiceClient;

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

    [AuthorizeForScopes(Scopes = new[] { "User.Read" })]
    public async Task<IActionResult> Index()
    {
        // User.Read is in the appsettings.json
        var model = new IndexViewModel();
        var user = await _graphServiceClient.Me.Request().GetAsync().ConfigureAwait(false);
        model.GraphApiResult = user.DisplayName;

        return View(model);
    }

    [AuthorizeForScopes(Scopes = new[] { "Mail.Read" })]
    public async Task<IActionResult> Privacy()
    {
        // Mail.Read is not in the appsettings.json
        var mail = await _graphServiceClient.Me.Messages.Request().Top(10).GetAsync();
        return View();
    }
}

Regression

No response

Expected behavior

I'd like to see a seamless challenge so that the user is prompted to consent to the new scopes.

DaleyKD commented 12 months ago

I've got a much better understanding of how this library works since I posted two weeks ago. I'd STILL love a way that I could not HAVE to duplicate my scopes (one for the attribute and one in the _graphServiceClient call). But I at least understand how to make this work properly now.

jmprieur commented 11 months ago

@DaleyKD : the readme you referenced is for MVC controllers for Blazor pages, see here: https://github.com/AzureAD/microsoft-identity-web/wiki/Managing-incremental-consent-and-conditional-access#in-razor-pages