Аутентификация через ЕСИА OAuth2

в 9:11, , рубрики: C#, Веб-разработка, есиа, метки:

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

Первым делом надо создать сертификат для генерации подписи в формате 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; }
    }

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

Автор: Ascar

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js