Contact   •   Articles   •   Products   •   Search

Rick Strahl's Web Log

Wind, waves, code and everything in between...
ASP.NET • C# • HTML5 • JavaScript • AngularJs

Adding minimal OWIN Identity Authentication to an Existing ASP.NET MVC Application


As of ASP.NET 4, ASP.NET provides a fairly useful identity system. If you create a new project and choose an MVC project and choose to add both internal and external authentication, it’s fairly straight forward to get a reasonable identity implementation into your application.

However, if you have an existing application, or if the full Entity Framework based identity structure doesn’t work for you, then the process to hook up a minimal and custom implementation that uses your own domain/business model and classes is not exactly as straightforward. You have to either rip out the pieces you don’t need from an full template install, or add the necessary pieces. In this post I hope I can show you how to do the latter, showing only the pieces that you need.

The process is not necessarily hard – but it’s not very well documented. It’s difficult to find the information necessary to just create the necessary handlers that can deal with linking accounts to external oAuth providers like Google, Facebook, GitHub and so on. So in this post I’ll go over this scenario.

A Real World Use Case

I’ve come down this path myself over the last weekend as one of my old sites – CodePaste.net – required an update to the old OpenID authentication I had running on that site. Codepaste is a pretty old site; in fact it is was the first MVC application I ever built :-). But it’s been running unencumbered for the last 7 years. That is until Google decided to pull the plug on the old OpenId implementation. Promptly I started getting a ton of emails, and decided I had to re-implement the external providers.

The old implementation used DotNetOpenAuth combined with FormsAuthentication which I blogged about years ago, but DotnetOpenAuth seems to have fallen out of favor over the years, so I decided to bite the bullet and switch over to the newer, built-in OWIN based Identity functionality in ASP.NET 4.

When you first start looking at Identity the amount of information out there is rather overwhelming. Lots of intro articles that talk about how to use the stuff ‘as is’ without customization. But there’s not a lot of information on using the core Identity pieces without the full bore UserManager and Entity Framework data store, in order to use just the Authentication/Authorization on their own and integrate them with my own business objects/domain model.

Just the Core Identity Features for Local and External Logins

The goal of this post is to show the minimal pieces of the OWIN Identity system to handle Local and External account logins and hook them to a custom domain model rather than using the Entity Framework based UserManager. In short focus on the system components that manage authentication and leave the user management to the application.

Just Show me what I need!

The main text of this article is lengthy as it covers the CodePaste.NET login and user management features relevant to the discussion in addition to the core login features. For those of you that want just the nuts and bolts there’s also a summary section at the end that shows just the relevant code in skeleton format that you can just plug into your controllers. I’ve also linked to the full source code of the Account Controller I discuss here if you want to see the full code, rather than bite-sized snippets.

For more detail… read on.

OWIN based Identity in ASP.NET MVC 5

If you haven’t used the new Identity features in ASP.NET MVC 5 at all, I suggest that you check it out first by creating a new MVC Web site and letting it create a default Web site with Individual User Authentication enabled. This will let you see how the default implementation works. If you’re looking at code specific implementation details you can check out the AccountController and ManageController which deal with the user management interfaces. AccountController deals with logging in for both local and external accounts, while ManageController deals with setting up new accounts, email confirmations etc. This is part of the reason why Identity feels pretty overwhelming – when you look at the code there’s a ton of stuff going on and it’s all tightly intermixed with the specific Entity Framework based implementation.

Nevertheless I encourage you to at least take a brief glance at it, maybe stepping through to understand the overall flow of how logins are processed.

Extracting just the necessary Pieces

In order to use the OWIN Idenity pieces in your own application that doesn’t use the EF based UserManager, you’ll have to do a few things. Again, my goal as part of this article is to:

  • Support Cookie based User Logins (username/password)
  • Support External Logins (Google, Github, Twitter)
  • Support ability to create Accounts using both local or external Accounts
  • Support logging in with either local or external accounts

In order to accomplish this with an existing application I have to:

  • Turn off IIS Authentication for the Application
  • Add the appropriate NuGet Packages
  • Implement the Account Registration for Local and External Accounts
  • Implement Logging in for Local and External Accounts
  • Allow for signing in using the OWIN based Login mechanism

Let’s take a look and see what each of these steps look like.

Turn off IIS Authentication for the Application

ASP.NET Identity works using the OWIN platform which is a custom subsystem that doesn’t rely on standard IIS security. Because OWIN can be self hosted there’s no dependency on IIS in this system. On IIS OWIN plugs into the IIS pipeline using a few dynamically inject modules but it essentially completely takes over the Authentication/Authorization process for your application. So, in order to use ASP.NET Identity the first thing you have to do is actually turn off standard IIS authentication in web.config for your application.

<system.web>
<authentication mode="None" />
<system.web>

I did not do this initially and it took me a while to figure why my application kept throwing up browser authentication dialogs instead of navigating to my login page or sending me off to an external provider login. Make sure standard authentication is off!

Adding NuGet Packages

If you have an existing project that doesn’t have any identity features installed you’ll need to get the right assemblies into your project. If you look at a newly created MVC project and the litany of assemblies in there it’s not obvious what’s actually required to get just the basic features.

Luckily, thanks to the power of NuGet, to get just the core identity features you can add just the following  packages to get all the references you needed for the core identity features plus a few of the external providers needed.

  • Microsoft.Owin.Host.SystemWeb
  • Microsoft.Owin.Security.Cookies

The first two packages are enough to light up the OWIN identity framework. If you’re not running on IIS use Microsoft.Owin.Host.SelfHost instead of the SystemWeb host.

You’ll also need to install the the specific external provider packages:

  • Microsoft.Owin.Security.Google
  • Microsoft.Owin.Security.Twitter
  • Owin.Security.Providers

The provider packages add support for specific external providers you can log in with. The Owin.Security.Providers package is a third party library that includes a ton of additional providers you can integrate with and that’s what I used to support GitHub logins, since this is a developer focused site.

Startup Class: Provider Configuration

The next step is to configure OWIN pipeline to actually handle the various login solutions. To do so you have to create a Startup class that configures the various authentication mechanisms. In my case I support Local user authentication (Cookie Auth) as well as external providers for Google, Twitter and GitHub.

Here’s the config code:

namespace CodePasteMvc { public partial class Startup { public void Configuration(IAppBuilder app) { ConfigureAuth(app); } // For more information on configuring authentication, please visit http://go.microsoft.com/fwlink/?LinkId=301864 public void ConfigureAuth(IAppBuilder app) { // Enable the application to use a cookie to store information for the signed in user app.UseCookieAuthentication(new CookieAuthenticationOptions { AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie, LoginPath = new PathString("/Account/LogOn") });

app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);

// App.Secrets is application specific and holds values in CodePasteKeys.json // Values are NOT included in repro – auto-created on first load
if (!string.IsNullOrEmpty(App.Secrets.GoogleClientId)) { app.UseGoogleAuthentication( clientId: App.Secrets.GoogleClientId, clientSecret: App.Secrets.GoogleClientSecret); } if (!string.IsNullOrEmpty(App.Secrets.TwitterConsumerKey)) { app.UseTwitterAuthentication( consumerKey: App.Secrets.TwitterConsumerKey, consumerSecret: App.Secrets.TwitterConsumerSecret); } if (!string.IsNullOrEmpty(App.Secrets.GitHubClientId)) { app.UseGitHubAuthentication( clientId: App.Secrets.GitHubClientId, clientSecret: App.Secrets.GitHubClientSecret); } AntiForgeryConfig.UniqueClaimTypeIdentifier = ClaimTypes.NameIdentifier; } } }

This is an implementation of an OWIN startup class. When the OWIN runtime fires up, it looks via Reflection for a class named Startup class with a Configuration(IAppBuilder app) method and when it finds it executes that method.

ConfigureAuth() then configures Cookie Authentication for local logins and the external providers for Google, Twitter and Github.  I use a custom ApplicationConfiguration class to hold my secret values in a JSON file using my ApplicationConfiguration class (mainly to keep them out of the the GitHub repo), but you can either hardcode these values or read them from a configuration setting.

Each provider requires what amounts to an application ID and a secret key that you have to configure. In order to use external providers you have to create an application for each at the providers Developer Web site  and then pick up the keys these applications generate. Here are links to each of those (assumes you’re logged in to each site):

Note that you can pick and choose your providers. You can use only Cookie authentication, or only externals or both in combination as I’m doing on this site.

Customizing the AccountController

All of the code I’ll be discussing in relation to these logins is located in an MVC controller – specifically the AccountController. There are really 3 main set of features that need to implemented – local logins, external logins and the actual sign in/out operations. Here’s roughly what this looks like in AccountController class:

AccountControllerOverview

There are also a few more traditional Controller actions that deal with some of the supporting feature of my implementation – password recovery and account activation specifically. This is my entire implementation, so as you can see it’s considerably simpler – and less featured – than the full (overkill?) Identity implementation you get in a default full features MVC site which is perfect for this post.

Signing in and Out

I’m going to start with signing in and out first, because it’s a core feature that will be used by all the the others operations. A user is signed in whenever either a local or external login succeeds, and this process essentially creates the authentication Cookie that identifies the user and allows the Identity framework to figure out whether the user is already logged in and setup the User Principal object for each request.

If you’ve used FormsAuthentication in ASP.NET you know that there’s a global object that handles management of the user tracking cookie that associates a user with an account. OWIN has its own version of an authentication manager in the IAuthenticationManager interface which is attached to the HttpContext object. To get a reference to it you can use:

HttpContext.GetOwinContext().Authentication;

This object handles creation and deleting of the secure cookie that is used to track the user through the site. The identity cookie is used to track all logged in users, regardless of whether they logged in locally with a username and password or using an external provider like Google. Once a user is authenticated, the SignIn method is called to create the cookie. On subsequent requests, OWIN based Identity subsystem then picks up the Cookie and authorizes the user the appropriate IPrinciple (a ClaimsPrinciple with a ClaimsIdentity) based User whenever the user accesses your site.

Identity sign-ins work with ClaimsIdentity objects, which contains user information stored in claims. The claims provide the user ID and names and any other information you want to store with the authenticated user as cached state.

To simplify sign-in I use a couple of helper functions that look like this:

public void IdentitySignin(AppUserState appUserState, string providerKey = null, bool isPersistent = false)
{
    var claims = new List<Claim>();

    // create required claims
    claims.Add(new Claim(ClaimTypes.NameIdentifier, appUserState.UserId));
    claims.Add(new Claim(ClaimTypes.Name, appUserState.Name));

    // custom – my serialized AppUserState object
    claims.Add(new Claim("userState", appUserState.ToString()));

    var identity = new ClaimsIdentity(claims, DefaultAuthenticationTypes.ApplicationCookie);

    AuthenticationManager.SignIn(new AuthenticationProperties()
    {
        AllowRefresh = true,
        IsPersistent = isPersistent,
        ExpiresUtc = DateTime.UtcNow.AddDays(7)
    }, identity);
}

public void IdentitySignout()
{
    AuthenticationManager.SignOut(DefaultAuthenticationTypes.ApplicationCookie,
                                    DefaultAuthenticationTypes.ExternalCookie);
}

private IAuthenticationManager AuthenticationManager
{
    get { return HttpContext.GetOwinContext().Authentication; }
}

SignIn/SignOut

The key methods are SignIn() and SignOut() on the AuthenticationManager, which create or delete the application cookies on the executing request. SignIn() takes an Identity object that includes any claims you have assigned to it. This identity is what you also get back once the user is logged in and you look at Context.User.Identity later to check for authorization.

A Word about AppUserState

Note I’m using an application specific AppUserState class to represent the logged in user’s state which is then added to the Identity’s claims! This is a custom object in my application that basically holds basic User that are the bare minimum needed by the application to display user info like name, admin status, theme etc. This object is persisted and cached inside of the ClaimsIdentity claims and therefore in the cookie, so that the data is available without having to look up a user in the database for each request.

Above I show how the AppUserState is persisted in the identity cookie. For retrieval and storage in a property on the controller,  I have a base controller that has an internal AppUserState property that is loaded up from a valid ClaimsPrincipal when a request comes in in BaseController.Initialize():

protected override void Initialize(RequestContext requestContext)
{
    base.Initialize(requestContext);

    // Grab the user's login information from Identity
    AppUserState appUserState = new AppUserState();
    if (User is ClaimsPrincipal)
    {
        var user = User as ClaimsPrincipal;
        var claims = user.Claims.ToList();

        var userStateString = GetClaim(claims, "userState");
        //var name = GetClaim(claims, ClaimTypes.Name);
        //var id = GetClaim(claims, ClaimTypes.NameIdentifier);

        if (!string.IsNullOrEmpty(userStateString))
            appUserState.FromString(userStateString);
    }
    AppUserState = appUserState;
            
    ViewData["UserState"] = AppUserState;
    ViewData["ErrorDisplay"] = ErrorDisplay;
}

The net effect is that anywhere within Controller and most Views this AppUserState object is available for user info display or visual display options.

This approach made great sense when I was using FormsAuthentication because you could effectively only store a single string value which was the serialized AppUserState value. But now ClaimsIdentity can contain multiple values as claims explicitly as a dictionary so it may be much cleaner to simply store any values as claims on the ClaimsIdentity. In the future the AppUserState code could probably be abstracted away, using a custom ClaimsIdentity instead that knows how to persist and retrieve its state from the attached claims instead. I’ll leave that exercise for another day though because AppUserState is used widely in this application.

Either way I want to make it clear that AppUserState is a custom implementation and an application specific implementation detail for my application.

Local Logins using Cookies

There are two steps for each login type: Creating the user and account initially, and then actually logging in the user. So lets start with the registration process for local logins.

Here’s what the User Registration form looks like:

RegistrationForm

The top of the form holds the local login, while the bottom has the external provider logins. From this form you can choose to sign up with an external login, which fills the local user registration form with any data that might be received from the external provider. Google provides the email address, GitHub both email and name, and Twitter provides only the name for example. In either case a new user is created in the application.

The code that responds to the Register button essentially creates a new account (if there are no validation errors), and signs the user in.

Here’s what the local user login code looks like (keep in mind this application specific):

[AcceptVerbs(HttpVerbs.Post)]
[ValidateAntiForgeryToken]
public ActionResult Register(FormCollection formVars)
{
    string id = formVars["Id"];
    string confirmPassword = formVars["confirmPassword"];

    bool isNew = false;
    User user = null;
    if (string.IsNullOrEmpty(id) || busUser.Load(id) == null)
    {
        user = busUser.NewEntity();
        user.InActive = true;
        isNew = true;
    }
    else
        user = busUser.Entity;
            
    UpdateModel<User>(busUser.Entity,
        new string[] { "Name", "Email", "Password", "Theme" });
            
    if (ModelState.Count > 0)
        ErrorDisplay.AddMessages(ModelState);


    if (ErrorDisplay.DisplayErrors.Count > 0)
        return View("Register", ViewModel);

    if (!busUser.Validate())
    {
        ErrorDisplay.Message = "Please correct the following:";
        ErrorDisplay.AddMessages(busUser.ValidationErrors);
        return View("Register", ViewModel);
    }

    if (!busUser.Save())
    {
        ErrorDisplay.ShowError("Unable to save User: " + busUser.ErrorMessage);
        return View("Register", ViewModel);
    }

    AppUserState appUserState = new AppUserState();
    appUserState.FromUser(user);
    IdentitySignin(appUserState, appUserState.UserId);            

    if (isNew)
    {
        SetAccountForEmailValidation();
        ErrorDisplay.HtmlEncodeMessage = false;
        ErrorDisplay.ShowMessage(@"Thank you for creating an account...");
        return View("Register", ViewModel);
    }


    return RedirectToAction("New","Snippet");
}  

This code really isn’t very different from how we used to do it previously. You check to see if the saved is a user we already have in the system and if so update her, or else create a new one and update the new user instead.

The key item here that is different is simply that I set the AppUserState as described before and then create call IdentitySignIn() to authenticate that user. On subsequent hits I then get a Context.User with the ClaimsIdentity for that user.

Logging in a Local Account

Once an account is registered you can actually log in. Here’s what the login form looks like:

LogInform

The code to handle the login looks like this:

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult LogOn(string email, string password, bool rememberMe, string returnUrl, bool emailPassword)
{
    if (emailPassword)
        return EmailPassword(email);

    var user = busUser.ValidateUserAndLoad(email, password);
    if (user == null)
    {
        ErrorDisplay.ShowError(busUser.ErrorMessage);
        return View(ViewModel);
    }

    AppUserState appUserState = new AppUserState()
    {
        Email = user.Email,
        Name = user.Name,
        UserId = user.Id,
        Theme = user.Theme,
        IsAdmin = user.IsAdmin
    };
    IdentitySignin(appUserState, user.OpenId, rememberMe);

    if (!string.IsNullOrEmpty(returnUrl))
        return Redirect(returnUrl);

    return RedirectToAction("New", "Snippet", null);
}

Again as you may guess, the code here simply looks up the username and password and if valid, updates the AppUserState object and then calls IdentitySignin() to log the user in.

Both of these workflows are not different at all from what I used to do with FormsAuthentication. The only real difference is that I’m calling  IdentitySignin() instead of FormsAuthentication.Authenticate().

Now on to the fun stuff that you couldn’t easily do before – external logins.

Linking to an External Login

As you can see in the screen shots above both the registration and login forms support using external providers to handle authentication of an account. For the registration form the external login can either be performed at the beginning of registration to pre-fill the registration information from the external provider, or by attaching the external login to an existing local account.

For external logins, when you click any of the provider buttons you are redirected to the provider site (Google, Twitter, GitHub), which checks whether your account is logged in. If it isn’t you’re shuttled to the providers log in page on their server where you can log in and/or specify what kind of rights you want to give to the application that is requesting the login (ie. my site). After you click accept, the server fires a callback request on your server and provides the Claims that the provider makes available. Typically this is a provider key (an identifier for your user’s login) as well as either name or email or both.

Advertisement

External logins are handled via an OAuth2 flow that is managed internally by the OWIN authentication pipeline in ASP.NET. When the requests are fired off they include a callback url which fires first into the OWIN handler. The callback URL is /signin-google or /signin-twitter or /signin-github.

Both creation of an initial account link between a local account and the external account as well as logging has a two endpoint request flow: One to actually start the remote authentication process via a Challenge operation (which is a Redirect really), and one to receive the callback when the authentication is complete.

Here’s the first method that initiates the Linking of an External Account by Challenging and Redirecting to the provider:

[AllowAnonymous]
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> ExternalLinkLogin(string provider)
{
    // Request a redirect to the external login provider to link a login for the current user
    return new ChallengeResult(provider, Url.Action("ExternalLinkLoginCallback"), AppUserState.UserId);
}

ChallengeResult is a helper class that is part of the stock ASP.NET MVC default controller implementation from which I simply copied it:

private const string XsrfKey = "CodePaste_$31!.2*#";

public class ChallengeResult : HttpUnauthorizedResult { public ChallengeResult(string provider, string redirectUri) : this(provider, redirectUri, null) { } public ChallengeResult(string provider, string redirectUri, string userId) { LoginProvider = provider; RedirectUri = redirectUri; UserId = userId; } public string LoginProvider { get; set; } public string RedirectUri { get; set; } public string UserId { get; set; } public override void ExecuteResult(ControllerContext context) { var properties = new AuthenticationProperties { RedirectUri = RedirectUri }; if (UserId != null) properties.Dictionary[XsrfKey] = UserId; var owin = context.HttpContext.GetOwinContext(); owin.Authentication.Challenge(properties, LoginProvider); } }

The key of this class is the OWIN Authentication.Challenge() method which issues a 302 Redirect to the provider to handle the login with a URL that includes the Redirect URL and some state information. The state in this case is an a user identifier (our user id in this case) that lets us check and make sure the result is the one we’re interested in when it comes back.

When the provider has validated (or failed to validate) the user, it makes  callback to your server using a specific URL. The URL that is called back to is ~/signin-google or ~/signin-github or ~/signin-twitter for example. The OWIN pipeline internally handles this callback for you and after it has validated redirects to your actual endpoint so you can handle the authenticated request.

To illustrate check out this Fiddler trace of a Registration Link request, when already logged into my Google account:

FiddlerRegistration

Notice all the 302 requests. The first request is initiated by your code using the ChallengeResult which redirects to Google. Google then redirects back to the OWIN internal endpoint to handle the provider OAuth parsing and finally the OWIN pipeline calls into your code with ExternalLinkLoginCallback().

Here’s the Callback method that is called when the Link process is complete:

[AllowAnonymous]
[HttpGet]
public async Task<ActionResult> ExternalLinkLoginCallback()
{
    // Handle external Login Callback
    var loginInfo = await AuthenticationManager.GetExternalLoginInfoAsync(XsrfKey, AppUserState.UserId);
    if (loginInfo == null)
    {
        IdentitySignout(); // to be safe we log out
        return RedirectToAction("Register", new {message = "Unable to authenticate with external login."});
    }

    // Authenticated!
    string providerKey = loginInfo.Login.ProviderKey;
    string providerName = loginInfo.Login.LoginProvider;

    // Now load, create or update our custom user

    // normalize email and username if available
    if (string.IsNullOrEmpty(AppUserState.Email))
        AppUserState.Email = loginInfo.Email;
    if (string.IsNullOrEmpty(AppUserState.Name))
        AppUserState.Name = loginInfo.DefaultUserName;

    var userBus = new busUser();
    User user = null;

    if (!string.IsNullOrEmpty(AppUserState.UserId))
        user = userBus.Load(AppUserState.UserId);

    if (user == null && !string.IsNullOrEmpty(providerKey))
        user = userBus.LoadUserByProviderKey(providerKey);

    if (user == null && !string.IsNullOrEmpty(loginInfo.Email))
        user = userBus.LoadUserByEmail(loginInfo.Email);

    if (user == null)
    {
        user = userBus.NewEntity();
        userBus.SetUserForEmailValidation(user);
    }

    if (string.IsNullOrEmpty(user.Email))
        user.Email = AppUserState.Email;

    if (string.IsNullOrEmpty(user.Name))
        user.Name = AppUserState.Name ?? "Unknown (" + providerName + ")";


    if (loginInfo.Login != null)
    {
        user.OpenIdClaim = loginInfo.Login.ProviderKey;
        user.OpenId = loginInfo.Login.LoginProvider;
    }
    else
    {
        user.OpenId = null;
        user.OpenIdClaim = null;
    }

    // finally save user inf
    bool result = userBus.Save(user);

    // update the actual identity cookie
    AppUserState.FromUser(user);
    IdentitySignin(AppUserState, loginInfo.Login.ProviderKey);

    return RedirectToAction("Register");
}

There’s a fair bit of code in this snippet, but… most of the code is application specific. The most important piece of the code is the first line that is responsible for returning the LoginInfo object that contains the providerKey and any other claims that the provider makes available. Typically those are a name and/or email. The rest of the code then checks to see if the user already exists and updates it or if not creates a new one and saves it. If all goes where IdenitySignin() is called to effectively log the user in by creating the cookie and authorized ClaimsIdentity on subsequent requests.

When done the user is logged in, but I want to re-display the Registration form to show the external account registration:

LinkedAccountDisplay

Logging in with an External Login

Finally we still have to hook up the logic to log in with an external provider. As with the Linking of the provider, this is a two step process – fire off the initial authentication request. As with the link operation a challenge request handles this:

[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public ActionResult ExternalLogin(string provider)
{
    string returnUrl = Url.Action("New","Snippet",null);            
    return new ChallengeResult(provider, Url.Action("ExternalLoginCallback",
"Account", new
{ ReturnUrl = returnUrl })); }

Again the same 302 requests are issued to eventually bring back the result into the OWIN pipeline which in turn redirects to the ExternalLoginCallback() method:

[AllowAnonymous]
public async Task<ActionResult> ExternalLoginCallback(string returnUrl)
{
    var loginInfo = await AuthenticationManager.GetExternalLoginInfoAsync();
    if (loginInfo == null)
        return RedirectToAction("LogOn");

    // AUTHENTICATED!
    var providerKey = loginInfo.Login.ProviderKey;


    // Aplication specific code goes here.
    var userBus = new busUser();
    var user = userBus.ValidateUserWithExternalLogin(providerKey);
    if (user == null)
    {
        return RedirectToAction("LogOn", new
        {
            message = "Unable to log in with " + loginInfo.Login.LoginProvider +
                        ". " + userBus.ErrorMessage
        });
    }

    // store on AppUser
    AppUserState appUserState = new AppUserState();
    appUserState.FromUser(user);
    IdentitySignin(appUserState, providerKey, isPersistent: true);

    return Redirect(returnUrl);
}

The process is similar  to the link operation in the previous example except the login code simply checks to see if the user is valid and if so logs him in using the now familiar IdentySignin() method. 

The user is logged in at this point so simply redirect where we want to go.

Unlink an Account

Finally if you want to unlink the account, all I have to do is remove the link in the domain model. In my case I remove the field values for OpenId and OpenIdClaim, so that any subsequent login using an external account will fail as we won’t match the provider key provided by the external provider.

Here’s the code for the unlink operation:

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult ExternalUnlinkLogin()
{
    var userId = AppUserState.UserId;
    var user = busUser.Load(userId);
    if (user == null)
    {
        ErrorDisplay.ShowError("Couldn't find associated User: " + busUser.ErrorMessage);
        return RedirectToAction("Register", new { id = userId });
    }
    user.OpenId = string.Empty;
    user.OpenIdClaim = string.Empty;

    if (busUser.Save())
        return RedirectToAction("Register", new { id = userId });

    return RedirectToAction("Register", new { message = "Unable to unlink OpenId. " + busUser.ErrorMessage });
}

After this code executes, the external account link is gone. I show the register page again and this time the external link account is no longer shown replaced again by the three external providers one of which can be hooked up.

And that’s all the base operations!

A quick Review

You may think that that’s a lot of code you have to write for something that should be pretty simple. But keep in mind that this code includes some application specific logic that I provided here in order to provide a somewhat realistic context. While the actual underlying Identity code is pretty small (and I’ve highlighted the core requirements in the snippets in bold), the code shown here is probably the bare minimum you need for a basic self managed user management implementation you probably want to run in a real application.

If you want to see the complete controller code you can check it out on Github.

In summary you basically deal with:

IdentitySignIn, IdentitySignOut for standard LogIn/LogOut functions

Implementing ExternalLinkLogin() and ExternalLinkLoginCallback()

Implementing ExternalLogin() and ExternalLoginCallback()

The various ExternalXXXX methods follow a simple boilerplate of using ChallengeResult for the initial request and calling GetExternalLoginInfoAsync() to pick up the result data. It’s really fairly straight forward as long as you know the pieces you actually need to implement.

Minimal Code Summary

Because the code above is quite lengthy, here’s a summary of just the relevant parts you have to implement with just the basic pieces:

Startup Configuration Class

public partial class Startup
{
    public void Configuration(IAppBuilder app)
    {
        ConfigureAuth(app);
    }

    // For more information on configuring authentication, please visit http://go.microsoft.com/fwlink/?LinkId=301864
    public void ConfigureAuth(IAppBuilder app)
    {
        app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);

        // Enable the application to use a cookie to store information for the signed in user
        app.UseCookieAuthentication(new CookieAuthenticationOptions
        {
            AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
            LoginPath = new PathString("/Account/LogOn")
        });
            
        // these values are stored in CodePasteKeys.json
        // and are NOT included in repro - autocreated on first load
        if (!string.IsNullOrEmpty(App.Secrets.GoogleClientId))
        {
            app.UseGoogleAuthentication(                
                clientId: App.Secrets.GoogleClientId,
                clientSecret: App.Secrets.GoogleClientSecret);
        }

        if (!string.IsNullOrEmpty(App.Secrets.TwitterConsumerKey))
        {
            app.UseTwitterAuthentication(
                consumerKey: App.Secrets.TwitterConsumerKey,
                consumerSecret: App.Secrets.TwitterConsumerSecret);
        }

        if (!string.IsNullOrEmpty(App.Secrets.GitHubClientId))
        {
            app.UseGitHubAuthentication(
                clientId: App.Secrets.GitHubClientId,
                clientSecret: App.Secrets.GitHubClientSecret);
        }

        AntiForgeryConfig.UniqueClaimTypeIdentifier = ClaimTypes.NameIdentifier;
    }
}

IdentitySignIn/SignOut

public void IdentitySignin(string userId, string name, string providerKey = null, bool isPersistent = false)
{
    var claims = new List<Claim>();

    // create *required* claims
    claims.Add(new Claim(ClaimTypes.NameIdentifier, userId));
    claims.Add(new Claim(ClaimTypes.Name, name));

    var identity = new ClaimsIdentity(claims, DefaultAuthenticationTypes.ApplicationCookie);

    // add to user here!
    AuthenticationManager.SignIn(new AuthenticationProperties()
    {
        AllowRefresh = true,
        IsPersistent = isPersistent,
        ExpiresUtc = DateTime.UtcNow.AddDays(7)
    }, identity);
}

public void IdentitySignout()
{
    AuthenticationManager.SignOut(DefaultAuthenticationTypes.ApplicationCookie,
                                  DefaultAuthenticationTypes.ExternalCookie);
}

private IAuthenticationManager AuthenticationManager
{
    get
    {
        return HttpContext.GetOwinContext().Authentication;
    }
}

External Link Login

[AllowAnonymous] [HttpPost] [ValidateAntiForgeryToken] public async Task<ActionResult> ExternalLinkLogin(string provider) //Google,Twitter etc. { return new ChallengeResult(provider, Url.Action("ExternalLinkLoginCallback"), userId); } [AllowAnonymous] [HttpGet] public async Task<ActionResult> ExternalLinkLoginCallback() { // Handle external Login Callback var loginInfo = await AuthenticationManager.GetExternalLoginInfoAsync(XsrfKey,userId); if (loginInfo == null) { IdentitySignout(); // to be safe we log out return RedirectToAction("Register", new {message = "Unable to authenticate with external login."}); } // Authenticated! string providerKey = loginInfo.Login.ProviderKey; string providerName = loginInfo.Login.LoginProvider;


// Your code here…



// when all good make sure to sign in user
IdentitySignin(userId, name, providerKey, isPersistent: true);

return RedirectToAction("Register"); }

This code and the External Login also require the ChallengeResult helper class:

// Used for XSRF protection when adding external logins
private const string XsrfKey = "CodePaste_$31!.2*#";

public class ChallengeResult : HttpUnauthorizedResult
{
    public ChallengeResult(string provider, string redirectUri)
        : this(provider, redirectUri, null)
    {  }

    public ChallengeResult(string provider, string redirectUri, string userId)
    {
        LoginProvider = provider;
        RedirectUri = redirectUri;
        UserId = userId;
    }

    public string LoginProvider { get; set; }
    public string RedirectUri { get; set; }
    public string UserId { get; set; }

    public override void ExecuteResult(ControllerContext context)
    {
        var properties = new AuthenticationProperties { RedirectUri = RedirectUri };
        if (UserId != null)
            properties.Dictionary[XsrfKey] = UserId;
                
        var owin = context.HttpContext.GetOwinContext();
        owin.Authentication.Challenge(properties, LoginProvider);
    }
}

External Login

[HttpPost] [AllowAnonymous] [ValidateAntiForgeryToken] public ActionResult ExternalLogin(string provider) { string returnUrl = Url.Action("New","Snippet",null); return new ChallengeResult(provider, Url.Action("ExternalLoginCallback",
"Account", new { ReturnUrl = returnUrl })); } [AllowAnonymous] public async Task<ActionResult> ExternalLoginCallback(string returnUrl) { if ( string.IsNullOrEmpty(returnUrl) ) returnUrl = "~/"; var loginInfo = await AuthenticationManager.GetExternalLoginInfoAsync(); if (loginInfo == null) return RedirectToAction("LogOn"); // AUTHENTICATED! var providerKey = loginInfo.Login.ProviderKey; // Your code goes here.

// when all good make sure to sign in user
IdentitySignin(userId, name, providerKey, isPersistent: true);
return Redirect(returnUrl); }

Local Account Registration

[AcceptVerbs(HttpVerbs.Post)]
[ValidateAntiForgeryToken]
public ActionResult Register(FormCollection formVars)
{

// Capture User Data and Create/Update account

// when all good make sure to sign in user
IdentitySignin(userId, name, appUserState.UserId);
return RedirectToAction("New","Snippet",null); }

Local Account Login

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult LogOn(string email, string password, bool rememberMe, string returnUrl, bool emailPassword)
{
    // validate your user 
    
// if all OK sign in
IdentitySignin(userId, name, user.OpenId, rememberMe); return RedirectToAction("New", "Snippet", null); } public ActionResult LogOff() { IdentitySignout(); return RedirectToAction("LogOn"); }

Summary

For new projects it’s probably a good idea to think long and hard whether you want to roll your own, or just stick with the stock Identity implementation. Personally I’m not a fan of the EF based implementation that comes out of the box. While it can be customized you still have to massively tweak the UI to make things fit with your application and probably add and remove fields from the Identity models. Worst of all though is the default EF dependency that doesn’t easily integrate into even another EF model. Personally, I prefer to have user management integrated as part of my own domain models of the application rather than linking users to customers for example.

The other advantage of using your own is that you’re not stuck to Microsoft’s whims. We’ve gone through too many authentication frameworks with Microsoft seemingly changing the model with every major release cycle. Granted the new Identity is probably the closest thing that they ever had that is actually usable out of the box, but I’m still wary to rely on anything that Microsoft sticks in the box in this regard for fear of getting it yanked out from under me in the next version. Using my own I don’t have to worry about at least the user management features that can travel with my applications. With my own implementation I might have a little more setup to do, but at least I have a standard way that I can easily carry forward through applications of any version of ASP.NET.

In the end I have to say that although it took me quite a while to get my head around EXACTLY what I needed to implement, was not very difficult. The hard part was just finding the right information on what you need to implement by digging into the generate code and ripping out the relevant pieces. Once you know what’s needed the implementation of the actual code pieces is relatively straight forward.

I hope that this post provides a good summary of what’s required and especially with the Minimal Code Summary it’ll be easier to create the skeleton code required to plug your own domain driven user management into the core identity framework.

Resources

Make Donation
Posted in ASP.NET  MVC  OWIN  

The Voices of Reason


 

Jonathas Morais
April 29, 2015

# re: Adding minimal OWIN Identity Authentication to an Existing ASP.NET MVC Application

What about a SAML integration?
I'm keen to integrate .NET apps with an Identity Server, which in turn contains several Services and Identity Providers but there's not much material out there explaining how to do it in .NET.

Nice post by the way!

Thanks.

Jeff P.
April 29, 2015

# re: Adding minimal OWIN Identity Authentication to an Existing ASP.NET MVC Application

I went through this exercise with my open source forum app, and mostly it works pretty well. However, something changed dramatically in v3.x of the OWIN stack, and it breaks. I haven't been able to figure out why and it's completely frustrating. I assume it's something configuration related, but I can't nail it down.

http://stackoverflow.com/questions/28704700/updating-owin-from-2-1-to-3-0-1-breaks-external-auth

I notice that you're calling GetExternalLoginInfoAsync from the AuthenticationManager. I'm using GetAuthenticationResult. Any idea what the difference is?

Rick Strahl
April 29, 2015

# re: Adding minimal OWIN Identity Authentication to an Existing ASP.NET MVC Application

@Jeff - I lifted the code from the original templates, so I'm just doing what the default implementation would do which I assume is safe. When I run into problems like this sadly this is my go-to solution: Check what the default templates do and then adjust my code to match.

I didn't look closely at the lower level AuthenticationManager methods to see what's needed given that the code does what it's supposed to using ExternalLinkLoginInfoAsync(). Have you tied using that instead? Also notice the use of app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie) instead of your app.SetDefaultSignInAsAuthenticationType().

Bummer to hear that this broke for you though - that really shouldn't be happening with components that are as integral as this.

Jeff P.
April 29, 2015

# re: Adding minimal OWIN Identity Authentication to an Existing ASP.NET MVC Application

Yeah, it's weird that a major version update would break stuff. But your article gives me some ideas to experiment with, so I'll see what I can do. I'll post back here if I discover anything interesting.

Matt
April 30, 2015

# Great Article

Great Article Rick! I look forward to putting some of this is use in my up coming project.

Jeff P.
May 02, 2015

# re: Adding minimal OWIN Identity Authentication to an Existing ASP.NET MVC Application

I'm starting to see that the things I'm not using, that you are referencing, are in the Microsoft.AspNet.Identity.Owin package, which to this point I haven't added a reference to. For example the app builder extension method for UseExternalSignInCookie(). That's pretty frustrating that you need more dependencies and that the existing code is broken just from a version upgrade. Will let you know if this gets me there...

Firegarden
August 24, 2015

# re: Adding minimal OWIN Identity Authentication to an Existing ASP.NET MVC Application

Your tutorial was the best Rick. I tried to get this to work quickly with every other tutorial out there but could not get past the need to expose

IAuthenticationManager AuthenticationManager
{
get { return HttpContext.GetOwinContext().Authentication; }
}

Also if you don't have using System.Web which is common in an MVC project you will have an even harder time. Word to your mother.
 


West Wind  © Rick Strahl, West Wind Technologies, 2005 - 2015