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

  • Randall Eike on 2014-12-14

    Great article and sample project. Thank you!

  • thomas on 2014-12-16

    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

  • Antoine Aubry on 2015-03-11

    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

    • Anders Abel on 2015-03-11

      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.

      • Antoine Aubry on 2015-03-12

        Thanks a lot! I will try that and report back my findings.

  • Antoine Aubry on 2015-03-12

    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!

  • Khiem Nguyen on 2015-04-09

    Hi Abel !

    How can I get Email Address or Username to save data ?

    • Anders Abel on 2015-04-09

      Everything is in the ClaimsPrincipal.Current.Claims collection. You have to grab it from there and save it yourself.

      • Khiem Nguyen on 2015-04-10

        Thanks Abel, I’ll try it.

  • Khiem Nguyen on 2015-04-16

    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.

    • Anders Abel on 2015-04-16

      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.

      • Khiem Nguyen on 2015-04-16

        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 ?

      • Anders Abel on 2015-04-16

        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.

  • Sergio Sánchez on 2015-04-29

    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” });

    • Pranay on 2017-10-21

      I got the same issue while trying this tutorial but your fix resolved it. Thanks @Sergio

  • Mark G on 2015-06-04

    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.

  • Martin on 2015-06-09

    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.

    • Anders Abel on 2015-06-12

      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.

      • Martin on 2015-06-13

        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.

  • Igor Dvorkin on 2015-06-13

    Awesome article – I was able to get everything you mentioned working. How do I implement logout?

    • Anders Abel on 2015-06-13

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

      AuthenticationManager.SignOut();
      return RedirectToAction("Index", "Home");

  • Ricky Nguyen on 2015-12-08

    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?

    • Anders Abel on 2015-12-09

      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);

      • Thanks Anbel. on 2016-02-03

        Thanks Abel.

  • VIc on 2016-02-05

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

    • Anders Abel on 2016-02-05

      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).

  • Zent on 2016-02-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?

    • Anders Abel on 2016-02-10

      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.

  • Rachit on 2016-03-14

    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.

    • Anders Abel on 2016-03-16

      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

  • bablu on 2016-03-21

    nice……

  • bob03 on 2016-05-26

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

    • Anders Abel on 2016-05-27

      I can help you if you want a consultancy service. Please mail me if that’s interesting.

  • bob03 on 2016-05-26

    It returns an empty page. My provider is Cas I have set up all settings correct Since I copy them from a project with identity.

    http://localhost:42000/Account/Login?ReturnUrl=%2FHome%2FSecure

    Thank you

  • edward on 2016-08-07

    how this can be achieved using asp.net web forms instead of mvc application?

  • Arjun Singh on 2016-09-18

    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?

    • Anders Abel on 2016-10-22

      Yes, you can adjust the expiration time in the options to the cookie middleware.

  • Naveen Raina on 2017-02-17

    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?

    • Anders Abel on 2017-02-21

      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

  • Marcus H on 2017-02-18

    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?

    • Anders Abel on 2017-02-21

      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.

  • Chitra on 2017-11-03

    After google login , I am not able to see the secure page,
    In the url it shows ,
    http://localhost:62506/signin-googleReturnUrl=%2FHome%2FSecure&error=access_denied#

    What is that i am missing?

    • Anders Abel on 2017-11-03

      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.

  • Leave a Reply

    Your name as it will be displayed on the posted comment.
    Your e-mail address will not be published. It is only used if I want to get in touch during comment moderation.
    Your name will be a link to this address.
Software Development is a Job – Coding is a Passion

I'm Anders Abel, an independent systems architect and developer in Stockholm, Sweden.

profile for Anders Abel at Stack Overflow, Q&A for professional and enthusiast programmers

Code for most posts is available on my GitHub account.

Popular Posts

Archives

Series

Powered by WordPress with the Passion for Coding theme.