Thursday, January 6, 2011

Facebook OAuth and MVC

[DEPRECATED: This is all pretty much too old to be useful. I left it up, but I wouldn't use it.]


Website registration bugs me. Every time I touch it I feel like I'm reinventing something. Although there are some nice account management providers that come with default projects (I'm using MVC 3 for the examples below), there are no default providers for OpenId and now, more importantly, Facebook OAuth.

You can read all about the many <a href="http://www.codinghorror.com/blog/2010/11/your-internet-drivers-license.html">discussions</a> on the topic, but I'm convinced. Sort of.

Here is the problem I have. I just don't trust anyone. I don't trust them to be always available, I don't trust them to not monkey around with their authorization schemes, or to simply stop offering them, or to change what data you can associate with a user logged in under their authorization code.

So what I have done, usually, is to allow users to log on either by creating a local account which I maintain, or login through OpenId or Facebook but then creating an associated local account. Should Facebook go down, or become untenable forwhatever reason, for example, I still have a local user account that I could verify by various means and then open back up as a purely local account.
So, when a user logs in from FB, for example, I create a local username (and autogenerate a password), save all the personalization data I'm allowed, and associate all local content with my local user. I'm just authenticating via FB.

In my case, I dont' want to rewrite much at all, don't want to mess with the database structure you get from aspnet_regsql (though this db feels crusty and in need of updating).

So I have a couple of issues:
a) username: When registering locally the user can create their own username. With OpenId it can come back with a usernam that is all over the map (email address, etc.) and FB gives you a numerical Id. So, at a minium I need a 'display name' of some kind so I don't have to use the username to know them. This requires some additional fields but not too onerous.
Also, I need to avoid username duplication, and I want some sense of where the username is being generated. So in my case, currently I'm just appending fb_ to facebook accounts, oid_ to OpenId accounts, and excluding those as options for locally created usernames.
You could do some of this using some of the Facebook C# SDKs out there. But, at least on my examination the ones I looked at didn't do the specific login scenario I'm looking for. So, I rolled my own. I might still use them for the rest of mu interaction with Facebook.
So here, is my code. I'm sure I'll update this further:
First, in my Web.config file I add three app settings:


<add key="FacebookAppId" value="[YOUR FACEBOOK APP ID]" />
<add key="ValidateOAuthAgainstLocalStore" value="[TRUE / FALSE" />
<add key="FacebookNewAccountFormatString" value="fb__{0}" />
If you want to validate the user against your local database (as I do), then make ValidateOAuthAgainstLocalStore value="true", otherwise "false" will allow you to make them an authenticated user. The FacebookNewAccountFormatString appends fb_ to the new local user account created.
Here is the _LogOnFacebookPartial.cshtml code:

@using System.Configuration;
<div id="fb-root"></div>

@* To Make FB Synchronous use this code...
        <script src="http://connect.facebook.net/en_US/all.js"></script>
        <script> 
        FB.init({appId: @ConfigurationManager.AppSettings["FacebookAppId"], cookie: true,status: true, xfbml: true});
        FB.Event.subscribe('auth.sessionChange', function (response) {if (response.session) {login();} else {logout();}});
        FB.api('/me', function (user) {if (user != null && user.id != null) {var image = document.getElementById('image');if (image != null) {image.src = 'http://graph.facebook.com/' + user.id + '/picture';var name = document.getElementById('name');name.innerHTML = user.name}}});
...*@

@* Aysnc FB code is from here..._____________________________________________________________________________________ *@ 
<script>
    window.fbAsyncInit = function() {
        FB.init({
            appId: @ConfigurationManager.AppSettings["FacebookAppId"], cookie: true,
            status: true, xfbml: true
        });
        FB.Event.subscribe('auth.sessionChange', function (response) {
            if (response.session) {
                login();
            } else {
                logout();
            }

        });
        FB.api('/me', function (user) {
            if (user != null && user.id != null) {
                var image = document.getElementById('image');
                if (image != null) {
                    image.src = 'http://graph.facebook.com/' + user.id + '/picture';
                    var name = document.getElementById('name');
                    name.innerHTML = user.name
                }
            }
        });
    };
    (function() {
        var e = document.createElement('script'); e.async = true;
        e.src = document.location.protocol +
        '//connect.facebook.net/en_US/all.js';
        document.getElementById('fb-root').appendChild(e);
    }());
   @* ...to here___________________________________________________________________________________________________ *@
    
    function login() {
        var url = "/account/facebooklogon";
        $.post(url, null, function (data) {
            window.location = window.location.href.toString();
        });
    }

    function logout() {
        var url = "/account/logoff";
        $.post(url, null, function (data) {
            window.location = window.location.href.toString();
        });
    }
        
</script>

@if (Request.IsAuthenticated) {
    <text>
    <div align="center">
        <img id="image" />
        <div id="name"></div>
    </div>
    <fb:login-button autologoutlink="true" size="small">Log Out</fb:login-button></text>
}
else {
    <text><fb:login-button size="small" autologoutlink="true">Login</fb:login-button></text>
} 



I add one method to the AccountController:

// **************************************
        // URL: /account/facbooklogon
        // **************************************

        public ActionResult FacebookLogOn() {
            MembershipService = new FacebookMembershipService();
            var fbms = MembershipService as FacebookMembershipService;
            if (MembershipService.ValidateOpenAuthUser(bool.Parse(ConfigurationManager.AppSettings["ValidateOpenAuthAgainstLocalStore"]))) {
                FormsService.SignIn(fbms.UserName, true);
                return new EmptyResult();
            }
            MembershipService.CreateUser(fbms.UserName, fbms.NewPassword, fbms.Email);
            return new EmptyResult();
        }


I add some RandomPassword creation code (I'll attach something below, but you can create your own or use their username or whatever, as long as you block their ability to login locally — i.e. w/o first authenticating via Facebook — using their Facebook Id).

I add the following dataannotation to the AccountModel class (that comes with the default MVC 3 (and other) projects:
[RegularExpression(@"^(?!fb__).+", ErrorMessage = "Sorry! You cannot start the user name with 'fb__'.")]


The partial view has my Facebook login code (I use async code but you can use sync code which I've included). I reference the webconfig AppSetting with my Facebook AppId. After login or logout it gets the user data from the cookie Facebook sends down, authenticates it, and authenticates/creates a local account if ValidateOAuthAgainstLocalStore value="true".
I add one method to the AccountModel interface IMembershipService. You could get by without this change, but then you would have to do some additional monkey business in the Controller to get the method, which I wanted to avoid... and I'm not worried about breaking any legacy code.
bool ValidateOAuthUser(bool validateAgainstLocalStore);


I add a not implemented method to AccountMembershipService:
public bool ValidateOAuthUser(bool validateAgainstLocalStore) {
            throw new NotImplementedException();
        }


and I create a local FacebookUser class (don't use this much, so could actually get by without it, but I'm including it below for completeness, and I create a new FacebookMembershipService that inherits from IMembershipService. This is responsible for extracting the data from the cookie and then getting user info from Facebook and doing the local account authentication and creation. The full AccountModel code is below:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Configuration;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Web;
using System.Web.Mvc;
using System.Web.Script.Serialization;
using System.Web.Security;

namespace FloatingFactory.AccountManagement.Models {

    #region Models

    public class ChangePasswordModel {
        [Required]
        [DataType(DataType.Password)]
        [Display(Name = "Current password")]
        public string OldPassword { get; set; }

        [Required]
        [ValidatePasswordLength]
        [DataType(DataType.Password)]
        [Display(Name = "New password")]
        public string NewPassword { get; set; }

        [DataType(DataType.Password)]
        [Display(Name = "Confirm new password")]
        [Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")]
        public string ConfirmPassword { get; set; }
    }

    public class LogOnModel {
        [Required]
        [Display(Name = "User name")]
        public string UserName { get; set; }

        [Required]
        [DataType(DataType.Password)]
        [Display(Name = "Password")]
        public string Password { get; set; }

        [Display(Name = "Remember me?")]
        public bool RememberMe { get; set; }
    }

    public class RegisterModel {
        [Required]
        [Display(Name = "User name")]
        [RegularExpression(@"^(?!fb__).+", ErrorMessage = "Sorry! You cannot start the user name with 'fb__'.")]
        public string UserName { get; set; }

        [Required]
        [DataType(DataType.EmailAddress)]
        [Display(Name = "Email address")]
        public string Email { get; set; }

        [Required]
        [ValidatePasswordLength]
        [DataType(DataType.Password)]
        [Display(Name = "Password")]
        public string Password { get; set; }

        [DataType(DataType.Password)]
        [Display(Name = "Confirm password")]
        [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
        public string ConfirmPassword { get; set; }
    }

    public class FacebookUser {
        public string id { get; set; }
        public string name { get; set; }
        public string first_name { get; set; }
        public string last_name { get; set; }
        public string birthday { get; set; }
        public string email { get; set; }
        public string gender { get; set; }
        public string link { get; set; }
    }
    #endregion

    #region Services
    // The FormsAuthentication type is sealed and contains static members, so it is difficult to
    // unit test code that calls its members. The interface and helper class below demonstrate
    // how to create an abstract wrapper around such a type in order to make the AccountController
    // code unit testable.

    public enum LogonTypeIs {
        Facebook,
        OpenId
    }

    public interface IMembershipService {
        int MinPasswordLength { get; }

        bool ValidateOAuthUser(bool validateAgainstLocalStore);
        bool ValidateUser(string userName, string password);
        MembershipCreateStatus CreateUser(string userName, string password, string email);
        bool ChangePassword(string userName, string oldPassword, string newPassword);
    }

    public class AccountMembershipService : IMembershipService {
        private readonly MembershipProvider _provider;

        public AccountMembershipService()
            : this(null) {
        }

        public AccountMembershipService(MembershipProvider provider) {
            _provider = provider ?? Membership.Provider;
        }

        public int MinPasswordLength {
            get {
                return _provider.MinRequiredPasswordLength;
            }
        }

        public bool ValidateOAuthUser(bool validateAgainstLocalStore) {
            throw new NotImplementedException();
        }

        public bool ValidateUser(string userName, string password) {
            if (String.IsNullOrEmpty(userName)) throw new ArgumentException("Value cannot be null or empty.", "userName");
            if (String.IsNullOrEmpty(password)) throw new ArgumentException("Value cannot be null or empty.", "password");

            return _provider.ValidateUser(userName, password);
        }

        public MembershipCreateStatus CreateUser(string userName, string password, string email) {
            if (String.IsNullOrEmpty(userName)) throw new ArgumentException("Value cannot be null or empty.", "userName");
            if (String.IsNullOrEmpty(password)) throw new ArgumentException("Value cannot be null or empty.", "password");
            if (String.IsNullOrEmpty(email)) throw new ArgumentException("Value cannot be null or empty.", "email");

            MembershipCreateStatus status;
            _provider.CreateUser(userName, password, email, null, null, true, null, out status);
            return status;
        }

        public bool ChangePassword(string userName, string oldPassword, string newPassword) {
            if (String.IsNullOrEmpty(userName)) throw new ArgumentException("Value cannot be null or empty.", "userName");
            if (String.IsNullOrEmpty(oldPassword)) throw new ArgumentException("Value cannot be null or empty.", "oldPassword");
            if (String.IsNullOrEmpty(newPassword)) throw new ArgumentException("Value cannot be null or empty.", "newPassword");

            // The underlying ChangePassword() will throw an exception rather
            // than return false in certain failure scenarios.
            try {
                var currentUser = _provider.GetUser(userName, true /* userIsOnline */);
                return currentUser.ChangePassword(oldPassword, newPassword);
            }
            catch (ArgumentException) {
                return false;
            }
            catch (MembershipPasswordException) {
                return false;
            }
        }
    }

    public class FacebookMembershipService : IMembershipService {

        private readonly MembershipProvider _provider;

        private const string LogonRequestUrl = "https://graph.facebook.com/me?access_token={0}";
        private FacebookUser FbUser { get; set; }
        public string UserName { get { return FbUser.id; } }
        public string Email { get { return FbUser.email; } }
        public string NewPassword { get { return RandomPassword.Generate(MinPasswordLength, 9); } }
        public string NewAnswer { get { return RandomPassword.Generate(MinPasswordLength, 9); } }

        public FacebookMembershipService() {
            _provider = Membership.Provider;
        }

        private static FacebookUser GetFacebookUserFromCookie() {
            var context = HttpContext.Current;
            var cookieName = String.Format("fbs_{0}", ConfigurationManager.AppSettings["FacebookAppId"]);
            var cook = context.Request.Cookies[cookieName];
            if (cook == null) throw new Exception("No cookie!");

            var accessToken = cook["\"access_token"];
            context.Session.Add("facebook_access_token", accessToken);
            context.Session.Add("hasfacebooksession", true);

            var requestUrl = String.Format(LogonRequestUrl, accessToken);
            var webRequest = WebRequest.Create(requestUrl);

            FacebookUser user = null;
            using (var response = (HttpWebResponse)webRequest.GetResponse()) {
                if (response != null) {
                    using (var dataStream = response.GetResponseStream()) {
                        if (dataStream != null) {
                            using (var reader = new StreamReader(dataStream)) {
                                var jsonFromFacebook = reader.ReadToEnd();
                                var ser = new JavaScriptSerializer();
                                user = ser.Deserialize<FacebookUser>(jsonFromFacebook);
                            }
                        }
                    }
                }
            }
            return user;
        }

        public bool ValidateOpenAuthUser(bool validateAgainstLocalStore) {
            FbUser = GetFacebookUserFromCookie();
            if (validateAgainstLocalStore) {
                if (FbUser == null || String.IsNullOrWhiteSpace(FbUser.id)) throw new Exception("Value cannot be null or empty.");
                var user = _provider.GetUser(FbUser.id, true);
                return user != null;
            }
            return FbUser != null && !String.IsNullOrWhiteSpace(FbUser.id);

        }


        public MembershipCreateStatus CreateUser(string userName, string password, string email) {
            MembershipCreateStatus status;
            _provider.CreateUser(userName, password, email, null, null, true, null, out status);
            return status;
        }

        public bool ValidateUser(string userName, string password) {
            throw new NotImplementedException();
        }

        public int MinPasswordLength {
            get {
                return _provider.MinRequiredPasswordLength;
            }
        }

        public bool ChangePassword(string userName, string oldPassword, string newPassword) {
            throw new NotImplementedException();
        }
    }

    public interface IFormsAuthenticationService {
        void SignIn(string userName, bool createPersistentCookie);
        void SignOut();
    }

    public class FormsAuthenticationService : IFormsAuthenticationService {
        public void SignIn(string userName, bool createPersistentCookie) {
            if (String.IsNullOrEmpty(userName)) throw new ArgumentException("Value cannot be null or empty.", "userName");

            FormsAuthentication.SetAuthCookie(userName, createPersistentCookie);
        }

        public void SignOut() {
            FormsAuthentication.SignOut();
        }
    }
    #endregion

    #region Validation
    public static class AccountValidation {
        public static string ErrorCodeToString(MembershipCreateStatus createStatus) {
            // See http://go.microsoft.com/fwlink/?LinkID=177550 for
            // a full list of status codes.
            switch (createStatus) {
                case MembershipCreateStatus.DuplicateUserName:
                    return "Username already exists. Please enter a different user name.";

                case MembershipCreateStatus.DuplicateEmail:
                    return "A username for that e-mail address already exists. Please enter a different e-mail address.";

                case MembershipCreateStatus.InvalidPassword:
                    return "The password provided is invalid. Please enter a valid password value.";

                case MembershipCreateStatus.InvalidEmail:
                    return "The e-mail address provided is invalid. Please check the value and try again.";

                case MembershipCreateStatus.InvalidAnswer:
                    return "The password retrieval answer provided is invalid. Please check the value and try again.";

                case MembershipCreateStatus.InvalidQuestion:
                    return "The password retrieval question provided is invalid. Please check the value and try again.";

                case MembershipCreateStatus.InvalidUserName:
                    return "The user name provided is invalid. Please check the value and try again.";

                case MembershipCreateStatus.ProviderError:
                    return "The authentication provider returned an error. Please verify your entry and try again. If the problem persists, please contact your system administrator.";

                case MembershipCreateStatus.UserRejected:
                    return "The user creation request has been canceled. Please verify your entry and try again. If the problem persists, please contact your system administrator.";

                default:
                    return "An unknown error occurred. Please verify your entry and try again. If the problem persists, please contact your system administrator.";
            }
        }
    }

    [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
    public sealed class ValidatePasswordLengthAttribute : ValidationAttribute, IClientValidatable {
        private const string _defaultErrorMessage = "'{0}' must be at least {1} characters long.";
        private readonly int _minCharacters = Membership.Provider.MinRequiredPasswordLength;

        public ValidatePasswordLengthAttribute()
            : base(_defaultErrorMessage) {
        }

        public override string FormatErrorMessage(string name) {
            return String.Format(CultureInfo.CurrentCulture, ErrorMessageString,
                name, _minCharacters);
        }

        public override bool IsValid(object value) {
            string valueAsString = value as string;
            return (valueAsString != null && valueAsString.Length >= _minCharacters);
        }

        public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context) {
            return new[]{
                new ModelClientValidationStringLengthRule(FormatErrorMessage(metadata.GetDisplayName()), _minCharacters, int.MaxValue)
            };
        }
    }
    #endregion

}
 
And here is a random password generator from Obviex:
 
///////////////////////////////////////////////////////////////////////////////
    // SAMPLE: Generates random password, which complies with the strong password
    //         rules and does not contain ambiguous characters.
    //
    // To run this sample, create a new Visual C# project using the Console
    // Application template and replace the contents of the Class1.cs file with
    // the code below.
    //
    // THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND,
    // EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED
    // WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE.
    // 
    // Copyright (C) 2004 Obviex(TM). All rights reserved.
    // 


    /// <summary>
    /// This class can generate random passwords, which do not include ambiguous 
    /// characters, such as I, l, and 1. The generated password will be made of
    /// 7-bit ASCII symbols. Every four characters will include one lower case
    /// character, one upper case character, one number, and one special symbol
    /// (such as '%') in a random order. The password will always start with an
    /// alpha-numeric character; it will not start with a special symbol (we do
    /// this because some back-end systems do not like certain special
    /// characters in the first position).
    /// </summary>
    public class RandomPassword {
        // Define default min and max password lengths.
        private const int DefaultMinPasswordLength = 8;
        private const int DefaultMaxPasswordLength = 10;

        // Define supported password characters divided into groups.
        // You can add (or remove) characters to (from) these groups.
        private const string PasswordCharsLcase = "abcdefgijkmnopqrstwxyz";
        private const string PasswordCharsUcase = "ABCDEFGHJKLMNPQRSTWXYZ";
        private const string PasswordCharsNumeric = "23456789";
        private const string PasswordCharsSpecial = "*$-+?_&=!%{}/";

        /// <summary>
        /// Generates a random password.
        /// </summary>
        /// <returns>
        /// Randomly generated password.
        /// </returns>
        /// <remarks>
        /// The length of the generated password will be determined at
        /// random. It will be no shorter than the minimum default and
        /// no longer than maximum default.
        /// </remarks>
        public static string Generate() {
            return Generate(DefaultMinPasswordLength,
                            DefaultMaxPasswordLength);
        }

        /// <summary>
        /// Generates a random password of the exact length.
        /// </summary>
        /// <param name="length">
        /// Exact password length.
        /// </param>
        /// <returns>
        /// Randomly generated password.
        /// </returns>
        public static string Generate(int length) {
            return Generate(length, length);
        }

        /// <summary>
        /// Generates a random password.
        /// </summary>
        /// <param name="minLength">
        /// Minimum password length.
        /// </param>
        /// <param name="maxLength">
        /// Maximum password length.
        /// </param>
        /// <returns>
        /// Randomly generated password.
        /// </returns>
        /// <remarks>
        /// The length of the generated password will be determined at
        /// random and it will fall with the range determined by the
        /// function parameters.
        /// </remarks>
        public static string Generate(int minLength,
                                      int maxLength) {
            // Make sure that input parameters are valid.
            if (minLength <= 0 || maxLength <= 0 || minLength > maxLength)
                return null;

            // Create a local array containing supported password characters
            // grouped by types. You can remove character groups from this
            // array, but doing so will weaken the password strength.
            var charGroups = new[] 
        {
            PasswordCharsLcase.ToCharArray(),
            PasswordCharsUcase.ToCharArray(),
            PasswordCharsNumeric.ToCharArray(),
            PasswordCharsSpecial.ToCharArray()
        };

            // Use this array to track the number of unused characters in each
            // character group.
            int[] charsLeftInGroup = new int[charGroups.Length];

            // Initially, all characters in each group are not used.
            for (int i = 0; i < charsLeftInGroup.Length; i++)
                charsLeftInGroup[i] = charGroups[i].Length;

            // Use this array to track (iterate through) unused character groups.
            int[] leftGroupsOrder = new int[charGroups.Length];

            // Initially, all character groups are not used.
            for (int i = 0; i < leftGroupsOrder.Length; i++)
                leftGroupsOrder[i] = i;

            // Because we cannot use the default randomizer, which is based on the
            // current time (it will produce the same "random" number within a
            // second), we will use a random number generator to seed the
            // randomizer.

            // Use a 4-byte array to fill it with random bytes and convert it then
            // to an integer value.
            var randomBytes = new byte[4];

            // Generate 4 random bytes.
            var rng = new RNGCryptoServiceProvider();
            rng.GetBytes(randomBytes);

            // Convert 4 bytes into a 32-bit integer value.
            var seed = (randomBytes[0] & 0x7f) << 24 |
                        randomBytes[1] << 16 |
                        randomBytes[2] << 8 |
                        randomBytes[3];

            // Now, this is real randomization.
            var random = new Random(seed);

            // This array will hold password characters.
            char[] password;

            // Allocate appropriate memory for the password.
            password = minLength < maxLength
                ? new char[random.Next(minLength, maxLength + 1)]
                : new char[minLength];

            // Index of the next character to be added to password.
            int nextCharIdx;

            // Index of the next character group to be processed.
            int nextGroupIdx;

            // Index which will be used to track not processed character groups.
            int nextLeftGroupsOrderIdx;

            // Index of the last non-processed character in a group.
            int lastCharIdx;

            // Index of the last non-processed group.
            var lastLeftGroupsOrderIdx = leftGroupsOrder.Length - 1;

            // Generate password characters one at a time.
            for (var i = 0; i < password.Length; i++) {
                // If only one character group remained unprocessed, process it;
                // otherwise, pick a random character group from the unprocessed
                // group list. To allow a special character to appear in the
                // first position, increment the second parameter of the Next
                // function call by one, i.e. lastLeftGroupsOrderIdx + 1.
                if (lastLeftGroupsOrderIdx == 0)
                    nextLeftGroupsOrderIdx = 0;
                else
                    nextLeftGroupsOrderIdx = random.Next(0,
                                                         lastLeftGroupsOrderIdx);

                // Get the actual index of the character group, from which we will
                // pick the next character.
                nextGroupIdx = leftGroupsOrder[nextLeftGroupsOrderIdx];

                // Get the index of the last unprocessed characters in this group.
                lastCharIdx = charsLeftInGroup[nextGroupIdx] - 1;

                // If only one unprocessed character is left, pick it; otherwise,
                // get a random character from the unused character list.
                nextCharIdx = lastCharIdx == 0
                    ? 0
                    : random.Next(0, lastCharIdx + 1);

                // Add this character to the password.
                password[i] = charGroups[nextGroupIdx][nextCharIdx];

                // If we processed the last character in this group, start over.
                if (lastCharIdx == 0)
                    charsLeftInGroup[nextGroupIdx] =
                                              charGroups[nextGroupIdx].Length;
                // There are more unprocessed characters left.
                else {
                    // Swap processed character with the last unprocessed character
                    // so that we don't pick it until we process all characters in
                    // this group.
                    if (lastCharIdx != nextCharIdx) {
                        var temp = charGroups[nextGroupIdx][lastCharIdx];
                        charGroups[nextGroupIdx][lastCharIdx] =
                                    charGroups[nextGroupIdx][nextCharIdx];
                        charGroups[nextGroupIdx][nextCharIdx] = temp;
                    }
                    // Decrement the number of unprocessed characters in
                    // this group.
                    charsLeftInGroup[nextGroupIdx]--;
                }

                // If we processed the last group, start all over.
                if (lastLeftGroupsOrderIdx == 0)
                    lastLeftGroupsOrderIdx = leftGroupsOrder.Length - 1;
                // There are more unprocessed groups left.
                else {
                    // Swap processed group with the last unprocessed group
                    // so that we don't pick it until we process all groups.
                    if (lastLeftGroupsOrderIdx != nextLeftGroupsOrderIdx) {
                        var temp = leftGroupsOrder[lastLeftGroupsOrderIdx];
                        leftGroupsOrder[lastLeftGroupsOrderIdx] =
                                    leftGroupsOrder[nextLeftGroupsOrderIdx];
                        leftGroupsOrder[nextLeftGroupsOrderIdx] = temp;
                    }
                    // Decrement the number of unprocessed groups.
                    lastLeftGroupsOrderIdx--;
                }
            }

            // Convert password characters into a string and return the result.
            return new string(password);
        }
    }
}


No comments:

Post a Comment