public async Task <IActionResult> LinkAccountsAsync(
        [FromBody] AccountLinkRequestBody tokenClaimRequest)
    {
        var userId   = User.FindFirstValue(ClaimConstants.ObjectId);
        var tenantId = User.FindFirstValue(ClaimConstants.TenantId);

        if (tokenClaimRequest.Code == default)
        {
            return(new BadRequestObjectResult(new {
                Error = "No code in query parameters"
            }));
        }

        if (tokenClaimRequest.CodeVerifier == default)
        {
            return(new BadRequestObjectResult(new {
                Error = "No code verifier in query parameters"
            }));
        }

        await _tokenProvider.ClaimTokenAsync(
            accountLinkingToken : tokenClaimRequest.Code, // our 'state' was given back to the caller as the 'code' for claiming
            tenantId : tenantId,
            userId : userId,
            codeVerifier : tokenClaimRequest.CodeVerifier);

        _logger.LogInformation("Linked GitHub account for: ({userId},{tenantId})", userId, tenantId);
        return(new StatusCodeResult((int)HttpStatusCode.NoContent));
    }
    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);
    }
    protected override async Task <MessagingExtensionResponse> OnTeamsMessagingExtensionQueryAsync(
        ITurnContext <IInvokeActivity> turnContext,
        MessagingExtensionQuery query,
        CancellationToken cancellationToken)
    {
        var userId   = turnContext.Activity.From.AadObjectId;
        var tenantId = turnContext.Activity.Conversation.TenantId;

        if (!string.IsNullOrEmpty(query.State))
        {
            var authResponseObject = JsonSerializer.Deserialize <AuthResponse>(query.State);
            if (authResponseObject == default)
            {
                _logger.LogWarning("Invalid state object provided: {state}", query.State);
                throw new Exception("Invalid state format");
            }
            _logger.LogInformation("Params:\nState: {state}\nCode: {code}", authResponseObject.State, authResponseObject.AccountLinkingState);
            var codeVerifier = _dataProtector.Unprotect(authResponseObject.State);
            await _oAuthTokenProvider.ClaimTokenAsync(
                accountLinkingToken : authResponseObject.AccountLinkingState, // these are inverted because
                codeVerifier : codeVerifier,
                tenantId : tenantId,
                userId : userId);
        }

        // Attempt to retrieve the github token
        var tokenResult = await _oAuthTokenProvider.GetAccessTokenAsync(tenantId : tenantId, userId : userId);

        if (tokenResult is NeedsConsentResult needsConsentResult)
        {
            _logger.LogInformation("Messaging Extension query with no GitHub token, sending login prompt");
            var(codeChallenge, codeVerifier) = Pkce.GeneratePkceCodes();
            var queryParams = HttpUtility.ParseQueryString(needsConsentResult.AuthorizeUri.Query);
            queryParams.Add("state", _dataProtector.Protect(codeVerifier));
            queryParams.Add("code_challenge", codeChallenge);
            var loginConsentUri = new UriBuilder(needsConsentResult.AuthorizeUri)
            {
                Query = queryParams.ToString()
            };
            return(new MessagingExtensionResponse
            {
                ComposeExtension = new MessagingExtensionResult
                {
                    Type = "auth",
                    SuggestedActions = new MessagingExtensionSuggestedAction
                    {
                        Actions = new List <CardAction>
                        {
                            new CardAction
                            {
                                Type = ActionTypes.OpenUrl,
                                Title = "Please login to GitHub",
                                Value = loginConsentUri.ToString()
                            },
                        },
                    },
                },
            });
        }
        else if (tokenResult is AccessTokenResult accessTokenResult)
        {
            var repos = await _gitHubServiceClient.GetRepositoriesAsync(accessTokenResult.AccessToken);

            return(new MessagingExtensionResponse
            {
                ComposeExtension = new MessagingExtensionResult
                {
                    Type = "result",
                    AttachmentLayout = "list",
                    Attachments = repos.Select(r =>
                                               new MessagingExtensionAttachment
                    {
                        ContentType = HeroCard.ContentType,
                        Content = new HeroCard {
                            Title = $"{r.Name} ({r.Stars})"
                        },
                        Preview = new HeroCard {
                            Title = $"{r.Name} ({r.Stars})"
                        }.ToAttachment(),
                    }).ToList(),
                },
            });
        }
        // There was an error
        return(new MessagingExtensionResponse
        {
        });
    }