前言
一旦进入遥远的星系,我们便需要使用GosUslugi上的ESIA帐户对用户进行身份验证。因为 我们生活在.Net星系中,我们研究的第一件事是为一架现成的宇宙飞船准备整个谷歌,以免自己独自to取一切,但搜索没有任何值得的结果。因此,决定研究该主题并自行实施同一艘宇宙飞船。

介绍
, , , — , . , .
... , . , :
3 , 2 – , . 2 : SAML OpenID Connect. ,
01.01.2018 . SAML 2.0 ( ). OAuth 2.0 / OpenID Connect ( ).
. , :
- - - « »
- -
- -
- .
Windows, CSP. docker Linux- , . :
- .Net Framework 4.8
- CSP, .Net
- , ( 1 )
client_secret, 4 UTF-8:
- Scope ( , , ). , «fullname gender email mobile usr_org»
- Timestamp ( «yyyy.MM.dd HH:mm:ss +0000»)
- ClientId ( , )
- State ( , Guid.NewGuid().ToString(«D»))
private string GetClientSecret(
X509Certificate2 certificate,
string scope,
string timestamp,
string clientId,
string state)
{
var signMessage = Encoding.UTF8.GetBytes($"{scope}{timestamp}{clientId}{state}");
byte[] encodedSignature = SignatureProvider.Sign(signMessage, certificate);
return Base64UrlEncoder.Encode(encodedSignature);
}
SignatureProvider – , . – .
1-4: (EsiaAuthUrl). ( ) url – https://esia-portal1.test.gosuslugi.ru/aas/oauth2/ac
. :
{EsiaAuthUrl}?client_id={ClientId}&scope={Scope}&response_type=code&state={State}& timestamp={Timestamp}&access_type=online&redirect_uri={RedirectUri}&client_secret={ClientSecret}
其中,RedirectUri是ESIA响应将定向到的地址,ClientSecret是GetClientSecret函数的结果。其他参数已在前面介绍。
因此,我们获得了URL,将用户重定向到那里。用户输入登录密码,确认对系统的访问权限。此外,ESIA会在地址RedirectUri上向您的系统发送响应,其中包含授权码。我们将需要此代码在ESIA中进行进一步查询。
获取访问令牌
要获取ESIA中的任何数据,我们需要获取访问令牌。为此,我们在ESIA中形成POST请求(对于测试环境,基本URL为:https://esia-portal1.test.gosuslugi.ru/aas/oauth2/te
-EsiaTokenUrl)。这里的请求的主要字段以类似的方式构成,在代码中,您将得到如下所示的内容:
public async Task<EsiaAuthToken> GetAccessToken(
string code,
string callbackUrl = null,
X509Certificate2 certificate = null)
{
var timestamp = DateTime.UtcNow.ToString("yyyy.MM.dd HH:mm:ss +0000");
var state = Guid.NewGuid().ToString("D");
var clientSecret = GetClientSecret(
certificate,
Configuration.Scope,
timestamp,
Configuration.ClientId,
state);
var requestParams = new List<KeyValuePair<string, string>>
{
new KeyValuePair<string, string>("client_id", Configuration.ClientId),
new KeyValuePair<string, string>("code", code),
new KeyValuePair<string, string>("grant_type", "authorization_code"),
new KeyValuePair<string, string>("state", state),
new KeyValuePair<string, string>("scope", Configuration.Scope),
new KeyValuePair<string, string>("timestamp", timestamp),
new KeyValuePair<string, string>("token_type", "Bearer"),
new KeyValuePair<string, string>("client_secret", clientSecret),
new KeyValuePair<string, string>("redirect_uri", callbackUrl)
};
using (var client = new HttpClient())
using (var response = await client.PostAsync(Configuration.EsiaTokenUrl, new FormUrlEncodedContent(requestParams)))
{
response.EnsureSuccessStatusCode();
var tokenResponse = await response.Content.ReadAsStringAsync();
var token = JsonConvert.DeserializeObject<EsiaAuthToken>(tokenResponse);
Argument.NotNull(token?.AccessToken, " ");
Argument.Require(state == token.State, " ");
return token;
}
}
( Configuration). , code , . :
public class EsiaAuthToken
{
[JsonProperty("access_token")]
public string AccessToken { get; set; }
public string State { get; set; }
public EsiaAuthTokenPayload Payload
{
get
{
if (string.IsNullOrEmpty(AccessToken))
{
return null;
}
string[] parts = AccessToken.Split('.');
if (parts.Length < 2)
{
throw new System.Exception($" . : {AccessToken}");
}
var payload = Encoding.UTF8.GetString(Base64UrlEncoder.Decode(parts[1]));
return JsonConvert.DeserializeObject<EsiaAuthTokenPayload>(payload);
}
}
}
public class EsiaAuthTokenPayload
{
[JsonProperty("urn:esia:sid")]
public string TokenId { get; set; }
[JsonProperty("urn:esia:sbj_id")]
public string UserId { get; set; }
}
. . GET REST API , url (EsiaRestUrl) : https://esia-portal1.test.gosuslugi.ru/rs
. , :
public async Task<EsiaUser> GetUser(string userId, string accessToken)
{
using (var client = new HttpClient())
{
client.DefaultRequestHeaders.Clear();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
var response = await client.GetStringAsync($"{Configuration.EsiaRestUrl}/prns/{userId}?embed=(organizations)");
var user = JsonConvert.DeserializeObject<EsiaUser>(response);
user.Id = user.Id ?? userId;
return user;
}
}
– . , . , . , :
public class EsiaUser
{
[JsonProperty("oid")]
public string Id { get; set; }
[JsonProperty("firstName")]
public string FirstName { get; set; }
[JsonProperty("lastName")]
public string LastName { get; set; }
[JsonProperty("middleName")]
public string MiddleName { get; set; }
[JsonProperty("trusted")]
public bool Trusted { get; set; }
[JsonProperty("organizations")]
public EsiaUserOrganizations OrganizationLinks { get; set; }
}
public class EsiaUserOrganizations
{
[JsonProperty("elements")]
public List<string> Links { get; set; }
}
, .. scope. . . , , State . scope scope’ , , :
http:
public async Task<EsiaAuthToken> GetOrganizationAccessToken(
string organizationId,
string code,
string state,
string callbackUrl = null,
X509Certificate2 certificate = null)
{
var timestamp = DateTime.UtcNow.ToString("yyyy.MM.dd HH:mm:ss +0000");
var scope = string.Join(" ", Configuration.OrganizationScope.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries)
.Select(orgScope => $"{Configuration.EsiaBaseUrl}/{orgScope}?org_oid={organizationId}"));
var clientSecret = GetClientSecret(
certificate,
scope,
timestamp,
Configuration.ClientId,
state);
var requestParams = new List<KeyValuePair<string, string>>
{
new KeyValuePair<string, string>("client_id", Configuration.ClientId),
new KeyValuePair<string, string>("code", code),
new KeyValuePair<string, string>("grant_type", "client_credentials"),
new KeyValuePair<string, string>("state", state),
new KeyValuePair<string, string>("scope", scope),
new KeyValuePair<string, string>("timestamp", timestamp),
new KeyValuePair<string, string>("token_type", "Bearer"),
new KeyValuePair<string, string>("client_secret", clientSecret),
new KeyValuePair<string, string>("redirect_uri", callbackUrl)
};
using (var client = new HttpClient())
using (var response = await client.PostAsync(Configuration.EsiaTokenUrl, new FormUrlEncodedContent(requestParams)))
{
response.EnsureSuccessStatusCode();
var tokenResponse = await response.Content.ReadAsStringAsync();
var token = JsonConvert.DeserializeObject<EsiaAuthToken>(tokenResponse);
Argument.NotNull(token?.AccessToken, " ");
Argument.Require(state == token.State, " ");
return token;
}
}
, .
public async Task<EsiaOrganization> GetOrganization(string organizationLink, string accessToken)
{
using (var client = new HttpClient())
{
client.DefaultRequestHeaders.Clear();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
var response = await client.GetStringAsync(organizationLink);
var organization = JsonConvert.DeserializeObject<EsiaOrganization>(response);
return organization;
}
}
:
var accessToken = await IntegrationService.GetAccessToken(request.Code, request.CallbackUrl);
var user = await IntegrationService.GetUser(accessToken.Payload.UserId, accessToken.AccessToken);
if (user.OrganizationLinks?.Links?.Any() == true)
{
var link = user.OrganizationLinks.Links.First();
var organizationId = link.Split('/').Last();
var organizationAccessToken = await IntegrationService.GetOrganizationAccessToken(organizationId, request.Code, accessToken.State, request.CallbackUrl);
user.Organization = await IntegrationService.GetOrganization(link, organizationAccessToken.AccessToken);
}
return user;
对于与ESIA进行交互的基本方案而言,这也许就足够了。通常,如果您知道实施的功能,则系统到ESIA的软件连接将不会超过1天。如有任何疑问,欢迎发表评论。感谢您仔细阅读我的帖子,希望它对您有所帮助。