question

DanijelBokan-7544 avatar image
0 Votes"
DanijelBokan-7544 asked ZhiLv-MSFT answered

External Login

Dear,

I have problem with external login, I had follow tutorial from microsoft docs, but for some reason when I need to post ExternalLoginConfirmation , Principal are null.
But I had pass the login on the . google

Here is my code.
Program.cs
using System.Security.Claims;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OAuth.Claims;
using Microsoft.AspNetCore.Identity;
using Milking.Infrastructure;

 var builder = WebApplication.CreateBuilder(args);
    
 builder.Services.AddInfrastructure(builder.Configuration);
 // Add services to the container.
 builder.Services.AddControllersWithViews();
    
    
    
 builder.Services.AddAuthentication()
     .AddGoogle(options =>
     {
         IConfigurationSection googleAuthSection = builder.Configuration.GetSection("Authentication:Google");
         options.ClientId = googleAuthSection["ClientId"];
         options.ClientSecret = googleAuthSection["ClientSecret"];
    
     })
     .AddMicrosoftAccount(options =>
     {
         IConfigurationSection microsoftAuthSection = builder.Configuration.GetSection("Authentication:Microsoft");
         options.ClientId = microsoftAuthSection["ClientId"];
         options.ClientSecret = microsoftAuthSection["ClientSecret"];
    
     });
    
 var app = builder.Build();
    
 // Configure the HTTP request pipeline.
 if (!app.Environment.IsDevelopment())
 {
     app.UseExceptionHandler("/Home/Error");
     // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
     app.UseHsts();
 }
    
    
 app.UseHttpsRedirection();
 app.UseStaticFiles();
    
 app.UseRouting();
    
 app.UseAuthentication();
 app.UseAuthorization();
    
 app.MapControllerRoute(
     name: "default",
     pattern: "{controller=Home}/{action=Index}/{id?}");
    
 app.Run();

AuthController.cs

 using System.Security.Claims;
 using Microsoft.AspNetCore.Identity;
 using Microsoft.AspNetCore.Mvc;
 using Milking.WebMVC.Models;
    
 namespace Milking.WebMVC.Controllers;
    
 public class AuthController : Controller
 {
     private readonly SignInManager<IdentityUser> _signInManager;
     private readonly UserManager<IdentityUser> _userManager;
    
     public AuthController(SignInManager<IdentityUser> signInManager, UserManager<IdentityUser> userManager)
     {
         _signInManager = signInManager;
         _userManager = userManager;
     }
    
     [HttpGet]
     public IActionResult Login()
     {
         return View();
     }
    
     [HttpPost]
     [ValidateAntiForgeryToken]
     public IActionResult ExternalLogin(string provider)
     {
         var redirectUrl = Url.Action(nameof(ExternalLoginCallback), "Auth");
         var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl);
         return Challenge(properties, provider);
     }
    
     [HttpGet]
     public async Task<IActionResult> ExternalLoginCallback()
     {
         var info = await _signInManager.GetExternalLoginInfoAsync();
         if (info is null)
         {
             //return RedirectToAction(nameof(Login));
             return BadRequest();
         }
         var signInResult = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false, bypassTwoFactor: false);
         if (signInResult.Succeeded)
         {
             return RedirectToAction(nameof(Login));
         }
    
         ViewData["Provider"] = info.LoginProvider;
         var email = info.Principal.FindFirstValue(ClaimTypes.Email);
         return View("ExternalLogin", new ExternalLoginModel { Email = email });
     }
    
     [HttpPost]
     [ValidateAntiForgeryToken]
     public async Task<IActionResult> ExternalLoginConfirmation(ExternalLoginModel model)
     {
         if (!ModelState.IsValid)
             return View(model);
    
         var info = await _signInManager.GetExternalLoginInfoAsync();
         if (info == null)
             return Content("system has error");
    
         var user = await _userManager.FindByEmailAsync(model.Email);
         IdentityResult result = new();
    
         if (user != null)
         {
             var logins = await _userManager.GetLoginsAsync(user);
             if (!logins.Any())
             {
                 result = await _userManager.AddLoginAsync(user, info);
                 if (!result.Succeeded)
                 {
                     ModelState.TryAddModelError(string.Empty, result.Errors.Select(_ => _.Description).FirstOrDefault());
                     return View(nameof(ExternalLogin), model);
                 }
             }
    
             await _signInManager.SignInAsync(user, isPersistent: false);
             return RedirectToAction("Index", "Home");
         }
         else
         {
             model.Principal = info.Principal;
    
             user = new(model.Email)
             {
                 Email = model.Email
             };
    
             result = await _userManager.CreateAsync(user);
             if (result.Succeeded)
             {
                 result = await _userManager.AddLoginAsync(user, info);
                 if (result.Succeeded)
                 {
                     //TODO: Send an email for the email confirmation and add a default role as in the Register action
                     await _signInManager.SignInAsync(user, isPersistent: false);
                     return RedirectToAction("Index", "Home");
                 }
             }
         }
    
         foreach (var error in result.Errors)
         {
             ModelState.TryAddModelError(error.Code, error.Description);
         }
    
         return View(nameof(ExternalLogin), model);
     }
    
 }

And from another layer which is calling AddInfrastructure

 using Microsoft.AspNetCore.Identity;
 using Microsoft.EntityFrameworkCore;
 using Microsoft.Extensions.Configuration;
 using Microsoft.Extensions.DependencyInjection;
 using Milking.Infrastructure.Data;
    
 namespace Milking.Infrastructure;
    
 public static class DependencyInjection
 {
     public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration configuration)
     {
    
         services.AddDbContext<IdentityAppContext>(options =>
         {
             options.UseSqlServer(configuration.GetConnectionString("IdentityConnection"));
         });
    
         services.AddIdentity<IdentityUser, IdentityRole>(options =>
             {
                 options.SignIn.RequireConfirmedEmail = true;
             })
             .AddEntityFrameworkStores<IdentityAppContext>()
             .AddDefaultTokenProviders()
             .AddErrorDescriber<IdentityErrorDescriber>();
    
         return services;
     }
 }



Error picture:
192731-error.png


dotnet-aspnet-core-generaldotnet-aspnet-core-auth
error.png (8.7 KiB)
· 2
5 |1600 characters needed characters left characters exceeded

Up to 10 attachments (including images) can be used with a maximum of 3.0 MiB each and 30.0 MiB total.

Please check the ExternalLoginModel model, it might contain the Principal property and it is required. You could add a new input tag helper in the ExternalLogin page to enter the Principal property. Or you can try to remove the Principal from the ExternalLoginModel model or create a new view model just contains the email.

0 Votes 0 ·

From examples https://code-maze.com/external-identity-provider-aspnet-core-identity/ they have it like I, and their example is working and my not ...
Another think if I populate in ExternalLoginCallback even Principal when come to ExternalLoginConfirmation it is lost. I temporary solution I had found that I create one private field for Principal and populate that one and later in ExternalLoginConfirmation if is null I'm giving him that value. But for me this is not okay and it should be temporary...

0 Votes 0 ·

1 Answer

ZhiLv-MSFT avatar image
0 Votes"
ZhiLv-MSFT answered

Hi @DanijelBokan-7544,

From examples https://code-maze.com/external-identity-provider-aspnet-core-identity/ they have it like I, and their example is working and my not ...
Another think if I populate in ExternalLoginCallback even Principal when come to ExternalLoginConfirmation it is lost. I temporary solution I had found that I create one private field for Principal and populate that one and later in ExternalLoginConfirmation if is null I'm giving him that value. But for me this is not okay and it should be temporary...

The tutorial and the official document are using the default Identity's ExternalLogin page. You could refer the following steps to add the Identity ExternalLogin page:

Right click the project, click Add => New Scaffolded Item...=> Identity => Add

192999-image.png

Then, in the popup window, select the ExternalLogin and select your dbcontext, then click the Add button, like this:

192967-image.png

After that, it will add the Identity page like this:

192938-image.png

Then, you can use the ExternalLogin and refer the following document to achieve the External Login function.

Microsoft Account external login setup with ASP.NET Core

Google external login setup in ASP.NET Core

View or download official sample code


If the answer is the right solution, please click "Accept Answer" and kindly upvote it. If you have extra questions about this answer, please click "Comment".
Note: Please follow the steps in our documentation to enable e-mail notifications if you want to receive the related email notification for this thread.

Best regards,
Dillion


image.png (15.9 KiB)
image.png (39.5 KiB)
image.png (31.8 KiB)
5 |1600 characters needed characters left characters exceeded

Up to 10 attachments (including images) can be used with a maximum of 3.0 MiB each and 30.0 MiB total.