DuendeSoftware / Support

Support for Duende Software products
21 stars 0 forks source link

Error after upgrading BFF #826

Closed krispenner closed 1 year ago

krispenner commented 1 year ago

Which version of Duende BFF are you using? 2.1.1

Which version of .NET are you using? 6.0

Describe the issue

We are a paying customer with a license for BFF. Since we upgraded to your latest version we now get an error with the bearer token in AccessTokenRequestTransform. I'm looking at code changes in GitHub and I see a lot of changes with added support for DPoP and how the token is now passed around (no longer a simple string). However, from digging through my logs and your code I can't see why my requests are failing now.

Access token is missing. token type: 'Unknown token type', local path: 'Unknown Route', detail: 'Missing access token type - should be one of 'DPoP' or 'Bearer''

There were 8 commits between June 8th and 14th that drastically changed how the AccessTokenRequestTransform uses the bearer token.

Just hoping you can tell me anything obvious (configuration maybe) that I may need to change to make my code work again as everything worked prior to this upgrade.

josephdecock commented 1 year ago

I suspect that this might be a bug related to sessions obtaining access tokens before any of the dpop support existed, and so not reporting a token type at all (bearer vs dpop). Do you get this error for existing users with existing sessions only? Or do you also get it for new sessions?

krispenner commented 1 year ago

It is happening for new sessions both after clearing all cookies and using an Incognito browser. I do have some custom sign-in code that may be affecting it. When we get an access token returned from a backend (BFF) call, we automatically sign-in the user using Cookie authentication. I had to sort of hack this together to make it work, maybe this is now the issue?


    internal class SignInAccessTokenTransform : ResponseTransform
    {
        private readonly ILogger _logger;

        public SignInAccessTokenTransform(ILogger<SignInAccessTokenTransform> logger)
        {
            ArgCheck.NotNull(nameof(logger), logger);

            _logger = logger;
        }

        public override async ValueTask ApplyAsync(ResponseTransformContext context)
        {
            // Check for an access token returned in the API response.

            if (context?.HttpContext == null || context.ProxyResponse?.Headers?.TryGetValues("X-API-AuthorizationToken", out var authorizationToken) != true || authorizationToken?.Count() != 1)
            {
                return;
            }

            var accessToken = authorizationToken.LastOrDefault()?.TrimToNull();

            if (accessToken == null)
            {
                return;
            }

            _logger.LogTrace("Access token found in proxied response: " + accessToken);

            // If an access token was found in the API response, validate it.

            var principal = context.HttpContext.RequestServices
                .GetRequiredService<ITokenValidationService>()
                .ValidateToken(accessToken, isIdToken: false, out var validatedToken);

            if (principal == null || validatedToken == null)
            {
                _logger.LogWarning(("An access token was found in the proxied response but it could not be validated. " + principal?.Identity?.Name + " " + validatedToken?.ToString()).CollapseAndTrim());
                return;
            }

            await context.HttpContext.SignInAsync(AuthenticationSchemes.FrontendUserSession, principal, new()
            {
                AllowRefresh = true,
                IsPersistent = false,
                IssuedUtc = validatedToken.ValidFrom,
                ExpiresUtc = validatedToken.ValidTo,
                // TODO: Shouldn't be hacking this in, there must be a proper way to store it. 
                Items = { { ".Token.access_token", accessToken } }
            }).ConfigureAwait(false);

            context.HttpContext.Response?.Headers?.Remove("X-API-AuthorizationToken");
        }
    }

The key line in question is: Items = { { ".Token.access_token", accessToken } }.

Could this be causing the issue now? Do I need to store this differently? Is there a better built-in mechanism to sign-in a user myself with an access token?

krispenner commented 1 year ago

@josephdecock I have now changed my code to the following and it seems to be working again. Maybe you can direct me to a better way to add a token to the authentication properties?

            await context.HttpContext.SignInAsync(AuthenticationSchemes.FrontendUserSession, principal, new()
            {
                AllowRefresh = true,
                IsPersistent = false,
                IssuedUtc = validatedToken.ValidFrom,
                ExpiresUtc = validatedToken.ValidTo,
                // TODO: Shouldn't be hacking this in, there must be a cleaner way to store it. 
                Items =
                {
                    { $".Token.{OpenIdConnectParameterNames.AccessToken}", accessToken },
                    { $".Token.{OpenIdConnectParameterNames.TokenType}", OidcConstants.AuthenticationSchemes.AuthorizationHeaderBearer },
                    { $".Token.{OpenIdConnectParameterNames.ExpiresIn}", validatedToken.ValidTo.ToString("o", CultureInfo.InvariantCulture) },
                }
            }).ConfigureAwait(false);
josephdecock commented 1 year ago

Under the hood, the BFF manages tokens using the Duende.AccessTokenManagement library. That includes a service which can store tokens for you.

From Duende.AccessTokenManagement.OpenIdConnect, inject a IUserTokenStore, and then call its StoreTokenAsync method.

josephdecock commented 1 year ago

I'm closing this issue, and we'll track further developments in https://github.com/DuendeSoftware/BFF/issues/182