Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Windows Security Custom login validation

Tags:

c#

.net

windows

I'm creating an Xaml/C# application and I would like it to popup with a Login Prompt.

I would like to know if its possible to use CredUIPromptForWindowsCredentials.

  • Show Windows Security Dialog
  • Get the entered username & password
  • Perform Custom validation
  • If validation succes -> continue app
  • else if validation failed -> -inform user of invalid username or password

I have already looked at Windows Security login form? and http://www.pinvoke.net/default.aspx/credui/creduipromptforwindowscredentials.html?diff=y but they don't explain how to handle the validation.

I would really like a small example, where if the user enters username = "Bo" and password = "123" then succes else display error message and allow the user to try again.

The App is going to be installed on multiple computers.

Or is this simply not possible?

Update

Inspired by the answer in this question Show Authentication dialog in C# for windows Vista/7

I have modified the code to work as expected.

Please not, that the validation part is only for proof of concept.

WindowsSecurityDialog.cs

 public class WindowsSecurityDialog
    {

       public string CaptionText { get; set; }
       public string MessageText { get; set; }

        [DllImport("ole32.dll")]
        public static extern void CoTaskMemFree(IntPtr ptr);

        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
        private struct CREDUI_INFO
        {
            public int cbSize;
            public IntPtr hwndParent;
            public string pszMessageText;
            public string pszCaptionText;
            public IntPtr hbmBanner;
        }


        [DllImport("credui.dll", CharSet = CharSet.Auto)]
        private static extern bool CredUnPackAuthenticationBuffer(int dwFlags,
                                                                   IntPtr pAuthBuffer,
                                                                   uint cbAuthBuffer,
                                                                   StringBuilder pszUserName,
                                                                   ref int pcchMaxUserName,
                                                                   StringBuilder pszDomainName,
                                                                   ref int pcchMaxDomainame,
                                                                   StringBuilder pszPassword,
                                                                   ref int pcchMaxPassword);

        [DllImport("credui.dll", CharSet = CharSet.Auto)]
        private static extern int CredUIPromptForWindowsCredentials(ref CREDUI_INFO notUsedHere,
                                                                     int authError,
                                                                     ref uint authPackage,
                                                                     IntPtr InAuthBuffer,
                                                                     uint InAuthBufferSize,
                                                                     out IntPtr refOutAuthBuffer,
                                                                     out uint refOutAuthBufferSize,
                                                                     ref bool fSave,
                                                                     int flags);



        public bool ValidateUser()
        {
            var credui = new CREDUI_INFO
                                     {
                                         pszCaptionText = CaptionText,
                                         pszMessageText = MessageText
                                     };
            credui.cbSize = Marshal.SizeOf(credui);
            uint authPackage = 0;
            IntPtr outCredBuffer;
            uint outCredSize;
            bool save = false;


            const int loginErrorCode = 1326;    //Login Failed
            var authError = 0;

            while (true)
            {




                var result = CredUIPromptForWindowsCredentials(ref credui,
                                                               authError,
                                                               ref authPackage,
                                                               IntPtr.Zero,
                                                               0,
                                                               out outCredBuffer,
                                                               out outCredSize,
                                                               ref save,
                                                               1 /* Generic */);

                var usernameBuf = new StringBuilder(100);
                var passwordBuf = new StringBuilder(100);
                var domainBuf = new StringBuilder(100);

                var maxUserName = 100;
                var maxDomain = 100;
                var maxPassword = 100;
                if (result == 0)
                {
                    if (CredUnPackAuthenticationBuffer(0, outCredBuffer, outCredSize, usernameBuf, ref maxUserName,
                                                       domainBuf, ref maxDomain, passwordBuf, ref maxPassword))
                    {
                        //TODO: ms documentation says we should call this but i can't get it to work
                        //SecureZeroMem(outCredBuffer, outCredSize);

                        //clear the memory allocated by CredUIPromptForWindowsCredentials 
                        CoTaskMemFree(outCredBuffer);
                        var networkCredential = new NetworkCredential()
                                                {
                                                    UserName = usernameBuf.ToString(),
                                                    Password = passwordBuf.ToString(),
                                                    Domain = domainBuf.ToString()
                                                };

                        //Dummy Code replace with true User Validation
                        if (networkCredential.UserName == "Bo" && networkCredential.Password == "1234")
                            return true;
                        else //login failed show dialog again with login error
                        {
                            authError = loginErrorCode;
                        }



                    }
                }
                else return false;


            }
        }
    }

App.xaml.cs

protected override void OnStartup(StartupEventArgs e)
        {
            var windowsSecurityDialog = new WindowsSecurityDialog
                                            {
                                                CaptionText = "Enter your credentials",
                                                MessageText = "These credentials will be used to connect to YOUR APP NAME";
                                            };

            if (windowsSecurityDialog.ValidateUser())
                base.OnStartup(e);
        }
like image 607
gulbaek Avatar asked Apr 20 '12 07:04

gulbaek


People also ask

How do I configure Windows authentication?

On the taskbar, click Start, and then click Control Panel. In Control Panel, click Programs and Features, and then click Turn Windows Features on or off. Expand Internet Information Services, then World Wide Web Services, then Security. Select Windows Authentication, and then click OK.

What is error code 0xC0000224?

Account logon to account disabled by administrator. 0xC0000193. Account logon with expired account. 0xC0000224. Account logon with "Change Password at Next Logon" flagged.

Is Windows authentication the same as Active Directory?

Windows authentication means the account resides in Active Directory for the Domain.


2 Answers

You'll find a complete implementation for WPF and WinForms using CredUIPromptForWindowsCredentials at Ookii dialogs.

like image 66
Phil Avatar answered Oct 27 '22 01:10

Phil


I was a little horrified when I started thinking this might be possible.

The answer is yes and no. You can get a hold of the network domain and username, but (thank goodness), you can't get a hold of the actual password, only a hash of the password.

Borrowing heavily from PInvoke, here's a sample WPF App that brings in and outputs the username and password.

Code

using System;
using System.Runtime.InteropServices;
using System.Text;
using System.Windows;
using System.Windows.Interop;

namespace LoginDialog
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            // Declare/initialize variables.
            bool save = false;
            int errorcode = 0;
            uint dialogReturn;
            uint authPackage = 0;
            IntPtr outCredBuffer;
            uint outCredSize;

            // Create the CREDUI_INFO struct.
            CREDUI_INFO credui = new CREDUI_INFO();
            credui.cbSize = Marshal.SizeOf(credui);
            credui.pszCaptionText = "Connect to your application";
            credui.pszMessageText = "Enter your credentials!";
            credui.hwndParent = new WindowInteropHelper(this).Handle;

            // Show the dialog.
            dialogReturn = CredUIPromptForWindowsCredentials(
                ref credui, 
                errorcode, 
                ref authPackage,
                (IntPtr)0,  // You can force that a specific username is shown in the dialog. Create it with 'CredPackAuthenticationBuffer()'. Then, the buffer goes here...
                0,          // ...and the size goes here. You also have to add CREDUIWIN_IN_CRED_ONLY to the flags (last argument).
                out outCredBuffer, 
                out outCredSize, 
                ref save, 
                0); // Use the PromptForWindowsCredentialsFlags Enum here. You can use multiple flags if you seperate them with | .

            if (dialogReturn == 1223) // Result of 1223 means the user canceled. Not sure if other errors are ever returned.
                textBox1.Text += ("User cancelled!");
            if (dialogReturn != 0) // Result of something other than 0 means...something, I'm sure. Either way, failed or canceled.
                return;

            var domain = new StringBuilder(100);
            var username = new StringBuilder(100);
            var password = new StringBuilder(100);
            int maxLength = 100; // Note that you can have different max lengths for each variable if you want.

            // Unpack the info from the buffer.
            CredUnPackAuthenticationBuffer(0, outCredBuffer, outCredSize, username, ref maxLength, domain, ref maxLength, password, ref maxLength);

            // Clear the memory allocated by CredUIPromptForWindowsCredentials.
            CoTaskMemFree(outCredBuffer);

            // Output info, escaping whitespace characters for the password.
            textBox1.Text += String.Format("Domain: {0}\n", domain);
            textBox1.Text += String.Format("Username: {0}\n", username);
            textBox1.Text += String.Format("Password (hashed): {0}\n", EscapeString(password.ToString()));
        }

        public static string EscapeString(string s)
        {
            // Formatted like this only for you, SO.
            return s
                .Replace("\a", "\\a")
                .Replace("\b", "\\b")
                .Replace("\f", "\\f")
                .Replace("\n", "\\n")
                .Replace("\r", "\\r")
                .Replace("\t", "\\t")
                .Replace("\v", "\\v");
        }

        #region DLLImports
        [DllImport("ole32.dll")]
        public static extern void CoTaskMemFree(IntPtr ptr);

        [DllImport("credui.dll", CharSet = CharSet.Unicode)]
        private static extern uint CredUIPromptForWindowsCredentials(ref CREDUI_INFO notUsedHere, int authError, ref uint authPackage, IntPtr InAuthBuffer,
          uint InAuthBufferSize, out IntPtr refOutAuthBuffer, out uint refOutAuthBufferSize, ref bool fSave, PromptForWindowsCredentialsFlags flags);

        [DllImport("credui.dll", CharSet = CharSet.Unicode)]
        private static extern bool CredUnPackAuthenticationBuffer(int dwFlags, IntPtr pAuthBuffer, uint cbAuthBuffer, StringBuilder pszUserName, ref int pcchMaxUserName, StringBuilder pszDomainName, ref int pcchMaxDomainame, StringBuilder pszPassword, ref int pcchMaxPassword);
        #endregion

        #region Structs and Enums
        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
        private struct CREDUI_INFO
        {
            public int cbSize;
            public IntPtr hwndParent;
            public string pszMessageText;
            public string pszCaptionText;
            public IntPtr hbmBanner;
        }

        private enum PromptForWindowsCredentialsFlags
        {
            /// <summary>
            /// The caller is requesting that the credential provider return the user name and password in plain text.
            /// This value cannot be combined with SECURE_PROMPT.
            /// </summary>
            CREDUIWIN_GENERIC = 0x1,
            /// <summary>
            /// The Save check box is displayed in the dialog box.
            /// </summary>
            CREDUIWIN_CHECKBOX = 0x2,
            /// <summary>
            /// Only credential providers that support the authentication package specified by the authPackage parameter should be enumerated.
            /// This value cannot be combined with CREDUIWIN_IN_CRED_ONLY.
            /// </summary>
            CREDUIWIN_AUTHPACKAGE_ONLY = 0x10,
            /// <summary>
            /// Only the credentials specified by the InAuthBuffer parameter for the authentication package specified by the authPackage parameter should be enumerated.
            /// If this flag is set, and the InAuthBuffer parameter is NULL, the function fails.
            /// This value cannot be combined with CREDUIWIN_AUTHPACKAGE_ONLY.
            /// </summary>
            CREDUIWIN_IN_CRED_ONLY = 0x20,
            /// <summary>
            /// Credential providers should enumerate only administrators. This value is intended for User Account Control (UAC) purposes only. We recommend that external callers not set this flag.
            /// </summary>
            CREDUIWIN_ENUMERATE_ADMINS = 0x100,
            /// <summary>
            /// Only the incoming credentials for the authentication package specified by the authPackage parameter should be enumerated.
            /// </summary>
            CREDUIWIN_ENUMERATE_CURRENT_USER = 0x200,
            /// <summary>
            /// The credential dialog box should be displayed on the secure desktop. This value cannot be combined with CREDUIWIN_GENERIC.
            /// Windows Vista: This value is not supported until Windows Vista with SP1.
            /// </summary>
            CREDUIWIN_SECURE_PROMPT = 0x1000,
            /// <summary>
            /// The credential provider should align the credential BLOB pointed to by the refOutAuthBuffer parameter to a 32-bit boundary, even if the provider is running on a 64-bit system.
            /// </summary>
            CREDUIWIN_PACK_32_WOW = 0x10000000,
        }
        #endregion
    }
}

Test

  1. Create a new WPF application called LoginDialog.
  2. Drop a TextBox into the MainWindow.xaml file provided named textBox1.
  3. Replace the code in the MainWindow.xaml.cs file.
  4. Run!

Sample Output

Given the password "password", here is the output.

Domain: 
Username: EXAMPLE\fake
Password (hashed): @@D\a\b\f\n\rgAAAAAU-JPAAAAAAweFpM4nPlOUfKi83JLsl4jjh6nMX34yiH

Comments

This works for WPF. It can work for Silverlight with the right permissions.

I don't know why anyone would ever do this for legitimate custom validation. If you want to create a login for your app, I'd suggest having the client connect via SSL (https://) to an ASP.NET page or web service that will check the credentials provided using LINQ to SQL. It can then send the client a pass/fail response.

Oh, and for the love of god and all that is holy, salt and hash your users' passwords.

Note: If you're wanting to use this login to prevent the user from using your app without having an account/paying, all the above stands, but will not be sufficient to stop people from reverse engineering and cracking the app (e.g., tricking it into thinking that it's received the pass message). That sort of DRM is a whole 'nother ballgame.

like image 34
lordcheeto Avatar answered Oct 26 '22 23:10

lordcheeto