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
11 changes: 11 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,16 @@ 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.
/// </summary>
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.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<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
49 changes: 49 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,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()
{
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.
/// </summary>
/// <param name="options">The Apple authentication options to configure.</param>
/// <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;
}
}
105 changes: 105 additions & 0 deletions src/AspNet.Security.OAuth.Alipay/AlipayCertificationUtil.cs
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
/// </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)
{
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<char> rootCertContent, string signType = "RSA2")
{
var rootCertSN = string.Join('_', GetRootCertSNCore(rootCertContent, signType));
return rootCertSN;
}

private static IEnumerable<string> 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<string> GetRootCertSNCore(ReadOnlySpan<char> rootCertContent, string signType)
{
X509Certificate2Collection x509Certificates = [];
x509Certificates.ImportFromPem(rootCertContent);
return GetRootCertSNCore(x509Certificates, signType);
}

/// <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)
{
var lenU8 = Encoding.UTF8.GetMaxByteCount(chars.Length);
byte[]? array = null;
Span<byte> bytes = lenU8 <= StackallocByteThreshold ?
stackalloc byte[StackallocByteThreshold] :
(array = ArrayPool<byte>.Shared.Rent(lenU8));
try
{
Encoding.UTF8.TryGetBytes(chars, bytes, out var bytesWritten);
bytes = bytes[..bytesWritten];

Span<byte> 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<byte>.Shared.Return(array);
}
}
}
}