首页 > 解决方案 > Oauth2 令牌,来自 MVC 的 JS spa 的句柄

问题描述

关于 OAuth

从我的 SPA 中,我可以执行 window.open 并将用户重定向到登录页面,注意:必须是一个新窗口,因为 xframeoptions 设置为拒绝。

我如何返回令牌并与 SPA 关联,因为它们位于单独的窗口/会话中?

我正在查看的选项

使用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>

感谢您的关注

标签: c#asp.netasp.net-mvcoauth-2.0single-page-application

解决方案


似乎(如果我弄错了,请纠正我)主要问题是启动 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 页面,执行反向通道请求以获取令牌,使用身份为用户创建我自己的个人资料并使用委托给我的服务的授权代表用户执行请求的信息。

让我知道这是否需要更多解释,但希望它是有用的。


推荐阅读