Search code examples
c#asp.net-mvcgoogle-oauth

Connecting OAuth with User or Person Model in ASP.NET MVC C#


So there's plenty of tutorials online on how to get oAuth working with ASP.NET, even a tutorial on how to set roles for those logged in accounts with MVC but none of these tutorials explain what's next. What is that called when you connect a user you've registered with Google to the person or user model in your app? How do you act as one of those persons based on who you're logged in as with Google? I've figured out how to register first and last name and all that jazz but still I'm not sure how to make it so that I'm in or acting as the person.

For example, I have a person model and some other models that inherit from the person model, such as Engineer and Manager:

public class Person
    {
        [Key]
        public int PersonId { get; set; }

        [Required(ErrorMessage = "The First Name field is required.")]
        [Display(Name = "First Name")]
        [MaxLength(50)]
        public string FirstName { get; set; }

        [Required(ErrorMessage = "The Last Name field is required.")]
        [Display(Name = "Last Name")]
        [MaxLength(50)]
        public string LastName { get; set; }

        [NotMapped]
        [Display(Name = "Name")]
        public string FullName
        {
            get
            {
                return FirstName + " " + LastName;
            }
        }

    }

public class Engineer : Person
    {
        public ICollection<Manager> managers { get; set; }
    }

public class Manager : Person
    {
        public ICollection<Engineer> engineers { get; set; }
    }

Here I have the following added to the AccountController:

[AllowAnonymous]
        public async Task<ActionResult> ExternalLoginCallback(string returnUrl)
        {
            string firstName;
            string lastName;
            var loginInfo = await AuthenticationManager.GetExternalLoginInfoAsync();
            if (loginInfo == null)
            {
                return RedirectToAction("Login");
            }
            if (loginInfo.Login.LoginProvider == "Google")
            {
                var externalIdentity = AuthenticationManager.GetExternalIdentityAsync(DefaultAuthenticationTypes.ExternalCookie);
                var emailClaim = externalIdentity.Result.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Email);
                var lastNameClaim = externalIdentity.Result.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Surname);
                var givenNameClaim = externalIdentity.Result.Claims.FirstOrDefault(c => c.Type == ClaimTypes.GivenName);

                firstName = givenNameClaim.Value;
                lastName = lastNameClaim.Value;

            } else
            {
                firstName = "Error";
                lastName = "Error";
            }
            // Sign in the user with this external login provider if the user already has a login
            var result = await SignInManager.ExternalSignInAsync(loginInfo, isPersistent: false);
            switch (result)
            {
                case SignInStatus.Success:
                    return RedirectToLocal(returnUrl);
                case SignInStatus.LockedOut:
                    return View("Lockout");
                case SignInStatus.RequiresVerification:
                    return RedirectToAction("SendCode", new { ReturnUrl = returnUrl, RememberMe = false });
                case SignInStatus.Failure:
                default:
                    // If the user does not have an account, then prompt the user to create an account
                    ViewBag.ReturnUrl = returnUrl;
                    ViewBag.LoginProvider = loginInfo.Login.LoginProvider;
                    return View("ExternalLoginConfirmation", new ExternalLoginConfirmationViewModel { Email = loginInfo.Email, FirstName = firstName, LastName = lastName });
            }

And the account view model:

public class ExternalLoginConfirmationViewModel
{
    [Required]
    [Display(Name = "Email")]
    public string Email { get; set; }

    [Required]
    [Display(Name = "First Name")]
    public string FirstName { get; set; }

    [Required]
    [Display(Name = "Last Name")]
    public string LastName { get; set; }
}

So how do I log in with google and then be one of the persons? What is this even called? Is there a name for doing this? I assume everyone does this at some point. I can create persons, managers and engineers in my initialization class but I don't know how to do it with oAuth.

Note, I would prefer to force all users to log in with Google rather than create a local user account so I didn't bother with registering site accounts.

Also for good measure here's my IdentityModel:

public class ApplicationUser : IdentityUser
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public async Task<ClaimsIdentity> GenerateUserIdentityAsync(UserManager<ApplicationUser> manager)
        {
            // Note the authenticationType must match the one defined in CookieAuthenticationOptions.AuthenticationType
            var userIdentity = await manager.CreateIdentityAsync(this, DefaultAuthenticationTypes.ApplicationCookie);
            // Add custom user claims here
            return userIdentity;
        }
    }

Solution

  • During the external login process, an authorization token is returned by the third party API. You should add this token as a claim for the user. You can do this by changing the code in Startup.Auth.cs to something like the following:

    var googleOptions = new GoogleOAuth2AuthenticationOptions
    {
        ClientId = Properties.Settings.Default.GoogleClientId,
        ClientSecret = Properties.Settings.Default.GoogleClientSecret,
        Provider = new GoogleOAuth2AuthenticationProvider
        {
            OnAuthenticated = (context) =>
            {
                context.Identity.AddClaim(new System.Security.Claims.Claim("urn:google:access_token", context.AccessToken, XmlSchemaString, "Google"));
                return Task.FromResult(0);
            }
        }
    };
    googleOptions.Scope.Add("foo");
    app.UseGoogleAuthentication(googleOptions);
    

    Where "foo" is above, you'll need to add the scope(s) you need the user to authorize your application for. Consult the Google API docs for a full list of scopes. If you need multiple scopes, add additional googleOptions.Scope.Add lines.

    Later, you can retrieve the token using:

    var googleTokenClaim = user.Claims.FirstOrDefault(m => m.ClaimType.EndsWith("google:access_token"));
    

    For the purposes of Google's APIs, the token returned is a "refresh token". The refresh token is good until the user revokes access to your app, but you need to "trade it in" for an access token. Details for how to do that can be found in the Google Identity Platform docs, or you can just use one of the client libraries to handle all this for you.

    Long and short, once you have a true access token, you simply authorize requests that need authorization by adding an Authorization: Bearer [token] header to the request.

    Now, different APIs handle some of this differently, but you specifically mentioned Google. Consult the docs of other APIs you may need to utilize to see what you should do with those.

    UPDATE: Getting User Instance

    With Identity, the user principal exposed by User.Identity is not a complete representation of the user instance in the database. In order to get all the information for a user, including their claims, you need to query the user from the database. First, you need to get the user's id, so you have something to query by. Identity exposes an extension GetUserId() on User.Identity for that:

    var userId = User.Identity.GetUserId();
    

    If you've customized the primary key for IdentityUser, then you need to use the generic version instead:

    var userId = User.Identity.GetUserId<int>();
    

    In either case, you then use that user id to query the user from the database. You can actually query your context directly, since "users" are just another DbSet on IdentityDbContext:

    var user = db.Users.Find(userId);
    

    However, you'll most often use an instance of UserManager since it essentially wraps your context and provides additional, helpful features:

    var user = UserManager.FindById(userId);
    

    This is based off the default implementation of AccountController when you've scaffolded your project with Individual Authentication. By default, the UserManager property on AccountController is an instance of UserManager<ApplicationUser>, or if you've customized the primary key, something like UserManager<ApplicationUser, int>, for example, where int would be the type of the primary key. If you're doing this in a different controller or simply not using the scaffolded version of AccountController, then you just need to similarly define an instance of UserManager.