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
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();
});