예제 #1
0
        public void DateIsProjectedForwardCorrectly(int input)
        {
            var fixedDate = DateTime.UtcNow;

            var dateMock = new Mock <IDateExtra>();

            dateMock.Setup(d => d.UtcNow)
            .Returns(fixedDate);

            Assert.True(
                TimerExtra.ProjectSeconds(dateMock.Object, input) > new BsonDateTime(fixedDate),
                "ProjectSeconds is larger when projecting into the future.");
        }
        /// <summary>
        /// Initializes a new instance of the <see cref="UsersAndAuthentication"/> class.
        /// </summary>
        /// <param name="accessRepo">Supplied through DI.</param>
        /// <param name="signUpRepo">Supplied through DI.</param>
        /// <param name="claimRepo">Supplied through DI.</param>
        /// <param name="personRepo">Supplied through DI.</param>
        /// <param name="env">Supplied through DI.</param>
        /// <param name="date">Supplied through DI.</param>
        public UsersAndAuthentication(
            IRepository <AccessTokenSchema> accessRepo,
            IRepository <SignUpTokenSchema> signUpRepo,
            IRepository <ClaimTokenSchema> claimRepo,
            IRepository <PersonSchema> personRepo,
            IAppEnvironment env,
            IDateExtra date)
            : base("/")
        {
            this.Post <PostUsers>("users", async(req, res) =>
            {
                var newPerson = await req.BindAndValidate <PersonRequest>();

                if (!newPerson.ValidationResult.IsValid)
                {
                    res.StatusCode = Status400BadRequest;
                    return;
                }

                var person = await GoogleApi.ValidateUser(newPerson.Data.idToken, env);
                if (person == null)
                {
                    res.StatusCode = Status401Unauthorized;
                    return;
                }

                var filter = Builders <SignUpTokenSchema>
                             .Filter
                             .Where(t => t.Hash == PasswordHasher.Hash(newPerson.Data.signUpToken));

                var signUpDoc = await signUpRepo.FindOne(filter);

                if (signUpDoc == null || signUpDoc.CreatedAt.IsAfter(date, signUpDoc.ExpirationSeconds))
                {
                    res.StatusCode = Status401Unauthorized;
                    return;
                }

                if (!signUpDoc.IsExisting)
                {
                    res.StatusCode = Status404NotFound;
                    return;
                }

                var update = Builders <SignUpTokenSchema> .Update
                             .Set(s => s.IsExisting, false);

                var existingPerson = await personRepo.FindById(person.Subject);

                // Check if the person already exists
                if (existingPerson != null)
                {
                    // Recreate them if they where deleted
                    if (!existingPerson.IsExisting)
                    {
                        var filterPerson = Builders <PersonSchema> .Filter
                                           .Where(p => p.Id == person.Subject);

                        var updatePerson = Builders <PersonSchema> .Update
                                           .Set(p => p.IsExisting, true);

                        await personRepo.Update(filterPerson, updatePerson);
                        await signUpRepo.Update(filter, update);

                        await res.FromString();
                    }
                    else
                    {
                        // Return error is they really exist
                        res.StatusCode = Status409Conflict;
                    }

                    return;
                }

                var personObject = new PersonSchema
                {
                    InternalId = ObjectId.GenerateNewId(),
                    Id         = person.Subject,
                    Fullname   = person.Name,
                    Firstname  = person.GivenName,
                    Lastname   = person.FamilyName,
                    Email      = person.Email,
                    CreatedAt  = new BsonDateTime(date.UtcNow),
                    Role       = signUpDoc.Role,
                };

                await personRepo.Add(personObject);
                await signUpRepo.Update(filter, update);

                await res.FromString();
            });

            // Due to a bug with adding multiple routes of the same path
            // Path in different modules, we manual auth here
            this.Get <GetUsers>("users", async(req, res) =>
            {
                var isAllowed = await PreSecurity.CheckAccessDirectly(req, res, accessRepo, date, RoleOptions.Admin);

                if (!isAllowed)
                {
                    return;
                }

                var filter = Builders <PersonSchema> .Filter
                             .Where(p => p.IsExisting == true);

                var sorter = Builders <PersonSchema> .Sort
                             .Descending(p => p.Fullname);

                var persons = await personRepo.FindAll(filter, sorter);

                var personsWithoutId = persons.Select((p) =>
                {
                    p.InternalId = null;
                    return(p);
                });

                var responceBody = new PersonsResponce
                {
                    ContentList = new List <PersonSchema>(personsWithoutId),
                };

                string json = JsonQuery.FulfilEncoding(req.Query, responceBody);
                if (json != null)
                {
                    await res.FromJson(json);
                    return;
                }

                await res.FromBson(responceBody);
            });

            this.Post <PostAccessToken>("users/{id}/tokens/", async(req, res) =>
            {
                string userID = req.RouteValues.As <string>("id");
                var userDoc   = await personRepo.FindById(userID);

                if (userDoc == null || !userDoc.IsExisting)
                {
                    res.StatusCode = Status404NotFound;
                    return;
                }

                var newAccessRequest = await req.BindAndValidate <AccessTokenRequest>();
                if (!newAccessRequest.ValidationResult.IsValid)
                {
                    res.StatusCode = Status400BadRequest;
                    return;
                }

                var person = await GoogleApi.ValidateUser(newAccessRequest.Data.idToken, env);
                if (person == null)
                {
                    res.StatusCode = Status401Unauthorized;
                    return;
                }

                if (person.Subject != userID)
                {
                    res.StatusCode = Status409Conflict;
                    return;
                }

                string newToken = Generate.GetRandomToken();
                string newHash  = PasswordHasher.Hash(newToken);

                var tokenObject = new AccessTokenSchema
                {
                    InternalId = ObjectId.GenerateNewId(),
                    Hash       = newHash,
                    User       = userDoc.InternalId,
                    CreatedAt  = new BsonDateTime(date.UtcNow),
                    Role       = userDoc.Role,
                };

                await accessRepo.Add(tokenObject);

                if (newAccessRequest.Data.claimToken != null)
                {
                    var filterClaims = Builders <ClaimTokenSchema> .Filter
                                       .Where(c => c.Hash == PasswordHasher.Hash(newAccessRequest.Data.claimToken));

                    var claimDoc = await claimRepo.FindOne(filterClaims);

                    // 401 returned twice, which may be hard for the client to interpret
                    if (claimDoc == null || claimDoc.CreatedAt.IsAfter(date, claimDoc.ExpirationSeconds))
                    {
                        res.StatusCode = Status401Unauthorized;
                        return;
                    }

                    // Don't allow overwriting an access token
                    if (claimDoc.Access == null && claimDoc.IsExisting)
                    {
                        var update = Builders <ClaimTokenSchema> .Update
                                     .Set(c => c.Access, tokenObject.InternalId)
                            #pragma warning disable SA1515
                                     // Unfortunately claim access tokens have to be saved to the database
                                     // So that state can be communicated between clients
                            #pragma warning restore SA1515
                                     .Set(c => c.AccessToken, newToken);

                        await claimRepo.Update(filterClaims, update);
                    }

                    await res.FromString();
                }
                else
                {
                    var responce = new AccessTokenResponce
                    {
                        AccessToken = newToken,
                        Expiration  = TimerExtra.ProjectSeconds(date, tokenObject.ExpirationSeconds),
                    };

                    string json = JsonQuery.FulfilEncoding(req.Query, responce);
                    if (json != null)
                    {
                        await res.FromJson(json);
                        return;
                    }

                    await res.FromBson(responce.ToBsonDocument());
                }
            });

            this.Post <PostClaims>("authentication/claims/", async(req, res) =>
            {
                string newToken = Generate.GetRandomToken();
                string newHash  = PasswordHasher.Hash(newToken);

                var tokenDoc = new ClaimTokenSchema
                {
                    InternalId = ObjectId.GenerateNewId(),
                    Hash       = newHash,
                    CreatedAt  = new BsonDateTime(date.UtcNow),
                };

                await claimRepo.Add(tokenDoc);

                var responce = new ClaimTokenResponce
                {
                    ClaimToken = newToken,
                    Expiration = TimerExtra.ProjectSeconds(date, tokenDoc.ExpirationSeconds),
                };

                string json = JsonQuery.FulfilEncoding(req.Query, responce);
                if (json != null)
                {
                    await res.FromJson(json);
                    return;
                }

                await res.FromBson(responce.ToBsonDocument());
            });

            this.Get <GetClaims>("authentication/claims/", async(req, res) =>
            {
                string claimToken = req.Cookies["ExperienceCapture-Claim-Token"];
                if (claimToken == null)
                {
                    res.StatusCode = Status400BadRequest;
                    return;
                }

                var filter = Builders <ClaimTokenSchema> .Filter
                             .Where(c => c.Hash == PasswordHasher.Hash(claimToken));
                var claimDoc = await claimRepo.FindOne(filter);

                if (claimDoc == null)
                {
                    res.StatusCode = Status404NotFound;
                    return;
                }

                if (!claimDoc.IsExisting || claimDoc.CreatedAt.IsAfter(date, claimDoc.ExpirationSeconds))
                {
                    res.StatusCode = Status404NotFound;
                    return;
                }

                if (claimDoc.Access == null)
                {
                    res.StatusCode = Status202Accepted;
                    await res.FromString("PENDING");
                    return;
                }

                var update = Builders <ClaimTokenSchema> .Update
                             .Set(c => c.IsExisting, false)
                    #pragma warning disable SA1515
                             // Removes the access token from the database
                             // Important to increase security
                    #pragma warning restore SA1515
                             .Set(c => c.AccessToken, null);

                await claimRepo.Update(filter, update);

                var responce = new AccessTokenResponce
                {
                    AccessToken = claimDoc.AccessToken,
                    Expiration  = TimerExtra.ProjectSeconds(date, claimDoc.ExpirationSeconds),
                };

                string json = JsonQuery.FulfilEncoding(req.Query, responce);
                if (json != null)
                {
                    await res.FromString(json);
                    return;
                }

                await res.FromBson(responce.ToBsonDocument());
            });

            this.Post <PostAdmin>("authentication/admins/", async(req, res) =>
            {
                var newAdmin = await req.BindAndValidate <AdminPasswordRequest>();
                if (!newAdmin.ValidationResult.IsValid)
                {
                    res.StatusCode = Status400BadRequest;
                    return;
                }

                if (!PasswordHasher.Check(newAdmin.Data.password, env.PasswordHash) && env.SkipValidation != "true")
                {
                    res.StatusCode = Status401Unauthorized;
                    return;
                }

                string newToken = Generate.GetRandomToken();

                var tokenDoc = new SignUpTokenSchema
                {
                    InternalId = ObjectId.GenerateNewId(),
                    Hash       = PasswordHasher.Hash(newToken),
                    CreatedAt  = new BsonDateTime(date.UtcNow),
                    Role       = RoleOptions.Admin,
                };

                await signUpRepo.Add(tokenDoc);

                var responce = new SignUpTokenResponce
                {
                    SignUpToken = newToken,
                    Expiration  = TimerExtra.ProjectSeconds(date, tokenDoc.ExpirationSeconds),
                };

                string json = JsonQuery.FulfilEncoding(req.Query, responce);
                if (json != null)
                {
                    await res.FromJson(json);
                    return;
                }

                await res.FromBson(responce.ToBsonDocument());
            });
        }
        /// <summary>
        /// Initializes a new instance of the <see cref="ProtectedUsersAndAuthentication"/> class.
        /// </summary>
        /// <param name="accessRepo">Supplied through DI.</param>
        /// <param name="signUpRepo">Supplied through DI.</param>
        /// <param name="personRepo">Supplied through DI.</param>
        /// <param name="env">Supplied through DI.</param>
        /// <param name="date">Supplied through DI.</param>
        public ProtectedUsersAndAuthentication(
            IRepository <AccessTokenSchema> accessRepo,
            IRepository <SignUpTokenSchema> signUpRepo,
            IRepository <PersonSchema> personRepo,
            IAppEnvironment env,
            IDateExtra date)
            : base("/")
        {
            this.Before += PreSecurity.CheckAccess(accessRepo, date);

            this.Get <GetUser>("users/{id}/", async(req, res) =>
            {
                string userID = req.RouteValues.As <string>("id");
                var person    = await personRepo.FindById(userID);

                if (person == null || !person.IsExisting)
                {
                    res.StatusCode = Status404NotFound;
                    return;
                }

                // Has to exit due to pre-security check
                string token    = req.Cookies["ExperienceCapture-Access-Token"];
                var accessToken = await accessRepo.FindOne(
                    Builders <AccessTokenSchema>
                    .Filter
                    .Where(a => a.Hash == PasswordHasher.Hash(token)));

                if (person.InternalId != accessToken.User && accessToken.Role != RoleOptions.Admin)
                {
                    res.StatusCode = Status401Unauthorized;
                    return;
                }

                string json = JsonQuery.FulfilEncoding(req.Query, person);
                if (json != null)
                {
                    await res.FromJson(json);
                    return;
                }

                person.InternalId = null;

                await res.FromBson(person);
            });

            this.Delete <DeleteUser>("users/{id}/", async(req, res) =>
            {
                string userID = req.RouteValues.As <string>("id");
                var person    = await personRepo.FindById(userID);

                if (person == null)
                {
                    res.StatusCode = Status404NotFound;
                    return;
                }

                // Has to exit due to pre-security check
                string token     = req.Cookies["ExperienceCapture-Access-Token"];
                var accessFilter = Builders <AccessTokenSchema>
                                   .Filter
                                   .Where(a => a.Hash == PasswordHasher.Hash(token));
                var accessToken = await accessRepo.FindOne(accessFilter);

                // Check if the user being requested is the same
                // As the one requesting, unless they have the admin role
                if (person.InternalId != accessToken.User && accessToken.Role != RoleOptions.Admin)
                {
                    res.StatusCode = Status401Unauthorized;
                    return;
                }

                var filter = Builders <PersonSchema> .Filter
                             .Where(p => p.Id == userID);

                var update = Builders <PersonSchema> .Update
                             .Set(p => p.IsExisting, false);

                await personRepo.Update(filter, update);

                await res.FromString();
            });

            this.Post <PostSignUp>("authentication/signUps", async(req, res) =>
            {
                string newToken = Generate.GetRandomToken();

                var tokenDoc = new SignUpTokenSchema
                {
                    InternalId = ObjectId.GenerateNewId(),
                    Hash       = PasswordHasher.Hash(newToken),
                    CreatedAt  = new BsonDateTime(date.UtcNow),
                };

                await signUpRepo.Add(tokenDoc);

                var responce = new SignUpTokenResponce
                {
                    SignUpToken = newToken,
                    Expiration  = TimerExtra.ProjectSeconds(date, tokenDoc.ExpirationSeconds),
                };
                var responceDoc = responce.ToBsonDocument();

                string json = JsonQuery.FulfilEncoding(req.Query, responceDoc);
                if (json != null)
                {
                    await res.FromJson(json);
                    return;
                }

                await res.FromBson(responceDoc);
            });
        }