dotnet / aspnetcore

ASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.
https://asp.net
MIT License
35.44k stars 10.01k forks source link

UserManager.GetUserAsync(HttpContext.User) fails to find user when using OAuth #13623

Closed JayJamieson closed 5 years ago

JayJamieson commented 5 years ago

UserManager.GetUser(HttpContext.User) fails to find user when using OAuth

Describe the bug

I have setup a project based on scaffold-identity and have successfully configured MS OAuth to authenticate with my microsoft account but visiting the configured Identity/Account/Manage page fails to load my details.

I have determined the issue is to do with UserManager.GetUserAsync(HttpContext.User) trying to use the OAuth ClaimType.NameIdentifier which is not the same as the database Id for the user.

To Reproduce

Steps to reproduce the behavior:

  1. setup a project based on scaffold-identity using full UI override
  2. login with Microsoft account
  3. create account from external email
  4. visit Identity/Account/Manage and get greeted with error of user not being found

Expected behavior

I would expect that the HttpContext.User be able to locate the currently logged in user using the ClaimType.NameIdentifier

Im open to what best practice for this sort of thing is as out of the box the Scaffold Identity UI with OAuth doesnt work.

blowdart commented 5 years ago

Is this using app.UseMicrosoftAccount? How have you activated Microsoft Account? Can we see come code for your configuration? The way external logins work is we do the lookup and replace it with the actual identity user.

JayJamieson commented 5 years ago

Is this using app.UseMicrosoftAccount? How have you activated Microsoft Account? Can we see come code for your configuration? The way external logins work is we do the lookup and replace it with the actual identity user.

No this is using services.AddMicrosoftAccount(...) as the use.MicrosoftAccount() warns of obsolete? image

how ive configured in startup

services.AddMicrosoftAccount(MicrosoftAccountDefaults.AuthenticationScheme, c => {
                    c.ClientId = Configuration["MicrosoftOAuth:ClientId"];
                    c.ClientSecret = Configuration["MicrosoftOAuth:Secret"];
                    c.SaveTokens = true;
                })
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2).AddRazorPagesOptions(options =>
            {
                options.AllowAreas = true;
                options.Conventions.AuthorizeAreaFolder("Identity", "/Account/Manage");
                options.Conventions.AuthorizeAreaPage("Identity", "/Account/Logout");
            });

            services.ConfigureApplicationCookie(options =>
            {
                options.LoginPath = $"/Identity/Account/Login";
                options.LogoutPath = $"/Identity/Account/Logout";
                options.AccessDeniedPath = $"/Identity/Account/AccessDenied";
            })

scaffolded code from vs 2019

            builder.ConfigureServices((context, services) => {
                services.AddDbContext<PortalContext>(options =>
                    options.UseNpgsql(
                        context.Configuration.GetConnectionString("SqlConnection")));

                services.AddDefaultIdentity<PortalUser>()
                    .AddDefaultUI(UIFramework.Bootstrap4)
                    .AddEntityFrameworkStores<PortalContext>();
            });

this is what gets called to add the user after ms OAuth flow completes

public async Task<IActionResult> OnPostConfirmationAsync(string returnUrl = null)
        {
            returnUrl = returnUrl ?? Url.Content("~/");
            // Get the information about the user from the external login provider
            var info = await _signInManager.GetExternalLoginInfoAsync();
            if (info == null)
            {
                ErrorMessage = "Error loading external login information during confirmation.";
                return RedirectToPage("./Login", new { ReturnUrl = returnUrl });
            }

            if (ModelState.IsValid) {

                var user = new PortalUser {UserName = Input.Email, Email = Input.Email };
                var result = await _userManager.CreateAsync(user);

                if (result.Succeeded)
                {
                    result = await _userManager.AddLoginAsync(user, info);

                    if (result.Succeeded)
                    {
                        await _signInManager.SignInAsync(user, isPersistent: false);

                        await _signInManager.UpdateExternalAuthenticationTokensAsync(info);
                        _logger.LogInformation("User created an account using {Name} provider.", info.LoginProvider);
                        return LocalRedirect(returnUrl);
                    }
                }
                foreach (var error in result.Errors)
                {
                    ModelState.AddModelError(string.Empty, error.Description);
                }
            }

            LoginProvider = info.LoginProvider;
            ReturnUrl = returnUrl;
            return Page();
        }

This is the code that fails after login is confirm and user created, i get redirected to the manage user page and fails to find user

 public async Task<IActionResult> OnGetAsync()
        {
            var user = await _userManager.GetUserAsync(User);

            if (user == null)
            {
                return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
            }

            var userName = await _userManager.GetUserNameAsync(user);
            var email = await _userManager.GetEmailAsync(user);
            var phoneNumber = await _userManager.GetPhoneNumberAsync(user);

            Username = userName;

            Input = new InputModel
            {
                Email = email,
                PhoneNumber = phoneNumber
            };

            IsEmailConfirmed = await _userManager.IsEmailConfirmedAsync(user);

            return Page();
        }
JayJamieson commented 5 years ago

Any update on this or need more information?

HaoK commented 5 years ago

You need to do a lookup to find the matching user in the identity database, this is typically done via FindByLogin where you pass in the provider (MicrosoftAccount), and the provider key which is usually the name identifiter claim from there, which will give you back the identity user.

This is what gets setup by the call to AddLogin in your PostConfirmationFlow

                  result = await _userManager.AddLoginAsync(user, info);
JayJamieson commented 5 years ago

Right, so the scaffolded project is incorrectly using GetUserAsync? I did some digging in the source code and it seems to me that in order for GetUserAsync to work I would need to save the user to my db using the external user id found in User?

HaoK commented 5 years ago

Everything should work if you scaffold all files and use them as is, if you are modifying particular pages and what not, you will need to stick to the same conventions the scaffolded pages use, or modify things appropriately. Nothing in the user itself links itself to an external login, its the external login table that does that lookup by default.