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 { }); }