microsoft-identity-web
microsoft-identity-web copied to clipboard
AuthorizeForScopes Not Challenging for GraphServiceClient
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
- Use the standard templates.
- Ensure that your app registration in Azure AD has delegated Mail.Read
- Tweak the HomeController as below.
- 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.
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.
@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