diff --git a/src/AspNet.Security.OAuth.Alipay/AlipayAuthenticationConstants.cs b/src/AspNet.Security.OAuth.Alipay/AlipayAuthenticationConstants.cs
index d6188fc4f..67122afe5 100644
--- a/src/AspNet.Security.OAuth.Alipay/AlipayAuthenticationConstants.cs
+++ b/src/AspNet.Security.OAuth.Alipay/AlipayAuthenticationConstants.cs
@@ -25,5 +25,16 @@ public static class Claims
/// The user's gender. F: Female; M: Male.
///
public const string Gender = "urn:alipay:gender";
+
+ ///
+ /// OpenID is the unique identifier for Alipay users at the application level.
+ /// See https://opendocs.alipay.com/mini/0ai2i6
+ ///
+ public const string OpenId = "urn:alipay:open_id";
+
+ ///
+ /// The internal identifier for Alipay users will no longer be independently available going forward and will be replaced by OpenID.
+ ///
+ public const string UserId = "urn:alipay:user_id";
}
}
diff --git a/src/AspNet.Security.OAuth.Alipay/AlipayAuthenticationHandler.cs b/src/AspNet.Security.OAuth.Alipay/AlipayAuthenticationHandler.cs
index b33da5630..70893e103 100644
--- a/src/AspNet.Security.OAuth.Alipay/AlipayAuthenticationHandler.cs
+++ b/src/AspNet.Security.OAuth.Alipay/AlipayAuthenticationHandler.cs
@@ -44,6 +44,21 @@ protected override Task HandleRemoteAuthenticateAsync()
return base.HandleRemoteAuthenticateAsync();
}
+ private const string SignType = "RSA2";
+
+ private async Task AddCertificateSignatureParametersAsync(SortedDictionary parameters)
+ {
+ ArgumentNullException.ThrowIfNull(Options.PrivateKey);
+ ArgumentNullException.ThrowIfNull(Options.ApplicationCertificateSnKeyId);
+ ArgumentNullException.ThrowIfNull(Options.RootCertificateSnKeyId);
+
+ var app_cert_sn = await Options.PrivateKey(Options.ApplicationCertificateSnKeyId, Context.RequestAborted);
+ var alipay_root_cert_sn = await Options.PrivateKey(Options.RootCertificateSnKeyId, Context.RequestAborted);
+
+ parameters["app_cert_sn"] = AlipayCertificationUtil.GetCertSN(app_cert_sn.Span);
+ parameters["alipay_root_cert_sn"] = AlipayCertificationUtil.GetRootCertSN(alipay_root_cert_sn.Span, SignType);
+ }
+
protected override async Task ExchangeCodeAsync([NotNull] OAuthCodeExchangeContext context)
{
// See https://opendocs.alipay.com/apis/api_9/alipay.system.oauth.token for details.
@@ -55,10 +70,16 @@ protected override async Task 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
@@ -103,10 +124,16 @@ protected override async Task 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);
diff --git a/src/AspNet.Security.OAuth.Alipay/AlipayAuthenticationOptions.cs b/src/AspNet.Security.OAuth.Alipay/AlipayAuthenticationOptions.cs
index 08677ddd5..e61e84851 100644
--- a/src/AspNet.Security.OAuth.Alipay/AlipayAuthenticationOptions.cs
+++ b/src/AspNet.Security.OAuth.Alipay/AlipayAuthenticationOptions.cs
@@ -29,5 +29,54 @@ public AlipayAuthenticationOptions()
ClaimActions.MapJsonKey(Claims.Gender, "gender");
ClaimActions.MapJsonKey(Claims.Nickname, "nick_name");
ClaimActions.MapJsonKey(Claims.Province, "province");
+ ClaimActions.MapJsonKey(Claims.OpenId, "open_id");
+ ClaimActions.MapJsonKey(Claims.UserId, "user_id");
+ }
+
+ ///
+ /// Gets or sets a value indicating whether to use certificate mode for signing calls.
+ /// https://opendocs.alipay.com/common/057k53?pathHash=e18d6f77#%E8%AF%81%E4%B9%A6%E6%A8%A1%E5%BC%8F
+ ///
+ public bool UseCertificateSignatures { get; set; }
+
+ ///
+ /// Gets or sets the optional ID for your Sign in with Application Public Key Certificate SN(app_cert_sn).
+ /// https://opendocs.alipay.com/support/01raux
+ ///
+ public string? ApplicationCertificateSnKeyId { get; set; }
+
+ ///
+ /// Gets or sets the optional ID for your Sign in with Alipay Root Certificate SN.
+ /// https://opendocs.alipay.com/support/01rauy
+ ///
+ public string? RootCertificateSnKeyId { get; set; }
+
+ ///
+ /// Gets or sets an optional delegate to get the client's private key which is passed
+ /// the value of the or property and the
+ /// associated with the current HTTP request.
+ ///
+ ///
+ /// The private key should be in PKCS #8 (.p8) format.
+ ///
+ public Func>>? PrivateKey { get; set; }
+
+ ///
+ 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));
+ }
+ }
}
}
diff --git a/src/AspNet.Security.OAuth.Alipay/AlipayAuthenticationOptionsExtensions.cs b/src/AspNet.Security.OAuth.Alipay/AlipayAuthenticationOptionsExtensions.cs
new file mode 100644
index 000000000..2b754f3bc
--- /dev/null
+++ b/src/AspNet.Security.OAuth.Alipay/AlipayAuthenticationOptionsExtensions.cs
@@ -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;
+
+///
+/// Extension methods to configure Sign in with Alipay authentication capabilities for an HTTP application pipeline.
+///
+public static class AlipayAuthenticationOptionsExtensions
+{
+ ///
+ /// Configures the application to use a specified private key to generate a client secret for the provider.
+ ///
+ /// The Apple authentication options to configure.
+ ///
+ /// A delegate to a method to return the for the private
+ /// key which is passed the value of or .
+ ///
+ ///
+ /// The value of the argument.
+ ///
+ public static AlipayAuthenticationOptions UsePrivateKey(
+ [NotNull] this AlipayAuthenticationOptions options,
+ [NotNull] Func privateKeyFile)
+ {
+ options.UseCertificateSignatures = true;
+ options.PrivateKey = async (keyId, cancellationToken) =>
+ {
+ var fileInfo = privateKeyFile(keyId);
+
+ using var stream = fileInfo.CreateReadStream();
+ using var reader = new StreamReader(stream);
+
+ return (await reader.ReadToEndAsync(cancellationToken)).AsMemory();
+ };
+
+ return options;
+ }
+}
diff --git a/src/AspNet.Security.OAuth.Alipay/AlipayCertificationUtil.cs b/src/AspNet.Security.OAuth.Alipay/AlipayCertificationUtil.cs
new file mode 100644
index 000000000..c627280e8
--- /dev/null
+++ b/src/AspNet.Security.OAuth.Alipay/AlipayCertificationUtil.cs
@@ -0,0 +1,105 @@
+/*
+ * 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.Buffers;
+using System.Globalization;
+using System.Numerics;
+using System.Security.Cryptography;
+using System.Security.Cryptography.X509Certificates;
+using System.Text;
+
+namespace AspNet.Security.OAuth.Alipay;
+
+///
+/// https://github.com/alipay/alipay-sdk-net-all/blob/b482d75d322e740760f9230d2a3859090af642a7/v2/AlipaySDKNet.Standard/Util/AntCertificationUtil.cs
+///
+internal static class AlipayCertificationUtil
+{
+ public static string GetCertSN(ReadOnlySpan certContent)
+ {
+ using var cert = X509Certificate2.CreateFromPem(certContent);
+ return GetCertSN(cert);
+ }
+
+ public static string GetCertSN(X509Certificate2 cert)
+ {
+ var issuerDN = cert.Issuer.Replace(", ", ",", StringComparison.InvariantCulture);
+ 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 rootCertContent, string signType = "RSA2")
+ {
+ var rootCertSN = string.Join('_', GetRootCertSNCore(rootCertContent, signType));
+ return rootCertSN;
+ }
+
+ private static IEnumerable GetRootCertSNCore(X509Certificate2Collection x509Certificates, string signType)
+ {
+ foreach (X509Certificate2 cert in x509Certificates)
+ {
+ var signatureAlgorithm = cert.SignatureAlgorithm.Value;
+ if (signatureAlgorithm != null)
+ {
+ if ((signType.StartsWith("RSA", StringComparison.InvariantCultureIgnoreCase) &&
+ signatureAlgorithm.StartsWith("1.2.840.113549.1.1", StringComparison.InvariantCultureIgnoreCase)) ||
+ (signType.StartsWith("SM2", StringComparison.InvariantCultureIgnoreCase) &&
+ signatureAlgorithm.StartsWith("1.2.156.10197.1.501", StringComparison.InvariantCultureIgnoreCase)))
+ {
+ yield return GetCertSN(cert);
+ }
+ }
+ }
+ }
+
+ private static IEnumerable GetRootCertSNCore(ReadOnlySpan rootCertContent, string signType)
+ {
+ X509Certificate2Collection x509Certificates = [];
+ x509Certificates.ImportFromPem(rootCertContent);
+ return GetRootCertSNCore(x509Certificates, signType);
+ }
+
+ ///
+ /// https://github.com/dotnet/runtime/blob/v9.0.8/src/libraries/System.Text.Json/Common/JsonConstants.cs#L12
+ ///
+ private const int StackallocByteThreshold = 256;
+
+ private static string CalculateMd5(ReadOnlySpan chars)
+ {
+ var lenU8 = Encoding.UTF8.GetMaxByteCount(chars.Length);
+ byte[]? array = null;
+ Span bytes = lenU8 <= StackallocByteThreshold ?
+ stackalloc byte[StackallocByteThreshold] :
+ (array = ArrayPool.Shared.Rent(lenU8));
+ try
+ {
+ Encoding.UTF8.TryGetBytes(chars, bytes, out var bytesWritten);
+ bytes = bytes[..bytesWritten];
+
+ Span hash = stackalloc byte[MD5.HashSizeInBytes];
+#pragma warning disable CA5351
+ MD5.HashData(bytes, hash);
+#pragma warning restore CA5351
+
+ return Convert.ToHexStringLower(hash);
+ }
+ finally
+ {
+ if (array != null)
+ {
+ ArrayPool.Shared.Return(array);
+ }
+ }
+ }
+}