Skip to content
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

Add support for Alipay certificate signing and openid. #968

Open
wants to merge 2 commits into
base: dev
Choose a base branch
from
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 of Alipay users in the application dimension.
/// See https://opendocs.alipay.com/mini/0ai2i6
/// </summary>
public const string OpenId = "urn:alipay:open_id";

/// <summary>
/// Alipay user system internal identifier, will no longer be independently open in the future, and will be replaced by OpenID.
/// </summary>
public const string UserId = "urn:alipay:user_id";
}
}
14 changes: 12 additions & 2 deletions src/AspNet.Security.OAuth.Alipay/AlipayAuthenticationHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ protected override async Task<OAuthTokenResponse> ExchangeCodeAsync([NotNull] OA
["timestamp"] = TimeProvider.GetUtcNow().ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture),
["version"] = "1.0",
};
if (Options.EnableCertSignature)
{
tokenRequestParameters["app_cert_sn"] = Options.AppCertSN;
tokenRequestParameters["alipay_root_cert_sn"] = Options.RootCertSN;
}

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

// PKCE https://tools.ietf.org/html/rfc7636#section-4.5, see BuildChallengeUrl
Expand Down Expand Up @@ -107,6 +113,12 @@ protected override async Task<AuthenticationTicket> CreateTicketAsync(
["timestamp"] = TimeProvider.GetUtcNow().ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture),
["version"] = "1.0",
};
if (Options.EnableCertSignature)
{
parameters["app_cert_sn"] = Options.AppCertSN;
parameters["alipay_root_cert_sn"] = Options.RootCertSN;
}

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

var address = QueryHelpers.AddQueryString(Options.UserInformationEndpoint, parameters);
Expand Down Expand Up @@ -134,8 +146,6 @@ protected override async Task<AuthenticationTicket> CreateTicketAsync(
throw new AuthenticationFailureException($"An error (Code:{code}) occurred while retrieving user information.");
}

identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, mainElement.GetString("user_id")!, ClaimValueTypes.String, Options.ClaimsIssuer));

var principal = new ClaimsPrincipal(identity);
var context = new OAuthCreatingTicketContext(principal, properties, Context, Scheme, Options, Backchannel, tokens, mainElement);

Expand Down
41 changes: 41 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,46 @@ 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");
ClaimActions.MapCustomJson(System.Security.Claims.ClaimTypes.NameIdentifier, user => user.GetString(NameIdentifierKey));
}

public override void Validate()
{
base.Validate();

if (!string.IsNullOrEmpty(AppCertSN) && !string.IsNullOrEmpty(RootCertSN))
{
EnableCertSignature = true;
}
else if (!string.IsNullOrEmpty(AppCertPath) && !string.IsNullOrEmpty(RootCertPath))
{
try
{
AppCertSN = AlipayCertificationUtils.GetCertSN(AppCertPath);
RootCertSN = AlipayCertificationUtils.GetRootCertSN(RootCertPath);
EnableCertSignature = true;
}
catch (Exception ex)
{
throw new ArgumentException($"The '{nameof(AppCertPath)}' and '{nameof(RootCertPath)}' options must be set to the correct certificate files.", ex);
}
}
}

/// <summary>
/// See https://opendocs.alipay.com/mini/0ai2i6?pathHash=13dd5946
/// </summary>
public string NameIdentifierKey { get; set; } = "user_id";

public string? AppCertPath { get; set; }

public string? RootCertPath { get; set; }
Comment on lines +65 to +67
Copy link
Member

@martincostello martincostello Oct 30, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't assume that certificates are on the file system - they could (should?) be stored in a secure location, such as the Windows Certificate Manager, or loaded from a remote source such as Azure Key Vault. Take a look at the Apple provider for examples on handling such scenarios.

This example might also help: configuring Sign in With Apple


public bool EnableCertSignature { get; set; }

public string AppCertSN { get; set; } = string.Empty;

public string RootCertSN { get; set; } = string.Empty;
}
55 changes: 55 additions & 0 deletions src/AspNet.Security.OAuth.Alipay/AlipayCertificationUtils.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* 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.Numerics;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;

namespace AspNet.Security.OAuth.Alipay;

/// <summary>
/// This class of code refers to the Alipay official SDK code.
/// See https://github.com/alipay/alipay-sdk-net-all/blob/1b7b73909954b107bddb6476dec68aafcc3f16e9/v2/AlipaySDKNet.Standard/Util/AntCertificationUtil.cs
/// See https://opendocs.alipay.com/common/056zub?pathHash=91c49771
/// </summary>
internal static class AlipayCertificationUtils
{
internal static string GetCertSN([NotNull] string certFilePath)
{
using var cert = X509Certificate.CreateFromCertFile(certFilePath);
return GetCertSN(cert);
}

internal static string GetCertSN([NotNull] X509Certificate cert)
{
var issuerDN = cert.Issuer.Replace(", ", ",", StringComparison.InvariantCulture);
var input = issuerDN + new BigInteger(cert.GetSerialNumber());
#pragma warning disable CA5351 // Do Not Use Broken Cryptographic Algorithms
var certSN = Convert.ToHexString(MD5.HashData(Encoding.UTF8.GetBytes(input))).ToLowerInvariant();
#pragma warning restore CA5351 // Do Not Use Broken Cryptographic Algorithms
return certSN;
}

internal static string GetRootCertSN([NotNull] string rootCertPath, [NotNull] string signType = "RSA2")
{
var certSNs = new List<string>();
var certCollection = new X509Certificate2Collection();
certCollection.ImportFromPemFile(rootCertPath);

foreach (var cert in certCollection)
{
if ((signType.StartsWith("RSA", StringComparison.Ordinal) && cert.SignatureAlgorithm.Value?.StartsWith("1.2.840.113549.1.1", StringComparison.Ordinal) == true) ||
(signType.Equals("SM2", StringComparison.Ordinal) && cert.SignatureAlgorithm.Value?.StartsWith("1.2.156.10197.1.501", StringComparison.Ordinal) == true))
{
certSNs.Add(GetCertSN(cert));
}
}

var rootCertSN = string.Join('_', certSNs);
return rootCertSN;
}
}