public void Handle_error(string id_, string jwtResponse, Type expectedExceptionType, int expectedCode, int expectedStatus, string expectedMessage, string expectedDeveloperMessage)
        {
            var testApiKey = ClientApiKeys.Builder()
                .SetId("2EV70AHRTYF0JOA7OEFO3SM29")
                .SetSecret("goPUHQMkS4dlKwl5wtbNd91I+UrRehCsEDJrIrMruK8")
                .Build();
            var fakeRequestExecutor = Substitute.For<IRequestExecutor>();
            fakeRequestExecutor.ApiKey.Returns(testApiKey);

            this.dataStore = TestDataStore.Create(fakeRequestExecutor, Caches.NewInMemoryCacheProvider().Build());

            var request = new DefaultHttpRequest(HttpMethod.Get, new CanonicalUri($"https://foo.bar?{IdSiteClaims.JwtResponse}={jwtResponse}"));

            IIdSiteSyncCallbackHandler callbackHandler = new DefaultIdSiteSyncCallbackHandler(this.dataStore, request);

            try
            {
                var accountResult = callbackHandler.GetAccountResult();

                throw new Exception("Should not reach here. Proper exception was not thrown.");
            }
            catch (IdSiteRuntimeException e) when (expectedExceptionType.IsAssignableFrom(e.GetType()))
            {
                e.Code.ShouldBe(expectedCode);
                e.HttpStatus.ShouldBe(expectedStatus);
                e.Message.ShouldBe(expectedMessage);
                e.DeveloperMessage.ShouldBe(expectedDeveloperMessage);
            }
            catch (Exception e) when (expectedExceptionType.IsAssignableFrom(e.GetType()))
            {
                e.Message.ShouldStartWith(expectedMessage);
            }
        }
        public void Throws_for_empty_request_URI()
        {
            var apiKey = new DefaultClientApiKey("foo", "bar");
            var request = new DefaultHttpRequest(HttpMethod.Get, null);

            Should.Throw<RequestAuthenticationException>(() =>
            {
                this.authenticator.AuthenticateCore(request, apiKey, this.fakeNow, this.fakeNonce);
            });
        }
        public void Adds_XStormpathDate_header()
        {
            var request = new DefaultHttpRequest(HttpMethod.Get, new CanonicalUri(this.uriQualifier.EnsureFullyQualified("/bar")));
            var now = new DateTimeOffset(2015, 08, 01, 06, 30, 00, TimeSpan.Zero);

            this.authenticator.AuthenticateCore(request, this.apiKey, now);

            // X-Stormpath-Date -> current time in UTC
            var stormpathDateHeader = Iso8601.Parse(request.Headers.GetFirst<string>("X-Stormpath-Date"));
            stormpathDateHeader.ShouldBe(now);
        }
        public void Adds_Host_header()
        {
            var apiKey = new DefaultClientApiKey("foo", "bar");
            var uriQualifier = new UriQualifier("http://api.foo.bar");
            var request = new DefaultHttpRequest(HttpMethod.Get, new CanonicalUri(uriQualifier.EnsureFullyQualified("/stuff")));

            this.authenticator.AuthenticateCore(request, apiKey, this.fakeNow, this.fakeNonce);

            // Host: [hostname]
            request.Headers.Host.ShouldBe("api.foo.bar");
        }
        public void Adds_XStormpathDate_header()
        {
            var apiKey = new DefaultClientApiKey("foo", "bar");
            var uriQualifier = new UriQualifier("http://api.foo.bar");
            var request = new DefaultHttpRequest(HttpMethod.Get, new CanonicalUri(uriQualifier.EnsureFullyQualified("/stuff")));

            this.authenticator.AuthenticateCore(request, apiKey, this.fakeNow, this.fakeNonce);

            // X-Stormpath-Date -> current time in UTC
            var stormpathDateHeader = Iso8601.Parse(request.Headers.GetFirst<string>("X-Stormpath-Date"));
            stormpathDateHeader.ShouldBe(this.fakeNow);
        }
        public void Adds_Basic_authorization_header()
        {
            var request = new DefaultHttpRequest(HttpMethod.Get, new CanonicalUri(this.uriQualifier.EnsureFullyQualified("/bar")));
            var now = new DateTimeOffset(2015, 08, 01, 06, 30, 00, TimeSpan.Zero);

            this.authenticator.AuthenticateCore(request, this.apiKey, now);

            // Authorization: "Basic [base64 stuff]"
            var authenticationHeader = request.Headers.Authorization;
            authenticationHeader.Scheme.ShouldBe("Basic");
            Base64.Decode(authenticationHeader.Parameter, Encoding.UTF8).ShouldBe($"{this.fakeApiKeyId}:{this.fakeApiKeySecret}");
        }
            public void Retries_request_on_recoverable_error()
            {
                // Set up a fake HttpClient that mysteriously always fails with recoverable errors
                var failingHttpClient = GetSynchronousClient(
                    new DefaultHttpResponse(0, null, new HttpHeaders(), null, null, transportError: true));

                var defaultBackoffStrategy = GetFakeBackoffStrategy();
                var throttlingBackoffStrategy = GetFakeBackoffStrategy();
                var requestExecutor = GetRequestExecutor(failingHttpClient, defaultBackoffStrategy, throttlingBackoffStrategy);
                var dummyRequest = new DefaultHttpRequest(HttpMethod.Delete, new CanonicalUri("http://api.foo.bar/foo"));

                var response = requestExecutor.Execute(dummyRequest);
                response.TransportError.ShouldBeTrue();

                // Used default backoff strategy to pause after each retry (4 times)
                defaultBackoffStrategy.ReceivedWithAnyArgs(4).GetDelayMilliseconds(0);
                throttlingBackoffStrategy.DidNotReceiveWithAnyArgs().GetDelayMilliseconds(0);
            }
            public async Task Retries_request_on_HTTP_503()
            {
                // Set up a fake HttpClient that awlays returns HTTP 503
                var failingHttpClient = GetAsynchronousClient(new DefaultHttpResponse(503, null, new HttpHeaders(), null, null, transportError: false));

                var defaultBackoffStrategy = GetFakeBackoffStrategy();
                var throttlingBackoffStrategy = GetFakeBackoffStrategy();
                var requestExecutor = GetRequestExecutor(failingHttpClient, defaultBackoffStrategy, throttlingBackoffStrategy);
                var dummyRequest = new DefaultHttpRequest(HttpMethod.Delete, new CanonicalUri("http://api.foo.bar/foo"));

                var response = await requestExecutor.ExecuteAsync(dummyRequest, CancellationToken.None);
                response.IsServerError().ShouldBeTrue();

                // Used default backoff strategy to pause after each retry (4 times)
                defaultBackoffStrategy.ReceivedWithAnyArgs(4).GetDelayMilliseconds(0);
                throttlingBackoffStrategy.DidNotReceiveWithAnyArgs().GetDelayMilliseconds(0);
            }
            public async Task Does_not_retry_when_task_is_canceled()
            {
                // Set up a fake HttpClient that throws for cancellation
                var throwingHttpClient = Substitute.For<IAsynchronousHttpClient>();
                throwingHttpClient.IsAsynchronousSupported.Returns(true);
                throwingHttpClient
                    .When(fake => fake.ExecuteAsync(Arg.Any<IHttpRequest>(), Arg.Any<CancellationToken>()))
                    .Do(call =>
                    {
                        call.Arg<CancellationToken>().ThrowIfCancellationRequested();
                    });

                var requestExecutor = GetRequestExecutor(throwingHttpClient);

                var canceled = new CancellationTokenSource();
                canceled.Cancel();

                var dummyRequest = new DefaultHttpRequest(HttpMethod.Delete, new CanonicalUri("http://api.foo.bar/foo"));

                await Assert.ThrowsAsync<OperationCanceledException>(async () =>
                {
                    await requestExecutor.ExecuteAsync(dummyRequest, canceled.Token);
                });

                // Should only have 1 call: no retries!
                await throwingHttpClient.Received(1).ExecuteAsync(Arg.Any<IHttpRequest>(), Arg.Any<CancellationToken>());
            }
            public void Retries_request_with_throttling_on_HTTP_429()
            {
                // Set up a fake HttpClient that always returns HTTP 429
                var failingHttpClient = GetSynchronousClient(
                    new DefaultHttpResponse(429, null, new HttpHeaders(), null, null, transportError: false));

                var defaultBackoffStrategy = GetFakeBackoffStrategy();
                var throttlingBackoffStrategy = GetFakeBackoffStrategy();
                var requestExecutor = GetRequestExecutor(failingHttpClient, defaultBackoffStrategy, throttlingBackoffStrategy);
                var dummyRequest = new DefaultHttpRequest(HttpMethod.Delete, new CanonicalUri("http://api.foo.bar/foo"));

                var response = requestExecutor.Execute(dummyRequest);
                response.IsClientError().ShouldBeTrue();

                // Used throttling backoff strategy to pause after each retry (4 times)
                defaultBackoffStrategy.DidNotReceiveWithAnyArgs().GetDelayMilliseconds(0);
                throttlingBackoffStrategy.ReceivedWithAnyArgs(4).GetDelayMilliseconds(0);
            }
        public void Should_authenticate_request_without_query_params()
        {
            IClientApiKey apiKey = new DefaultClientApiKey("MyId", "Shush!");
            var uriQualifier = new UriQualifier("http://api.stormpath.com/v1");
            var request = new DefaultHttpRequest(HttpMethod.Get, new CanonicalUri(uriQualifier.EnsureFullyQualified("/")));
            var now = new DateTimeOffset(2013, 7, 1, 0, 0, 0, TimeSpan.Zero);
            var nonce = "a43a9d25-ab06-421e-8605-33fd1e760825";

            this.authenticator.AuthenticateCore(request, apiKey, now, nonce);

            var sauthc1Id = request.Headers.Authorization.Parameter.Split(' ')[0];
            var sauthc1SignedHeaders = request.Headers.Authorization.Parameter.Split(' ')[1];
            var sauthc1Signature = request.Headers.Authorization.Parameter.Split(' ')[2];

            request.Headers.Authorization.Scheme.ShouldBe("SAuthc1");
            sauthc1Id.ShouldBe("sauthc1Id=MyId/20130701/a43a9d25-ab06-421e-8605-33fd1e760825/sauthc1_request,");
            sauthc1SignedHeaders.ShouldBe("sauthc1SignedHeaders=host;x-stormpath-date,");
            sauthc1Signature.ShouldBe("sauthc1Signature=990a95aabbcbeb53e48fb721f73b75bd3ae025a2e86ad359d08558e1bbb9411c");
        }
        public void Adds_SAuthc1_authorization_header()
        {
            IClientApiKey apiKey = new DefaultClientApiKey("myAppId", "super-secret");
            var uriQualifier = new UriQualifier("http://api.stormpath.com/v1");
            var request = new DefaultHttpRequest(HttpMethod.Get, new CanonicalUri(uriQualifier.EnsureFullyQualified("/accounts")));

            this.authenticator.AuthenticateCore(request, apiKey, this.fakeNow, this.fakeNonce);

            // Authorization: "SAuthc1 [signed hash]"
            var authenticationHeader = request.Headers.Authorization;
            authenticationHeader.Scheme.ShouldBe("SAuthc1");
            authenticationHeader.Parameter.ShouldNotBe(null);

            // Format "sauthc1Id=[id string], sauthc1SignedHeaders=[host;x-stormpath-date;...], sauthc1Signature=[signature in hex]"
            var parts = authenticationHeader.Parameter.Split(' ');
            var sauthc1Id = parts[0].TrimEnd(',').SplitToKeyValuePair('=');
            var sauthc1SignedHeaders = parts[1].TrimEnd(',').SplitToKeyValuePair('=');
            var sauthc1Signature = parts[2].SplitToKeyValuePair('=');
            var dateString = this.fakeNow.ToString("yyyyMMdd", CultureInfo.InvariantCulture);

            sauthc1Id.Key.ShouldBe("sauthc1Id");
            sauthc1Id.Value.ShouldBe($"{apiKey.GetId()}/{dateString}/{this.fakeNonce}/sauthc1_request");

            sauthc1SignedHeaders.Key.ShouldBe("sauthc1SignedHeaders");
            sauthc1SignedHeaders.Value.ShouldBe("host;x-stormpath-date");

            sauthc1Signature.Key.ShouldBe("sauthc1Signature");
            sauthc1Signature.Value.ShouldNotBe(null);
        }
        public void Should_authenticate_request_with_multiple_query_params()
        {
            IClientApiKey apiKey = new DefaultClientApiKey("MyId", "Shush!");
            var uriQualifier = new UriQualifier("http://api.stormpath.com/v1");
            var request = new DefaultHttpRequest(HttpMethod.Get, new CanonicalUri(uriQualifier.EnsureFullyQualified("/applications/77JnfFiREjdfQH0SObMfjI/groups?q=group&limit=25&offset=25")));
            var now = new DateTimeOffset(2013, 7, 1, 0, 0, 0, TimeSpan.Zero);
            var nonce = "a43a9d25-ab06-421e-8605-33fd1e760825";

            this.authenticator.AuthenticateCore(request, apiKey, now, nonce);

            var sauthc1Id = request.Headers.Authorization.Parameter.Split(' ')[0];
            var sauthc1SignedHeaders = request.Headers.Authorization.Parameter.Split(' ')[1];
            var sauthc1Signature = request.Headers.Authorization.Parameter.Split(' ')[2];

            request.Headers.Authorization.Scheme.ShouldBe("SAuthc1");
            sauthc1Id.ShouldBe("sauthc1Id=MyId/20130701/a43a9d25-ab06-421e-8605-33fd1e760825/sauthc1_request,");
            sauthc1SignedHeaders.ShouldBe("sauthc1SignedHeaders=host;x-stormpath-date,");
            sauthc1Signature.ShouldBe("sauthc1Signature=e30a62c0d03ca6cb422e66039786865f3eb6269400941ede6226760553a832d3");
        }
        public void Should_authenticate_request_with_query_params()
        {
            IClientApiKey apiKey = new DefaultClientApiKey("MyId", "Shush!");
            var uriQualifier = new UriQualifier("http://api.stormpath.com/v1");
            var request = new DefaultHttpRequest(HttpMethod.Get, new CanonicalUri(uriQualifier.EnsureFullyQualified("/directories?orderBy=name+asc")));
            var now = new DateTimeOffset(2013, 7, 1, 0, 0, 0, TimeSpan.Zero);
            var nonce = "a43a9d25-ab06-421e-8605-33fd1e760825";

            this.authenticator.AuthenticateCore(request, apiKey, now, nonce);

            var sauthc1Id = request.Headers.Authorization.Parameter.Split(' ')[0];
            var sauthc1SignedHeaders = request.Headers.Authorization.Parameter.Split(' ')[1];
            var sauthc1Signature = request.Headers.Authorization.Parameter.Split(' ')[2];

            request.Headers.Authorization.Scheme.ShouldBe("SAuthc1");
            sauthc1Id.ShouldBe("sauthc1Id=MyId/20130701/a43a9d25-ab06-421e-8605-33fd1e760825/sauthc1_request,");
            sauthc1SignedHeaders.ShouldBe("sauthc1SignedHeaders=host;x-stormpath-date,");
            sauthc1Signature.ShouldBe("sauthc1Signature=fc04c5187cc017bbdf9c0bb743a52a9487ccb91c0996267988ceae3f10314176");
        }
        public void Handle_response(string id_, string jwtResponse, string expectedStatus, bool isNewAccount, string expectedState)
        {
            IAccountResult accountResultFromListener = null;

            var listener = new InlineIdSiteSyncResultListener(
                onAuthenticated: result =>
                {
                    if (expectedStatus == IdSiteResultStatus.Authenticated)
                    {
                        accountResultFromListener = result;
                    }
                    else
                    {
                        throw new InvalidOperationException("This method should not have been executed");
                    }
                },
                onLogout: result =>
                {
                    if (expectedStatus == IdSiteResultStatus.Logout)
                    {
                        accountResultFromListener = result;
                    }
                    else
                    {
                        throw new InvalidOperationException("This method should not have been executed");
                    }
                },
                onRegistered: result =>
                {
                    if (expectedStatus == IdSiteResultStatus.Registered)
                    {
                        accountResultFromListener = result;
                    }
                    else
                    {
                        throw new InvalidOperationException("This method should not have been executed");
                    }
                });

            var testApiKey = ClientApiKeys.Builder().SetId("2EV70AHRTYF0JOA7OEFO3SM29").SetSecret("goPUHQMkS4dlKwl5wtbNd91I+UrRehCsEDJrIrMruK8").Build();
            var fakeRequestExecutor = Substitute.For<IRequestExecutor>();
            fakeRequestExecutor.ApiKey.Returns(testApiKey);

            this.dataStore = TestDataStore.Create(fakeRequestExecutor, Caches.NewInMemoryCacheProvider().Build());

            var request = new DefaultHttpRequest(HttpMethod.Get, new CanonicalUri($"https://foo.bar?{IdSiteClaims.JwtResponse}={jwtResponse}"));

            IIdSiteSyncCallbackHandler callbackHandler = new DefaultIdSiteSyncCallbackHandler(this.dataStore, request);
            callbackHandler.SetResultListener(listener);

            var accountResult = callbackHandler.GetAccountResult();

            // Validate result
            (accountResult as DefaultAccountResult).Account.Href.ShouldBe("https://api.stormpath.com/v1/accounts/7Ora8KfVDEIQP38KzrYdAs");
            (accountResultFromListener as DefaultAccountResult).Account.Href.ShouldBe("https://api.stormpath.com/v1/accounts/7Ora8KfVDEIQP38KzrYdAs");

            accountResult.IsNewAccount.ShouldBe(isNewAccount);
            accountResultFromListener.IsNewAccount.ShouldBe(isNewAccount);

            accountResult.State.ShouldBe(expectedState);
            accountResultFromListener.State.ShouldBe(expectedState);

            var expectedResultStatus = IdSiteResultStatus.Parse(expectedStatus);
            accountResult.Status.ShouldBe(expectedResultStatus);
            accountResultFromListener.Status.ShouldBe(expectedResultStatus);
        }
        private async Task<IHttpResponse> CoreRequestLoopAsync(
            IHttpRequest request,
            Func<IHttpRequest, CancellationToken, Task<IHttpResponse>> executeAction,
            Func<int, bool, CancellationToken, Task> pauseAction,
            CancellationToken cancellationToken)
        {
            var attempts = 0;
            bool throttling = false;

            Uri currentUri = request.CanonicalUri.ToUri();

            while (true)
            {
                var currentRequest = new DefaultHttpRequest(request, overrideUri: currentUri);

                // Sign and build request
                this.requestAuthenticator.Authenticate(currentRequest, this.apiKey);

                try
                {
                    if (attempts > 1)
                    {
                        this.logger.Trace("Pausing before retry", "DefaultRequestExecutor.CoreRequestLoopAsync");
                        await pauseAction(attempts - 1, throttling, cancellationToken).ConfigureAwait(false);
                    }

                    var response = await executeAction(currentRequest, cancellationToken).ConfigureAwait(false);
                    if (this.IsRedirect(response))
                    {
                        currentUri = response.Headers.Location;
                        this.logger.Trace($"Redirected to {currentUri}", "DefaultRequestExecutor.CoreRequestLoopAsync");

                        continue; // re-execute request, not counted as a retry
                    }

                    var statusCode = response.StatusCode;

                    if (response.TransportError && attempts < this.maxAttemptsPerRequest)
                    {
                        this.logger.Warn($"Recoverable transport error during request, retrying", "DefaultRequestExecutor.CoreRequestLoopAsync");

                        attempts++;
                        continue; // retry request
                    }

                    // HTTP 429
                    if (statusCode == TooManyRequests && attempts < this.maxAttemptsPerRequest)
                    {
                        throttling = true;
                        this.logger.Warn($"Got HTTP 429, throttling, retrying", "DefaultRequestExecutor.CoreRequestLoopAsync");

                        attempts++;
                        continue; // retry request
                    }

                    // HTTP 5xx
                    if (response.IsServerError() && attempts < this.maxAttemptsPerRequest)
                    {
                        this.logger.Warn($"Got HTTP {statusCode}, retrying", "DefaultRequestExecutor.CoreRequestLoopAsync");

                        attempts++;
                        continue; // retry request
                    }

                    // HTTP 409 (modified) during delete
                    if (statusCode == Conflict && request.Method == HttpMethod.Delete)
                    {
                        this.logger.Warn($"Got HTTP {statusCode} during delete, retrying", "DefaultRequestExecutor.CoreRequestLoopAsync");

                        attempts++;
                        continue; // retry request
                    }

                    return response;
                }
                catch (Exception ex)
                {
                    if (this.WasCanceled(ex, cancellationToken))
                    {
                        this.logger.Trace("Request task was canceled. Rethrowing TaskCanceledException", "DefaultRequestExecutor.CoreRequestLoopAsync");
                        throw;
                    }
                    else
                    {
                        throw new RequestException("Unable to execute HTTP request.", ex);
                    }
                }
            }
        }
        private async Task <IHttpResponse> CoreRequestLoopAsync(
            IHttpRequest request,
            Func <IHttpRequest, CancellationToken, Task <IHttpResponse> > executeAction,
            Func <int, bool, CancellationToken, Task> pauseAction,
            CancellationToken cancellationToken)
        {
            var  attempts   = 0;
            bool throttling = false;

            Uri currentUri = request.CanonicalUri.ToUri();

            while (true)
            {
                var currentRequest = new DefaultHttpRequest(request, overrideUri: currentUri);

                // Sign and build request
                this.requestAuthenticator.Authenticate(currentRequest, this.apiKey);

                try
                {
                    if (attempts > 1)
                    {
                        this.logger.Trace("Pausing before retry", "DefaultRequestExecutor.CoreRequestLoopAsync");
                        await pauseAction(attempts - 1, throttling, cancellationToken).ConfigureAwait(false);
                    }

                    var response = await executeAction(currentRequest, cancellationToken).ConfigureAwait(false);

                    if (this.IsRedirect(response))
                    {
                        currentUri = response.Headers.Location;
                        this.logger.Trace($"Redirected to {currentUri}", "DefaultRequestExecutor.CoreRequestLoopAsync");

                        continue; // re-execute request, not counted as a retry
                    }

                    var statusCode = response.StatusCode;

                    if (response.TransportError && attempts < this.maxAttemptsPerRequest)
                    {
                        this.logger.Warn($"Recoverable transport error during request, retrying", "DefaultRequestExecutor.CoreRequestLoopAsync");

                        attempts++;
                        continue; // retry request
                    }

                    // HTTP 429
                    if (statusCode == TooManyRequests && attempts < this.maxAttemptsPerRequest)
                    {
                        throttling = true;
                        this.logger.Warn($"Got HTTP 429, throttling, retrying", "DefaultRequestExecutor.CoreRequestLoopAsync");

                        attempts++;
                        continue; // retry request
                    }

                    // HTTP 5xx
                    if (response.IsServerError() && attempts < this.maxAttemptsPerRequest)
                    {
                        this.logger.Warn($"Got HTTP {statusCode}, retrying", "DefaultRequestExecutor.CoreRequestLoopAsync");

                        attempts++;
                        continue; // retry request
                    }

                    // HTTP 409 (modified) during delete
                    if (statusCode == Conflict && request.Method == HttpMethod.Delete)
                    {
                        this.logger.Warn($"Got HTTP {statusCode} during delete, retrying", "DefaultRequestExecutor.CoreRequestLoopAsync");

                        attempts++;
                        continue; // retry request
                    }

                    return(response);
                }
                catch (Exception ex)
                {
                    if (this.WasCanceled(ex, cancellationToken))
                    {
                        this.logger.Trace("Request task was canceled. Rethrowing TaskCanceledException", "DefaultRequestExecutor.CoreRequestLoopAsync");
                        throw;
                    }
                    else
                    {
                        throw new RequestException("Unable to execute HTTP request.", ex);
                    }
                }
            }
        }