ZignSec

C# Code Example

Swedish BankID Authentice / Sign with the Server-to-Server API

This demo first asks for the type of operation (A or S), then asks for personal number and if needed a text message to sign. Next the application submits the request to Swedish BankID agency, and waits for the user to complete the Authentication or Signing in the BankID app on any device that is connected to the BankID Service. Result polling is demoed through a key press.

This demo application is a Windows Console application with strong JSON typing.

Paste the code below into program.cs in a new C# Console Application. Also add Newtonsoft.Json and RestSharp nuget packages, to enable easy rest calls and parsing of the JSON results into a strong typed C# object.

            using Newtonsoft.Json; //  install NuGet Package NewtonSoft.Json
using Newtonsoft.Json.Converters;
using System;
using System.IO;
using System.Net;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using RestSharp; //  install NuGet Package RestSharp


namespace BankIDSE_Demo
{
    // This demo/sample does a real Auhtenticate and Sign against Swedish BankID via the BankID app.
    //   (The only necessary code changes is the the accessToken row below.)
    // Copy and paste the code into a new VS Console application (Windows Or .Net Core).
    class Program
    {
        static void Main(string[] args)
        {
            // IMPORTANT: Exchange the "YOUR KEY..." below for you personal access token
            // this sample is setup to use the BankID production environment, 
            // for test instead send in https://test.zignsec.com/v2 as apiRoot        
            IBankIDSE svc = new BankIdSvcImpl();

            Console.WriteLine("Type \"A\", and press Enter, for doing a BankID " 
                + "Authentication(login) or type \"S\" for Signing of a text. ");

            var key = Console.ReadLine().ToUpper();
            if (key != "A" && key != "S")
            { Console.WriteLine("    Wrong key?"); Thread.Sleep(1000); return; }

            var oper = key == "A" ? "Authenticate" : "Sign";

            Console.WriteLine(
                "\nEnter a Swedish personalnumber to do a <"
                + oper + "> in the BankID app: (12 digit-format: YYYYMMDDCCCC)");
            var personalNumber = Console.ReadLine();


            string textToSign = null;
            if (key == "S")
            {
                Console.WriteLine("\nEnter a text to sign:");
                textToSign = Console.ReadLine();
            }

            OrderResponseType orderResp;

            // POST Initiation info to BankIDSE via ZignSec
            if (oper == "Authenticate")
                orderResp = svc.InitiateAuthentication(personalNumber);
            else // oper is "Sign"
                orderResp = svc.InitiateSigning(personalNumber, textToSign);

            if (orderResp.errors != null && orderResp.errors.Length > 0)
            {
                waitForKey("\nERROR: "+ orderResp.errors[0].code + "; " + 
                    orderResp.errors[0].description + "\nPress any key to exit.");
                return;
            }
            var autoStartUrl = $"bankid:///?autostarttoken={orderResp.autoStartToken}&redirect=null";
            Console.WriteLine($"\nNow, you should go to the BankID app on your device to complete <{oper}>.");
            Console.WriteLine($"Or quickly navigate to {autoStartUrl} on your device to autostart the app.");
            Console.WriteLine("  First 30 s status is OUTSTANDING_TRANSACTION, then status is NO_CLIENT ");
            Console.WriteLine("  for another 2,5 minutes.");

            CollectResponseType results;
            // loop until user logged in via the BankID app and results are colleced
            do
            {
                waitForKey("  ...and press any key here to recheck the progress status...");
                // GET status from BankIDSE via ZignSec
                results = svc.CheckForProgressAndResults(orderResp.orderRef);
            } while (results.progressStatus != ProgressStatusType.COMPLETE); 
            

            var user = results.userInfo;
            waitForKey($"BankID Success! Some of the ID data received from BankID after <{oper}>: \n"
                + $"  user.givenName: {user.givenName}\n  user.surname: {user.surname}\n"
                + $"  user.personalNumber: {user.personalNumber}.\n"
                + "Press any key to exit."); ;
        }

        static Action<string> waitForKey = (msg) => { Console.WriteLine("\n" + msg); Console.ReadKey(); };
    }

    // BankIDSE has these three operations, both on the BankID frontend and mirrored on ZignSec REST api.
    public interface IBankIDSE
    {
        OrderResponseType InitiateAuthentication(string personalNumber);
        OrderResponseType InitiateSigning(string personalNumber, string textToSign, byte[] optionalHiddenData = null);
        CollectResponseType CheckForProgressAndResults(string orderRef);
    }

    public class BankIdSvcImpl : IBankIDSE
    {
        string _accessToken = "a6366e00-YOUR KEY TOKEN";
        string _apiRoot = "https://api.zignsec.com/v2";

        OrderResponseType IBankIDSE.InitiateAuthentication(string personalNumber)
        {
            return Initiate("Authenticate", personalNumber);
        }
        
        OrderResponseType IBankIDSE.InitiateSigning(string personalNumber, 
            string textToSign, byte[] optionalHiddenData)
        {
            return Initiate("Sign", personalNumber, textToSign, optionalHiddenData);
        }
    
        CollectResponseType IBankIDSE.CheckForProgressAndResults(string orderRef)
        {
            var client = new RestClient(_apiRoot);

            var request = new RestRequest("/BankIDSE/Collect");

            request.AddParameter("orderRef", orderRef);
            request.AddHeader("Authorization", _accessToken);

            Console.WriteLine($"\nNow Getting /BankIDSE/Collect");
            var resp = client.Get(request); // Restsharps json deserialization with client.Get<CollectResponseType>(request) does not work?
            var results = JsonConvert.DeserializeObject<CollectResponseType>(resp.Content);

            Console.WriteLine($"  {resp.StatusCode} -> {results.progressStatus} ({resp.ContentLength} bytes) ");

            return results;
        }

        // operations Authenticate and Sign can share most of the code internally
        private OrderResponseType Initiate(string oper, string personalNumber, 
            string textToSign = null, byte[] optionalHiddenData = null)
        {
            //var initUrl = _apiRoot + "/BankIDSE/" + oper;
            string textToSign_Base64 = null; //only set when oper=Sign
            string hiddenData_Base64 = null; //only set when oper=Sign
            object req;

            if (oper == "Sign")
            {
                textToSign_Base64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(textToSign));
                hiddenData_Base64 = optionalHiddenData == null ? null : Convert.ToBase64String(optionalHiddenData);
                req = new SignRequestType()
                {
                    personalNumber = personalNumber,
                    userVisibleData = textToSign_Base64,
                    userNonVisibleData = hiddenData_Base64
                };
            }
            else // oper = Authenticate          
                req = new AuthenticateRequestType() { personalNumber = personalNumber };

            var client = new RestClient(_apiRoot);
                       
            var request = new RestRequest("/BankIDSE/" + oper);

            request.AddJsonBody(req);
            request.AddHeader("Authorization", _accessToken);

            Console.WriteLine($"\nNow Posting /BankIDSE/" + oper);
            var response = client.Post(request); // Restsharps json deserialization with client.Post<OrderResponseType>(request) does not work?

            Console.WriteLine($"  {response.StatusCode} -> {response.Content}");

            var ans2 = JsonConvert.DeserializeObject<OrderResponseType>(response.Content);

            return ans2;
        }
    }

    // EVERYTHING BELOW IS ONLY FOR STRONG TYPING (replicates the soap types on the backend BankID Soap service)
    //    see also: https://www.bankid.com/assets/bankid/rp/bankid-relying-party-guidelines-v2.15.pdf 
    // You can skip strong typing and use Json instead or deserialize the json to .net dynamic type or anonymous type.:
    //   Object obj = JsonConvert.DeserializeObject(bodyString);   OR
    //   Dynamic dyn = JsonConvert.DeserializeObject<dynamic>(bodyString);

    /// <summary>
    /// The inparameters to the Authenticate-method in the swedish BankID-Api
    /// </summary>
    public class AuthenticateRequestType
    {
        /// <summary>
        /// PersonalNumberType - The ID number of the user trying to be authenticated (optional). 
        /// If the ID number is omitted the user must use the same device and the client must be started
        /// with the autoStartToken returned in orderResponse. 
        /// </summary>
        public string personalNumber { get; set; }

        /// <summary>
        /// List of EndUserInfoType (optional). 
        /// Used to provide information related to the user and the user’s computer/device to the BankID-server. 
        /// </summary>
        public EndUserInfoType[] endUserInfo { get; set; }

        /// <summary>
        /// RequirementAlternativesType (optional). 
        /// Used by RP to set requirements how the authentication or sign operation must be performed. 
        /// Default rules are applied if omitted.
        /// </summary>
        public RequirementType[] requirementAlternatives { get; set; }
    }

    /// <summary>
    /// The inparameters to the Sign-method in the swedish BankID-Api
    /// </summary>
    public class SignRequestType
    {
        /// <summary>
        /// PersonalNumberType - The ID number of the user trying to sign (optional). 
        /// If the ID number is omitted the user must use the same device and the client must be 
        /// started with the autoStartToken returned in orderResponse.  
        /// </summary>
        public string personalNumber { get; set; }

        /// <summary>
        /// List of EndUserInfoType (optional).
        /// Used to provide information related to the user and the user’s 
        /// computer/device to the BankID-server. 
        /// </summary>
        public EndUserInfoType[] endUserInfo { get; set; }

        /// <summary>
        /// RequirementAlternativesType (optional). 
        /// Used by RP to set requirements how the login or sign operation must be performed. 
        /// Default rules are applied if omitted. 
        /// </summary>
        public RequirementType[] requirementAlternatives { get; set; }

        /// <summary>
        /// The text to be displayed and signed. 
        /// Must be UTF-8 encoded. The value must be base 64-encoded. 
        /// 1-40 000 characters (after base 64-encoding). The text can be formatted using CR = new line,
        /// LF = new line and CRLF = new line 
        /// </summary>
        public string userVisibleData { get; set; }

        /// <summary>
        /// Data not displayed to the user (optional). 
        /// The value must be base 64-encoded. 0 - 200 000 characters (after base 64-encoding). 
        /// </summary>
        public string userNonVisibleData { get; set; }
    }

    /// <summary>
    /// Return value from auth/sign 
    /// </summary>
    public class OrderResponseType
    {
        /// <summary>
        /// OrderRef must be used by RP when using the collect method. UUID-string: 36-50 characters.
        /// </summary>
        public string orderRef { get; set; }

        /// <summary>
        /// AutoStartToken must be used when the user ID is not provided. UUID-string: 36-50 characters. 
        /// </summary>
        public string autoStartToken { get; set; }

        // only set on exceptions and error situations
        public error[] errors { get; set; }
    }


    /// <summary>
    /// Used to pass information related to the user and the user’s computer/device to the BankID server.
    /// A list of types and values. 
    /// Allowed types are (just one): 
    /// IP_ADDR. used to include the users IP-address as seen by RP. It is recommended to use this parameter
    /// to enable future controls of the IP-address (no controls are done in the current solution). 
    /// </summary>
    public class EndUserInfoType
    {
        public string type { get; set; }
        public string value { get; set; }
    }

    /// <summary>
    /// An item in a list of alternative requirements. 
    /// Used by RP to put one or more requirement on how the order must be created and verified. 
    /// A requirement consists of one or more conditions. Every condition has a type/key and can
    /// have one or more values. If no requirement is included a set of default conditions is applied.
    /// The order of the requirement is significant. The first requirement where all conditions are
    /// true will be used. The used requirement is included in the resulting signature. 
    /// </summary>
    public class RequirementType
    {
        public string type { get; set; }
        public string value { get; set; }
    }

    /// <summary>
    ///  The status of an order.
    /// </summary>
    public class CollectResponseType
    {
        /// <summary>
        /// ProgressStatusType 
        /// </summary>
        [JsonConverter(typeof(StringEnumConverter))]
        public ProgressStatusType progressStatus { get; set; }

        /// <summary>
        /// String (b64). XML-signature. (If the order is COMPLETE).
        /// The content of the signature is described in BankID Signature Profile specification.
        /// </summary>
        public string signature { get; set; }

        /// <summary>
        /// UserInfoType (If the order is COMPLETE) 
        /// </summary>
        public UserInfoType userInfo { get; set; }

        /// <summary>
        /// String (b64). OCSP-response (If the order is COMPLETE). 
        /// The OCSP 0 response is signed by a certificate that has the same issuer as the certificate
        /// being verified. The OSCP response has an extension for Nonce.
        /// </summary>
        public string ocspResponse { get; set; }
    }

    /// <summary>
    /// The progress status for an order
    /// </summary>
    public enum ProgressStatusType
    {
        /// <summary>
        /// The order is being processed. 
        /// The client has not yet received the order. 
        /// The status will later change to NO_CLIENT, STARTED or USER_SIGN. 
        /// </summary>
        OUTSTANDING_TRANSACTION,

        /// <summary>
        /// The order is being processed. 
        /// The client has not yet received the order. 
        /// If the user did not provide her ID number the error START_FAILED will be returned
        /// </summary>
        NO_CLIENT,

        /// <summary>
        /// A client has been started with the autostarttoken but a usable ID has not
        /// yet been found in the started client. 
        /// When the client starts there may be a short delay until all ID:s are registered. 
        /// The user may not have any usable ID:s at all, or has not yet inserted
        /// their smart card. 
        /// </summary>
        STARTED,

        /// <summary>
        /// The client has received the order.
        /// </summary>
        USER_SIGN,

        /// <summary>
        /// Not used 
        /// </summary>
        USER_REQ,

        /// <summary>
        /// The user has provided the security code and completed the order. 
        /// Collect response includes the signature, user information and the ocsp response. 
        /// </summary>
        COMPLETE,
    }

    /// <summary>
    /// UserInfoType
    /// </summary>
    public class UserInfoType
    {
        /// <summary>
        /// PersonalNumberType - ID number (swe personnummer) 
        /// </summary>
        public string personalNumber { get; set; }

        /// <summary>
        /// The given name of the user 
        /// </summary>
        public string givenName { get; set; }

        /// <summary>
        /// The surname of the user 
        /// </summary>
        public string surname { get; set; }

        /// <summary>
        /// The given name and surname of the user 
        /// </summary>
        public string name { get; set; }

        /// <summary>
        /// Start of validity of the users BankID
        /// </summary>
        public DateTime notBefore { get; set; }

        /// <summary>
        /// End of validity of the Users BankID 
        /// </summary>
        public DateTime notAfter { get; set; }

        /// <summary>
        /// The IP-address of the user agent as the BankID server discovers it 
        /// </summary>
        public string ipAddress { get; set; }
    }

    public class error
    {
        public string code { get; set; }
        public string description { get; set; }
    }
}