AzureAD / microsoft-identity-web

Helps creating protected web apps and web APIs with Microsoft identity platform and Azure AD B2C
MIT License
681 stars 211 forks source link

.Net Project Graph SDK Token is expiring when deployed to Azure App Service #3106

Open ricardomatos95 opened 1 week ago

ricardomatos95 commented 1 week ago

Microsoft.Identity.Web Library

Microsoft.Identity.Web

Microsoft.Identity.Web version

3.2.2

Web app

Sign-in users and call web APIs

Web API

Protected web APIs (validating tokens)

Token cache serialization

In-memory caches

Description

I developed a .NET webapp and after deploying it into Azure App servicey the access token seems to be expiring after 1h and not refreshing and throwing me the error:

ODataError: Lifetime validation failed, the token is expired.

Currently I have a class called Invite.cshtml.cs that looks like this:

namespace Test_Web_App.Pages
{
    [AuthorizeForScopes(ScopeKeySection = "MicrosoftGraph:Scopes")]
    public class InviteModel : PageModel
    {
        private readonly GraphServiceClient _graphServiceClient;
        private readonly ILogger<IndexModel> _logger;
        private readonly IConfiguration _configuration;
        private readonly IHttpContextAccessor _httpContextAccessor;

        public InviteModel(  ILogger<IndexModel> logger, IConfiguration configuration, GraphServiceClient graphServiceClient, IHttpContextAccessor httpContextAccessor)
        {
            _logger = logger;
            _graphServiceClient = graphServiceClient; ;
            _configuration = configuration;
            _httpContextAccessor = httpContextAccessor;
        }

public async Task<IActionResult> OnPostAsync()
{
        var user = await _graphServiceClient.Me.GetAsync(); ;
//Other actions
 }

The code after 1 hour of the user being signed in throws a server error for token expiration on my OnPostAsync method as soon as it tries to load the graphServiceClient:

public async Task<IActionResult> OnPostAsync()
{
        var user = await _graphServiceClient.Me.GetAsync(); ;
}

On my Program.cs I have setup the following:

var builder = WebApplication.CreateBuilder(args);

var initialScopes = builder.Configuration["AzureAd:Scopes"]?.Split(' ') ?? builder.Configuration["MicrosoftGraph:Scopes"]?.Split(' ');

// Add services to the container.
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApp( options =>
    {
        builder.Configuration.Bind("AzureAd", options);
        options.SaveTokens = true; // Ensure tokens are saved
    })
        .EnableTokenAcquisitionToCallDownstreamApi(initialScopes)
            .AddInMemoryTokenCaches().AddMicrosoftGraph();

builder.Services.AddAuthorization(options =>
{
    // By default, all incoming requests will be authorized according to the default policy.
    options.FallbackPolicy = options.DefaultPolicy;
});
builder.Services.AddRazorPages().AddMicrosoftIdentityUI();
var app = builder.Build();

Reproduction steps

  1. Create builder for Initialize GraphServiceClient.
  2. Create class to send call to graphServiceClient (e.g., graphServiceClient.Me.GetAsync(); )
  3. Publish application to Azure Web App
  4. Open Azure Web app trigger Post method.
  5. Wait 1 hour for token to expire and trigger post method again

Error message

ODataError: Lifetime validation failed, the token is expired.

Microsoft.Kiota.Http.HttpClientLibrary.HttpClientRequestAdapter.ThrowIfFailedResponseAsync(HttpResponseMessage response, Dictionary<string, ParsableFactory> errorMapping, Activity activityForAttributes, CancellationToken cancellationToken) Microsoft.Kiota.Http.HttpClientLibrary.HttpClientRequestAdapter.SendAsync(RequestInformation requestInfo, ParsableFactory factory, Dictionary<string, ParsableFactory> errorMapping, CancellationToken cancellationToken) Microsoft.Kiota.Http.HttpClientLibrary.HttpClientRequestAdapter.SendAsync(RequestInformation requestInfo, ParsableFactory factory, Dictionary<string, ParsableFactory> errorMapping, CancellationToken cancellationToken) Microsoft.Graph.Me.MeRequestBuilder.GetAsync(Action<RequestConfiguration> requestConfiguration, CancellationToken cancellationToken) External_Guest_Web_App.Pages.InviteGuestModel.OnPostAsync() in InviteGuest.cshtml.cs Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.ExecutorFactory+GenericTaskHandlerMethod.Convert(object taskAsObject) Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.ExecutorFactory+GenericTaskHandlerMethod.Execute(object receiver, object[] arguments) Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker.InvokeHandlerMethodAsync() Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker.InvokeNextPageFilterAsync() Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker.Rethrow(PageHandlerExecutedContext context) Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker.Next(ref State next, ref Scope scope, ref object state, ref bool isCompleted) Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker.InvokeInnerFilterAsync() Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.gAwaited|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.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.gAwaited|20_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, object state, bool isCompleted) Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.gLogged|17_1(ResourceInvoker invoker) Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.g__Logged|17_1(ResourceInvoker invoker) Microsoft.AspNetCore.Routing.EndpointMiddleware.gAwaitRequestTask|7_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:

// Add services to the container.
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApp( options =>
    {
        builder.Configuration.Bind("AzureAd", options);
        options.SaveTokens = true; // Ensure tokens are saved
    })
        .EnableTokenAcquisitionToCallDownstreamApi(initialScopes)
            .AddInMemoryTokenCaches().AddMicrosoftGraph();

 Invite.cshtml.cs: 
namespace Test_Web_App.Pages
{
    [AuthorizeForScopes(ScopeKeySection = "MicrosoftGraph:Scopes")]
    public class InviteModel : PageModel
    {
        private readonly GraphServiceClient _graphServiceClient;
        private readonly ILogger<IndexModel> _logger;
        private readonly IConfiguration _configuration;
        private readonly IHttpContextAccessor _httpContextAccessor;

        public InviteModel(  ILogger<IndexModel> logger, IConfiguration configuration, GraphServiceClient graphServiceClient, IHttpContextAccessor httpContextAccessor)
        {
            _logger = logger;
            _graphServiceClient = graphServiceClient; ;
            _configuration = configuration;
            _httpContextAccessor = httpContextAccessor;
        }

public async Task<IActionResult> OnPostAsync()
{
        var user = await _graphServiceClient.Me.GetAsync(); ;
//Other actions
 }

Regression

No response

Expected behavior

I expected the builder settings in Program.cs to automatically refresh the Access token when making requests to graph API.

TanguyPa commented 1 week ago

I seem to have a similar issue, also on my side the refresh token never seems to be used. The difference is that I don't get an error but simply a redirection to the identity provider (login.microsoft.com) to retrieve a new access token and another token id. I'm using ASP MVC in .NET 8. I've tried inmemory cache and distributed cache with SQL Server, considerably increasing cookie and/or cache expiration. For my part, I expect the access token or id token to be renewed with the refresh token when the latter is used, and the redirection to take place only when the refresh token is not usable. I've analyzed the information stored in debug and in my cache, and I do have a refresh token, but it's not being used.

ricardomatos95 commented 6 days ago

@TanguyPa Well on my case I solved it after checking this:

https://github.com/AzureAD/microsoft-identity-web/issues/2880

Was indeed the "easy auth" or App Service Authentication being enabled from Azure App Service that was causing it. After disabling it it just used my code for authentication and seems to work perfectly.

jmprieur commented 2 days ago

Are you using App services authentication?

TanguyPa commented 23 hours ago

No, i'm using IIS Server to host my app. I have already opened an issue some time ago on this problem. I tried to increase the lifetime of cookies so as not to depend on the lifetime of the session and the SQL cache. https://github.com/AzureAD/microsoft-identity-web/issues/1593