I have seen many same or similar questions, and tried all their answers if there was one, but none of those works for me.
I'm using this example from Microsoft's Github account as my project base.
It works well for just signing in users.
The project has 1 WebApi, 1 Angular App.
Then I followed this Microsoft example to add code to call Graph API. Here is the controller code:
[Authorize]
[Route("api/[controller]")]
[ApiController]
public class BillsController : ControllerBase
{
static readonly string[] scopeRequiredByApi = new string[] { "access_as_user" };
readonly ITokenAcquisition tokenAcquisition;
readonly WebOptions webOptions;
public BillsController(ITokenAcquisition tokenAcquisition,
IOptions<WebOptions> webOptionValue)
{
this.tokenAcquisition = tokenAcquisition;
this.webOptions = webOptionValue.Value;
}
[HttpGet]
[AuthorizeForScopes(Scopes = new[] { Constants.ScopeUserRead, Constants.ScopeMailRead })]
public async Task<IActionResult> Profile()
{
HttpContext.VerifyUserHasAnyAcceptedScope(scopeRequiredByApi);
var subject = string.Empty;
try
{
// Initialize the GraphServiceClient.
Graph::GraphServiceClient graphClient = GetGraphServiceClient(new[] { Constants.ScopeUserRead, Constants.ScopeMailRead });
var me = await graphClient.Me.Request().GetAsync();
// Get user photo
var messages = await graphClient.Me.MailFolders.Inbox.Messages.Request().GetAsync();
subject = messages.First().Subject;
return Ok(subject);
}
catch (System.Exception ex)
{
throw ex;
}
}
private Graph::GraphServiceClient GetGraphServiceClient(string[] scopes)
{
return GraphServiceClientFactory.GetAuthenticatedGraphClient(async () =>
{
string result = await tokenAcquisition.GetAccessTokenForUserAsync(scopes);
return result;
}, webOptions.GraphApiUrl);
}
}
For Startup.cs
public void ConfigureServices(IServiceCollection services)
{
// Setting configuration for protected web api
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddProtectedWebApi(Configuration);
services.AddWebAppCallsProtectedWebApi(Configuration, new string[] { Constants.ScopeUserRead, Constants.ScopeMailRead })
.AddInMemoryTokenCaches();
services.AddOptions();
services.AddGraphService(Configuration);
// Creating policies that wraps the authorization requirements
services.AddAuthorization();
services.AddDbContext<TodoContext>(opt => opt.UseInMemoryDatabase("TodoList"));
services.AddControllers();
// Allowing CORS for all domains and methods for the purpose of sample
services.AddCors(o => o.AddPolicy("default", builder =>
{
builder.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader();
}));
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
// Since IdentityModel version 5.2.1 (or since Microsoft.AspNetCore.Authentication.JwtBearer version 2.2.0),
// Personal Identifiable Information is not written to the logs by default, to be compliant with GDPR.
// For debugging/development purposes, one can enable additional detail in exceptions by setting IdentityModelEventSource.ShowPII to true.
// Microsoft.IdentityModel.Logging.IdentityModelEventSource.ShowPII = true;
app.UseDeveloperExceptionPage();
}
else
{
// 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.UseExceptionHandler("/error");
app.UseCors("default");
app.UseHttpsRedirection();
app.UseCookiePolicy();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
On the Angular App, I added one button to call this Profile() controller action.
todo-view.component.ts
getEmails(): void {
this.service.getEmails().subscribe({
next: (emails: any) => {
alert(emails);
},
error: (err: any) => {
console.log("error happened~!");
console.log(err);
}
});
}
todo-view.component.html
<button (click)="getEmails();">Get Emails</button>

I added the below code into my Startup.cs and removed the AddWebAppCallsProtectedWebApi. services.AddProtectedWebApiCallsProtectedWebApi(Configuration).AddInMemoryTokenCaches();
Now it throws me a different error message:

I was having the same issue with a react app. Since the AuthorizeForScopes is for returning views, it does not work for API solutions. I was able to add some configuration options to get it working.
The first thing I did was use a SQL cache. This helped stop the "No login hint" error when the site restarted. After that, the token would work fine until timeout, after which the token would get removed from the cache and the error would reappear.
For that, I started looking at the configuration settings. I changed my configuration to the following.
services
.AddWebAppCallsProtectedWebApi(new string[] { "User.Read" }, idOps =>
{
Configuration.Bind("AzureAd", idOps);
idOps.SaveTokens = true;
idOps.RefreshOnIssuerKeyNotFound = true;
idOps.SingletonTokenAcquisition = true;
idOps.UseTokenLifetime = true;
},
ops => Configuration.Bind("AzureAd", ops))
.AddDistributedTokenCaches()
.AddDistributedSqlServerCache(options =>
{
options.ConnectionString = Configuration.GetConnectionString("Site_DbContext");
options.SchemaName = "dbo";
options.TableName = "_TokenCache";
});
I haven't done much testing on it to find out the magic combination, but the sweet spot seems to be SingletonTokenAcquisition. With that set, it seems to be behaving like a hybrid cache. When first set, it pulls the token into memory and holds it so if it is removed from the database cache, it still has access to it.
The other settings may be necessary for the refreshing but I haven't tested that yet.
The one thing I did notice is the token doesn't get added back to the SQL cache until it refreshes so if something happens where the token is removed and the site goes down clearing the memory, the error may reappear, but this is the best solution I found so far. I was able to have my SPA running for 24 hours and was still able to pull new data.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With