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 Pkce CreatePkceData() { var pkce = new Pkce { CodeVerifier = CryptoRandom.CreateUniqueId() }; using (var sha256 = SHA256.Create()) { var challengeBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(pkce.CodeVerifier)); pkce.CodeChallenge = Base64Url.Encode(challengeBytes); } return(pkce); }
public async Task <IActionResult> Login() { var pkce = new Pkce { // uses the IdentityModel NuGet package to create a strongly random URL safe identifier // this will be our Code Verifier CodeVerifier = CryptoRandom.CreateUniqueId(32) }; using (var sha256 = SHA256.Create()) { // Here we create a hash of the code verifier var challengeBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(pkce.CodeVerifier)); // and produce the "Code Challenge" from it by base64Url encoding it. pkce.CodeChallenge = Base64Url.Encode(challengeBytes); } // Save the CodeVerifier in the memorycache so we are able to use it later // This is just for demonstration. Please consider saving this in a more robust solution. _memoryCache.Set(AppConstants.PKCECacheKey, pkce); // Build the authorize url var authorizeArgs = new Dictionary <string, string> { { "client_id", _appConfiguration.ClientId }, { "scope", "read:core:entities read:owneraccounts offline_access" }, { "redirect_uri", "http://localhost:55183/Auth/LoginCallback" }, { "response_type", "code" }, // Provide the Code Challenge along with the method (Sha256) { "code_challenge", pkce.CodeChallenge }, { "code_challenge_method", "S256" } }; var content = new FormUrlEncodedContent(authorizeArgs); var contentAsString = await content.ReadAsStringAsync(); // prepare the URL for the authorize endpoint var url = $"{_appConfiguration.TapkeyAuthorizationServerUrl}/{_appConfiguration.TapkeyAuthorizationEndpointPath}?{contentAsString}"; return(Redirect(url)); }
public override async Task <DialogTurnResult> BeginDialogAsync(DialogContext dc, object?options = null, CancellationToken cancellationToken = default) { dc = dc ?? throw new ArgumentNullException(nameof(dc)); if (options != null) { throw new ArgumentException($"{nameof(options)} cannot be defined for {nameof(AccountLinkingPrompt)}"); } // Initialize state var state = dc.ActiveDialog.State; state[ExpirationKey] = DateTimeOffset.UtcNow + _options.Timeout; var userId = dc.Context.Activity.From.AadObjectId; var tenantId = dc.Context.Activity.Conversation.TenantId; // Attempt to get the users token var tokenResult = await _oauthTokenProvider.GetAccessTokenAsync(tenantId : tenantId, userId : userId); if (tokenResult is NeedsConsentResult needsConsentResult) { var(codeChallenge, codeVerifier) = Pkce.GeneratePkceCodes(); var queryParams = HttpUtility.ParseQueryString(needsConsentResult.AuthorizeUri.Query); queryParams.Add("state", codeChallenge); // For bot we'll just use the codeChallenge as the 'state' queryParams.Add("code_challenge", codeChallenge); var loginConsentUri = new UriBuilder(needsConsentResult.AuthorizeUri) { Query = queryParams.ToString() }; // we can keep the code verifier out of the state[CodeVerifierKey] = codeVerifier; var activity = MessageFactory.Attachment(new Attachment { ContentType = SigninCard.ContentType, Content = new SigninCard { Text = "Please sign in", Buttons = new[] { new CardAction { Title = "Sign in", Type = ActionTypes.Signin, Value = loginConsentUri.ToString() }, }, }, }); var response = await dc.Context.SendActivityAsync(activity, cancellationToken : cancellationToken).ConfigureAwait(false); state[CardActivityKey] = response.Id; return(EndOfTurn); } else if (tokenResult is AccessTokenResult accessTokenResult) { return(await dc.EndDialogAsync(accessTokenResult, cancellationToken).ConfigureAwait(false)); } // Prompt user to login _logger.LogWarning("Unknown token result, ending turn"); return(EndOfTurn); }
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 { }); }