-
Notifications
You must be signed in to change notification settings - Fork 551
Add support for Alipay certificate signing #1131
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from 5 commits
acc2fe0
eabb122
e54d133
db212e0
872164d
ab6e026
59de161
c9aae07
13e690e
fe4159d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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"); | ||
| } | ||
|
|
||
| /// <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 private 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 private key should be in PKCS #8 (<c>.p8</c>) format. | ||
| /// </remarks> | ||
| public Func<string, CancellationToken, Task<ReadOnlyMemory<char>>>? PrivateKey { get; set; } | ||
|
|
||
| /// <inheritdoc /> | ||
| public override void Validate() | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shouldn't this also validate that
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| { | ||
| 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 private key to generate a client secret for the provider. | ||
AigioL marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| /// </summary> | ||
| /// <param name="options">The Apple authentication options to configure.</param> | ||
AigioL marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| /// <param name="privateKeyFile"> | ||
| /// A delegate to a method to return the <see cref="IFileInfo"/> for the private | ||
| /// 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 UsePrivateKey( | ||
| [NotNull] this AlipayAuthenticationOptions options, | ||
| [NotNull] Func<string, IFileInfo> 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; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -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; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| /// <summary> | ||||||||||||||||||||
| /// https://github.com/alipay/alipay-sdk-net-all/blob/b482d75d322e740760f9230d2a3859090af642a7/v2/AlipaySDKNet.Standard/Util/AntCertificationUtil.cs | ||||||||||||||||||||
AigioL marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||||||||||||||||
| /// </summary> | ||||||||||||||||||||
| internal static class AlipayCertificationUtil | ||||||||||||||||||||
| { | ||||||||||||||||||||
| public static string GetCertSN(ReadOnlySpan<char> certContent) | ||||||||||||||||||||
|
||||||||||||||||||||
| { | ||||||||||||||||||||
| using var cert = X509Certificate2.CreateFromPem(certContent); | ||||||||||||||||||||
| return GetCertSN(cert); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| public static string GetCertSN(X509Certificate2 cert) | ||||||||||||||||||||
AigioL marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
AigioL marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||||||||||||||||
| { | ||||||||||||||||||||
| var issuerDN = cert.Issuer.Replace(", ", ",", StringComparison.InvariantCulture); | ||||||||||||||||||||
AigioL marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||||||||||||||||
| 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> rootCertContent, string signType = "RSA2") | ||||||||||||||||||||
| { | ||||||||||||||||||||
| var rootCertSN = string.Join('_', GetRootCertSNCore(rootCertContent, signType)); | ||||||||||||||||||||
| return rootCertSN; | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| private static IEnumerable<string> GetRootCertSNCore(X509Certificate2Collection x509Certificates, string signType) | ||||||||||||||||||||
AigioL marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||||||||||||||||
| { | ||||||||||||||||||||
| foreach (X509Certificate2 cert in x509Certificates) | ||||||||||||||||||||
AigioL marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||||||||||||||||
| { | ||||||||||||||||||||
| 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<string> GetRootCertSNCore(ReadOnlySpan<char> rootCertContent, string signType) | ||||||||||||||||||||
| { | ||||||||||||||||||||
| X509Certificate2Collection x509Certificates = []; | ||||||||||||||||||||
| x509Certificates.ImportFromPem(rootCertContent); | ||||||||||||||||||||
| return GetRootCertSNCore(x509Certificates, signType); | ||||||||||||||||||||
AigioL marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| /// <summary> | ||||||||||||||||||||
| /// https://github.com/dotnet/runtime/blob/v9.0.8/src/libraries/System.Text.Json/Common/JsonConstants.cs#L12 | ||||||||||||||||||||
| /// </summary> | ||||||||||||||||||||
| private const int StackallocByteThreshold = 256; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| private static string CalculateMd5(ReadOnlySpan<char> chars) | ||||||||||||||||||||
|
||||||||||||||||||||
| 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); | |
| } |
AigioL marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The return value should be checked.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The MD5 algorithm has a fixed length, so there is no need to check the returned result. If the input array is too short, it will throw an exception.

There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On reflection, if this is going to be deprecated, it would be better not to expose it as a
publicmember. Otherwise it feels like we're adding a new API that's immediately obsolete.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
UserId cannot be modified to
privatebecause applications created before OpenId or legacy code still require it. For newly created applications, OpenId should be used unless a support ticket is submitted to request the platform to enable legacy UserId. I have updated the code and added theObsoleteAttribute.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Adding a new public API that is immediately obsolete serves no useful purpose to the user and adds maintenance burden for us to remove it in the future (which is then itself a breaking change).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My intention was to delete the property entirely, not just to remove the
[Obsolete]attribute.