public async Task ClaimTokenAsync(string accountLinkingToken, string tenantId, string userId, string codeVerifier)
    {
        var(acctState, exp) = await _oAuthStateService.GetAsync(accountLinkingToken);

        // ensure the PKCE verifier is correct
        var codeGuess = Pkce.Base64UrlEncodeSha256(codeVerifier);

        if (!string.Equals(acctState.CodeChallenge, codeGuess, StringComparison.OrdinalIgnoreCase))
        {
            _logger.LogWarning("PKCE verification failed:\nChallenge:{codeChallenge}\nVerifier:{verifier}\nH(Verifier):{guess}", acctState.CodeChallenge, codeVerifier, codeGuess);
            throw new Exception("PKCE code verification failed");
        }

        // ensure this isn't a replay
        await _replayValidator.ClaimIdAsync(acctState.Id, exp);

        // Claim the oauth code from the downstream
        var oAuthResult = await _oAuthServiceClient.ClaimCodeAsync(acctState.OAuthCode);

        var dto = new OAuthUserTokenDto
        {
            AccessToken           = oAuthResult.AccessToken,
            AccessTokenExpiration = DateTimeOffset.Now + TimeSpan.FromSeconds(oAuthResult.ExpiresInSeconds),
            RefreshToken          = oAuthResult.RefreshToken
        };

        var serializedDto = JsonSerializer.Serialize(dto);

        await _userTokenStore.SetTokenAsync(tenantId : tenantId, userId : userId, serializedDto);
    }
    public override async Task <DialogTurnResult> ContinueDialogAsync(DialogContext dc, CancellationToken cancellationToken = default)
    {
        dc = dc ?? throw new ArgumentNullException(nameof(dc));
        var userId   = dc.Context.Activity.From.AadObjectId;
        var tenantId = dc.Context.Activity.Conversation.TenantId;

        // Check for timeout
        var state     = dc.ActiveDialog.State;
        var expires   = (DateTimeOffset)state[ExpirationKey];
        var isMessage = dc.Context.Activity.Type == ActivityTypes.Message;

        // If the incoming Activity is a message, or an Activity Type normally handled by OAuthPrompt,
        // check to see if this OAuthPrompt Expiration has elapsed, and end the dialog if so.
        var isTokenResponse       = IsTokenResponseEvent(dc.Context);
        var isTimeoutActivityType = isMessage || IsTokenResponseEvent(dc.Context);
        var hasTimedOut           = isTimeoutActivityType && DateTimeOffset.UtcNow >= expires;

        if (hasTimedOut)
        {
            _logger.LogWarning("User completed logout after timeout, bailing on dialog");
            // if the token fetch request times out, complete the prompt with no result.
            return(await dc.EndDialogAsync(cancellationToken : cancellationToken).ConfigureAwait(false));
        }

        if (isTokenResponse)
        {
            _logger.LogInformation("Detected token response, attempting to complete auth flow");
            var value         = dc.Context.Activity.Value as JObject;
            var stateString   = value?.GetValue("state")?.ToString() ?? string.Empty;
            var authResponse  = JsonSerializer.Deserialize <AuthResponse>(stateString);
            var codeVerifier  = state[CodeVerifierKey] as string ?? string.Empty;
            var expectedState = Pkce.Base64UrlEncodeSha256(codeVerifier);
            if (!string.Equals(authResponse?.State, expectedState))
            {
                // The state returned doesn't match the expected. potentially a forgery attempt.
                _logger.LogWarning("Potential forgery attempt: {expectedState} | {actualState} | {verifier}", expectedState, authResponse?.State, codeVerifier);
                return(await dc.EndDialogAsync(cancellationToken : cancellationToken).ConfigureAwait(false));
            }

            await _oauthTokenProvider.ClaimTokenAsync(
                accountLinkingToken : authResponse?.AccountLinkingState ?? string.Empty,
                tenantId : tenantId,
                userId : userId,
                codeVerifier : codeVerifier
                );
        }

        var tokenResult = await _oauthTokenProvider.GetAccessTokenAsync(tenantId : tenantId, userId : userId);

        if (tokenResult is NeedsConsentResult)
        {
            _logger.LogWarning("User failed to consent, bailing on dialog");
            return(await dc.EndDialogAsync(cancellationToken : cancellationToken).ConfigureAwait(false));
        }
        else if (tokenResult is AccessTokenResult accessTokenResult)
        {
            var activityId = state[CardActivityKey] as string;
            if (activityId != default)
            {
                // Since the signin "state" is only good for one login, we need to ensure the card for 'login' is overwritten
                var activity = MessageFactory.Attachment(new Attachment
                {
                    ContentType = HeroCard.ContentType,
                    Content     = new HeroCard(title: "You are now logged in")
                });
                activity.Id = activityId;
                await dc.Context.UpdateActivityAsync(activity, cancellationToken : cancellationToken);
            }
            return(await dc.EndDialogAsync(accessTokenResult, cancellationToken).ConfigureAwait(false));
        }

        return(EndOfTurn);
    }