Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions src/AspNet.Security.OAuth.Alipay/AlipayAuthenticationConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,18 @@ public static class Claims
/// The user's gender. F: Female; M: Male.
/// </summary>
public const string Gender = "urn:alipay:gender";

/// <summary>
/// OpenID is the unique identifier for Alipay users at the application level.
/// See https://opendocs.alipay.com/mini/0ai2i6
/// </summary>
public const string OpenId = "urn:alipay:open_id";

/// <summary>
/// The internal identifier for Alipay users will no longer be independently available going forward and will be replaced by OpenID.
/// See https://opendocs.alipay.com/common/0ai736
/// </summary>
[Obsolete("The internal identifier for Alipay users will no longer be independently available going forward and will be replaced by OpenID. See https://opendocs.alipay.com/common/0ai736")]
public const string UserId = "urn:alipay:user_id";
}
}
31 changes: 29 additions & 2 deletions src/AspNet.Security.OAuth.Alipay/AlipayAuthenticationHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,21 @@ protected override Task<HandleRequestResult> HandleRemoteAuthenticateAsync()
return base.HandleRemoteAuthenticateAsync();
}

private const string SignType = "RSA2";

private async Task AddCertificateSignatureParametersAsync(SortedDictionary<string, string?> parameters)
{
ArgumentNullException.ThrowIfNull(Options.PublicKey);
ArgumentNullException.ThrowIfNull(Options.ApplicationCertificateSnKeyId);
ArgumentNullException.ThrowIfNull(Options.RootCertificateSnKeyId);

var appPublicKey = await Options.PublicKey(Options.ApplicationCertificateSnKeyId, Context.RequestAborted);
var alipayRootPublicKey = await Options.PublicKey(Options.RootCertificateSnKeyId, Context.RequestAborted);

parameters["app_cert_sn"] = AlipayCertificationUtil.GetCertSN(appPublicKey.Span);
parameters["alipay_root_cert_sn"] = AlipayCertificationUtil.GetRootCertSN(alipayRootPublicKey.Span, SignType);
}

protected override async Task<OAuthTokenResponse> ExchangeCodeAsync([NotNull] OAuthCodeExchangeContext context)
{
// See https://opendocs.alipay.com/apis/api_9/alipay.system.oauth.token for details.
Expand All @@ -55,10 +70,16 @@ protected override async Task<OAuthTokenResponse> ExchangeCodeAsync([NotNull] OA
["format"] = "JSON",
["grant_type"] = "authorization_code",
["method"] = "alipay.system.oauth.token",
["sign_type"] = "RSA2",
["sign_type"] = SignType,
["timestamp"] = TimeProvider.GetUtcNow().ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture),
["version"] = "1.0",
};

if (Options.UseCertificateSignatures)
{
await AddCertificateSignatureParametersAsync(tokenRequestParameters);
}

tokenRequestParameters.Add("sign", GetRSA2Signature(tokenRequestParameters));

// PKCE https://tools.ietf.org/html/rfc7636#section-4.5, see BuildChallengeUrl
Expand Down Expand Up @@ -103,10 +124,16 @@ protected override async Task<AuthenticationTicket> CreateTicketAsync(
["charset"] = "utf-8",
["format"] = "JSON",
["method"] = "alipay.user.info.share",
["sign_type"] = "RSA2",
["sign_type"] = SignType,
["timestamp"] = TimeProvider.GetUtcNow().ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture),
["version"] = "1.0",
};

if (Options.UseCertificateSignatures)
{
await AddCertificateSignatureParametersAsync(parameters);
}

parameters.Add("sign", GetRSA2Signature(parameters));

var address = QueryHelpers.AddQueryString(Options.UserInformationEndpoint, parameters);
Expand Down
51 changes: 51 additions & 0 deletions src/AspNet.Security.OAuth.Alipay/AlipayAuthenticationOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,56 @@ public AlipayAuthenticationOptions()
ClaimActions.MapJsonKey(Claims.Gender, "gender");
ClaimActions.MapJsonKey(Claims.Nickname, "nick_name");
ClaimActions.MapJsonKey(Claims.Province, "province");
ClaimActions.MapJsonKey(Claims.OpenId, "open_id");
#pragma warning disable CS0618
ClaimActions.MapJsonKey(Claims.UserId, "user_id");
#pragma warning restore CS0618
}

/// <summary>
/// Gets or sets a value indicating whether to use certificate mode for signing calls.
/// <para>https://opendocs.alipay.com/common/057k53?pathHash=e18d6f77#%E8%AF%81%E4%B9%A6%E6%A8%A1%E5%BC%8F</para>
/// </summary>
public bool UseCertificateSignatures { get; set; }

/// <summary>
/// Gets or sets the optional ID for your Sign in with Application Public Key Certificate SN(app_cert_sn).
/// <para>https://opendocs.alipay.com/support/01raux</para>
/// </summary>
public string? ApplicationCertificateSnKeyId { get; set; }

/// <summary>
/// Gets or sets the optional ID for your Sign in with Alipay Root Certificate SN.
/// <para>https://opendocs.alipay.com/support/01rauy</para>
/// </summary>
public string? RootCertificateSnKeyId { get; set; }

/// <summary>
/// Gets or sets an optional delegate to get the client's public key which is passed
/// the value of the <see cref="ApplicationCertificateSnKeyId"/> or <see cref="RootCertificateSnKeyId"/> property and the <see cref="CancellationToken"/>
/// associated with the current HTTP request.
/// </summary>
/// <remarks>
/// The public key should be in PKCS #8 (<c>.p8</c>) format.
/// </remarks>
public Func<string, CancellationToken, Task<ReadOnlyMemory<char>>>? PublicKey { get; set; }

/// <inheritdoc />
public override void Validate()
{
base.Validate();

if (UseCertificateSignatures)
{
if (string.IsNullOrEmpty(ApplicationCertificateSnKeyId))
{
throw new ArgumentException($"The '{nameof(ApplicationCertificateSnKeyId)}' option must be provided if the '{nameof(UseCertificateSignatures)}' option is set to true.", nameof(ApplicationCertificateSnKeyId));
}

if (string.IsNullOrEmpty(RootCertificateSnKeyId))
{
throw new ArgumentException($"The '{nameof(RootCertificateSnKeyId)}' option must be provided if the '{nameof(UseCertificateSignatures)}' option is set to true.", nameof(RootCertificateSnKeyId));
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
* See https://github.com/aspnet-contrib/AspNet.Security.OAuth.Providers
* for more information concerning the license and the contributors participating to this project.
*/

using AspNet.Security.OAuth.Alipay;
using Microsoft.Extensions.FileProviders;

namespace Microsoft.Extensions.DependencyInjection;

/// <summary>
/// Extension methods to configure Sign in with Alipay authentication capabilities for an HTTP application pipeline.
/// </summary>
public static class AlipayAuthenticationOptionsExtensions
{
/// <summary>
/// Configures the application to use a specified public key to generate a client secret for the provider when using certificate signatures.
/// </summary>
/// <param name="options">The Alipay authentication options to configure.</param>
/// <param name="publicKeyFile">
/// A delegate to a method to return the <see cref="IFileInfo"/> for the public
/// key which is passed the value of <see cref="AlipayAuthenticationOptions.ApplicationCertificateSnKeyId"/> or <see cref="AlipayAuthenticationOptions.RootCertificateSnKeyId"/>.
/// </param>
/// <returns>
/// The value of the <paramref name="options"/> argument.
/// </returns>
public static AlipayAuthenticationOptions UsePublicKey(
[NotNull] this AlipayAuthenticationOptions options,
[NotNull] Func<string, IFileInfo> publicKeyFile)
{
options.UseCertificateSignatures = true;
options.PublicKey = async (keyId, cancellationToken) =>
{
var fileInfo = publicKeyFile(keyId);

using var stream = fileInfo.CreateReadStream();
using var reader = new StreamReader(stream);

return (await reader.ReadToEndAsync(cancellationToken)).AsMemory();
};

return options;
}
}
76 changes: 76 additions & 0 deletions src/AspNet.Security.OAuth.Alipay/AlipayCertificationUtil.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
* See https://github.com/aspnet-contrib/AspNet.Security.OAuth.Providers
* for more information concerning the license and the contributors participating to this project.
*/

using System.Globalization;
using System.Numerics;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;

namespace AspNet.Security.OAuth.Alipay;

/// <summary>
/// Based on https://github.com/alipay/alipay-sdk-net-all/blob/b482d75d322e740760f9230d2a3859090af642a7/v2/AlipaySDKNet.Standard/Util/AntCertificationUtil.cs.
/// </summary>
internal static class AlipayCertificationUtil
{
public static string GetCertSN(ReadOnlySpan<char> certPem)
{
using var cert = X509Certificate2.CreateFromPem(certPem);
return GetCertSN(cert);
}

private static string GetCertSN(X509Certificate2 cert)
{
var issuerDN = cert.Issuer.Replace(", ", ",", StringComparison.Ordinal);
var serialNumber = new BigInteger(cert.GetSerialNumber()).ToString(CultureInfo.InvariantCulture);

if (issuerDN.StartsWith("CN", StringComparison.InvariantCulture))
{
return CalculateMd5(issuerDN + serialNumber);
}

var attributes = issuerDN.Split(',');
Array.Reverse(attributes);
return CalculateMd5(string.Join(',', attributes) + serialNumber);
}

public static string GetRootCertSN(ReadOnlySpan<char> certPem, string signType = "RSA2")
{
var certificates = new X509Certificate2Collection();
certificates.ImportFromPem(certPem);
var rootCertSN = string.Join('_', GetRootCertSN(certificates, signType));
return rootCertSN;
}

private static IEnumerable<string> GetRootCertSN(X509Certificate2Collection certificates, string signType)
{
foreach (X509Certificate2 cert in certificates)
{
var signatureAlgorithm = cert.SignatureAlgorithm.Value;
if (signatureAlgorithm != null)
{
if ((signType.StartsWith("RSA", StringComparison.OrdinalIgnoreCase) &&
signatureAlgorithm.StartsWith("1.2.840.113549.1.1", StringComparison.OrdinalIgnoreCase)) ||
(signType.StartsWith("SM2", StringComparison.OrdinalIgnoreCase) &&
signatureAlgorithm.StartsWith("1.2.156.10197.1.501", StringComparison.OrdinalIgnoreCase)))
{
yield return GetCertSN(cert);
}
}
}
}

private static string CalculateMd5(string s)
{
var buffer = Encoding.UTF8.GetBytes(s);
Span<byte> hash = stackalloc byte[MD5.HashSizeInBytes];
#pragma warning disable CA5351
MD5.HashData(buffer, hash);
#pragma warning restore CA5351
return Convert.ToHexStringLower(hash);
}
}