Estoy tratando de descubrir cómo configurar un inicio de sesión a través de Discord Oauth2 mientras uso Dapper como mi ORM.
Microsoft tiene una guía aquí que he seguido para configurar todas mis tiendas. De hecho, puedo llamar al método CreateAsync() y se crea un usuario en mi base de datos, por lo que creo que ese lado de las cosas está completamente configurado.
Mis problemas se encuentran dentro del inicio de sesión externo. A continuación encontrará lo que he probado.
Programa.cs:
//omitted code that binds interfaces and classes - this code works and is fully tested. it is not related to problem at hand. builder.Services.AddIdentity<User, Role>() .AddDefaultTokenProviders(); builder.Services.AddAuthentication() .AddCookie(options => { options.LoginPath = "/signin"; options.LogoutPath = "/signout"; }) .AddDiscord(options => { options.ClientId = "some id"; options.ClientSecret = "some secret"; 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")); }); builder.Services.AddRazorPages(); var app = builder.Build(); app.UseDeveloperExceptionPage(); app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseRouting(); app.UseAuthentication(); app.MapControllerRoute("default", "{controller=Home}/{action=Index}/{id?}"); app.Run();Aquí está el código del controlador de cuenta:
public class AccountController : Controller { private readonly ISignInService _signInService; private readonly IUserService _userService; public AccountController(ISignInService signInService, IUserService userService) { _signInService = signInService; _userService = userService; } [HttpGet("~/signin")] public async Task<IActionResult> SignIn() => View("SignIn", await HttpContext.GetExternalProvidersAsync()); [HttpPost("~/signin")] public async Task<IActionResult> SignIn([FromForm] string provider, string returnUrl) { if (string.IsNullOrWhiteSpace(provider)) { return BadRequest(); } if (!await HttpContext.IsProviderSupportedAsync(provider)) { return BadRequest(); } var redirectUrl = Url.Action(nameof(LoginCallback), "Account", new { returnUrl }); var properties = _signInService.ConfigureExternalAuthenticationProperties(provider, redirectUrl, null); properties.Items.Add("XsrfKey", "Test"); return Challenge(properties, provider); } [HttpGet("~/signout")] [HttpPost("~/signout")] public IActionResult SignOutCurrentUser() { return SignOut(new AuthenticationProperties {RedirectUri = "/"}, CookieAuthenticationDefaults.AuthenticationScheme); } //[HttpGet("~/Account/LoginCallback")] [HttpGet] public async Task<IActionResult> LoginCallback(string returnUrl = null, string remoteError = null) { if (remoteError != null) { return RedirectToAction("Index", "Home"); } var info = await _signInService.GetExternalLoginInfoAsync("Test"); if (info == null) { return RedirectToAction("Index", "Home"); } var result = await _signInService.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false, bypassTwoFactor: true); if (result.Succeeded) { return RedirectToLocal(returnUrl); } if (result.IsLockedOut) { return RedirectToAction("Index", "Home"); } else { // If the user does not have an account, then ask the user to create an account. ViewData["ReturnUrl"] = returnUrl; ViewData["LoginProvider"] = info.LoginProvider; var email = info.Principal.FindFirstValue(ClaimTypes.Email); return RedirectToAction("Index", "Home"); } } private IActionResult RedirectToLocal(string returnUrl) { if (Url.IsLocalUrl(returnUrl)) { return Redirect(returnUrl); } else { return RedirectToAction(nameof(HomeController.Index), "Home"); } } }Esto es lo que sucede:
var info = await _signInService.GetExternalLoginInfoAsync("Test"); esa línea siempre es nula.He estado luchando para descubrir lo que he pasado por alto en mi configuración, ya que no tengo ningún error sobre nada.
En primer lugar... Necesitamos echar un vistazo a la implementación del método interno GetExternalLoginInfoAsync dentro de SignInManager.cs y tomar nota de todas las condiciones que podrían llevar a que se devuelva un valor nulo.
Proporcionaré mi respuesta como comentarios dentro del siguiente código:
/// <summary> /// Gets the external login information for the current login, as an asynchronous operation. /// </summary> /// <param name="expectedXsrf">Flag indication whether a Cross Site Request Forgery token was expected in the current request.</param> /// <returns>The task object representing the asynchronous operation containing the <see name="ExternalLoginInfo"/> /// for the sign-in attempt.</returns> public virtual async Task<ExternalLoginInfo> GetExternalLoginInfoAsync(string expectedXsrf = null) { var auth = await Context.AuthenticateAsync(IdentityConstants.ExternalScheme); var items = auth?.Properties?.Items; if (auth?.Principal == null || items == null || !items.ContainsKey(LoginProviderKey)) { // What cases can lead us here? // * The authentication was unsuccessful maybe due to // - Login cancellation // - Project not running on a secured environment (https) // - SignInScheme property of auth options not // equal to IdentityConstants.ExternalScheme return null; } if (expectedXsrf != null) { // It is important to note that XsrfKey is a constant // declared above in this class whose value is "XsrfId". if (!items.ContainsKey(XsrfKey)) { // What cases can lead us here? // * You passed an argument for expectedXsrf but // the initialized key-value pairs does not contain // any key for XsrfKey ("XsrfId"). // In your case the below is wrong: // properties.Items.Add("XsrfKey", "Test"); <= remove // Pass the value as 3rd parameter in // "ConfigureExternalAuthenticationProperties" method call instead // _signInService.ConfigureExternalAuthenticationProperties(provider, redirectUrl, "Test") return null; } var userId = items[XsrfKey] as string; if (userId != expectedXsrf) { // What cases can lead us here? // * The argument passed for expectedXsrf does not // match the value of initialized key-value pair // for XsrfKey ("XsrfId"). // Ensure "Test" should go with "XsrfId" as key // by passing the value as 3rd parameter in // "ConfigureExternalAuthenticationProperties" method call instead. return null; } } var providerKey = auth.Principal.FindFirstValue(ClaimTypes.NameIdentifier); var provider = items[LoginProviderKey] as string; if (providerKey == null || provider == null) { return null; } var providerDisplayName = (await GetExternalAuthenticationSchemesAsync()).FirstOrDefault(p => p.Name == provider)?.DisplayName ?? provider; return new ExternalLoginInfo(auth.Principal, provider, providerKey, providerDisplayName) { AuthenticationTokens = auth.Properties.GetTokens() }; }Entonces, a partir de la revisión del código, estas son algunas de las posibles causas de nulo:
La autenticación no tuvo éxito tal vez debido a
Cancelación de inicio de sesión
El proyecto no se ejecuta en un entorno seguro (https)
La propiedad SignInScheme de las opciones de autenticación en StartUp.cs o appsettings.json no es igual a IdentityConstants.ExternalScheme
Pasó un argumento para expectXsrf pero el initialized key-value pair no contiene ninguna clave para XsrfKey ("XsrfId") .
En tu caso lo siguiente es incorrecto:
propiedades.Items.Add("XsrfKey", "Prueba"); <= elimine esta línea ya que "XsrfKey" es desconocido.
En su lugar, pasa el valor como tercer parámetro en la llamada al método "ConfigureExternalAuthenticationProperties":
_signInService.ConfigureExternalAuthenticationProperties(provider, redirectUrl, "Test");