Using Owin External Login without ASP.NET Identity

ASP.NET MVC5 has excellent support for external social login providers (Google, Facebook, Twitter) integrating with the ASP.NET Identity system. But what if we want to use external logins directly without going through ASP.NET Identity? Using external logins together with ASP.NET Identity is very simple to get started with, but it requires all users to register with the application. External logins are just another authentication method against the internal ASP.NET Identity user. In some cases there is no need for that internal database, it would be better to get rid of it and use the external login providers without ASP.NET Identity. That’s possible, but requires a bit of manual coding.

For public facing web applications I think that it is often a good idea to use ASP.NET Identity as it doesn’t tie the user to a specific login provider. But if we are fine with using one and only one specific login provider for each user it’s possible to skip ASP.NET Identity. It could be an organization that heavily relies on Google Apps already so that all users are known to have Google accounts. It could be an application that uses SAML2 based federative login through Kentor.AuthServices.

In this post I’ll start with a freshly created ASP.NET MVC Application without any authentication at all and make it use Google authentication, without ASP.NET Identity being involved at all. The complete code is available on my GitHub account.

Creating the Project

2014-11-06 14_32_12-Code Coverage Results - Microsoft Visual Studio
2014-11-06 14_32_58-Change AuthenticationFirst, I’ve created a normal ASP.NET MVC 5.2.0 using Visual Studio 2013. The default is to enable individual user accounts, so that have to be changed to using no authentication.

Running the created project gives the default ASP.NET MVC template, with no trace of any sign in link or anything. Exactly as we want it. The next step is to add a resource that requires authentication and see how it fails when there is no way to login.

Adding a Secure Page

To test the login, there must be some page that requires authentication. I’ve created a simple Secure action in the HomeController.

[Authorize]
public ActionResult Secure()
{
  ViewBag.Message = "Secure page.";
  return View();
}

Running the application and trying to access /Home/Secure gives a standard 401.1 UnAuthorized error page, which is exactly what’s expected.

Adding the Cookie Middleware

The Owin authentication model differentiates between the login middleware and the session handling middleware. To use Google login we first need to set up a cookie middleware that will set and keep a cookie with the identity once we’ve logged in. First a number of nuget packages need to be added.

  • Microsoft.Owin.Security.Cookies for the cookie middleware.
  • Microsoft.Owin.Security.Google for the Google authentication.
  • Microsoft.Owin.Host.SystemWeb to run the Owin pipeline on top of IIS.

The cookie middleware is set up through a Startup.Auth.cs file. I’m following the conventions from the ASP.NET MVC template for new projects with individual user accounts. The file doesn’t have to be named that, but I think it makes it more easy to find.

public partial class Startup
{
  private void ConfigureAuth(IAppBuilder app)
  {
    var cookieOptions = new CookieAuthenticationOptions
      {
        LoginPath = new PathString("/Account/Login")
      };
 
    app.UseCookieAuthentication(cookieOptions);
  }
}

The cookie authentication middleware is both responsible for persisting the identity across calls and for redirecting any unauthenticated requests to secure pages to a login page. We’ll follow the convention and name it /Account/Login.

To get the ConfigureAuth method called some plumbing is done in a Startup.cs file.

[assembly: OwinStartupAttribute(typeof(SocialLoginWithoutIdentity.Startup))]
 
namespace SocialLoginWithoutIdentity
{
  public partial class Startup
  {
    public void Configuration(IAppBuilder app)
    {
      ConfigureAuth(app);
    }
  }
}

With these files added, running the application and clicking on the Secure link will give a 404 not found error on /Account/Login/. We need to have an action at that location that can start the login sequence.

Adding Google Login

The login is initiated from the /Account/Login location, by creating a ChallengeResult. The ChallengeResult class is a simplified version of the one from the standard template.

public ActionResult Login(string returnUrl)
{
  // Request a redirect to the external login provider
  return new ChallengeResult("Google",
    Url.Action("ExternalLoginCallback", "Account", new { ReturnUrl = returnUrl }));
}
 
// Implementation copied from a standard MVC Project, with some stuff
// that relates to linking a new external login to an existing identity
// account removed.
private class ChallengeResult : HttpUnauthorizedResult
{
  public ChallengeResult(string provider, string redirectUri)
  {
    LoginProvider = provider;
    RedirectUri = redirectUri;
  }
 
  public string LoginProvider { get; set; }
  public string RedirectUri { get; set; }
 
  public override void ExecuteResult(ControllerContext context)
  {
    var properties = new AuthenticationProperties() { RedirectUri = RedirectUri };
    context.HttpContext.GetOwinContext().Authentication.Challenge(properties, LoginProvider);
  }
}

The Google login provider needs to be configured in Startup.Auth.cs. The ClientId and ClientSecret is available by registering the application on Google developers console. Remember to also enable the Google+ API for the application or the Google middleware won’t work (I lost a few hours on debugging before finding out that was the problem…).

app.SetDefaultSignInAsAuthenticationType(cookieOptions.AuthenticationType);
 
app.UseGoogleAuthentication(new GoogleOAuth2AuthenticationOptions
  {
    ClientId = GoogleClientId,
    ClientSecret = GoogleClientSecret
  });

The call to UseGoogleAuthentication should be quite obvious why it’s needed. But the first one to SetDefaultSignInAsAuthenticationType is not as obvious. Looking at the owin external authentication pipeline a social login middleware normally relies on the external cookie middleware registered before the social login middleware. In the setup of the external cookie middleware, it sets itself as the default signin type. That’s how the social login middleware knows that it should use the external cookie. In this setup there is no external cookie, so we have to manually set the main cookie middleware as the default signin type. The cookie middleware will only issue a cookie if the AuthenticationType matches the one in the identity created by the social login middleware.

One more piece of code is needed. In the Login action there’s a reference to an ExternalLoginCallback action that is called after the external login is performed. In the default implementation in the template, the ExternalLoginCallback looks up the matching ASP.NET Identity account and signs in to that. But in this case everything is already done. The external sign in is complete and a cookie has been issued. The ExternalLoginCallback just needs to redirect the user back to the secure location that triggered the login in the first place.

public ActionResult ExternalLoginCallback(string returnUrl)
{
  return new RedirectResult(returnUrl);
}

Information Returned by Google

When using ASP.NET Identity, the information returned by Google is mostly ignored. When using this setup, all that information is readily available as claims on the current identity. In the example application, I’ve listed all the available claims. It’s a nameidentifier that is a unique user identifier for the Google User for this application. Another application using Google SignIn would receive another identifier. Then there’s the full name, first name and last name, the e-mail address and finally my Google plus profile URL. That’s all the information needed about a user in many applications. The name and the e-mail are useful. Having a unique name identifier makes it possible to save user data with that as an id and retrieve it later.

As long as it is fine to lock the user in to the identity provider that was used when signing up, there is no need for ASP.NET Identity in simple applications.

This post is part of the Owin Authentication series.<< Writing an Owin Authentication Middleware

45 comments

  1. Hello master
    I just finished developing a very interesting PC game, and I am trying to do a website for it.
    In this website users can upload custom files generated by my game. Users can only upload when logged in using only google, facebook and microsoft accounts.

    the default mvc 5 template with individual user authentication is difficult to understand as I just started learning all about making websites.

    So this tutorial is perfect, but how do I get the user emails after they log in? You wrote:

    “When using this setup, all that information is readily available as claims on the current identity. In the example application, I’ve listed all the available claims”

    where is that?

    sample form the game is in the URL

  2. I have been trying to make this work without success. I can sign-in successfully in Google, then I get redirected to /signin-google?etcetc, but that URL redirects to /Account/ExternalLoginCallback?ReturnUrl=%2FHome%2FSecure&error=access_denied without setting authentication cookies. Since that URL contains “error=access_denied”, I presume that MVC believes that the sign-in response is invalid, but I have no idea why.

    What I did was:

    1. Clone your project.
    2. Set my client secret and client app id.
    3. Access the /Secure url on the application.

    Do you have any advice on how to fix this issue?

    Thanks

    1. You could try to inject dummy middleware and set breakpoints, as I describe in the Understanding the Owin External Authentication Pipeline post.

      The interesting parts should be to inspect if there is a AuthenticationResponseGrant when the google middleware has returned and to inspect if the auth cookie is present after the cookie handler middleware.

      When I say after, it means you should inject the breakpoint middleware before the cookie handler in the code and set a breakpoint in it after the call to await next.Invoke();.

      That would give you a hint how far in the pipeline the process works. If everything looks fine and the cookie is lost anyway, you may be a victim of the Owin Cookie Monster.

  3. I have found the problem and it was obviously my fault. I had a debugging proxy configured, but that proxy was not running, so the GoogleOAuth2AuthenticationHandler was failing to retrieve the user_info. It was easy to figure out once I set up the breakpoints as you suggested. Thanks!

  4. Hi Abel !

    I have tried your example and It worked fine. But I have a another question. How do I remember external account info that will access to my website in next time.

    1. If you just want to keep track of a user’s assets (e.g. a shopping cart), to be able to show the same assets next time, you can use the ClaimsIdentity.Name as a key. It will be the same for subsequent logins by the same user (it’s really up to the provider to guarantee that, but most providers do).

      If you want to create the user in an own account database it might be better to look into ASP.NET Identity and tweak the default code in the MVC template by hiding the password login option.

      1. I have set [Authorize] for HomeController and It’ll automatic login with google account. But if I redirect to another controller and don’t set [Authorize] within Controller It not work. Will I must set [Authorize] for all controller ? have you anyway for me ?

      2. Yes, you have to set an [Authorize] for every controller. That is how you decide what controllers/action require login (and specific roles/claims) to be used.

  5. Had this problem running your code: The redirect URI in the request: http://localhost:3884/signin-google did not match a registered redirect URI.

    The steps in order to fix it (If someone has the same problem and reads this comment):

    1. Enable Google + API (Google Dev Console)
    2. The default redirect URI is /signin-google, so add this to the Authorized redirect URI. (Google Dev Console)
    3. Add this to the RouteConfig File:
    routes.MapRoute(name: “signin-google”, url: “signin-google”, defaults: new { controller = “Account”, action = “ExternalLoginCallback” });

  6. Kudos for a great article which was easy to follow. Although I was able to get the code working as described, there is a minor issue which I’ve been attempting to resolve.

    In the GoogleOAuth2AuthenticationHandler, the context.Identity class contains a read-only property named NameClaimType. It defaults to http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name, but I would like to change it to http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress.

    Clearly as it is read-only it cannot be changed in OnAuthenticated. As a workaround, I’ve altered the Name value to be the email address using the code below:

    context.Identity.RemoveClaim(context.Identity.FindFirst(ClaimTypes.Name));
    context.Identity.AddClaim(new Claim(ClaimTypes.Name, context.Email));

    Is there any a way to change NameClaimType earlier in the pipeline, or is there a configuration option that can be used? I’ve seen reference to TokenValidationParameters, but I’ve not been able to find that.

  7. Really useful article, thanks. Is there any way to get any of the user’s information so that I can restrict access by (for example) email domain? I notice the ControllerContext.User property has a claim on it, but it only contains the person’s full name (presumably this is all that Google provides by default).

    In the default MVC app, the ExternalLoginCallback method performs a call to “AuthenticationManager.GetExternalLoginInfoAsync()”. I assume this is what contains further information from the provider in question, but it isn’t populated unless I add “app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);” to my Startup.ConfigureAuth method. If I do that, it seems to inspect the wrong cookie, and never finishes authenticating, going into a redirect loop between /Auth/ExternalLoginCallback and /Login

    I appreciate that I might be trying to mix your solution with the more bloated MVC project default, which might not be a good idea. Any tips would be appreciated.

    1. The identity that is retreived by the call to AuthenticationManager.GetExternalLoginInfoAsync() is the one returned by Google. It is exactly the same as the one that is set as the identity in ControllerContext.User.

      If you need more information about the user, I think that is something you need to enable (ask for permission to) in the Google developer console for the application. I’m not sure if the Katana Google middleware will fetch any additional fields by default or if you might need to retreive it manually. In that case the project certainly grows as you would have to recreate a lot of what goes into the Google authentication middleware.

      1. Just realised I should have read the line about where the claims info is held a little more closely :) I was just looking in the wrong place for the email information. Thanks again.

    1. Logout is done in the same was as in the normal MVC template, you can copy that code:

      AuthenticationManager.SignOut();
      return RedirectToAction("Index", "Home");
  8. Dear !
    I have two the logins :
    1 using google authentication for user ( redirect –> /app/index)
    1 using Forms Authentication for Admin ( /admin/login–> Admin index)
    How can i do it in this case?

    1. That’s a scenario that is supported by ASP.NET Identity and makes sense if you want to keep track of what google users you have in the application and be able to assign application roles to them.

      If you just want them to be able to log in and also have a side-by-side you can use ASP.NET Identity for your admin users in combination with the setup described in this article for the Google Users. Starting with the default ASP.NET template you have to remove the external cookie handler and instead add app.SetDefaultSignInAsAuthenticationType(cookieOptions.AuthenticationType);

  9. Amazing article, we are moving to MVC 6, can you help me on what changes there. Thanks

    1. The principles are the same in ASP.NET Core (formerly named ASP.NET 5) and ASP.NET MVC6 (or whatever name it will have), but the implementation will be new. I’ve planned to write a similar series for ASP.NET Core, but I’m waiting for things to get a bit more settled first (like the name not having been changed for the last month).

  10. Hi Abel,

    Is it possible to have 2 or more external authentication in ASP.Net Identity to have multiple claims (e.g. access tokens)? how do I manange to do that?

    1. Do you mean you want the user to log in to two external authentication providers and then get a set of claims that originates from both of them? That should be possible, but requires that you change the external login sequence compared to the MVC template.

  11. I have configured the external login, but it requires to log in again if pc is restarted. We want that user remains logged in even if his pc is restarted. Can you guide me how to do this. We are using the built in template of mvc 5 with identity.

    1. If you’re using ASP.NET Identity and the default template (which is not what is described in this post) you can change the code>IsPersistent flag in the call to SignInManager.ExternalSignInAsync() in AccountController.ExternalLoginCallback

  12. Hello thanks for the this tutorial. Can you help me a little with the content that the Login page should have?
    thanks again.

  13. Nice article. I am not using identity . Just using you tutorial for google sign in. Issue is it creates cookie for session only . Can i add expiration time. I tried ading but it dint work. Can you help?

  14. The changes mentioned does not work now. I am not sure if it is because of any changes in owin. Can you confirm if this post is still valid?

    1. The Owin/Katana parts have not changed as far as I know and should still be valid. However I think that Google has made some updates on their end, so the example might not be completely relevant any more

  15. Hi Anders,

    Great article, Your blog rocks! Im using owin with web api. Do you know how I could setup the Account controller without using System.Web.Mvc?

    1. You would have to port the relevant methods from the MVC Account controller to a Web API controller. But I don’t know if it’s really worth it unless you have very specific reasons to not take a dependency on MVC.

    1. The "error=access-denied" is a generic “something went wrong” answer. Enable Katana logging to get output from the Google middleware on what the error is.

  16. Thanks for your detailed post @Anders,

    I have followed the above steps and implemented the google login in my website logicalfeed.com. It is working fine in my local website i.e., with localhost URL, but when I try the same in live website (https://www.logicalfeed.com/account/login) I am getting the access denied error. I have already enabled the Google+ API for my app. I have been facing this problem since a week. Can you please help me here.

  17. Hi Anders,
    thank you very much for this article, it was the only help I found in setting up a MVC app without database that just completely relies on an external oauth2 provider.

    I also first got some problems getting your solution to work and after a few hours of trying (confirm registration URL is correct, …), the final missing thing was just to update all nuget packages to the latest version (I usually first try to get it to work and THEN I try to update).

    Big Thanks!

    ps: By the way, I recommend to put a README.md at the github code that refers to this blog article.

    ps2: I ran in another issue on my dev computer, as I am working from a restricted company pc, that did not allow my local IIS server to connect through the internet without proxy, so I also had to get that fixed. … just as an additional fyi – in case s.o. else is pulling hairs of what else is the missing piece. ;)

Leave a comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.