【问题标题】:How to setup custom Oauth 2 in .Net Core如何在 .Net Core 中设置自定义 Oauth 2
【发布时间】:2022-01-15 07:13:34
【问题描述】:

请考虑 program.cs 中的以下代码:

var builder = WebApplication.CreateBuilder(args);

//omit some builder settings

builder.Services.AddAuthentication(options =>
{
    options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie(options =>
{
    options.LoginPath = "/signin";
    options.LogoutPath = "/signout";
})
.AddOAuth("Discord", options =>
{
    options.AuthorizationEndpoint = "https://discord.com/api/oauth2/authorize";
    options.Scope.Add("identify");
    options.Scope.Add("email");

    options.CallbackPath = new PathString("/Account/LoginCallback");

    options.ClientId = "some id";
    options.ClientSecret = "some secret";

    options.TokenEndpoint = "https://discord.com/api/oauth2/token";
    options.UserInformationEndpoint = "https://discord.com/api/users/@me";

    options.ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "id");
    options.ClaimActions.MapJsonKey(ClaimTypes.Name, "username");
    options.ClaimActions.MapJsonKey(ClaimTypes.Email, "email");
    options.ClaimActions.MapCustomJson("urn:discord:avatar:url", user =>
        string.Format(
            CultureInfo.InvariantCulture,
            "https://cdn.discordapp.com/avatars/{0}/{1}.{2}",
            user.GetString("id"),
            user.GetString("avatar"),
            user.GetString("avatar")!.StartsWith("a_") ? "gif" : "png"));

    options.AccessDeniedPath = "/Account/LoginFailed";

    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);

            var user = JsonDocument.Parse(await response.Content.ReadAsStringAsync()).RootElement;

            context.RunClaimActions(user);
        }
    };
});

以下内容在我的帐户控制器中:

[HttpGet("~/signin")]
public async Task<IActionResult> SignIn() => View("SignIn", await HttpContext.GetExternalProvidersAsync());

[HttpPost("~/signin")]
public async Task<IActionResult> SignIn([FromForm] string provider)
{
    if (string.IsNullOrWhiteSpace(provider))
    {
        return BadRequest();
    }

    if (!await HttpContext.IsProviderSupportedAsync(provider))
    {
        return BadRequest();
    }

    return Challenge(new AuthenticationProperties {RedirectUri = "/Account/LoginCallback" }, provider);
}

[HttpGet("~/signout")]
[HttpPost("~/signout")]
public IActionResult SignOutCurrentUser()
{
    return SignOut(new AuthenticationProperties {RedirectUri = "/"},
        CookieAuthenticationDefaults.AuthenticationScheme);
}

[HttpGet]
public IActionResult LoginCallback()
{
    return RedirectToAction("Index", "Home");
}

上面的代码有效。会发生以下情况:

  1. 我点击 LoginViaDiscord 按钮
  2. SignIn post 方法被调用
  3. 我被重定向到不和谐
  4. 我通过 discord 登录
  5. 我被重定向回 LoginCallback

当我查看网站时,我已完全登录。问题是我跳过了记录用户并创建帐户的整个数据库步骤。

当我使用 MVC 5 进行类似的身份验证时,我将有一个回调操作方法,并且该回调方法将完成所有设置。创建用户,围绕用户执行逻辑,然后登录。但是,使用此代码,我已经完全登录,并且没有涉及数据库步骤。

我的问题是当挑战返回到 LoginCallback 时如何正确完成与上述相同但不登录的操作,以及如何从 LoginCallback 操作中的不和谐中检索信息?我想在允许用户登录之前对用户执行一些逻辑,并且我还想确保在数据库中创建他们的帐户。

【问题讨论】:

    标签: c# asp.net-core oauth-2.0 discord asp.net-core-6.0


    【解决方案1】:

    我的第一个答案是错误的。

    您应该在AddOAuth 调用中使用options.Events 属性。

    Property Description
    OnAccessDenied Invoked when an access denied error was returned by the remote server.
    OnCreatingTicket Gets or sets the function that is invoked when the CreatingTicket method is invoked.
    OnRedirectToAuthorizationEndpoint Gets or sets the delegate that is invoked when the RedirectToAuthorizationEndpoint method is invoked.
    OnRemoteFailure Invoked when there is a remote failure.
    OnTicketReceived Invoked after the remote ticket has been received.

    最有趣的是OnTicketReceived

    在这里你可以检查用户和禁止登录等...

    OnTicketReceived = async context =>
    {
        ClaimsIdentity? identity = context.Principal!.Identity as ClaimsIdentity;
        string? userId = identity!.Claims.FirstOrDefault(x => x.Type == ClaimTypes.NameIdentifier)?.Value;
        //TODO make some logic here. For exapmle:
        string[] allowedUsers = new[] { "931350938345169620", "93123250938345169620", "896089335026528327" };
        if (allowedUsers.Contains(userId))
        {
            context.Success();
        }
        else
        {
            //Anonimously accessible page
            context.Response.Redirect("/Home/AccessDenied");
            context.Fail("You are not allowed");
            context.HandleResponse();
        }
    },
    

    UPD1、UPD2:

    如果你想将逻辑移动到支持依赖注入的单独类中,那么你可以这样做:

    
    public interface ITicketManager
    {
        Task HandleTicket(TicketReceivedContext context);
    }
    
    public class TicketManager : ITicketManager
    {
        public TicketManager(/*Inject here anything you want*/)
        {
            
        }
        
        public async Task HandleTicket(TicketReceivedContext context)
        {
            ClaimsIdentity? identity = context.Principal!.Identity as ClaimsIdentity;
            string? userId = identity!.Claims.FirstOrDefault(x => x.Type == ClaimTypes.NameIdentifier)?.Value;
            //TODO make some logic here. For exapmle:
            string[] disallowedUsers = new[] { "9313509382345169620", "931232509383451696220", "8960892335026528327" };
            if (!disallowedUsers.Contains(userId))
            {
                context.Success();
            }
            else
            {
                //Anonimously accessible page
                context.Response.Redirect("/Home/AccessDenied");
                context.Fail("You are not allowed");
                context.HandleResponse();
            }
        }
    }
    

    然后在DI中设置,从options.Events调用

    builder.Services.AddScoped<ITicketManager, TicketManager>();
    
    ...
    
    builder.Services.AddAuthentication(options =>
        {
            ...
        })
        .AddCookie(options =>
        {
            ...
        })
        .AddOAuth("Discord", options =>
        {
            ...
           
            options.Events = new OAuthEvents
            {
                ...
                
                OnTicketReceived = async context =>
                {
                    ITicketManager manager = 
                        context.HttpContext.RequestServices
                            .GetRequiredService<ITicketManager>();
                    await manager.HandleTicket(context);
                },
            };
        });
    

    【讨论】:

    • 这意味着我必须在 program.cs 中执行我的帐户逻辑。这对我来说是一个可怕的设计。那么有没有办法将其分开并实现适当的层并在帐户控制器中完成?
    • @Bagzli 请看我更新的答案,你可以使用 DI 来创造一些魔法。
    • 我觉得我对您在此处解释的内容有深刻的理解,但以这种方式设置它仍然感觉像是一个糟糕的设计。谢谢你的回答,如果我找不到更符合我更喜欢的设计的东西,肯定会考虑这样做。
    猜你喜欢
    • 1970-01-01
    • 2019-11-12
    • 2018-10-17
    • 1970-01-01
    • 1970-01-01
    • 2018-06-25
    • 2016-09-30
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多