Home > Mobile >  GraphServiceClient not working with Microsoft Identity Web in Razor Page model
GraphServiceClient not working with Microsoft Identity Web in Razor Page model

Time:11-05

I have a situation where I can successfully use GraphServiceClient in my Startup but not in my Razor Page. I am using Microsoft Identity Web with Microsoft Graph and have followed the MS Documentation enter image description here

My settings is below and although it's a multi-tenant app I only have one customer and therefore setting the TenantId to my customer tenant id for a streamlined login experience.

"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"TenantId": "<Customer Tenant Id>",
"ClientId": "<My App Reg Id>",
"ClientSecret": "<My App Reg Secret>",
"CallbackPath": "/signin-oidc"
},
"DownstreamApi": {
"BaseUrl": "https://graph.microsoft.com/v1.0",
"Scopes": "https://graph.microsoft.com/.default"
}

My Startup has the following ConfigureServices method. Note that the call to graphServiceClient.Me.Request().GetAsync() works here and returns the user info from my customer Azure AD. I plan to use this to populate my local database with extra user info (ie photo) when the user logs in to my app.

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddAuthorization(options =>
        {
            options.FallbackPolicy = options.DefaultPolicy;
        });
        services.AddRazorPages()
            .AddMvcOptions(options => { })
            .AddMicrosoftIdentityUI();

        var initialScopes = Configuration.GetValue<string>("DownstreamApi:Scopes")?.Split(',');
        services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
            .AddMicrosoftIdentityWebApp(Configuration.GetSection("AzureAd"))
            .EnableTokenAcquisitionToCallDownstreamApi(initialScopes)
            .AddMicrosoftGraph(Configuration.GetSection("DownstreamApi"))
            .AddInMemoryTokenCaches();

        services.Configure<OpenIdConnectOptions>(OpenIdConnectDefaults.AuthenticationScheme, options =>
        {
            options.TokenValidationParameters.IssuerValidator = ValidateSpecificIssuers; //restrict access for multi-tenant app
            options.Events = new OpenIdConnectEvents
            {
                OnTokenValidated = async context =>
                {
                    GraphServiceClient graphServiceClient = new GraphServiceClient(new DelegateAuthenticationProvider(async request =>
                    {
                        // Add the access token in the Authorization header of the API request.
                        request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", context.TokenEndpointResponse.AccessToken);
                    }));

                    try
                    {
                        var user = await graphServiceClient.Me.Request().GetAsync(); // THIS CALL WORKS!
                    }
                    catch (Exception ex)
                    {
                        throw new UnauthorizedAccessException("Cannot get user info from AAD");
                    }

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


    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseExceptionHandler("/Error");
            // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
            app.UseHsts();
        }

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

        app.UseRouting();

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

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapRazorPages();
            endpoints.MapControllers();
        });
    }

    private string ValidateSpecificIssuers(string issuer, SecurityToken securityToken, TokenValidationParameters validationParameters)
    {
        var validIssuers = Configuration["AllowedIssuers"];
        if (validIssuers != null && validIssuers.Split(',').Select(tid => $"https://login.microsoftonline.com/{tid}").Contains(issuer))
        {
            return issuer;
        }
        else
        {
            throw new SecurityTokenInvalidIssuerException("The user account does not belong to an allowed tenant");
        }
    }

}

Here is my Razor Page however it always fails on the same graph call even though it works in my Startup file. I have tried changing the scope from "https://graph.microsoft.com/v1.0" to "user.read" everywhere but it still fails at this line.

var user = await _graphServiceClient.Me.Request().GetAsync();

[AuthorizeForScopes(Scopes = new[] { "https://graph.microsoft.com/.default" })]
public class IndexModel : PageModel
{
    private readonly ILogger<IndexModel> _logger;
    private readonly GraphServiceClient _graphServiceClient;

    public IndexModel(ILogger<IndexModel> logger, GraphServiceClient graphServiceClient)
    {
        _logger = logger;
        _graphServiceClient = graphServiceClient;
    }

    public async void OnGet()
    {
        try
        {
            var user = await _graphServiceClient.Me.Request().GetAsync(); // THIS CALL FAILS!
            using (var photoStream = await _graphServiceClient.Me.Photo.Content.Request().GetAsync())
            {
                byte[] photoByte = ((MemoryStream)photoStream).ToArray();
                ViewData["photo"] = Convert.ToBase64String(photoByte);
            }
            ViewData["name"] = user.DisplayName;
        }
        catch (Exception e)
        {
            _logger.LogError(e.Message);
        }

    }
}

Here is the stack trace of the error.

Microsoft.Graph.ServiceException
  HResult=0x80131500
  Message=Code: generalException
Message: An error occurred sending the request.

  Source=Microsoft.Graph.Core
  StackTrace:
   at Microsoft.Graph.HttpProvider.<SendRequestAsync>d__19.MoveNext()
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.ConfiguredTaskAwaitable`1.ConfiguredTaskAwaiter.GetResult()
   at Microsoft.Graph.HttpProvider.<SendAsync>d__18.MoveNext()
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.ConfiguredTaskAwaitable`1.ConfiguredTaskAwaiter.GetResult()
   at Microsoft.Graph.BaseRequest.<SendRequestAsync>d__40.MoveNext()
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.ConfiguredTaskAwaitable`1.ConfiguredTaskAwaiter.GetResult()
   at Microsoft.Graph.BaseRequest.<SendAsync>d__34`1.MoveNext()
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.ConfiguredTaskAwaitable`1.ConfiguredTaskAwaiter.GetResult()
   at Microsoft.Graph.UserRequest.<GetAsync>d__5.MoveNext()
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
   at Test365.Pages.IndexModel.<OnGet>d__3.MoveNext() in C:\Users\ojmcf\source\repos\Test365\Pages\Index.cshtml.cs:line 28

  This exception was originally thrown at this call stack:
    [External Code]

Inner Exception 1:
MicrosoftIdentityWebChallengeUserException: IDW10502: An MsalUiRequiredException was thrown due to a challenge for the user. See https://aka.ms/ms-id-web/ca_incremental-consent. 

Inner Exception 2:
MsalUiRequiredException: No account or login hint was passed to the AcquireTokenSilent call. 

CodePudding user response:

Update

Try to add /v2.0 in your url like below.

validIssuers.Split(',').Select(tid => $"https://login.microsoftonline.com/{tid}/v2.0").Contains(issuer)

Replace the following code

[AuthorizeForScopes(Scopes = new[] { "https://graph.microsoft.com/v1.0" })]

with

[AuthorizeForScopes(Scopes = new[] { "user.read" })]

, and test it.

Tips

This url(https://graph.microsoft.com/v1.0) should be in appsettings.json file.

CodePudding user response:

Simple fix in the end. I had assumed Razor Pages was handling the authorization globally as setup in Startup file so I overlooked the [Authorize] attribute at the top of the Page Model. Once I added that it started working as expected.

However for convenience I ended up removing the [Authorize] attribute from Page Model and instead added it globally using RequireAuthorization() under endpoint routing in Configure method of Startup file.

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorPages().RequireAuthorization();
        endpoints.MapControllers();
    });
  • Related