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); }); }