Пример #1
0
        public async Task Can_Perform_Operations_On_Users_And_Get_Preferences_From_Api()
        {
            // Arrange
            var emailAddress = $"some.user.{Guid.NewGuid()}@some.domain.com";

            var user = new LondonTravelUser()
            {
                CreatedAt          = DateTime.UtcNow,
                Email              = emailAddress,
                EmailNormalized    = emailAddress,
                GivenName          = "Alexa",
                Surname            = "Amazon",
                UserName           = emailAddress,
                UserNameNormalized = emailAddress,
            };

            string accessToken = Controllers.AlexaController.GenerateAccessToken();

            string[] favoriteLines = new[] { "district", "northern" };
            string   userId;

            // HACK Force server start-up
            using (Fixture.CreateDefaultClient())
            {
            }
        /// <summary>
        /// Creates an instance of <see cref="UserManager{TUser}"/>.
        /// </summary>
        /// <param name="user">The optional user to return for calls to get the current user.</param>
        /// <param name="result">The optional identity result return return for calls to update the user.</param>
        /// <returns>
        /// The created instance of <see cref="UserManager{TUser}"/>.
        /// </returns>
        private static UserManager <LondonTravelUser> CreateUserManager(LondonTravelUser user = null, IdentityResult result = null)
        {
            var mock = new Mock <UserManager <LondonTravelUser> >(
                Mock.Of <IUserStore <LondonTravelUser> >(),
                null,
                null,
                null,
                null,
                null,
                null,
                null,
                null);

            if (user != null)
            {
                mock.Setup((p) => p.GetUserAsync(It.IsNotNull <ClaimsPrincipal>()))
                .ReturnsAsync(user);

                if (result != null)
                {
                    mock.Setup((p) => p.UpdateAsync(user))
                    .ReturnsAsync(result);
                }
            }

            return(mock.Object);
        }
Пример #3
0
        private LondonTravelUser CreateSystemUser(ExternalLoginInfo info)
        {
            var email = info.Principal.FindFirstValue(ClaimTypes.Email);

            if (string.IsNullOrEmpty(email))
            {
                return(null);
            }

            var givenName = info.Principal.FindFirstValue(ClaimTypes.GivenName);
            var surname   = info.Principal.FindFirstValue(ClaimTypes.Surname);

            var user = new LondonTravelUser()
            {
                CreatedAt      = _clock.GetCurrentInstant().ToDateTimeUtc(),
                Email          = email,
                GivenName      = givenName,
                Surname        = surname,
                UserName       = email,
                EmailConfirmed = false,
            };

            user.Logins.Add(LondonTravelLoginInfo.FromUserLoginInfo(info));

            foreach (var claim in info.Principal.Claims)
            {
                user.RoleClaims.Add(LondonTravelRole.FromClaim(claim));
            }

            return(user);
        }
Пример #4
0
        /// <inheritdoc />
        public async Task <string> CreateAsync(LondonTravelUser document)
        {
            if (document == null)
            {
                throw new ArgumentNullException(nameof(document));
            }

            Container container = await GetContainerAsync();

            _logger.LogTrace(
                "Creating document in collection {CollectionName} of database {DatabaseName}.",
                _options.CollectionName,
                _options.DatabaseName);

            document.Id = Guid.NewGuid().ToString();

            var result = await container.CreateItemAsync(document);

            _logger.LogTrace(
                "Created document in collection {CollectionName} of database {DatabaseName}. Id: {ResourceId}.",
                _options.CollectionName,
                _options.DatabaseName,
                result.Resource.Id);

            return(result.Resource.Id !);
        }
        public static async Task AuthorizeSkill_Returns_Error_If_Update_Fails()
        {
            // Arrange
            string state        = "Some State";
            string clientId     = "my-client-id";
            string responseType = "token";
            Uri    redirectUri  = new Uri("https://alexa.amazon.com/alexa-london-travel?foo=bar");

            var user   = new LondonTravelUser();
            var result = IdentityResult.Failed(new IdentityError()
            {
                Code = "Error", Description = "Problem"
            });

            var userManager = CreateUserManager(user, result);

            using (var target = CreateTarget(userManager))
            {
                // Act
                IActionResult actual = await target.AuthorizeSkill(state, clientId, responseType, redirectUri);

                // Assert
                AssertRedirect(actual, "https://alexa.amazon.com/alexa-london-travel?foo=bar#state=Some%20State&error=server_error");
            }
        }
Пример #6
0
        /// <inheritdoc />
        public Task <string> CreateAsync(LondonTravelUser document)
        {
            DocumentCollection collection = EnsureCollection();
            string             id         = collection.Create(document);

            return(Task.FromResult(id));
        }
Пример #7
0
        /// <inheritdoc />
        public Task <LondonTravelUser?> ReplaceAsync(LondonTravelUser document, string?etag)
        {
            DocumentCollection collection = EnsureCollection();
            LondonTravelUser?  result     = collection.Replace(document.Id !, document, etag);

            return(Task.FromResult(result));
        }
Пример #8
0
        public async Task <IActionResult> GetPreferences(
            [FromHeader(Name = "Authorization")] string authorizationHeader,
            CancellationToken cancellationToken = default)
        {
            _logger?.LogTrace("Received API request for user preferences.");

            // TODO Consider allowing implicit access if the user is signed-in (i.e. access from a browser)
            if (string.IsNullOrWhiteSpace(authorizationHeader))
            {
                _logger?.LogInformation(
                    "API request for preferences denied as no Authorization header/value was specified. IP: {RemoteIP}; User Agent: {UserAgent}.",
                    HttpContext.Connection.RemoteIpAddress,
                    Request.Headers["User-Agent"]);

                _telemetry.TrackApiPreferencesUnauthorized();

                return(Unauthorized("No access token specified."));
            }

            LondonTravelUser user        = null;
            string           accessToken = GetAccessTokenFromAuthorizationHeader(authorizationHeader, out string errorDetail);

            if (accessToken != null)
            {
                user = await FindUserByAccessTokenAsync(accessToken, cancellationToken);
            }

            if (user == null || !string.Equals(user.AlexaToken, accessToken, StringComparison.Ordinal))
            {
                _logger?.LogInformation(
                    "API request for preferences denied as the specified access token is unknown. IP: {RemoteIP}; User Agent: {UserAgent}.",
                    HttpContext.Connection.RemoteIpAddress,
                    Request.Headers["User-Agent"]);

                _telemetry.TrackApiPreferencesUnauthorized();

                return(Unauthorized("Unauthorized.", errorDetail));
            }

            _logger?.LogInformation(
                "Successfully authorized API request for preferences for user Id {UserId}. IP: {RemoteIP}; User Agent: {UserAgent}.",
                user.Id,
                HttpContext.Connection.RemoteIpAddress,
                Request.Headers["User-Agent"]);

            var data = new PreferencesResponse()
            {
                FavoriteLines = user.FavoriteLines,
                UserId        = user.Id,
            };

            _telemetry.TrackApiPreferencesSuccess(data.UserId);

            return(Ok(data));
        }
Пример #9
0
        /// <summary>
        /// Finds the user with the specified access token, if any, as an asynchronous operation.
        /// </summary>
        /// <param name="accessToken">The access token to find the associated user for.</param>
        /// <param name="cancellationToken">The cancellation token to use.</param>
        /// <returns>
        /// A <see cref="Task{TResult}"/> representing the asynchronous operation to
        /// find the London Travel user with the specified Alexa access token.
        /// </returns>
        private async Task <LondonTravelUser> FindUserByAccessTokenAsync(string accessToken, CancellationToken cancellationToken)
        {
            LondonTravelUser user = null;

            if (!string.IsNullOrEmpty(accessToken))
            {
                try
                {
                    user = (await _client.GetAsync <LondonTravelUser>((p) => p.AlexaToken == accessToken, cancellationToken)).FirstOrDefault();
                }
                catch (Exception ex)
                {
                    _logger?.LogError(default, ex, "Failed to find user by access token.");
        public static async Task Can_Serialize_To_Json_And_Deserialize()
        {
            // Arrange
            var options = new JsonOptions();

            Startup.ConfigureJsonFormatter(options);

            var serializer = new SystemTextJsonCosmosSerializer(options.JsonSerializerOptions);

            var input = new LondonTravelUser()
            {
                CreatedAt = DateTimeOffset.UtcNow.UtcDateTime,
                Email     = "*****@*****.**",
                ETag      = "the-etag",
                Id        = "my-id",
                Timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
            };

            // Act
            using var stream = serializer.ToStream(input);

            // Assert
            stream.ShouldNotBeNull();
            stream.Length.ShouldBeGreaterThan(0);

            using var document = await JsonDocument.ParseAsync(stream);

            document.RootElement.GetProperty("createdAt").GetDateTime().ShouldBe(input.CreatedAt);
            document.RootElement.GetProperty("email").GetString().ShouldBe(input.Email);
            document.RootElement.GetProperty("id").GetString().ShouldBe(input.Id);
            document.RootElement.GetProperty("_etag").GetString().ShouldBe(input.ETag);
            document.RootElement.GetProperty("_ts").GetInt64().ShouldBe(input.Timestamp);

            // Arrange
            stream.Seek(0, System.IO.SeekOrigin.Begin);

            // Act
            var actual = serializer.FromStream <LondonTravelUser>(stream);

            // Assert
            actual.ShouldNotBeNull();
            actual.CreatedAt.ShouldBe(input.CreatedAt);
            actual.Email.ShouldBe(input.Email);
            actual.ETag.ShouldBe(input.ETag);
            actual.Id.ShouldBe(input.Id);
            actual.Timestamp.ShouldBe(input.Timestamp);
        }
Пример #11
0
        /// <inheritdoc />
        public async Task <LondonTravelUser?> ReplaceAsync(LondonTravelUser document, string?etag)
        {
            if (document == null)
            {
                throw new ArgumentNullException(nameof(document));
            }

            Container container = await GetContainerAsync();

            string?id = document.Id;

            _logger.LogTrace(
                "Replacing document with Id {Id} in collection {CollectionName} of database {DatabaseName}.",
                id,
                _options.CollectionName,
                _options.DatabaseName);

            ItemRequestOptions?requestOptions = GetOptionsForETag(etag);

            try
            {
                LondonTravelUser updated = await container.ReplaceItemAsync(document, id, requestOptions : requestOptions);

                _logger.LogTrace(
                    "Replaced document with Id {Id} in collection {CollectionName} of database {DatabaseName}.",
                    id,
                    _options.CollectionName,
                    _options.DatabaseName);

                return(updated);
            }
            catch (CosmosException ex) when(ex.StatusCode == HttpStatusCode.PreconditionFailed)
            {
                _logger.LogWarning(
                    "Failed to replace document with Id {Id} in collection {CollectionName} of database {DatabaseName} as the write would conflict. ETag: {ETag}.",
                    id,
                    _options.CollectionName,
                    _options.DatabaseName,
                    etag);
            }

            return(null);
        }
        private async Task <IdentityResult> UpdateClaimsAsync(LondonTravelUser user, ExternalLoginInfo info)
        {
            bool commitUpdate = false;

            if (user.RoleClaims == null)
            {
                user.RoleClaims = new List <LondonTravelRole>();
                commitUpdate    = true;
            }

            foreach (var claim in info.Principal.Claims)
            {
                bool hasClaim = user?.RoleClaims
                                .Where((p) => p.ClaimType == claim.Type)
                                .Where((p) => p.Issuer == claim.Issuer)
                                .Where((p) => p.Value == claim.Value)
                                .Where((p) => p.ValueType == claim.ValueType)
                                .Any() == true;

                if (!hasClaim)
                {
                    user.RoleClaims.Add(LondonTravelRole.FromClaim(claim));
                    commitUpdate = true;
                }
            }

            if (commitUpdate)
            {
                var result = await _userManager.UpdateAsync(user);

                if (result.Succeeded)
                {
                    _telemetry.TrackClaimsUpdated(user.Id);
                }

                return(result);
            }
            else
            {
                return(IdentityResult.Success);
            }
        }
Пример #13
0
        public static async Task AuthorizeSkill_Returns_Correct_Redirect_Url_If_Token_Created_Or_Updated(string alexaToken)
        {
            // Arrange
            string state        = "Some State";
            string clientId     = "my-client-id";
            string responseType = "token";
            Uri    redirectUri  = new Uri("https://alexa.amazon.com/alexa-london-travel?foo=bar");

            var user = new LondonTravelUser()
            {
                AlexaToken = alexaToken,
            };

            var result = IdentityResult.Success;

            var userManager = CreateUserManager(user, result);

            using var target = CreateTarget(userManager);

            // Act
            IActionResult actual = await target.AuthorizeSkill(state, clientId, responseType, redirectUri);

            // Assert
            actual.ShouldNotBeNull();

            var viewResult = actual.ShouldBeOfType <RedirectResult>();

            viewResult.Permanent.ShouldBeFalse();

            viewResult.Url.ShouldNotBeNullOrWhiteSpace();
            viewResult.Url.ShouldStartWith("https://alexa.amazon.com/alexa-london-travel?foo=bar#state=Some%20State&access_token=");
            viewResult.Url.ShouldEndWith("&token_type=Bearer");

            if (alexaToken != null)
            {
                viewResult.Url.ShouldNotContain(alexaToken);
                viewResult.Url.ShouldNotContain(Uri.EscapeUriString(alexaToken));
            }

            user.AlexaToken.ShouldNotBe(alexaToken);
            user.AlexaToken.Length.ShouldBeGreaterThanOrEqualTo(64);
        }
Пример #14
0
        public async Task <IActionResult> ExternalSignInCallback(string returnUrl = null, string remoteError = null)
        {
            if (!_isEnabled)
            {
                return(NotFound());
            }

            if (remoteError != null)
            {
                ModelState.AddModelError(string.Empty, $"Error from external provider: {remoteError}");
                return(View(nameof(SignIn)));
            }

            var info = await _signInManager.GetExternalLoginInfoAsync();

            if (info == null)
            {
                return(RedirectToRoute(SiteRoutes.SignIn));
            }

            var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent : true);

            if (result.Succeeded)
            {
                string userId = _userManager.GetUserId(info.Principal);

                _logger.LogInformation("User Id {UserId} signed in with provider {LoginProvider}.", userId, info.LoginProvider);
                _telemetry.TrackSignIn(userId, info.LoginProvider);

                return(RedirectToLocal(returnUrl));
            }

            if (result.IsLockedOut)
            {
                return(View("LockedOut"));
            }
            else
            {
                LondonTravelUser user = CreateSystemUser(info);

                if (string.IsNullOrEmpty(user?.Email))
                {
                    ViewData["Message"]   = nameof(SiteMessage.PermissionDenied);
                    ViewData["ReturnUrl"] = returnUrl;

                    return(View("SignIn"));
                }

                var identityResult = await _userManager.CreateAsync(user);

                if (identityResult.Succeeded)
                {
                    await _signInManager.SignInAsync(user, isPersistent : true);

                    _logger.LogInformation("New user account {UserId} created through {LoginProvider}.", user.Id, info.LoginProvider);

                    _telemetry.TrackAccountCreated(user.Id, user.Email, info.LoginProvider);

                    if (IsRedirectAlexaAuthorization(returnUrl))
                    {
                        return(Redirect(returnUrl));
                    }
                    else
                    {
                        return(RedirectToRoute(SiteRoutes.Home, new { Message = SiteMessage.AccountCreated }));
                    }
                }

                bool isUserAlreadyRegistered = identityResult.Errors.Any((p) => p.Code.StartsWith("Duplicate"));

                AddErrors(identityResult);

                if (isUserAlreadyRegistered)
                {
                    ViewData["Message"]   = nameof(SiteMessage.AlreadyRegistered);
                    ViewData["ReturnUrl"] = Url.RouteUrl("Manage");
                }
                else
                {
                    ViewData["ReturnUrl"] = returnUrl;
                }

                return(View("SignIn"));
            }
        }
        public async Task Can_Perform_Operations_On_Users_And_Get_Preferences_From_Api()
        {
            // Arrange
            var emailAddress = $"some.user.{Guid.NewGuid()}@some.domain.com";

            var user = new LondonTravelUser()
            {
                CreatedAt          = DateTime.UtcNow,
                Email              = emailAddress,
                EmailNormalized    = emailAddress,
                GivenName          = "Alexa",
                Surname            = "Amazon",
                UserName           = emailAddress,
                UserNameNormalized = emailAddress,
            };

            string accessToken = Controllers.AlexaController.GenerateAccessToken();

            string[] favoriteLines = new[] { "district", "northern" };
            string   userId;

            using (IUserStore <LondonTravelUser> store = GetUserStore())
            {
                // Act
                IdentityResult createResult = await store.CreateAsync(user, default);

                // Assert
                Assert.NotNull(createResult);
                Assert.True(createResult.Succeeded);
                Assert.NotEmpty(user.Id);

                // Arrange
                userId = user.Id;

                // Act
                LondonTravelUser actual = await store.FindByIdAsync(userId, default);

                // Assert
                Assert.NotNull(actual);
                Assert.Equal(userId, actual.Id);
                Assert.Null(actual.AlexaToken);
                Assert.Equal(user.CreatedAt, actual.CreatedAt);
                Assert.Equal(user.Email, actual.Email);
                Assert.False(actual.EmailConfirmed);
                Assert.NotEmpty(actual.ETag);
                Assert.Equal(Array.Empty <string>(), actual.FavoriteLines);
                Assert.Equal(user.GivenName, actual.GivenName);
                Assert.Equal(Array.Empty <LondonTravelLoginInfo>(), actual.Logins);
                Assert.Equal(user.Surname, actual.Surname);
                Assert.Equal(user.UserName, actual.UserName);

                // Arrange
                string etag = actual.ETag;

                actual.AlexaToken    = accessToken;
                actual.FavoriteLines = favoriteLines;

                // Act
                IdentityResult updateResult = await store.UpdateAsync(actual, default);

                // Assert
                Assert.NotNull(updateResult);
                Assert.True(updateResult.Succeeded);

                // Act
                actual = await store.FindByNameAsync(emailAddress, default);

                // Assert
                Assert.NotNull(actual);
                Assert.Equal(userId, actual.Id);
                Assert.Equal(emailAddress, actual.Email);
                Assert.NotEqual(etag, actual.ETag);
                Assert.Equal(accessToken, actual.AlexaToken);
                Assert.Equal(favoriteLines, actual.FavoriteLines);
            }

            // Arrange
            using (var message = new HttpRequestMessage(HttpMethod.Get, "api/preferences"))
            {
                message.Headers.Authorization = new AuthenticationHeaderValue("bearer", accessToken);

                using (var client = Fixture.CreateClient())
                {
                    // Act
                    using (var response = await client.SendAsync(message, default))
                    {
                        // Assert
                        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
                        Assert.Equal("application/json", response.Content.Headers.ContentType.MediaType);

                        string json = await response.Content.ReadAsStringAsync();

                        dynamic preferences = JObject.Parse(json);

                        Assert.Equal(userId, (string)preferences.userId);
                        Assert.Equal(favoriteLines, preferences.favoriteLines.ToObject <string[]>() as IList <string>);
                    }
                }
            }

            // Arrange
            using (IUserStore <LondonTravelUser> store = GetUserStore())
            {
                // Act
                IdentityResult updateResult = await store.DeleteAsync(new LondonTravelUser()
                {
                    Id = userId
                }, default);

                // Assert
                Assert.NotNull(updateResult);
                Assert.True(updateResult.Succeeded);
            }

            // Arrange
            using (var message = new HttpRequestMessage(HttpMethod.Get, "api/preferences"))
            {
                message.Headers.Authorization = new AuthenticationHeaderValue("bearer", accessToken);

                using (var client = Fixture.CreateClient())
                {
                    // Act
                    using (var response = await client.SendAsync(message, default))
                    {
                        // Assert
                        Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
                        Assert.Equal("application/json", response.Content.Headers.ContentType.MediaType);
                    }
                }
            }
        }