There were two minor, but annoying, bumps on the road in getting this working.
Firstly, I wanted to be able to have repeatable password encryption key, meaning if I moved the site to another server, or to my development machine, I wanted the passwords to still work. More importantly, I wanted to be able to reset the password or generate a new account using one web app, and have it usable by the other. This is not the case by default - the encryption is carried out using a default key that is specific to each web server (or site ... had a quick look but I'm not sure).
Secondly, the automated password generated was too complex. The simplest password I could get using the Web.Config settings still frequently had several symbols in it, quite confronting for most average users, who really can't understand what a 'tilde' or 'asterisk' is.
1) Repeatable Password Encryption
Password encryption is controlled by the <machinekey> attribute in the <system.web> element of web.config
<machinekey decryption="AES" decryptionkey="E3134ACE29C6C28A3B9CFD58CFD764D0AA2E2EE3468488C1D64DD331765B256F" validation="SHA1" validationkey="220C13FA9033D18C11AF964785D0C06A224B700805B3184E29973FE6A5EA3AF2E7630E81D9E24150D38891BDCACEF075DCCB287271A035993B86663FE940B056">This would not be worth a comment in itself - this fact is pretty easy to find - but the tedious part is working out how to generate a new key for your site.
Luckily, there are several online generators for that:
http://aspnetresources.com/tools/machineKey
http://www.blackbeltcoder.com/Resources/MachineKey.aspx
Now simply ensure that all the sites you want to interoperate have the same Machine Key. Remember that this key needs to be kept secret. Don't go emailing your Web.Config around the place!
2) Auto-generating Simpler Passwords
This is trickier than it seems. The Web.Config settings offer a lot of control over the complexity of passwords entered by the user, but very little over what is auto-generated by the Membership provider itself. This great article basically shows how to do it, but surprisingly, the methods suggested for generating random passwords out there veer between the wildly over-complicated and the downright crazy.
I thought I'd post a very simple but complete solution here.
Step 1: Create a Password Generation Function
This is a very simple generator function. It simply chooses randomly between an array of approved characters, taking as a single parameter the length of the desired password.
public static string GenerateFriendlyPassword(int length) { string chars = "abcdefghijkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ0123456789"; var password = new StringBuilder(length); for (int i = 0; i < length; i++) { password.Append(chars[Gbl.RandomNumGenerator.Next(chars.Length)]); } return password.ToString(); }You may notice, however, the call to
Gbl.RandomNumGenerator.Next
. A lot of the password samples out there use something like
Random rnd = new Random(); for (int i = 0; i < length; i++) { password.Append(chars[rnd.Next(chars.Length)]); } ...This appears at first sight to work, but if generating batches of passwords, you'll quickly find that you get duplicate passwords being returned. Digging into the documentation reveals that Random(), like in pretty much every other language, uses a table of pseudo-random values to generate the numbers, but it uses the current clock tick value as a seed. This means that if multiple instances of Random() are instantiated quickly enough, they'll get the same seed and produce exactly the same string of 'random' numbers.
My solution is to keep a global (static) instance of Random() and use that for all number generation. Here's the code for that (in class Gbl):
private static Random randomNumGenerator = null; private static DateTime lastRandomNumGeneratorSeedTime = DateTime.Now; public static Random RandomNumGenerator { get { lock (typeof(Random)) { if (randomNumGenerator == null) { randomNumGenerator = new Random(); } else { if (DateTime.Now > lastRandomNumGeneratorSeedTime.AddSeconds(1)) { randomNumGenerator = new Random(); lastRandomNumGeneratorSeedTime = DateTime.Now; } } return randomNumGenerator; } } }This uses a global instance of Random, but also refreshes the seed value if it's been more than one second since the last use of the global instance. That ensures that there is some time-based randomness injected into the seed rather than just reeling out the values from the pseudo-random list.
EDIT: See updated post for a more secure solution to this !
Step 2: Override the Default Membership Provider
This is simple - we just override the password generation function of the provider and keep everything else the same.
using System; using System.Text; using System.Web.Security; namespace MyApp { public class MyAppMembershipProvider : System.Web.Security.SqlMembershipProvider { public int GeneratedPasswordLength = 6; public MyAppMembershipProvider () : base() { } public override string GeneratePassword() { return GenerateFriendlyPassword(GeneratedPasswordLength); } public static string GenerateFriendlyPassword(int length) { string chars = "abcdefghijkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ0123456789"; var password = new StringBuilder(length); for (int i = 0; i < length; i++) { password.Append(chars[Gbl.RandomNumGenerator.Next(chars.Length)]); } return password.ToString(); } } }
Step 3: Reference the New MembershipProvider in the Web.Config
The MembershipProvider will be referenced in your Web.Config something like this:
<membership defaultProvider="AspNetSqlMembershipProvider"> <providers> <clear/> <add name="AspNetSqlMembershipProvider" type="System.Web.Security.SqlMembershipProvider" connectionStringName="Db" ... </providers> </membership>All that's needed is to change the
type="System.Web.Security.SqlMembershipProvider"to
type="MyApp.MyAppMembershipProvider"That's it!
From a security standpoint using the basic Random class is a bad idea. It seems like it would be very easy to bruteforce those passwords, simply by knowing the time that a targeted account was created.
ReplyDeleteUse RNGCryptoServiceProvider for a better random generator:
http://msdn.microsoft.com/en-us/library/system.security.cryptography.rngcryptoserviceprovider.aspx
Thanks Joakim, it looks like you're right. In this case, I have left the default ASP .NET behaviour of account lockout after three failed login attempts within ten minutes, bruteforce is ruled out.
DeleteBut your point is certainly generally valid - it does introduce a vulnerability into the system that could be exploited in different circumstances. I've edited the post to link to a new post which offers alternative random number generation.