Demo of Swedish BankID Auth / Sign with the Server-to-Server API (by PNR and anonymous)

This C# code demos.

  • All of the basic BankID functionality
  • Authentication
  • Signing with text
  • Initialize with PersonalNumber (respond on any BankID device without autostart)
  • Initialize anonymously without PersonalNumber but start app locally with autostart-URL
  • Error handling, for example user pressing Cancel in the app.
  • Strong typing of all query and response data.

This demo first asks for the type of operation (A or S), then asks for an optional personal number and if Sign the needed 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 every 5 seconds.

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 RestSharp; //  install NuGet Package RestSharp
using Newtonsoft.Json; //  install NuGet Package NewtonSoft.Json
using System;
using System.Text;
using System.Threading;
using System.Diagnostics;

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)
        {
            var accessToken = "MY_API_KEY_HERE";
            var apiRoot = "https://test.zignsec.com/v2"; // for prod set to https://api.zignsec.com/v2

            IBankIDSE svc = new BankIdSvcImpl(apiRoot, accessToken);

            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)"
                + "\n\n -OR- press enter to do an anonymous BankID Auth/Sign using the locally installed BankID app on this PC.\nNote: this will in the background start the BankID app with the autostart-url shown !\n\n");
            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("\nInitiation call for <" + oper + "> failed with error code: " + 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.");

            if (string.IsNullOrWhiteSpace(personalNumber))
            {
                Thread.Sleep(1000);
                Process.Start(autoStartUrl);
            }

            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...");
                Thread.Sleep(5000);

                // GET status from BankIDSE via ZignSec
                results = svc.CheckForProgressAndResults(orderResp.orderRef);                
            }
            while (svc.ElapsedSecs() < 190 && results.userInfo == null);
            // this example runs for 190 secs, a bit more than the Bankid 3 minute timeout limit to 
            // show what happens with the collect calls after success or error or timeout. 
            // or until a user makes a succesful taransaction which changes the userinfo from null
            //while (results.progressStatus != ProgressStatusType.COMPLETE && results.errors.Length == 0);
           
            Console.WriteLine("Done. Press any key to exit");
            var s = Console.ReadKey();
        }

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

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

        int ElapsedSecs();
    }

    public class BankIdSvcImpl : IBankIDSE
    {
        private string _apiRoot, _accessToken;

        public BankIdSvcImpl(string apiRoot, string accessToken)
        { _apiRoot = apiRoot; _accessToken = accessToken; }

        public OrderResponseType InitiateAuthentication(string personalNumber)
        {
            return Initiate("Authenticate", personalNumber);
        }

        public OrderResponseType InitiateSigning(string personalNumber,
            string textToSign, byte[] optionalHiddenData)
        {
            return Initiate("Sign", personalNumber, textToSign, optionalHiddenData);
        }

        public CollectResponseType CheckForProgressAndResults(string orderRef)
        {
            var client = new RestClient(_apiRoot);

            var request = new RestRequest("/BankIDSE/Collect", Method.GET);

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

            if (firstCollect)
            {
                firstCollect = false;
                Console.WriteLine($"\nNow Getting /BankIDSE/Collect");
            }

            IRestResponse resp;

            resp = client.Execute(request); // Restsharps json deserialization with client.Get(request) does not work?
            var results = JsonConvert.DeserializeObject(resp.Content);

            var err = "";
            var usr = "";

            if (results.errors.Length > 0)
                err = "Error[0]:" + results.errors[0].code + " " + results.errors[0].description;
            if (results.userInfo != null)
                usr = results.userInfo.givenName + " " + results.userInfo.surname + " " + results.userInfo.personalNumber;
                
            var elapsed = ElapsedSecs();
            Console.WriteLine($"  {elapsed}s  {resp.StatusCode}({(int)resp.StatusCode}) ->  {err}  ProgressStatus:{results.progressStatus}   {usr}" );

            return results;
        }

        public int ElapsedSecs()
        {
            var ret = (int)(DateTime.Now - started).TotalSeconds;
            return ret;
        }

        DateTime started;
        bool firstCollect;

        // 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, Method.POST);
            var body = JsonConvert.SerializeObject(req);

            request.AddParameter("application/json", body, ParameterType.RequestBody);
            request.AddHeader("Authorization", _accessToken);

            Console.WriteLine($"\nNow Posting /BankIDSE/" + oper);

            var response = client.Execute(request); // Restsharps json deserialization with client.Post(request) does not work?

            started = DateTime.Now; firstCollect = true;

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

            var ans2 = JsonConvert.DeserializeObject(response.Content);

            return ans2; // response.Data; deserialisering med restsharp does not wok
        }
    }

    // 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(bodyString);

    public class AuthenticateRequestType // The inparameters to the Authenticate-method in the swedish BankID-Api
    {
        public string personalNumber { get; set; } // 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. 
        public EndUserInfoType[] endUserInfo { get; set; } // optional ist of EndUserInfoType(optional). Used to provide information related to the user and the user’s computer/device to the BankID-server. 
        public RequirementType[] requirementAlternatives { get; set; }  // optional list used by RP to set requirements how the authentication or sign operation must be performed. Default rules are applied if omitted.
    }

    public class SignRequestType // The inparameters to the Sign-method in the swedish BankID-Api
    {
        public string personalNumber { get; set; } // optional - The ID number of the user trying to sign. 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.  
        public EndUserInfoType[] endUserInfo { get; set; }  //  optional list, to provide information related to the user and the user’s computer/device to the BankID-server. 
        public RequirementType[] requirementAlternatives { get; set; }  // optional list, used by RP to set requirements how the login or sign operation must be performed. Default rules are applied if omitted. 
        public string userVisibleData { get; set; } // 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 
        public string userNonVisibleData { get; set; } // Data not displayed to the user (optional). The value must be base 64-encoded. 0 - 200 000 characters (after base 64-encoding). 
    }

    public class OrderResponseType // Return value from auth/sign 
    {
        public string orderRef { get; set; } // OrderRef must be used by RP when using the collect method. UUID-string: 36-50 characters.
        public string autoStartToken { get; set; } // AutoStartToken must be used when the user ID is not provided. UUID-string: 36-50 characters. 
        public error[] errors { get; set; } // 0 or 1 errors here, not more. This is Zignsecs error handling mapped from BankIDs SOAP fault 
    }

    /// 
    /// 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). 
    /// 
    public class EndUserInfoType
    {
        public string type { get; set; }
        public string value { get; set; }
    }

    /// 
    /// 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. 
    /// 
    public class RequirementType
    {
        public string type { get; set; }
        public string value { get; set; }
    }

    public class CollectResponseType //  The status of an order.
    {
        public ProgressStatusType progressStatus { get; set; } // for tracking of the bankid app workflow status 
        public string signature { get; set; } // string (b64). XML-signature. (If the order is COMPLETE). The content of the signature is described in BankID Signature Profile specification.
        public UserInfoType userInfo { get; set; } // the end user info (is available if the order has reached progressStatus = COMPLETED) 
        public string ocspResponse { get; set; } // 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.        
        public error[] errors { get; set; } // 0 or 1 errors here, not more. This is Zignsecs error handling mapped from BankIDs SOAP fault  
    }

    public enum ProgressStatusType // The progress status for an order in betweeen initation and success/failure
    {
        OUTSTANDING_TRANSACTION, // 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. 
        NO_CLIENT, // 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
        STARTED, // 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. 
        USER_SIGN, // The client has received the order.
        USER_REQ, // Not used 
        COMPLETE, // The user has provided the security code and completed the order. Collect response includes the signature, user information and the ocsp response. 
    }

    public class UserInfoType // the returned user info results
    {
        public string personalNumber { get; set; } // PersonalNumberType - ID number (swe personnummer) 
        public string givenName { get; set; } // The given name of the user 
        public string surname { get; set; } // The surname of the user 
        public string name { get; set; } // The given name and surname of the user 
        public DateTime notBefore { get; set; } // Start of validity of the users BankID
        public DateTime notAfter { get; set; } // End of validity of the Users BankID 
        public string ipAddress { get; set; } // The IP-address of the user agent as the BankID server discovers it 
    }

    public class error // added zignsec error handling
    {
        public FaultStatusType code { get; set; } // a soap exception thrown by the bankid backend
        public string description { get; set; }
    }

    public enum FaultStatusType  // These are the original SOAP exceptions thrown by the BankID backend  
    {
        INVALID_PARAMETERS, // Auth/Sign  (ex bad pnr-format)    Collect (quite common, fetching more than once)
        ACCESS_DENIED_RP,  // Auth/Sign     Collect 
        CLIENT_ERR, // Collect 
        CERTIFICATE_ERR,   // Collect    (quite common: 5 attempt->Bankid was locked; BankID is revoked; BankID is invalid)
        RETRY,  // Auth/Sign    Collect
        INTERNAL_ERROR,  // Auth/Sign     Collect 
        ALREADY_COLLECTED,  // not used
        EXPIRED_TRANSACTION,  // Collect 
        ALREADY_IN_PROGRESS,  // Auth/Sign  (quite common)
        USER_CANCEL,  // Collect (very common, user pressed cancel in the app)
        CANCELLED,  // Collect
        REQ_PRECOND, // not used
        REQ_ERROR, // not used
        REQ_BLOCKED, // not used
        START_FAILED,  // Collect
    }
}