与ESIA for .Net集成:比听起来容易

前言


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





介绍


, , , — , . , .


... , . , :



3 , 2 – , . 2 : SAML OpenID Connect. ,

01.01.2018 . SAML 2.0 ( ). OAuth 2.0 / OpenID Connect ( ).


. , :


  1. - - « »
  2. -
  3. -
  4. .



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)。这里的请求的主要字段以类似的方式构成,在代码中,您将得到如下所示的内容:

/// <summary>
///   
/// </summary>
/// <param name="code">     </param>
/// <param name="callbackUrl">     </param>
/// <param name="certificate">   </param>
/// <returns> </returns>
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");

	// Create signature in PKCS#7 detached signature UTF-8
	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 , . :


/// <summary>
///      
/// </summary>
public class EsiaAuthToken
{
	/// <summary>
	///  
	/// </summary>
	[JsonProperty("access_token")]
	public string AccessToken { get; set; }

	/// <summary>
	///  
	/// </summary>
	public string State { get; set; }

	/// <summary>
	///    
	/// </summary>
	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);
		}
	}
}

/// <summary>
///    
/// </summary>
public class EsiaAuthTokenPayload
{
	/// <summary>
	///  
	/// </summary>
	[JsonProperty("urn:esia:sid")]
	public string TokenId { get; set; }

	/// <summary>
	///  
	/// </summary>
	[JsonProperty("urn:esia:sbj_id")]
	public string UserId { get; set; }
}


. . GET REST API , url (EsiaRestUrl) : https://esia-portal1.test.gosuslugi.ru/rs. , :


/// <summary>
///   
/// </summary>
/// <param name="userId"> </param>
/// <param name="accessToken"> </param>
/// <returns> </returns>
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;
	}
}

– . , . , . , :


/// <summary>
///  
/// </summary>
public class EsiaUser
{
	/// <summary>
	/// 
	/// </summary>
	[JsonProperty("oid")]
	public string Id { get; set; }

	/// <summary>
	/// 
	/// </summary>
	[JsonProperty("firstName")]
	public string FirstName { get; set; }

	/// <summary>
	/// 
	/// </summary>
	[JsonProperty("lastName")]
	public string LastName { get; set; }

	/// <summary>
	/// 
	/// </summary>
	[JsonProperty("middleName")]
	public string MiddleName { get; set; }

	/// <summary>
	///   
	/// </summary>
	[JsonProperty("trusted")]
	public bool Trusted { get; set; }

	/// <summary>
	///   
	/// </summary>
	[JsonProperty("organizations")]
	public EsiaUserOrganizations OrganizationLinks { get; set; }
}

/// <summary>
///   
/// </summary>
public class EsiaUserOrganizations
{
	[JsonProperty("elements")]
	public List<string> Links { get; set; }
}


, .. scope. . . , , State . scope scope’ , , :


http://esia.gosuslugi.ru/org_shortname/?org_oid={organizationId} http://esia.gosuslugi.ru/ org_fullname/?org_oid={organizationId}

/// <summary>
///       
/// </summary>
/// <param name="organizationId"> </param>
/// <param name="code"> </param>
/// <param name="state"> </param>
/// <param name="callbackUrl">     </param>
/// <param name="certificate">   </param>
/// <returns>     </returns>
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}"));

	// Create signature in PKCS#7 detached signature UTF-8
	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;
	}
}

, .


/// <summary>
///   
/// </summary>
/// <param name="organizationLink">  </param>
/// <param name="accessToken"> </param>
/// <returns> </returns>
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天。如有任何疑问,欢迎发表评论。感谢您仔细阅读我的帖子,希望它对您有所帮助。


All Articles