Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Logon to a One Click Windows application using a SmartCard in C#

0.00/5 (No votes)
22 May 2014 1  

Introduction

SmartCard Logon for a One Click application

Background

I have looked for a solution for this project for about a week. All the reading I have done just ended up giving me bits and pieaces of code. I found some sites that said they where able to login a application using a smartcard yet they provided no explanation of how after about a week of research I finialy put all the pieaces togather and was able to succefully create a login script that would accept my cac cert and allow me to run the applcation as another user on a cac enforced system.

I know of lots of diffrent ways to start a application under diffrent credentials. The problem faced here was cac enforcement and the lack of a runas option on the rightclick menue of the app. This project was designed to allow a user to load the application and then change users using their cac. This gets around the OneClick problem of not having the runas option.

Using the code

First off most people break code down and explain each portion. I will break it down but first I will give you the intire code for the project and then explain some of the key components.

//SmartCard.cs file
using System;
using System.Runtime.InteropServices;
using System.Security.Principal;
using System.Security.Permissions;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Windows.Forms;
using System.Diagnostics;
using System.Security;
using System.ComponentModel;

namespace SmartCardApplication

{
   
       
       

    public class SmartCard
    {
      
        internal static int CertCredential = 1;
                      
        [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
        public static extern bool CredMarshalCredential(
            int credType,
            IntPtr credential,
            out IntPtr marshaledCredential
            );
      

        [DllImport("user32.dll")]
        private static extern IntPtr GetDC(IntPtr hWnd);
        [StructLayout(LayoutKind.Sequential)]
        internal struct CERT_CREDENTIAL_INFO
        {
            public uint cbSize;

            [MarshalAs(UnmanagedType.ByValArray, SizeConst = 20)]
            public byte[] rgbHashOfCert;
        }


        public static string pin = null;
        public static X509Certificate2 cert = null;
        public struct UserLoginInfo
        {
            public string domain;
            public string username;
            public SecureString password;
        }
        public void SmartCardLogon()
        {
            //First Get the Certificate
            cert = GetClientCertificate();
           //Create New login form for user to enter pin
            Form login = new Form();
            login.Height = 75;
            login.Width = 165;
            login.MaximizeBox = false;
            login.MinimizeBox = false;
            login.ControlBox = false;
            login.Name = "frmCaCLogin";
            login.Text = "Enter Pin";
            login.FormBorderStyle = FormBorderStyle.FixedSingle;
            
            //Create new text box for the pin
            TextBox TextBox1 = new TextBox();
            TextBox1.Name = "txtCaCLogin";
            TextBox1.PasswordChar = '*';
            TextBox1.Width = 152;
            //Add textbox to form
            login.Controls.Add(TextBox1);
          //Create new button for user to submit the pin
            Button b = new Button();
            b.FlatStyle = FlatStyle.Flat;
            b.Text = "Login";
            b.Name = "butCacLogin";
            b.Click += new EventHandler(b_Click);

            //Create new button for user to cancel the login
            Button c = new Button();
            c.Text = "Cancel";
            c.FlatStyle = FlatStyle.Flat;
            c.Name = "butCancel";
            c.Click += new EventHandler(c_Click);
            
          //Add buttons to form
            login.Controls.Add(b);
            login.Controls.Add(c);
            //position buttons under the textbox
            login.Controls["butCacLogin"].Top += 20;
            login.Controls["butCancel"].Left += login.Controls["butCacLogin"].Width + 2;
            login.Controls["butCancel"].Top += 20;
            login.TopMost = true;
            login.ShowDialog();

 

        }

        void c_Click(object sender, EventArgs e)
        {
            //if user cances the form show the main form and close the two login forms.
            Application.OpenForms["frmHome"].Show();
            Application.OpenForms["frmLogin"].Close();
            Application.OpenForms["frmCaCLogin"].Close();
        }

        void b_Click(object sender, EventArgs e)
        {
            //Retrive the pin from the txt filed you added to the created form
            pin = Application.OpenForms["frmCaCLogin"].Controls["txtCaCLogin"].Text;
            //just hides the form
            Application.OpenForms["frmCaCLogin"].Hide();
            //
           
        
            if (cert != null)
            {
                //This method is used to create a username from the selected certificate
                UserLoginInfo user = Login(cert);

                //using the returned username from the method above and the pin in securestring format from the user entry start the process over with new permissions
                if (user.username != string.Empty)
                {
                    try
                    {
                        ProcessStartInfo psi = new ProcessStartInfo();
                        psi.FileName = Application.ExecutablePath;
                        psi.UserName = user.username;
                        psi.Password = user.password;
                        psi.UseShellExecute = false;
                        Process.Start(psi);
                        Application.Exit();
                    }
                    catch (Exception ex)
                    {
                        if (ex.Message.Contains("Logon failure: unknown user name or bad password"))
                        {
                            MessageBox.Show("Ensure you SmartCard has been inserted and you have entered the correct pin!", ex.Message, MessageBoxButtons.OK, MessageBoxIcon.Error);
                        }
                        else
                        {
                            MessageBox.Show(ex.Message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
                        }
                        Application.OpenForms["frmCaCLogin"].Show();
                    }
                }

            }
        }

        public UserLoginInfo Login(X509Certificate2 cert)
        {
            UserLoginInfo uli = new UserLoginInfo();
            try
            {
                

                CERT_CREDENTIAL_INFO certInfo =
                     new CERT_CREDENTIAL_INFO();
                certInfo.cbSize = (uint)Marshal.SizeOf(typeof(CERT_CREDENTIAL_INFO));

                certInfo.rgbHashOfCert = cert.GetCertHash();

                int size = Marshal.SizeOf(certInfo);
                IntPtr pCertInfo = Marshal.AllocHGlobal(size);
                Marshal.StructureToPtr(certInfo, pCertInfo, false);

                IntPtr marshaledCredential = IntPtr.Zero;
                bool result =
                   CredMarshalCredential(CertCredential,
                                    pCertInfo,
                                    out marshaledCredential);
                string domainName = string.Empty;
                string userName = string.Empty;
                string password = string.Empty;
                if (result)
                {
                    // we need to do this here, before we free marshaledCredential
                    domainName = String.Empty;
                    userName = Marshal.PtrToStringUni(marshaledCredential);
                    password = pin;
                }
                SecureString sc = new SecureString();
                foreach (char c in pin)
                {
                    sc.AppendChar(c);
                }

                uli.domain = Environment.UserDomainName;
                uli.username = userName;
                uli.password = sc;


                return uli;
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message);
                return uli;
            }
        }

        
        public static X509Certificate2 GetClientCertificate()
        {
            IntPtr ptr = IntPtr.Zero;
            X509Certificate2 certificate = null;
            X509Certificate t = null;
            var store = new X509Store(StoreName.My, StoreLocation.CurrentUser);
            try
            {
                store.Open(OpenFlags.ReadOnly | OpenFlags.OpenExistingOnly);
                // Nothing to do if no cert found.
                if (store.Certificates != null && store.Certificates.Count > 0)
                {
                    if (store.Certificates.Count == 1)
                    {
                        // Return the certificate present.
                        certificate = store.Certificates[0];
                    }
                    else
                    {
                        // Request the user to select a certificate 

                        var certificates = X509Certificate2UI.SelectFromCollection(store.Certificates, "Digital Certificates", "Select a certificate from the following list:", X509SelectionFlag.SingleSelection, ptr);
                        // Check if one has been returned

                        if (certificates != null && certificates.Count > 0)
                            certificate = certificates[0];

                    }
                }
            }
            finally
            {
                store.Close();
            }

            return certificate;


        }
    }
 

}



The file above dose all the work and is simply a class file. This can easly be converted to a API and used with any windows application. Now lets hit some of the key points in this class. In order to login using you CaC certificate the first then one needs to do is be able to select from a list of certificates so that you can get the cert you need. The code below connects to your certifcate store and produces the certifcate selector that you are normally see. Once you select the cert it will return it to the caller so it can be used in the next step.

public static X509Certificate2 GetClientCertificate()
        {
            
            IntPtr ptr = IntPtr.Zero;
            X509Certificate2 certificate = null;
            X509Certificate t = null;
            var store = new X509Store(StoreName.My, StoreLocation.CurrentUser);
            try
            {
                store.Open(OpenFlags.ReadOnly | OpenFlags.OpenExistingOnly);
                // Nothing to do if no cert found.
                if (store.Certificates != null && store.Certificates.Count > 0)
                {
                    if (store.Certificates.Count == 1)
                    {
                        // Return the certificate present.
                        certificate = store.Certificates[0];
                    }
                    else
                    {
                        // Request the user to select a certificate 

                        var certificates = X509Certificate2UI.SelectFromCollection(store.Certificates, "Digital Certificates", "Select a certificate from the following list:", X509SelectionFlag.SingleSelection, ptr);
                        // Check if one has been returned

                        if (certificates != null && certificates.Count > 0)
                            certificate = certificates[0];

                    }
                }
            }
            finally
            {
                store.Close();
            }

            return certificate;


        }

In order to pass a certifcate to a process as a username you will need to convert it to a username. This is the more complicated pieace of the code and requires you to import advapi32 into your project. So this pieace contains the most work. This simply allows the application to use some of the data in the advapi32.dll wich is needed for the method that converts the cert to a username.

 internal static int CertCredential = 1;
                      
        [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
        public static extern bool CredMarshalCredential(
            int credType,
            IntPtr credential,
            out IntPtr marshaledCredential
            );
      

        [DllImport("user32.dll")]
        private static extern IntPtr GetDC(IntPtr hWnd);
        [StructLayout(LayoutKind.Sequential)]
        internal struct CERT_CREDENTIAL_INFO
        {
            public uint cbSize;

            [MarshalAs(UnmanagedType.ByValArray, SizeConst = 20)]
            public byte[] rgbHashOfCert;
        }

This is the actuall class that will do the conversion. The UserLoginInfo is just a simple structure I created to store the credentials. At this point the application only takes the pin you you typed in and converts it to a secure string and adds it to the UserLoginInfo.password field. Not autentication is taking place at this point. We are just getting the information we need.

public UserLoginInfo Login(X509Certificate2 cert)
        {
            UserLoginInfo uli = new UserLoginInfo();
            try
            {
                

                CERT_CREDENTIAL_INFO certInfo =
                     new CERT_CREDENTIAL_INFO();
                certInfo.cbSize = (uint)Marshal.SizeOf(typeof(CERT_CREDENTIAL_INFO));

                certInfo.rgbHashOfCert = cert.GetCertHash();

                int size = Marshal.SizeOf(certInfo);
                IntPtr pCertInfo = Marshal.AllocHGlobal(size);
                Marshal.StructureToPtr(certInfo, pCertInfo, false);

                IntPtr marshaledCredential = IntPtr.Zero;
                bool result =
                   CredMarshalCredential(CertCredential,
                                    pCertInfo,
                                    out marshaledCredential);
                string domainName = string.Empty;
                string userName = string.Empty;
                string password = string.Empty;
                if (result)
                {
                    // we need to do this here, before we free marshaledCredential
                    domainName = String.Empty;
                    userName = Marshal.PtrToStringUni(marshaledCredential);
                    password = pin;
                }
                SecureString sc = new SecureString();
                foreach (char c in pin)
                {
                    sc.AppendChar(c);
                }

                uli.domain = Environment.UserDomainName;
                uli.username = userName;
                uli.password = sc;


                return uli;
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message);
                return uli;
            }
        }

Now you will notice that the uli.username has a strange username associated with it. This is what windows uses for the username when authenticating using a cac. Now that you have all the information needed you simply need to start the process up. Below I call the method above and return the UserLoginInfo structure with the username and password set. I added the domain name but its not needed in this case. I read about using LogonUser class and a bunch of other ways to do this nothing worked the user would logon using that but whil impersanating a user I was unable to start the process no matter what I did. then I decided to just pass the username and pin directly to the process instead. What do you know it worked just fine. So once you get the cert converted to a user name and the pin converted to a secure string you can start the process with that information directly no need to use any other method of starting a process.

 UserLoginInfo user = Login(cert);

                //using the returned username from the method above and the pin in securestring format from the user entry start the process over with new permissions
                if (user.username != string.Empty)
                {
                    try
                    {
                        ProcessStartInfo psi = new ProcessStartInfo();
                        psi.FileName = Application.ExecutablePath;
                        psi.UserName = user.username;
                        psi.Password = user.password;
                        psi.UseShellExecute = false;
                        Process.Start(psi);
                        Application.Exit();
                    }
                    catch (Exception ex)
                    {
                        if (ex.Message.Contains("Logon failure: unknown user name or bad password"))
                        {
                            MessageBox.Show("Ensure you SmartCard has been inserted and you have entered the correct pin!", ex.Message, MessageBoxButtons.OK, MessageBoxIcon.Error);
                        }
                        else
                        {
                            MessageBox.Show(ex.Message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
                        }
                        Application.OpenForms["frmCaCLogin"].Show();
                    }
                }

            }

Now your probally wondering what creates the ping. If you have done the cert selection before when you select the cert you dont see a pin logon it just selects the cert. You actually have to create the pin login form yourself and since this was built in a class I will create the Login form directly. This portion of the code is what connects everything togather. It is also the method you would call to fire it. Basicly what this dose is create the form, textbox, two buttons (Submit, Cancel) formats it slightly so nothing overlaps.

Step1 GetClientCert is called the cert is returned

Step2 create the form for the pin login assign the event handelers and show the form

Step 3 submit the pin that the user entered

Step 4 Converts the cert to username and pin to secure string

Step 5 Take that information and pass it to the username and password field for the process

Step 6 Start the proces and close the existing process (Simply reloading the app with the new credentials)

In this case I wanted to reload the current application with the new credentials so I simply started a new instence of the current application then closed to old instance. Applcation loads with elevated rights and certifcate auntentication. So if your in a cac enforced enviroment this code will allow you to exacute as a diffrent user using you cac.

 public void SmartCardLogon()
        {
            //First Get the Certificate
            cert = GetClientCertificate();
           //Create New login form for user to enter pin
            Form login = new Form();
            login.Height = 75;
            login.Width = 165;
            login.MaximizeBox = false;
            login.MinimizeBox = false;
            login.ControlBox = false;
            login.Name = "frmCaCLogin";
            login.Text = "Enter Pin";
            login.FormBorderStyle = FormBorderStyle.FixedSingle;
            
            //Create new text box for the pin
            TextBox TextBox1 = new TextBox();
            TextBox1.Name = "txtCaCLogin";
            TextBox1.PasswordChar = '*';
            TextBox1.Width = 152;
            //Add textbox to form
            login.Controls.Add(TextBox1);
          //Create new button for user to submit the pin
            Button b = new Button();
            b.FlatStyle = FlatStyle.Flat;
            b.Text = "Login";
            b.Name = "butCacLogin";
            b.Click += new EventHandler(b_Click);

            //Create new button for user to cancel the login
            Button c = new Button();
            c.Text = "Cancel";
            c.FlatStyle = FlatStyle.Flat;
            c.Name = "butCancel";
            c.Click += new EventHandler(c_Click);
            
          //Add buttons to form
            login.Controls.Add(b);
            login.Controls.Add(c);
            //position buttons under the textbox
            login.Controls["butCacLogin"].Top += 20;
            login.Controls["butCancel"].Left += login.Controls["butCacLogin"].Width + 2;
            login.Controls["butCancel"].Top += 20;
            login.TopMost = true;
            login.ShowDialog();

 

        }


 

Below are the event handelers for the form I created above.

   void c_Click(object sender, EventArgs e)
        {
            //if user cances the form show the main form and close the two login forms.
            Application.OpenForms["frmHome"].Show();
            Application.OpenForms["frmLogin"].Close();
            Application.OpenForms["frmCaCLogin"].Close();
        }

        void b_Click(object sender, EventArgs e)
        {
            //Retrive the pin from the txt filed you added to the created form
            pin = Application.OpenForms["frmCaCLogin"].Controls["txtCaCLogin"].Text;
            //just hides the form
            Application.OpenForms["frmCaCLogin"].Hide();
            //
           
        
            if (cert != null)
            {
                //This method is used to create a username from the selected certificate
                UserLoginInfo user = Login(cert);

                //using the returned username from the method above and the pin in securestring format from the user entry start the process over with new permissions
                if (user.username != string.Empty)
                {
                    try
                    {
                        ProcessStartInfo psi = new ProcessStartInfo();
                        psi.FileName = Application.ExecutablePath;
                        psi.UserName = user.username;
                        psi.Password = user.password;
                        psi.UseShellExecute = false;
                        Process.Start(psi);
                        Application.Exit();
                    }
                    catch (Exception ex)
                    {
                        if (ex.Message.Contains("Logon failure: unknown user name or bad password"))
                        {
                            MessageBox.Show("Ensure you SmartCard has been inserted and you have entered the correct pin!", ex.Message, MessageBoxButtons.OK, MessageBoxIcon.Error);
                        }
                        else
                        {
                            MessageBox.Show(ex.Message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
                        }
                        Application.OpenForms["frmCaCLogin"].Show();
                    }
                }

            }
        }

One last pieace of code that I have to share is the application form that actually calls this. Create a windows form add a button the the form and add this code to the button_click event

 
  SmartCard sc = new SmartCard();
  sc.SmartCardLogon();
  

And thats it in a nutshell hopefully everyone will find this intresting.

Points of Interest

One thing I have found out researching this is that almost everything out there on this subject is only partialy there. I could not find any working examples on on how to achive this anywhere only partial examples. So I hope this will help somone else out that may need to achive this. I did see alot of question while looking reguarding starting a app up with a smart card but no working answers. I built this using visual studio 2010 on Windows 7 so As fare as compatibility it may or may not work using other windows enviroments ore versions of visual stuido.

History

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here