c# - Oauth2 令牌,来自 MVC 的 JS spa 的句柄
问题描述
关于 OAuth。
- 前端 SPA 反应
- MVC OAuth 后端,将用户签名到 3rd 方提供商,运行良好,返回令牌。
从我的 SPA 中,我可以执行 window.open 并将用户重定向到登录页面,注意:必须是一个新窗口,因为 xframeoptions 设置为拒绝。
我如何返回令牌并与 SPA 关联,因为它们位于单独的窗口/会话中?
我正在查看的选项
- 内容安全策略- 设置调用者的域
- 设置相同的站点 cookie
使用aspnet-contrib/AspNet.Security.OAuth.Providers
启动.cs
public class Startup
{
private const string policyName = "Cors";
public Startup(IConfiguration configuration, IHostEnvironment hostingEnvironment)
{
Configuration = configuration;
HostingEnvironment = hostingEnvironment;
}
public IConfiguration Configuration { get; }
private IHostEnvironment HostingEnvironment { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddRouting();
services.AddCors(opt =>
{
opt.AddPolicy(name: policyName, builder =>
{
builder.AllowAnyOrigin()
.AllowAnyHeader()
.AllowAnyOrigin()
.AllowAnyMethod();
});
});
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie(options =>
{
options.LoginPath = "/signin";
options.LogoutPath = "/signout";
})
.AddGitHub(options =>
{
options.ClientId = Configuration["GitHub:ClientId"];
options.ClientSecret = Configuration["GitHub:ClientSecret"];
options.Scope.Add("user:email");
options.Scope.Add("read:org");
options.Scope.Add("workflow");
options.SaveTokens=true;
});
services.AddMvc();
}
public void Configure(IApplicationBuilder app)
{
if (HostingEnvironment.IsDevelopment())
{
// IdentityModelEventSource.ShowPII = true;
}
// Required to serve files with no extension in the .well-known folder
//var options = new StaticFileOptions()
//{
// ServeUnknownFileTypes = true,
//};
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto,
});
app.UseCors(policyName);
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapDefaultControllerRoute();
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
}
});
认证控制器
public class AuthenticationController : Controller
{
[HttpGet("~/signin")]
public async Task<IActionResult> SignIn() => View("SignIn", await HttpContext.GetExternalProvidersAsync());
[HttpPost("~/signin")]
public async Task<IActionResult> SignIn([FromForm] string provider)
{
// Note: the "provider" parameter corresponds to the external
// authentication provider choosen by the user agent.
if (string.IsNullOrWhiteSpace(provider))
{
return BadRequest();
}
if (!await HttpContext.IsProviderSupportedAsync(provider))
{
return BadRequest();
}
// Instruct the middleware corresponding to the requested external identity
// provider to redirect the user agent to its own authorization endpoint.
// Note: the authenticationScheme parameter must match the value configured in Startup.cs
return Challenge(new AuthenticationProperties { RedirectUri = "/" }, provider);
}
[HttpGet("~/signout")]
[HttpPost("~/signout")]
public IActionResult SignOutCurrentUser()
{
// Instruct the cookies middleware to delete the local cookie created
// when the user agent is redirected from the external identity provider
// after a successful authentication flow (e.g Google or Facebook).
return SignOut(new AuthenticationProperties { RedirectUri = "/" },
CookieAuthenticationDefaults.AuthenticationScheme);
}
}
家庭控制器
public class HomeController : Controller
{
public async Task<IActionResult> IndexAsync()
{
var accessToken = await HttpContext.GetTokenAsync("GitHub", "access_token");
var refreshToken = await HttpContext.GetTokenAsync("GitHub", "refresh_token");
return View();
}
}
主页 (Index.cshtml)
<div class="jumbotron">
@if (User?.Identity?.IsAuthenticated ?? false)
{
<h1>Welcome, @User.Identity.Name</h1>
<p>
@foreach (var claim in Context.User.Claims)
{
<div><code>@claim.Type</code>: <strong>@claim.Value</strong></div>
}
</p>
<a class="btn btn-lg btn-danger" href="/signout?returnUrl=%2F">Sign out</a>
}
else
{
<h1>Welcome, anonymous</h1>
<a class="btn btn-lg btn-success" href="/signin?returnUrl=%2F">Sign in</a>
}
</div>
感谢您的关注
解决方案
似乎(如果我弄错了,请纠正我)主要问题是启动 github auth 的窗口并将该令牌发送回您的站点,这是一个不同的窗口(即弹出窗口的父窗口)。
一种选择是设置您的身份验证请求的redirect_uri
参数,以便第 3 方身份提供者重定向回您网站上的 URL(在弹出窗口内),即您的 ~/signin 端点。这允许您的服务器端获取令牌并执行诸如创建会话、存储 cookie 等操作。在弹出窗口中设置的 cookie 将在原始窗口中对您的站点可用(假设它是同一个域)一次父窗口已刷新。
接下来,一旦弹出窗口被重定向回您的 ~/signin 端点并且您已经创建了会话或存储了 cookie 等,您可能希望关闭该弹出窗口并刷新父窗口,以便它识别 cookie / new会议。您可以通过从 ~/signin 请求(仍在弹出窗口中)返回一个包含以下 JavaScript 的页面来执行此操作:
window.opener.document.location.reload();
// alternatively send the user to an authenticated homepage:
window.opener.document.location.href = '/signed-in-user-homepage';
// and then close the popup
window.close();
我认为这不是您的目标,但为了完整性,如果您希望从 SPA 本身执行 OAuth 身份验证/授权,因此服务器端无法获取令牌,您可能希望让oidc-client之类的 Javascript 库执行繁重的工作。这会启动它自己的窗口来执行身份验证并将令牌交还给调用 SPA 本身。这些令牌对服务器端不可见。
一个真正帮助我弄清楚这一点的资源是 JavaScript 客户端的 IdentityServer4 快速入门,如果这是您的用例,值得一试:
来自 JavaScript 客户端的 OpenID Connect 身份验证快速入门 上述快速入门的示例代码
一旦您在客户端(即在 JavaScript 中)获得了一个令牌,您可以将它传递给服务器端以用于代表用户发出请求,但这不是一个好习惯,因为它是模拟而不是正确委派授权,并且您需要弄清楚访问令牌过期时要做什么(将刷新令牌传递给服务器端有效地允许您的服务器无限期地模拟用户)。
我的偏好是允许服务器端使用 AspNet.Security.OAuth.Providers 执行身份验证码授权流程,将身份验证码接收到 ~/signin 页面,执行反向通道请求以获取令牌,使用身份为用户创建我自己的个人资料并使用委托给我的服务的授权代表用户执行请求的信息。
让我知道这是否需要更多解释,但希望它是有用的。
推荐阅读
- c++ - 此代码显示错误,弹出窗口“调试断言失败”
- .net - 程序作为控制台运行但不作为 Windows 服务运行
- r - dplyr / tidy 方法来过滤基于子字符串的向量?
- javascript - 我在状态中有一个 T 和 Z 格式的数据,React 输入接受 HH:MM ss 格式,所以如何转换
- ios - 在较旧的 iOS 设备上安装 IPA 时出错:无法为 64 位 Mach-O 输入文件找到匹配的拱门
- github - 文件 ios/Flutter/Flutter.framework/Flutter 为 351.71 MB;这超出了 GitHub 的文件大小限制 100.00 MB
- hyperledger-fabric - Hyperledger Fabric 无法创建只有一个组织的通道
- python - 如何使 Django Forms JSON 可序列化
- sql-server - 在 T-SQL 中提取两个破折号之间的数字
- assembly - 如何通过地址有效地测试内存的可缓存性?