/* * Tries to login with the given cookie as credentials */ static void LoginFromCookie(HttpCookie cookie, ApplicationContext context) { // User has persistent cookie associated with client var cookieSplits = cookie.Value.Split(' '); if (cookieSplits.Length != 2) { throw new SecurityException("Cookie not accepted"); } // Retrieving username and (hashed/salted) password from cookie. var cookieUsername = cookieSplits [0]; var cookiePassword = cookieSplits [1]; // Retrieving password file in Node format. var pwdFile = AuthFile.GetAuthFile(context); // Checking if user exist. var userNode = pwdFile ["users"] [cookieUsername]; if (userNode == null) { throw new SecurityException("Cookie not accepted"); } /* * Checking if user's password is a match. * * Notice, we need to double hash the password from "auth", since the * reference stored in cookie is double hashed, and has the client's fingerprint * added to it, to reduce probability of cookie theft. * * Notice also that since the credential cookie has roughly 1.0e+77 amount of * entropy, slow hashing or using blow fish at this point is pointless, and * would simply add additional overhead for the initial loading of our page * when the persistent credential cookie needs to be verified. Hence we * can safely get away with "fast hashing" at this point, but adding parts * of the clients fingerprint into the mix, to avoid at least to some extent * credential cookie theft. */ if (cookiePassword == Passwords.HashPasswordForCookieStorage( context, userNode ["password"].Get <string> (context))) { // MATCH, discarding previous Context Ticket and creating a new Ticket. SetTicket(context, new ContextTicket(userNode.Name, userNode ["role"].Get <string> (context), false)); // Evaluates user's [.onlogin] section, if it exists. EvaluateOnLoginIfExisting(context); } else { // Catched above, which destroys cookie, and associates the default context with user. throw new Exception(); } }
/* * Tries to login user according to given user credentials. */ public static void Login(ApplicationContext context, Node args) { // Defaulting result of Active Event to unsuccessful. args.Value = false; // Retrieving supplied credentials. var username = args.GetExChildValue <string> ("username", context); var password = args.GetExChildValue <string> ("password", context); args.FindOrInsert("password").Value = "xxx"; // In case an exception occurs. var persist = args.GetExChildValue("persist", context, false); // Retrieving password file as a Node. var pwdFile = AuthFile.GetAuthFile(context); /* * Checking for match on specified username. */ var userNode = pwdFile ["users"] [username]; if (userNode == null) { // Username doesn't exist. throw new LambdaSecurityException(_credentialsNotAcceptedException, args, context); } /* * Checking if current username has attempted to login just recently, and the * configured timespan for each successive login attempt per user, has not passed. * * This should be able to provide some rudimentary defense from a "brute force password attack". * * Notice, we do this after we have checked if the username exists, to avoid having an adversary * flood the server's cache with bogus usernames associated with DateTime objects. * We also us the web cache, which (of course) means the object will only exists for some * time, defaulting to the settings of the web cache for your system. */ var cooldown = context.RaiseEvent( ".p5.config.get", new Node(".p5.config.get", _cooldownPeriodConfigName)) [0]?.Get(context, -1) ?? -1; if (cooldown != -1) { // User has configured the system to have a "cooldown period" for successive login attempts. var bruteForceLastAttempt = new Node(".p5.web.cache.get", _bruteForceCacheName + username); var lastAttemptNode = context.RaiseEvent(".p5.web.cache.get", bruteForceLastAttempt); if (lastAttemptNode.Count > 0) { // Previous attempt has been attempted recently. var date = lastAttemptNode [0].Get <DateTime> (context, DateTime.MinValue); var timeSpanSeconds = Convert.ToInt32((DateTime.Now - date).TotalSeconds); if (timeSpanSeconds < cooldown) { // Cooldown period has not passed. throw new LambdaException("You need to wait " + (cooldown - timeSpanSeconds) + " seconds before you can try again", args, context); } } } // Checking for match on password. if (!Passwords.VerifyPasswordIsCorrect(password, userNode ["password"].Get <string> (context))) { /* * Making sure we guard against brute force password attacks, before we throw security exception. * * Notice, this prevents the same username from attempting to login more than once every n seconds, * which is configurable in the config file of the app. */ var bruteForceLastAttempt = new Node(".p5.web.cache.set", _bruteForceCacheName + username); bruteForceLastAttempt.Add("src", DateTime.Now); context.RaiseEvent(".p5.web.cache.set", bruteForceLastAttempt); throw new LambdaSecurityException(_credentialsNotAcceptedException, args, context); } // Success, creating our context ticket. var role = userNode ["role"].Get <string> (context); SetTicket(context, new ContextTicket(username, role, false)); // Signaling success to caller. args.Value = true; // Checking if we should create persistent cookie on disc to remember username/password for given client. if (persist) { // Caller wants to create persistent cookie to remember username/password. var cookie = new HttpCookie(_credentialCookieName); cookie.Expires = DateTime.Now.AddDays(context.RaiseEvent( ".p5.config.get", new Node(".p5.config.get", "p5.auth.credential-cookie-valid")) [0].Get <int> (context)); // To avoid JavaScript access to credential cookie. cookie.HttpOnly = true; /* * The value of our cookie is in "username hashed-password" format. * * This is an entropy of roughly 1.1579e+77, making a brute force attack * impossible, at least without a Rainbow/Dictionary attack, which should * be effectively prevented, by having a single static server salt, * which again is cryptographically secured and persisted to disc * in the "auth" file, and hence normally inaccessible for an adversary. * * Notice, we double hash the password we store in our cookie, to make * sure we never expose the parts of our password we store in our "auth" file. */ cookie.Value = username + " " + Passwords.HashPasswordForCookieStorage( context, userNode ["password"].Get <string> (context)); HttpContext.Current.Response.Cookies.Add(cookie); } // Evaluates user's [.onlogin] section, if it exists. EvaluateOnLoginIfExisting(context); }
/* * Creates a new user. */ public static void CreateUser(ApplicationContext context, Node args) { // Retrieving arguments. var username = args.GetExValue <string> (context); var password = args.GetExChildValue <string> ("password", context); var role = args.GetExChildValue <string> ("role", context); // Sanity checking role name towards guest account name. if (role == context.RaiseEvent(".p5.auth.get-default-context-role").Get <string> (context)) { throw new LambdaException("Sorry, but that's the name of our guest account role.", args, context); } // Sanity checking username towards guest account name. if (username == context.RaiseEvent(".p5.auth.get-default-context-username").Get <string> (context)) { throw new LambdaException("Sorry, but that's the name of our guest account.", args, context); } // Making sure [password] never leaves method in case of an exception. args.FindOrInsert("password").Value = "xxx"; // Retrieving password rules from web.config, if any. if (!Passwords.IsGoodPassword(context, password)) { // New password was not accepted, throwing an exception. throw new LambdaSecurityException( "Password didn't obey by your configuration settings, which are as follows; " + Passwords.PasswordRuleDescription(context), args, context); } // Basic sanity check new user's data. if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password) || string.IsNullOrEmpty(role)) { throw new LambdaException( "User must have username as value, [password] and [role] at the very least", args, context); } // Verifying username is valid, since we'll need to create a folder for user. VerifyUsernameValid(username); // To reduce lock time of "auth" file, we execute Blow Fish hashing before we enter lock. password = Passwords.SaltAndHashPassword(context, password); // Locking access to password file as we create new user object. AuthFile.ModifyAuthFile( context, delegate(Node authFile) { // Checking if user exist from before. if (authFile ["users"] [username] != null) { throw new LambdaException( "Sorry, that [username] is already taken by another user in the system", args, context); } // Adding user. var userNode = authFile ["users"].Add(username).LastChild; // Salting and hashing password, before storing it in "auth" file. userNode.Add("password", password); // Adding user to specified role. userNode.Add("role", role); // Adding all other specified objects to user. userNode.AddRange(args.Children.Where(ix => ix.Name != "password" && ix.Name != "role").Select(ix => ix.Clone())); }); // Creating newly created user's directory structure. CreateUserDirectory(context, username); }
/* * Edits an existing user. */ public static void EditUser(ApplicationContext context, Node args) { // Retrieving username, and sanity checking invocation. var username = args.GetExValue <string> (context); if (args ["username"] != null) { throw new LambdaSecurityException("Cannot change username for user", args, context); } // Retrieving new password and role, defaulting to null, which will not update existing values. var password = args.GetExChildValue <string> ("password", context, null); var role = args.GetExChildValue <string> ("role", context, null); // Sanity checking role name towards guest account name. if (role == context.RaiseEvent(".p5.auth.get-default-context-role").Get <string> (context)) { throw new LambdaException("Sorry, but that's the name of your system's guest account role.", args, context); } // Changing user's password, but only if a [password] argument was explicitly supplied by caller. if (!string.IsNullOrEmpty(password)) { // Verifying password conforms to password rules. if (!Passwords.IsGoodPassword(context, password)) { // New password was not accepted, throwing an exception. args.FindOrInsert("password").Value = "xxx"; var description = Passwords.PasswordRuleDescription(context); throw new LambdaSecurityException("Password didn't obey by your configuration settings, which are as follows; " + description, args, context); } } // To reduce lock time of "auth" file we execute Blow Fish hashing before we enter lock. password = password == null ? null : Passwords.SaltAndHashPassword(context, password); // Locking access to password file as we edit user object. AuthFile.ModifyAuthFile( context, delegate(Node authFile) { // Checking to see if user exist. if (authFile ["users"] [username] == null) { throw new LambdaException( "Sorry, that user does not exist", args, context); } // Updating user's password, but only if a new password was supplied by caller. if (!string.IsNullOrEmpty(password)) { authFile ["users"] [username] ["password"].Value = password; } // Updating user's role, if a new role was supplied by caller. if (role != null) { authFile ["users"] [username] ["role"].Value = role; } // Checking if caller wants to edit settings. if (args.Name == "p5.auth.users.edit") { // Removing old settings. authFile ["users"] [username].RemoveAll(ix => ix.Name != "password" && ix.Name != "role" && ix.Name != "salt"); // Adding all other specified objects to user. foreach (var idxNode in args.Children.Where(ix => ix.Name != "password" && ix.Name != "role" && ix.Name != "salt")) { authFile ["users"] [username].Add(idxNode.Clone()); } } }); }