Integration mit ESIA für .Net: einfacher als es sich anhört

Vorwort


Einmal in einer fernen, fernen Galaxie ... mussten wir Benutzer mithilfe eines ESIA-Kontos auf GosUslugi authentifizieren. weil Wir leben in der .Net-Galaxie. Das erste, was untersucht wurde, war das ganze Googol für ein fertiges Raumschiff, um nicht alles selbst zu krücken, aber die Suche führte zu nichts Wertvollem. Daher wurde beschlossen, das Thema zu studieren und dasselbe Raumschiff selbst zu implementieren.





Einführung


, , , — , . , .


... , . , :



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}

Dabei ist RedirectUri die Adresse, an die die Antwort von ESIA geleitet wird, und ClientSecret ist das Ergebnis der GetClientSecret-Funktion. Andere Parameter sind zuvor beschrieben.


Somit erhalten wir die URL, leiten den Benutzer dorthin weiter. Der Benutzer gibt ein Anmeldekennwort ein und bestätigt den Zugriff auf seine Daten für Ihr System. Außerdem sendet die ESIA eine Antwort an Ihr System unter der Adresse RedirectUri, die den Autorisierungscode enthält. Wir benötigen diesen Code für weitere Anfragen in ESIA.



Ein Zugriffstoken erhalten


Um Daten in ESIA zu erhalten, benötigen wir ein Zugriffstoken. Dazu bilden wir eine POST-Anfrage in ESIA (für eine Testumgebung lautet die Basis-URL: https://esia-portal1.test.gosuslugi.ru/aas/oauth2/te- EsiaTokenUrl). Die Hauptfelder der Anfrage hier sind auf ähnliche Weise gebildet, im Code erhalten Sie ungefähr Folgendes:

/// <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;


Vielleicht reicht dies für das grundlegende Szenario der Interaktion mit ESIA. Wenn Sie die Funktionen der Implementierung kennen, dauert die Softwareverbindung des Systems mit ESIA im Allgemeinen nicht länger als 1 Tag. Wenn Sie Fragen haben, können Sie diese gerne kommentieren. Vielen Dank, dass Sie meinen Beitrag bis zum Ende gelesen haben. Ich hoffe, er wird nützlich sein.


All Articles