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);
        }
Beispiel #3
0
        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
        {
        });
    }