Я не смог правильно реализовать API Microsoft Graph в моем веб -приложении. Основное использование на данный момент состоит в том, чтобы позволить пользователям создавать, обновлять и удалять события календаря с помощью определенных действий в приложении. Т.е. Создание встречи. У меня есть зарегистрированное приложение AAD со следующими делегированными разрешениями для Microsoft Graph, все с согласием администратора: < /p>
Microsoft Graph < /p>
li> calendars.read
[*]calendars.readwrite
[*] Электронная почта
[*]group.read.all
groupmember.read.all
[*] offline_access < /li>
openid < /li>
Профиль < /li>
user.read
< /ul>
Кроме того, у меня есть претензия «Группы». < /p>
Вот код и конфигурация, которые я пытался использовать: < /p>
appsettings.json:
{
"AppConfig": {
"Endpoint": "https://ipsumlorumconfig.azconfig.io"
},
"DownstreamApi": {
"BaseUrl": "https://graph.microsoft.com/v1.0",
"Scopes": "user.read calendars.readwrite"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}
< /code>
HomeController/TestGraph Метод:
// Test action method
[HttpGet]
public async Task TestGraph()
{
try
{
// Fetch the default calendar
var calendar = await _graphServiceClient.Me.Calendar
.GetAsync();
var resultString = $"Calendar Name: {calendar!.Name}\n";
return Content(resultString, "text/plain");
}
catch (ServiceException ex)
{
// Log and handle Graph API errors
_logger.LogError(ex, "Graph API call failed.");
return StatusCode(500, "Graph API call failed.");
}
}
< /code>
Program.cs:
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.EntityFrameworkCore;
using Microsoft.Identity.Web;
using CRMApp.WebApp.Services;
using Microsoft.AspNetCore.Mvc.Authorization;
using Microsoft.AspNetCore.Mvc.ViewEngines;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using CRMApp.WebApp.Middleware;
using Microsoft.IdentityModel.Tokens;
using CRMApp.WebApp.Constants;
using CRMApp.Shared.Data;
using CRMApp.Shared;
var builder = WebApplication.CreateBuilder(args);
string[]? initialScopes = builder.Configuration.GetValue("DownstreamApi:Scopes")?.Split(' ');
// Load configuration files
builder.Configuration
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", optional: true, reloadOnChange: true)
.AddJsonFile("appsettings.Local.json", optional: true, reloadOnChange: true)
.AddEnvironmentVariables();
// Configure Logging
builder.Logging.ClearProviders();
builder.Logging.AddConsole();
builder.Logging.SetMinimumLevel(LogLevel.Debug);
// Add services
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowSpecificOrigin",
policyBuilder =>
{
policyBuilder.WithOrigins(
"https://crmapp.azurewebsites.net",
"https://crmappcompany.co.za",
"https://crmapp-development.azurewebsites.net",
"https://crmapp-uat.azurewebsites.net",
"https://crmapp-staging.azurewebsites.net"
)
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials();
});
});
// Register IHttpContextAccessor
builder.Services.AddHttpContextAccessor();
// Register ICompositeViewEngine and ITempDataProvider for CustomExceptionMiddleware
builder.Services.AddSingleton();
builder.Services.AddSingleton();
// Configure Azure AD Authentication (all environments use Azure AD)
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(builder.Configuration)
.EnableTokenAcquisitionToCallDownstreamApi(initialScopes)
.AddMicrosoftGraph(builder.Configuration.GetSection("DownstreamApi"))
.AddInMemoryTokenCaches();
builder.Services.Configure(OpenIdConnectDefaults.AuthenticationScheme, options =>
{
options.ResponseType = "id_token";
options.Scope.Add("profile");
options.Scope.Add("openid");
options.Scope.Add("email");
options.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "preferred_username",
RoleClaimType = "groups"
};
options.Events = new OpenIdConnectEvents
{
OnTokenValidated = context =>
{
var logger = context.HttpContext.RequestServices.GetRequiredService();
var emailClaim = context.Principal?.FindFirst("preferred_username")?.Value;
if (string.IsNullOrEmpty(emailClaim))
{
logger.LogWarning("Email claim is missing in the token.");
context.Fail("Email claim is missing.");
return Task.CompletedTask;
}
logger.LogInformation("Token validated for {Email}", emailClaim);
if (context.Principal == null)
{
logger.LogWarning("Token validated, but Principal is null.");
context.Fail("Principal is null.");
return Task.CompletedTask;
}
foreach (var claim in context.Principal.Claims)
{
logger.LogDebug("Claim Type: {Type}, Value: {Value}", claim.Type, claim.Value);
}
return Task.CompletedTask;
},
OnAuthenticationFailed = context =>
{
var logger = context.HttpContext.RequestServices.GetRequiredService();
logger.LogError("Authentication failed: {Error}", context.Exception.Message);
return Task.CompletedTask;
},
OnRedirectToIdentityProvider = context =>
{
var logger = context.HttpContext.RequestServices.GetRequiredService();
logger.LogInformation("Redirecting to Identity Provider for {User}",
context.HttpContext.User.Identity?.Name ?? "Anonymous");
return Task.CompletedTask;
}
};
});
// Define Authorization Policies
builder.Services.AddAuthorizationBuilder()
.AddPolicy("CRMAccessGroupPolicy", policy =>
policy.RequireClaim("groups", GroupConstants.CRMAccessGroup))
.AddPolicy("CRMApprovalAccessGroupPolicy", policy =>
policy.RequireClaim("groups", GroupConstants.CRMApprovalAccessGroup));
builder.Services.AddControllersWithViews(options =>
{
options.Filters.Add(new AuthorizeFilter("CRMAccessGroupPolicy"));
});
builder.Services.AddRazorPages();
// Register Application Services
builder.Services.AddScoped();
builder.Services.AddScoped();
builder.Services.AddScoped();
// Configure DbContext with Retry Logic
builder.Services.AddDbContext((serviceProvider, options) =>
{
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
options.UseSqlServer(connectionString, sqlOptions =>
{
sqlOptions.EnableRetryOnFailure(
maxRetryCount: 5,
maxRetryDelay: TimeSpan.FromSeconds(10),
errorNumbersToAdd: null
);
});
});
// Register the AppointmentCleanupService
builder.Services.AddScoped();
var app = builder.Build();
// Register CustomExceptionMiddleware as the first middleware
app.UseMiddleware();
if (app.Environment.IsDevelopment() || app.Environment.EnvironmentName == "Local")
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseCors("AllowSpecificOrigin");
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
app.MapRazorPages();
app.Run();
< /code>
Edit 1: The one error I receive is:
An unhandled exception occurred while processing the request.
MsalUiRequiredException: No account or login hint was passed to the AcquireTokenSilent call.
Microsoft.Identity.Client.Internal.Requests.Silent.SilentRequest.ExecuteAsync(CancellationToken cancellationToken)
MicrosoftIdentityWebChallengeUserException: IDW10502: An MsalUiRequiredException was thrown due to a challenge for the user. See https://aka.ms/ms-id-web/ca_incremental-consent.
Microsoft.Identity.Web.TokenAcquisition.GetAuthenticationResultForUserAsync(IEnumerable scopes, string authenticationScheme, string tenantId, string userFlow, ClaimsPrincipal user, TokenAcquisitionOptions tokenAcquisitionOptions)
< /code>
Edit 2: New code that produces different error is below:
Program.cs
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.EntityFrameworkCore;
using Microsoft.Identity.Web;
using EnvisionCRM.WebApp.Services;
using Microsoft.AspNetCore.Mvc.Authorization;
using Microsoft.AspNetCore.Mvc.ViewEngines;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using EnvisionCRM.WebApp.Middleware;
using Microsoft.IdentityModel.Tokens;
using EnvisionCRM.WebApp.Constants;
using EnvisionCRM.Shared.Data;
using EnvisionCRM.Shared;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
var builder = WebApplication.CreateBuilder(args);
string[]? initialScopes = builder.Configuration.GetValue("DownstreamApi:Scopes")?.Split(' ');
// Load configuration files
builder.Configuration
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", optional: true, reloadOnChange: true)
.AddJsonFile("appsettings.Local.json", optional: true, reloadOnChange: true)
.AddEnvironmentVariables();
// Configure Logging
builder.Logging.ClearProviders();
builder.Logging.AddConsole();
builder.Logging.SetMinimumLevel(LogLevel.Debug);
// Add services
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowSpecificOrigin",
policyBuilder =>
{
policyBuilder.WithOrigins( // Values were removed for StackOverflow.
"https://.azurewebsites.net",
"https://*******",
"https://-development.azurewebsites.net",
"https://-uat.azurewebsites.net",
"https://-staging.azurewebsites.net"
)
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials();
});
});
// Register IHttpContextAccessor
builder.Services.AddHttpContextAccessor();
// Register ICompositeViewEngine and ITempDataProvider for CustomExceptionMiddleware
builder.Services.AddSingleton();
builder.Services.AddSingleton();
// Configure Azure AD Authentication (all environments use Azure AD)
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"))
.EnableTokenAcquisitionToCallDownstreamApi(initialScopes)
.AddMicrosoftGraph(builder.Configuration.GetSection("DownstreamApi"))
.AddInMemoryTokenCaches();
builder.Services.Configure(OpenIdConnectDefaults.AuthenticationScheme, options =>
{
options.ResponseType = "code";
options.Scope.Add("profile");
options.Scope.Add("openid");
options.Scope.Add("email");
options.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "preferred_username",
RoleClaimType = "groups"
};
options.Events = new OpenIdConnectEvents
{
OnTokenValidated = context =>
{
var logger = context.HttpContext.RequestServices.GetRequiredService();
var emailClaim = context.Principal?.FindFirst("preferred_username")?.Value;
if (string.IsNullOrEmpty(emailClaim))
{
logger.LogWarning("Email claim is missing in the token.");
context.Fail("Email claim is missing.");
return Task.CompletedTask;
}
logger.LogInformation("Token validated for {Email}", emailClaim);
if (context.Principal == null)
{
logger.LogWarning("Token validated, but Principal is null.");
context.Fail("Principal is null.");
return Task.CompletedTask;
}
foreach (var claim in context.Principal.Claims)
{
logger.LogDebug("Claim Type: {Type}, Value: {Value}", claim.Type, claim.Value);
}
return Task.CompletedTask;
},
OnAuthenticationFailed = context =>
{
var logger = context.HttpContext.RequestServices.GetRequiredService();
logger.LogError("Authentication failed: {Error}", context.Exception.Message);
return Task.CompletedTask;
},
OnRedirectToIdentityProvider = context =>
{
var logger = context.HttpContext.RequestServices.GetRequiredService();
logger.LogInformation("Redirecting to Identity Provider for {User}",
context.HttpContext.User.Identity?.Name ?? "Anonymous");
return Task.CompletedTask;
}
};
});
// Define Authorization Policies
builder.Services.AddAuthorizationBuilder()
.AddPolicy("CRMAccessGroupPolicy", policy =>
policy.RequireClaim("groups", GroupConstants.CRMAccessGroup))
.AddPolicy("CRMApprovalAccessGroupPolicy", policy =>
policy.RequireClaim("groups", GroupConstants.CRMApprovalAccessGroup));
builder.Services.AddControllersWithViews(options =>
{
options.Filters.Add(new AuthorizeFilter("CRMAccessGroupPolicy"));
});
builder.Services.AddRazorPages();
// Register Application Services
builder.Services.AddScoped();
builder.Services.AddScoped();
builder.Services.AddScoped();
// Configure DbContext with Retry Logic
builder.Services.AddDbContext((serviceProvider, options) =>
{
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
options.UseSqlServer(connectionString, sqlOptions =>
{
sqlOptions.EnableRetryOnFailure(
maxRetryCount: 5,
maxRetryDelay: TimeSpan.FromSeconds(10),
errorNumbersToAdd: null
);
});
});
// Register the AppointmentCleanupService
builder.Services.AddScoped();
var app = builder.Build();
// Register CustomExceptionMiddleware as the first middleware
app.UseMiddleware();
if (app.Environment.IsDevelopment() || app.Environment.EnvironmentName == "Local")
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseCors("AllowSpecificOrigin");
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
app.MapRazorPages();
app.Run();
< /code>
The error is:
warn: Microsoft.Identity.Web.TokenAcquisition[0]
False MSAL 4.67.2.0 MSAL.NetCore .NET 9.0.1 Microsoft Windows 10.0.26100 [2025-01-24 04:19:17Z] Only in-memory caching is used. The cache is not persisted and will be lost if the machine is restarted. It also does not scale for a web app or web API, where the number of users can grow large. In production, web apps and web APIs should use distributed caching like Redis. See https://aka.ms/msal-net-cca-token-cache-serialization
fail: Microsoft.Identity.Web.TokenAcquisition[0]
False MSAL 4.67.2.0 MSAL.NetCore .NET 9.0.1 Microsoft Windows 10.0.26100 [2025-01-24 04:19:17Z] Exception type: Microsoft.Identity.Client.MsalUiRequiredException
, ErrorCode: user_null
HTTP StatusCode 0
CorrelationId 00000000-0000-0000-0000-000000000000
To see full exception details, enable PII Logging. See https://aka.ms/msal-net-logging
at Microsoft.Identity.Client.Internal.Requests.Silent.SilentRequest.ExecuteAsync(CancellationToken cancellationToken)
at Microsoft.Identity.Client.Internal.Requests.RequestBase.c__DisplayClass11_1.d.MoveNext()
--- End of stack trace from previous location ---
at Microsoft.Identity.Client.Utils.StopwatchService.MeasureCodeBlockAsync(Func`1 codeBlock)
at Microsoft.Identity.Client.Internal.Requests.RequestBase.RunAsync(CancellationToken cancellationToken)
< /code>
Edit 3: Erro received in private browser
An unhandled exception occurred while processing the request.
MsalUiRequiredException: No account or login hint was passed to the AcquireTokenSilent call.
Microsoft.Identity.Client.Internal.Requests.Silent.SilentRequest.ExecuteAsync(CancellationToken cancellationToken)
MicrosoftIdentityWebChallengeUserException: IDW10502: An MsalUiRequiredException was thrown due to a challenge for the user. See https://aka.ms/ms-id-web/ca_incremental-consent.
Microsoft.Identity.Web.TokenAcquisition.GetAuthenticationResultForUserAsync(IEnumerable scopes, string authenticationScheme, string tenantId, string userFlow, ClaimsPrincipal user, TokenAcquisitionOptions tokenAcquisitionOptions)
Подробнее здесь: https://stackoverflow.com/questions/793 ... h-calendar
Подключить C# ASP.NET CORE WEB -приложение к календарю Microsoft Graph ⇐ C#
-
- Похожие темы
- Ответы
- Просмотры
- Последнее сообщение
-
-
Отказ в доступе к Календарю по-прежнему позволяет предоставить доступ к календарю приложению.
Anonymous » » в форуме IOS - 0 Ответы
- 80 Просмотры
-
Последнее сообщение Anonymous
-
-
-
Тип «ErrorDetails» отсутствует в Microsoft.Graph/Graph.Core после обновления.
Anonymous » » в форуме C# - 0 Ответы
- 64 Просмотры
-
Последнее сообщение Anonymous
-