В этой статье я напишу как я прикручивал ЕСИА к сайту, возможны и другие сценарии.

Первым делом надо создать сертификат для генерации подписи в формате pfx с закрытым ключом.
Для этого я использовал «PFX Certificate Generator». В настройках указывается служебная информация и информация для хеширования: sha256, длина ключа 2048.

После этого надо установить сертификат. Добавляем оснастку для сертификатов. открываем ветку «локальное хранилище», потом импортируем туда наш сгенеренный сертификат. Потом копируем его в ветку «Доверенные корневые».
После этого надо поставить к нему разрешение. В ветке «Личные» находим наш импортированный сертификат кликаем правой кнопкой мыши выбираем «Все задачи» потом «Управление закрытыми ключами». Далее добавляем разрешения на группу «Все».

После этого надо экспортировать наш сертификат в формате cer и передать в ЕСИА, там его зарегистрируют.

Теперь приступим к написанию кода. У себя я использовал NancyFx. Итак…

    public class EsiaModule : NancyModule
    {
        static string client_id = "123456"; //Мнемоника системы, его можно узнать у ЕСИА
        static string state = Guid.NewGuid().ToString("D"); //гуид для всяких проверок
        static string server_url = "https://esia.gosuslugi.ru/aas/oauth2/ac"; //адресс по которому есиа вернет авторизационный код
        static string server_url_2 = "https://esia.gosuslugi.ru/aas/oauth2/te"; //адресс по которому получим маркер
        static string server_url_prns = "https://esia.gosuslugi.ru/rs/prns/"; //тут мы узнаем ФИО пользователя который к нам хочет залогиниться

//тестовые настройки
       /* static string server_url = "https://esia-portal1.test.gosuslugi.ru/aas/oauth2/ac";
        static string server_url_2 = "https://esia-portal1.test.gosuslugi.ru/aas/oauth2/te";
        static string server_url_prns = "https://esia-portal1.test.gosuslugi.ru/rs/prns/";*/
        public EsiaModule()
        {
            Get["/ESIA"] = _ =>
            {

                //string scope = "openid";
                string scope = "fullname";
                string timestamp = DateTime.UtcNow.ToString("yyyy.MM.dd HH:mm:ss +0000");
                string access_type = "online";
                string response_type = "code";
                string redirect_uri = Request.Url.Scheme + "://" + Request.Url.HostName + (Request.Url.Port == 80 ? "" : (":" + Request.Url.Port.ToString())) + "/ESIA-OK";  //адресс редиректа, после того как пользователь ввел данные в есиа
                string client_secret = "";
                //Генерим подпись с помощью нашего сертификата
                string msg = scope + timestamp + client_id + state;
                byte[] msgBytes = Encoding.UTF8.GetBytes(msg);
                var signerCert = DetachedSignature.GetSignerCert();
                byte[] encodedSignature = DetachedSignature.SignMsg(msgBytes, signerCert);
                client_secret = HttpServerUtility.UrlTokenEncode(encodedSignature);
               //генерим строку с параметрами
                RequestBuilder builder = new RequestBuilder();
                builder.AddParam("client_id", client_id);
                builder.AddParam("client_secret", client_secret);
                builder.AddParam("redirect_uri", redirect_uri);
                builder.AddParam("scope", scope);
                builder.AddParam("response_type", response_type);
                builder.AddParam("state", state);
                builder.AddParam("timestamp", timestamp);
                builder.AddParam("access_type", access_type);

                string red_url = server_url + "?" + builder.ToString().Replace("+", "%2b");
                return Response.AsRedirect(red_url);
            };
            Get["/ESIA-OK"] = _ =>
            {
                //сюда нас редиректит есиа, мы проверяем state который был послан и который пришел, потом используем code
                string state_r = Request.Query["state"];
                string code = Request.Query["code"];

                if (state == state_r)
                {
                    //string scope = "openid";
                    string scope = "fullname";
                    string timestamp = DateTime.UtcNow.ToString("yyyy.MM.dd HH:mm:ss +0000");
                    string redirect_uri = Request.Url.Scheme + "://" + Request.Url.HostName + (Request.Url.Port == 80 ? "" : (":" + Request.Url.Port.ToString())) + "/ESIA-OK";
                    string client_secret = "";
                    string msg = scope + timestamp + client_id + state;
                    byte[] msgBytes = Encoding.UTF8.GetBytes(msg);
                    var signerCert = DetachedSignature.GetSignerCert();
                    byte[] encodedSignature = DetachedSignature.SignMsg(msgBytes, signerCert);
                    client_secret = HttpServerUtility.UrlTokenEncode(encodedSignature);

                    string result;
                    //генерим post запрос для получения маркера
                    {
                        RequestBuilder builder = new RequestBuilder();
                        builder.AddParam("client_id", client_id);
                        builder.AddParam("code", code);
                        builder.AddParam("grant_type", "authorization_code");
                        builder.AddParam("client_secret", client_secret);
                        builder.AddParam("state", state);
                        builder.AddParam("redirect_uri", redirect_uri);
                        builder.AddParam("scope", scope);
                        builder.AddParam("timestamp", timestamp);
                        builder.AddParam("token_type", "Bearer");

                        var httpWebRequest = (HttpWebRequest)WebRequest.Create(server_url_2);
                        httpWebRequest.ContentType = "application/x-www-form-urlencoded";
                        httpWebRequest.Method = "POST";
                        httpWebRequest.Timeout = int.MaxValue;     
       
                        httpWebRequest.Proxy = p;
                        using (var streamWriter = new StreamWriter(httpWebRequest.GetRequestStream()))
                        {
                            streamWriter.Write(builder.ToString().Replace("+", "%2b"));
                            streamWriter.Flush();
                            streamWriter.Close();
                        }

                        var httpResponse = (HttpWebResponse)httpWebRequest.GetResponse();
                        using (var streamReader = new StreamReader(httpResponse.GetResponseStream()))
                        {
                            result = streamReader.ReadToEnd();
                        }
                    }
                    ESIA_Marker_Answer marker = JsonConvert.DeserializeObject<ESIA_Marker_Answer>(result);//наш маркер

                    string[] marker_parts = marker.access_token.Split('.');

                    string header = Encoding.UTF8.GetString(base64urldecode(marker_parts[0]));
                    string payload = Encoding.UTF8.GetString(base64urldecode(marker_parts[1]));
                    string oid = (JsonConvert.DeserializeObject<dynamic>(payload))["urn:esia:sbj_id"];

                    //генерим запрос для получения иформации о пользователе
                    string user_info = "";
                    {
                        var httpWebRequest = (HttpWebRequest)WebRequest.Create(server_url_prns + oid);
                        httpWebRequest.ContentType = "application/x-www-form-urlencoded";
                        httpWebRequest.Method = "GET";
                        httpWebRequest.Headers["Authorization"] = "Bearer " + marker.access_token;
                        httpWebRequest.Timeout = int.MaxValue;                                 

                        var httpResponse = (HttpWebResponse)httpWebRequest.GetResponse();
                        using (var streamReader = new StreamReader(httpResponse.GetResponseStream()))
                        {
                            user_info = streamReader.ReadToEnd();
                        }
                    }

                    string firstName = JsonConvert.DeserializeObject<dynamic>(user_info)["firstName"];
                    string lastName = "";
                    try
                    {
                        lastName = JsonConvert.DeserializeObject<dynamic>(user_info)["lastName"];
                    }
                    catch { }
                    string middleName = "";
                    try
                    {
                        middleName = JsonConvert.DeserializeObject<dynamic>(user_info)["middleName"];
                    }
                    catch { }


                    // Записываю в бд данные если он не зареган и проверяю если зареган

                    MyEntities db = new MyEntities();
                    Helper h = new Helper();
                    users u;
                    if (db.users.Any(a => a.esia_oid.Trim() == oid.Trim()))
                    {
                        u = db.users.FirstOrDefault(a => a.esia_oid.Trim() == oid.Trim());
                    }
                    else
                    {
                        //create user                        
                        u = new users
                        {
                            mail = oid,
                            name = firstName,
                            pass = h.HashWithSalt(oid),
                            patronymic = middleName ?? "",
                            surname = lastName ?? "",
                            token = Guid.NewGuid().ToString("N"),
                            role_id = 2,
                            esia_oid = oid
                        };
                        db.users.Add(u);
                        db.SaveChanges();
                    }

                   //добавляю куки и отправляю на главную
                    DateTime expires = DateTime.UtcNow.AddYears(20);
                    List<NancyCookie> cs = new List<NancyCookie> { new NancyCookie("id", u.id.ToString(), false) { Expires = expires }, new NancyCookie("token", u.token, false) { Expires = expires } };
                    var resp = Response.AsRedirect("/");
                    resp.AddCookie(cs[0]);
                    resp.AddCookie(cs[1]);

                    return resp;
                }
                else
                {
                    return Response.AsJson(new { err = 1 });
                }
            };
        }
        static byte[] base64urldecode(string arg)
        {
            string s = arg;
            s = s.Replace('-', '+'); // 62nd char of encoding
            s = s.Replace('_', '/'); // 63rd char of encoding
            switch (s.Length % 4) // Pad with trailing '='s
            {
                case 0: break; // No pad chars in this case
                case 2: s += "=="; break; // Two pad chars
                case 3: s += "="; break; // One pad char
                default: throw new System.Exception(
                  "Illegal base64url string!");
            }
            return Convert.FromBase64String(s); // Standard base64 decoder
        }
    }

    public class RequestBuilder
    {
        List<RequesItemClass> items = new List<RequesItemClass>();
        public void AddParam(string name, string value)
        {
            items.Add(new RequesItemClass { name = name, value = value });
        }
        override public string ToString()
        {
            return string.Join("&", items.Select(a => a.name + "=" + a.value));
        }
    }

    class DetachedSignature
    {     
        static public X509Certificate2 GetSignerCert()
        {            
            X509Store storeMy = new X509Store(StoreName.My, StoreLocation.LocalMachine);
            storeMy.Open(OpenFlags.ReadOnly);
            X509Certificate2Collection certColl = storeMy.Certificates.Find(X509FindType.FindBySubjectKeyIdentifier, "12 34 56 78 90 00 00 00 11 11 11 11 11 11 11 11 11 22 33 44", false);
            storeMy.Close();
            return certColl[0];
        }        
        static public byte[] SignMsg(Byte[] msg, X509Certificate2 signerCert)
        {
            ContentInfo contentInfo = new ContentInfo(msg);
            SignedCms signedCms = new SignedCms(contentInfo, true);
            CmsSigner cmsSigner = new CmsSigner(signerCert);
            signedCms.ComputeSignature(cmsSigner);
            return signedCms.Encode();
        }

        static public bool VerifyMsg(Byte[] msg, byte[] encodedSignature)
        {
            ContentInfo contentInfo = new ContentInfo(msg);
            SignedCms signedCms = new SignedCms(contentInfo, true);
            signedCms.Decode(encodedSignature);

            try
            {
                signedCms.CheckSignature(true);              
            }
            catch (System.Security.Cryptography.CryptographicException e)
            {
                return false;
            }

            return true;
        }
    }

    public class ESIA_Marker_Answer
    {
        public string state { get; set; }
        public string token_type { get; set; }
        public int expires_in { get; set; }
        public string refresh_token { get; set; }
        public string id_token { get; set; }
        public string access_token { get; set; }
    }

    public class RequesItemClass
    {
        public string name;
        public string value;
    }


Вот в принципе и все. Один запрос, редирект + 2 запроса для получения маркера и информации о пользователе.
В дополнение можно проверять подпись маркера.

Комментарии (16)


  1. Razaz
    01.02.2016 12:32

    Я бы даже сказал обязательно проверять подпись.
    //string scope = «openid»; Это вроде не OIDC запрос без этого скоупа не? Или опять что-то в спеке менялось?


    1. Ascar
      01.02.2016 12:41

      В документации у есиа так и написано «openid», но мне потом надо получать данные о пользаке поэтому «fullname». А с openid валится такой запрос.


      1. Razaz
        01.02.2016 15:13

        Просто по спеке:
        openid — REQUIRED. Informs the Authorization Server that the Client is making an OpenID Connect request. If the openid scope value is not present, the behavior is entirely unspecified.

        Тоесть там непонятно что намешано и поддержкой OpenId Connect я бы не называл :)

        Что значит валится запрос?


        1. Ascar
          01.02.2016 15:18

          Возвращает ошибку, текст уже не помню. Но ясно, что это из за неверного scope.


          1. Razaz
            01.02.2016 15:25

            Тогда возможно там не OpenId Connect, а обычный OAuth 2? :)


    1. Ascar
      02.02.2016 16:00

      Насчет проверки подписи у маркера, я попробовал и она не проходит, чето мне кажется она не правильная приходит…

      DetachedSignature.VerifyMsg(Encoding.UTF8.GetBytes(marker_parts[0] + "." + marker_parts[1]), base64urldecode(marker_parts[2]))
      

      Падает еще в
      signedCms.Decode(encodedSignature);
      

      с ошибкой
      Additional information: Встречено неверное значение тега ASN1.


      1. Razaz
        02.02.2016 16:03

        А если вот это запустить? OpenID Connect Provider Certification


  1. lair
    01.02.2016 14:16

    У меня простой вопрос: а чем это отличается от обычного OAuth 2?


  1. monolithed
    01.02.2016 23:18

    А чего просто ссылку на Gist не дать?