В этой статье я напишу как я прикручивал ЕСИА к сайту, возможны и другие сценарии.
Первым делом надо создать сертификат для генерации подписи в формате 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