Intégration avec ESIA pour .Net: plus simple qu'il n'y paraît

Préface


Une fois dans une galaxie lointaine, lointaine ... il nous a fallu authentifier les utilisateurs à l'aide d'un compte ESIA sur GosUslugi. Parce que nous vivons dans la galaxie .Net, la première chose que nous avons étudiée était le googol entier pour un vaisseau spatial prêt à l'emploi afin de ne pas tout bousculer par nous-mêmes, mais les recherches n'ont abouti à rien de valable. Par conséquent, il a été décidé d'étudier le sujet et de mettre en œuvre le vaisseau spatial seul.





introduction


, , , — , . , .


... , . , :



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}

où RedirectUri est l'adresse vers laquelle la réponse de l'EIES sera dirigée et ClientSecret est le résultat de la fonction GetClientSecret. D'autres paramètres sont décrits précédemment.


Ainsi, nous obtenons l'URL, redirige l'utilisateur là-bas. L'utilisateur entre un mot de passe de connexion, confirme l'accès à ses données pour votre système. De plus, l'EIES envoie une réponse à votre système à l'adresse RedirectUri, qui contient le code d'autorisation. Nous aurons besoin de ce code pour de plus amples informations en EIES.



Obtention d'un jeton d'accès


Pour obtenir des données dans l'EIES, nous devons obtenir un jeton d'accès. Pour ce faire, nous formons une requête POST en ESIA (pour un environnement de test, l'url de base est: https://esia-portal1.test.gosuslugi.ru/aas/oauth2/te- EsiaTokenUrl). Les principaux champs de la demande ici sont formés de manière similaire, dans le code, vous obtenez quelque chose comme ceci:

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


C'est peut-être suffisant pour le scénario de base d'interaction avec l'EIES. En général, si vous connaissez les caractéristiques de l'implémentation, la connexion logicielle du système à ESIA ne prendra pas plus d'un jour. Si vous avez des questions, n'hésitez pas à commenter. Merci d'avoir lu mon post jusqu'au bout, j'espère qu'il vous sera utile.


All Articles