在构建新的内部解决方案(使用 ASP.NET Core MVC)时,我们的策略是使用我们的 Active Directory(又名 Entra)为用户进行身份验证。然后,这个新应用程序收集数据,然后使用其 API/SDK 生成 DocuSign 对象。这是我第一次需要通过 SDK 调用 API,我对这些机制有点困惑。我想遵循最佳安全实践。DocuSign 建议在用户在场时使用机密授权代码授予,并提供示例。但是,我认为修改 Program.cs 的示例(示例显示使用旧 .NET 版本的 Startup.cs)与 Entra 身份验证结合使用时不起作用。
简而言之,这个解决方案应该这样工作:
- 所有用户均通过 Entra ID (Azure AD) 进行身份验证。(参见黄色突出显示)
- 一旦通过身份验证,用户可以执行可能需要使用 OAuth 2.0 授权码流调用 DocuSign API 的操作。
我的问题(更新后,仍然想了解前两个问题):
我相信(但不是 100% 确定)Entra 不使用 cookie 身份验证。我们不应该改变这一点。
是否可以像我使用 所述那样在单个应用程序中进行多次身份验证
Program.cs
AddAuthentication()
? 的目的是什么EnableTokenAcquisitionToCallDownstreamApi()
?是使用用户的凭据来获取这些凭据允许的其他数据吗?(已更新回答 - 找到了另一种方法来实现这一点)我认为我应该在 Program.cs 中尝试
AddAuthentication()
,而不是创建 HttpClient 请求,在控制器中编写代码以获取访问令牌,然后以某种方式持久化它以调用其他 DocuSign API。我以为我会找到这样的例子,但我主要看到使用 JWT 令牌进行 api 到 api 调用的例子。对吗?答案:是的
更新:最终,从 Program.cs 中删除了第二个 AddAuthentication():
// Add Entra authentication
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"));
//pulled out all the other AddAuth, AddOauth...
我使用我的控制器来调用 Docusign SDK。底部有一个文档,其中包含可用的类和方法:https://developers.docusign.com/platform/auth/confidential-authcode-get-token/
在我的控制器层中使用了 DocuSignClient.cs 3 种方法。 (在此处查看其类代码https://github.com/docusign/docusign-esign-csharp-client/blob/master/sdk/src/DocuSign.eSign/Client/DocuSignClient.cs )
GetAuthorizationUri(), GenerateAccessToken(),GetUserInfo()
成功了。也许 DocuSign 会提供使用这些的示例和样本,我自己没有找到,但它相当简单。它不在他们的 QuickStart 代码中。
原文:当我尝试示例代码(下面的片段)时,我成功地通过了 Entra 的身份验证,当我使用 DocuSign 身份验证导航到控制器时,系统会提示我输入 DocuSign 凭据。一切都很好。在 中设置断点后OnCreatingTicket
,我完成了身份验证的这一部分。但是,在此之后,我得到了下面的异常。所有关于它的研究表明我需要在我的默认身份验证方案中启用 cookie。我不想这样做。
例外:
The authentication handler registered for scheme 'OpenIdConnect' is 'OpenIdConnectHandler' which cannot be used for SignInAsync. The registered sign-in schemes are: Cookies.
Microsoft.AspNetCore.Authentication.AuthenticationService.SignInAsync(HttpContext context, string scheme, ClaimsPrincipal principal, AuthenticationProperties properties)
Microsoft.AspNetCore.Authentication.RemoteAuthenticationHandler<TOptions>.HandleRequestAsync()
Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context)
var builder = WebApplication.CreateBuilder(args);
// Add Entra authentication
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"));
// Add DocuSign authentication
builder.Services.AddAuthentication(options =>
{
options.DefaultSignOutScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = "DocuSign";
})
.AddCookie()
.AddOAuth("DocuSign", options =>
{
options.ClientId = this.Configuration["DocuSign:ClientId"];
options.ClientSecret = this.Configuration["DocuSign:ClientSecret"];
options.CallbackPath = new PathString("/ds/callback");
options.AuthorizationEndpoint = this.Configuration["DocuSign:AuthorizationEndpoint"];
options.TokenEndpoint = this.Configuration["DocuSign:TokenEndpoint"];
options.UserInformationEndpoint = this.Configuration["DocuSign:UserInformationEndpoint"];
options.Events = new OAuthEvents
{
OnCreatingTicket = async context =>
{
var request = new HttpRequestMessage(HttpMethod.Get, context.Options.UserInformationEndpoint);
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", context.AccessToken);
var response = await context.Backchannel.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, context.HttpContext.RequestAborted);
response.EnsureSuccessStatusCode();
var user = JObject.Parse(await response.Content.ReadAsStringAsync());
user.Add("access_token", context.AccessToken);
user.Add("refresh_token", context.RefreshToken);
user.Add("expires_in", DateTime.Now.Add(context.ExpiresIn.Value).ToString());
using (JsonDocument payload = JsonDocument.Parse(user.ToString()))
{
context.RunClaimActions(payload.RootElement);
}
}
//lines 151 - 244 from https://github.com/docusign/code-examples-csharp/blob/master/launcher-csharp/Startup.cs
// Add controllers and Razor pages with Auth
builder.Services.AddControllersWithViews(options =>
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticateUser()
.Build();
options.Filters.Add(new AuthorizeFilter(policy));
}).AddMicrosoftIdentityUI();
—-----------------
[Authorize(AuthenticationSchemes = "DocuSign")]
public IActionResult Login(string authType = "CodeGrant", string returnUrl = "/")
{
if (authType == "CodeGrant")
{
return Challenge(new AuthenticationProperties() { RedirectUri = returnUrl });
}
}
以下是我用作指导的 DocuSign 文档:https://developers.docusign.com/docs/esign-rest-api/sdks/csharp/auth/
如果您的用户在使用您的“内部解决方案”(您的“应用程序”)时始终在线,那么最好设置您的 Docusign 帐户以使用您的 Entra/AD 系统作为 SSO 身份验证。这将通过浏览器使用 OAuth 授权码流程。
Cookies 用于创建/维护 Docusign 的登录会话。
由于您的应用将存储/使用访问令牌,因此即使没有 cookie 也可能有效。试试吧。
如果您的应用有时/总是在用户不在场的情况下使用,那么您可以使用 OAuth JWT 流程。在这种情况下,您的应用会(通过 Entra/AD)检查发起操作的用户是否经过身份验证(以及他们是谁)。然后,您的应用使用此信息通过 JWT OAuth 流程获取与用户关联的访问令牌来模拟用户。(浏览器不用于 JWT OAuth。)
即使用户始终在场,某些开发人员也会使用模拟/JWT 模式。这样做会增加额外的维护/配置复杂性,因为您的管理员需要维护授权 Entra/AD 用户和 Docusign 中定义的匹配用户之间的映射表。
额外
Docusign 大约 8 年前开始使用 OAuth JWT 模拟。从那时起,您有两种 JWT 模拟选项。要模拟某人,您现在需要他们的 Docusign UserID(可以查找和缓存)。在使用 OAuth 之前,您只需提供他们的电子邮件即可“扮演”他们。
使用“服务用户帐户”
例如,创建一个名为“HR”的 Docusign 用户。您的应用始终会获取访问令牌来模拟 HR 用户。这意味着您的应用发送的所有信封都将由 HR 用户发送,属于该用户,等等。
缺点是用户 1 可能能够看到用户 2 发送的信封,因为在 Docusign 级别,这两个信封都是由同一个用户(HR)发送的。
不要向服务用户帐户授予管理员权限!如果您的应用只是发送信封,则服务用户无需成为管理员。
冒充个人用户账户
这通常是最好的策略,因为它允许应用程序发送的每个用户的信封都从用户的 Docusign 帐户发送。如果授权 (AD) 系统提供了用户的电子邮件地址,则应用程序可以:
从电子邮件中查找用户的 Docusign UserID。(以管理员用户身份执行此操作。使用 JWT 创建管理员的访问令牌。然后查找用户以获取用户 ID。)
通过 JWT Auth 模拟用户。
电子邮件到用户 ID 的映射不会被改变,因此应用程序可以缓存该映射。(假设所有用户都有自己的 Docusign 用户帐户。)