diff --git a/src/AspNet.Security.OAuth.Alipay/AlipayAuthenticationConstants.cs b/src/AspNet.Security.OAuth.Alipay/AlipayAuthenticationConstants.cs
index d6188fc4f..35ccb8b90 100644
--- a/src/AspNet.Security.OAuth.Alipay/AlipayAuthenticationConstants.cs
+++ b/src/AspNet.Security.OAuth.Alipay/AlipayAuthenticationConstants.cs
@@ -25,5 +25,18 @@ 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.
+ /// See https://opendocs.alipay.com/common/0ai736
+ ///
+ [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";
}
}
diff --git a/src/AspNet.Security.OAuth.Alipay/AlipayAuthenticationHandler.cs b/src/AspNet.Security.OAuth.Alipay/AlipayAuthenticationHandler.cs
index b33da5630..463876e6f 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.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 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..33e89fbd3 100644
--- a/src/AspNet.Security.OAuth.Alipay/AlipayAuthenticationOptions.cs
+++ b/src/AspNet.Security.OAuth.Alipay/AlipayAuthenticationOptions.cs
@@ -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
+ }
+
+ ///
+ /// 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 public key which is passed
+ /// the value of the or property and the
+ /// associated with the current HTTP request.
+ ///
+ ///
+ /// The public key should be in PKCS #8 (.p8) format.
+ ///
+ public Func>>? PublicKey { 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..908f49304
--- /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 public key to generate a client secret for the provider when using certificate signatures.
+ ///
+ /// The Alipay authentication options to configure.
+ ///
+ /// A delegate to a method to return the for the public
+ /// key which is passed the value of or .
+ ///
+ ///
+ /// The value of the argument.
+ ///
+ public static AlipayAuthenticationOptions UsePublicKey(
+ [NotNull] this AlipayAuthenticationOptions options,
+ [NotNull] Func 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;
+ }
+}
diff --git a/src/AspNet.Security.OAuth.Alipay/AlipayCertificationUtil.cs b/src/AspNet.Security.OAuth.Alipay/AlipayCertificationUtil.cs
new file mode 100644
index 000000000..8bb9a2354
--- /dev/null
+++ b/src/AspNet.Security.OAuth.Alipay/AlipayCertificationUtil.cs
@@ -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;
+
+///
+/// Based on 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 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 certPem, string signType = "RSA2")
+ {
+ var certificates = new X509Certificate2Collection();
+ certificates.ImportFromPem(certPem);
+ var rootCertSN = string.Join('_', GetRootCertSN(certificates, signType));
+ return rootCertSN;
+ }
+
+ private static IEnumerable 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 hash = stackalloc byte[MD5.HashSizeInBytes];
+#pragma warning disable CA5351
+ MD5.HashData(buffer, hash);
+#pragma warning restore CA5351
+ return Convert.ToHexStringLower(hash);
+ }
+}