DuendeSoftware / Support

Support for Duende Software products
21 stars 0 forks source link

Access token is expired but not being refreshed #1336

Closed youssefbennour closed 3 months ago

youssefbennour commented 3 months ago

Which version of Duende BFF are you using? 2.2.0

Which version of .NET are you using? .NET 8

Describe the bug When forwarding requests to remote apis using yarp and including the user access token with the requests, even when the access token is expired, it's not refreshed, until the actual cookie session is expired and the user re-authenticates again.

Here's my authenticatino setup:

internal static IServiceCollection AddAuthentication(
        this WebApplicationBuilder builder, 
        OpenIdConnectEvents? events = null)
    {
        var authenticationConfig = builder.Configuration.GetSection(AuthenticationOptions.Key) 
                                   ?? throw new ArgumentException("Authentication config is missing!");

        builder.Services.AddOptions<AuthenticationOptions>()
            .Bind(authenticationConfig);

        builder.Services.AddAuthentication(options =>
            {
                options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
                options.DefaultSignOutScheme = OpenIdConnectDefaults.AuthenticationScheme;
            })
            .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
            {
                options.Cookie.Name = authenticationConfig["CookieName"] ?? "__BFF";
                options.ExpireTimeSpan = TimeSpan.FromHours(8);
                options.Cookie.HttpOnly = true;
            })
            .AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options =>
            {
                builder.Configuration.GetSection(AuthenticationOptions.Key).Bind(options);

                AuthenticationOptions authenticationOptions = 
                    authenticationConfig.Get<AuthenticationOptions>() 
                    ?? throw new ArgumentException("Authentication config is missing!") ;

                KeycloakOptions keycloakOptions =
                    builder.Configuration.GetSection(KeycloakOptions.Key)
                        .Get<KeycloakOptions>()
                    ?? throw new ArgumentException("keycloak config is missing!");

                options.Authority = authenticationOptions.GetAuthority(keycloakOptions.ServerUrl);

                if (events is not null) options.Events = events;

                options.RequireHttpsMetadata = false;
                var scopes = authenticationConfig["Scope"]?
                    .Split(" ")
                    .ToList() ?? [];

                options.Scope.Clear();
                scopes.ForEach(scope => options.Scope.Add(scope));
            });
        return builder.Services;
    }

OICD json config:

"AuthenticationOptions": {
    "AuthorityPath": "realms/contably-realm",
    "CookieName": "__BFF",
    "ClientId": ".....",
    "ClientSecret": "…..",
    "ResponseType": "code",
    "ResponseMode": "query",
    "GetClaimsFromUserInfoEndpoint": true,
    "MapInboundClaims": false,
    "SaveTokens": true,
    "Scope": "openid profile offline_access"
  },

Program.cs:

builder.Services.AddProxy(builder.Configuration);

builder.AddKeycloak();
builder.Services.AddBff()
    .AddRemoteApis();
builder.AddAuthentication();

var app = builder.Build();

app.UseHttpsRedirection();
app.UseExceptionHandling();
app.UseRouting();
app.UseBff();
app.UseAuthentication();
app.UseAuthorization();
app.MapReverseProxy();
app.MapBffManagementEndpoints();

app.MapTenants();

app.Run();

YARP configuration:

internal static IServiceCollection AddProxy(this IServiceCollection services, ConfigurationManager configuration)
    {
        var reverseProxyConfig = 
            configuration.GetSection("ReverseProxy") ?? throw new ArgumentException("ReverseProxy section is missing!");

        services.AddReverseProxy()
            .AddBffExtensions()
            .LoadFromConfig(reverseProxyConfig)
            .AddTransforms(builderContext =>
            {
                builderContext.AddRequestTransform(async transformContext =>
                {
                    var accessToken = await transformContext.HttpContext.GetTokenAsync("access_token");
                    if(accessToken is not null)
                    {
                        transformContext.ProxyRequest.Headers.Authorization = 
                            new AuthenticationHeaderValue(JwtBearerDefaults.AuthenticationScheme, accessToken);
                    }                            
                });
            });

        return services;
    } 

To Reproduce

Steps to reproduce the behavior.

Expected behavior I Expect the access token to refresh on expiry and a new cookie is issued with the new access token.

youssefbennour commented 3 months ago

In YARP's request transformation I was using this code to obtain the access token:

var accessToken = await transformContext.HttpContext.GetTokenAsync("access_token");

I've replaced it with this line of code, and It works fine, the access token is refreshed as needed:

var accessToken = await transformContext.HttpContext.GetUserAccessTokenAsync();
AndersAbel commented 3 months ago

Great to hear that you resolved it yourself. To add some context for anyone else reading this thread: