Story Details for articles

5 - DocumentDB - Golf Tracker Article - A word on Identity

kahanu
Author:
Version:
Views:
2790
Date Posted:
5/28/2015 1:31:57 PM
Date Updated:
5/28/2015 1:31:57 PM
Rating:
0/0 votes
Framework:
DocumentDB, AngularJS, Identity
Platform:
Windows
Programming Language:
C#, JavaScript
Technologies:
DocumentDB, AngularJS, Identity
Tags:
documentdb, angularjs, identity
Demo site:
Home Page:
https://github.com/kahanu/GolfTracker.DocumentDB
Share:

Authentication and DocumentDB

Authentication and authorization is always an important part of any application, and it's not always easy to implement.  Microsoft has made it fairly easy with their Visual Studio item templates to incorporate a specific authentication scheme into your application based on your preferences.  But if you want to do some modifications to customize the experience it can be a little daunting.

I will demonstrate a typical scenario for registering and authenticating users into your application using a new standard process which includes a secondary authentication mechanism, like email verification before allowing users to login.

DocumentDB.AspNet.Identity

For this application I will use an open source assembly that I found on github which customized Microsoft's Identity package, and integrate it into DocumentDB.

https://github.com/tracker086/DocumentDB.AspNet.Identity

Since I'm using WebApi and not something like ASP.NET MVC as the endpoint for authentication, I needed to add additional methods to the AccountController in order to cover some missing capabilities, like ForgotPassword, ConfirmEmail, etc.

I found that Adrian Fernandez's implementation of DocumentDB.AspNet.Identity fulfilled all the necessary functions for use and was a delight to use.

But I found that there were several gotchas that I had to overcome to get authentication to work the way I needed for this application.

Side note: Identity did not need to be used in this way where it stores Users in DocumentDB.  You can use it with a SQL Server database and just point to an endpoint that uses that version of Identity for authentication.  I just decided to try this and see how it will work, and it works fine.

Identity used with DocumentDB

Just by adding the DocumentDB.AspNet.Identity assembly to my project didn't make the application ready to use, I did have to make additional modifications.  None of which were very complicated.

Essentially what I need to do is replace all instances of where the Identity.EntityFramework assembly is being used, with this new one.  A good way to find out what code is affected is to comment out the EntityFramework using reference at the top of several classes.

App_Start/IdentityConfig.cs

In this class I had to just change a single line of code, from this:

01.public class ApplicationUserManager : UserManager<ApplicationUser>
02.{
03.    public ApplicationUserManager(IUserStore<ApplicationUser> store)
04.        : base(store)
05.    {
06.    }
07. 
08.    public static ApplicationUserManager Create(IdentityFactoryOptions<ApplicationUserManager> options, IOwinContext context)
09.    {
10.        var manager = new ApplicationUserManager(new UserStore<ApplicationUser>(context.Get<ApplicationDbContext>()));

... to this.

01.public class ApplicationUserManager : UserManager<ApplicationUser>
02.{
03.    public ApplicationUserManager(IUserStore<ApplicationUser> store)
04.        : base(store)
05.    {
06.    }
07. 
08.    public static ApplicationUserManager Create(IdentityFactoryOptions<ApplicationUserManager> options, IOwinContext context)
09.    {
10.        string endpoint = AppSettingsConfig.EndPoint;
11.        string authkey = AppSettingsConfig.AuthKey;
12.        string db = AppSettingsConfig.Db;
13. 
14.        var manager = new ApplicationUserManager(new UserStore<ApplicationUser>(new Uri(endpoint), authkey, db));

I changed line 10 in the first version, to lines 10 - 14 in the updated version.  Notice how this new UserStore class takes a DocumentDB Uri endpoint and authkey, and the database name.

This works when the using reference is included in the top of the file.

using DocumentDB.AspNet.Identity;

In addition, I added an EmailService class in this file that would handle sending emails for confirmation of tokens.

public class EmailService : IIdentityMessageService
{
    public Task SendAsync(IdentityMessage message)
    {
        // Credentials:
        var sentFrom = "sample@golftrackerdemo.com";
 
        // Configure the client:
        System.Net.Mail.SmtpClient client =
            new System.Net.Mail.SmtpClient();
 
        // Create the message:
        var mail =
            new System.Net.Mail.MailMessage();
 
        mail.From = new System.Net.Mail.MailAddress(sentFrom);
        mail.To.Add(message.Destination);
        mail.Subject = message.Subject;
        mail.Body = message.Body;
 
        // Send:
        return client.SendMailAsync(mail);
    }
}

Don't forget to update the web.config smtp settings.

​
App_Start/Startup.Auth.cs

I just modified two lines in the ConfigureAuth method.

1.public void ConfigureAuth(IAppBuilder app)
2.{
3.    // Configure the db context and user manager to use a single instance per request
4.    //app.CreatePerOwinContext(ApplicationDbContext.Create);
5.    //app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
6. 
7.    app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
8.    app.CreatePerOwinContext<ApplicationSignInManager>(ApplicationSignInManager.Create);

Models/IdentityModels.cs

 

​​In this class I needed to include a second method that took two arguments in the method signature, just so it would compile.  This new method would satisfy some existing methods that use Social Media authentication providers.

01.public class ApplicationUser : IdentityUser
02.{
03. 
04.    public async Task<ClaimsIdentity> GenerateUserIdentityAsync(UserManager<ApplicationUser> manager)
05.    {
06.        // Note the authenticationType must match the one defined in CookieAuthenticationOptions.AuthenticationType
07.        var userIdentity = await manager.CreateIdentityAsync(this, DefaultAuthenticationTypes.ApplicationCookie);
08.        // Add custom user claims here
09.        return userIdentity;
10.    }
11. 
12.    public async Task<ClaimsIdentity> GenerateUserIdentityAsync(UserManager<ApplicationUser> manager, string authenticationType)
13.    {
14.        // Note the authenticationType must match the one defined in CookieAuthenticationOptions.AuthenticationType
15.        var userIdentity = await manager.CreateIdentityAsync(this, authenticationType);
16.        // Add custom user claims here
17.        return userIdentity;
18.    }
19.}

In the AccountController class I did modify the Register method to not automatically sign the user in, but instead use email verification. (Code is shown later)

I also added some methods that aren't included out-of-the-box for WebApi projects.

  • ForgotPassword
  • ResetPassword
  • ConfirmEmail
  • ResendConfirmEmail

These methods normally exist in MVC AccountControllers, but I had to manually add them here and make modifications to them to work with Ajax calls.  Once that was all done, Identity with DocumentDB was complete.

So now I moved on to implement requirements for authentication.

Authentication Requirements

I had specific requirements for both Registration and Login procedures, that I will outline here.

Registration

  1. Enter credentials in sign up form
  2. Save information to database (DocumentDB)
  3. Require email confirmation via email message
  4. Perform email address validation from email message link

 Login

  1. Enter credentials
  2. Make sure email address is confirmed, otherwise display a message and link to validate email
  3. Authenticate if confirmed

I wanted to see if I could get this secondary authentication to work, and after some figuring, I was able to get it all to work.  As I mentioned, there were some gotchas, which I will explain.

Registration

When the user signs up and enters their email and password, after their data is stored in DocumentDB, I wanted to send them an email with a link for them to verify.  Clicking on this link in their email message would return them back to the website, where in the background a function would make an Api call to WebApi and validate their email and token.

If everything validates, then an "EmailConfirmed" property in their User record, would be updated from False to True.

Here's the Sign Up form after it's been submitted and the user record has been saved to DocumentDB.  We get a message to check our email to activate the account.



Let's see what this looks like in DocumentDB.
You'll see on line 5, the "EmailConfirmed" property is set to False.  This is simply a built-in feature of Identity and not a required attribute to use, but it's nice to have and I choose to use it.  You'll notice there's also a "PhoneNumberConfirmed" property where you can send a text to the user to validate their phone number.  Another nice feature. As the developer I now have to make sure that they cannot login until their email address has been confirmed.  So let's see what happens if I try to login before my email is confirmed.



This is exactly what I needed, but this was all done manually.  It doesn't come this way out of the box.  In order to capture whether the user's email is confirmed, I have to modify the OAuth provider in the WebApi project.  It is found here...



Inside this class I simply added a few lines of code.  I added lines 8 - 12.

01.if (user == null)
02.{
03.    context.SetError("invalid_grant", "The user name or password is incorrect.");
04.    return;
05.}
06. 
07.// I needed to add this in order to check if the email was confirmed when a user log on.
08.if (!user.EmailConfirmed)
09.{
10.    context.SetError("email_not_confirmed", "User did not confirm email.");
11.    return;
12.}

This would check that property for the user and if they weren't confirmed, it would return an error that I can then trap in my client-side code.

In my AngularJS website, the indexController handles the Login function. 

01.vm.submitLoginForm = function (isValid) {
02.    authService.login(vm.login).then(function (response) {
03.        vm.success = true;
04.        vm.isAuthenticated = true;
05.        $location.path("/home");
06.    }, function (err) {
07.        if (err.error === "email_not_confirmed") {
08.            vm.resendEmailConfirmationLinkIsVisible = true;
09.        }
10.        vm.message = err.error_description;
11.    });
12.};

I can then trap that error and display the message and a link the user can click that will re-send the email confirmation email message.  This helps make it very easy for the user to know what to do, if they forgot to check their email originally after signing up.

At this point, hopefully, the user will either check their email for the first message that was sent to confirm their email address, or click on this link, but either way they go to their email and see the new message.


Once they click the link, they will be sent back to the website where their credentials will either be confirmed or not.  Let's click the link and see what happens.



WTF?!?

Invalid Token

So something went wrong obviously, but what?  What does "Invalid Token" mean?  I spent a little over an hour trying to figure out why I was getting an "Invalid Token" error when trying to confirm the email token.

Somewhere along the line, something was breaking down.  One Stackoverflow response was to use HttpUtility.UrlEncode(code) when sending the code in the email.  This made sense since the token would be part of the email link Url.  But even that didn't fix the problem.

Then I thought, I need to UrlDecode(code) it in the ConfirmEmail method, but that too didn't solve the problem.  So I had to take matters into my own hands and solve this once and for all.

I knew that the token was the cause of the problem and it was probably due to encoding back and forth, so I captured the states of the token at different points in the pipeline and compared them side by side, and was finally able to solve the problem.

I'll go through where I captured the 4 states of the token.  They were all in the WebApi AccountController class.

Token Stages
  1. Register - line 22 - as the raw token from the creation method
  2. Register - line 25 - as the UrlEncoded(code) token
  3. ConfirmEmail - line 4 - as it comes into the method, in whatever state it's in
  4. ConfirmEmail - line 13 - as the token is UrlDecoded(code)

01.// POST api/Account/Register
02.[AllowAnonymous]
03.[Route("Register")]
04.public async Task<IHttpActionResult> Register(RegisterBindingModel model)
05.{
06.    if (!ModelState.IsValid)
07.    {
08.        return BadRequest(ModelState);
09.    }
10. 
11.    var user = new ApplicationUser() { UserName = model.Email, Email = model.Email };
12. 
13.    IdentityResult result = await UserManager.CreateAsync(user, model.Password);
14. 
15.    if (!result.Succeeded)
16.    {
17.        return GetErrorResult(result);
18.    }
19.    else
20.    {
21.        // Generate the Email Confirmation Token
22.        string code = await UserManager.GenerateEmailConfirmationTokenAsync(user.Id);
23. 
24.        // UrlEncode the token
25.        code = HttpUtility.UrlEncode(code);
26. 
27.        // Get the Host from the appSettings.  http://www.myclientsite.com
28.        string clientSite = AppSettingsConfig.ClientSite;
29. 
30.        // Build the Url that will be used for the link in the email message.
31.        var callbackUrl = clientSite + "/#/confirmemail?userId=" + user.Id + "&code=" + code;
32. 
33.        // Build the callback message for the email.
34.        var callbackMessage =
35.            "Please confirm your account by clicking <a href=\"" + callbackUrl + "\">here</a>";
36. 
37.        // Send the email. Remember to set the system.net/mailSettings/smtp/network configuration in the web.config.
38.        await UserManager.SendEmailAsync(user.Id, "Confirm your account", callbackMessage);
39.    }
40. 
41.    return Ok();
42.}


The AccountController/ConfirmEmail method.

01.[Route("confirmemail")]
02.[HttpGet]
03.[AllowAnonymous]
04.public async Task<IHttpActionResult> ConfirmEmail(string userId, string code)
05.{
06.    if (userId == null || code == null)
07.    {
08.        return BadRequest("UserId and/or Code is missing.");
09.    }
10.    try
11.    {
12.        // UrlDecode the token
13.        code = HttpUtility.UrlDecode(code);
14. 
15.        // Now confirm the email and it should work!
16.        var result = await UserManager.ConfirmEmailAsync(userId, code);
17.        if (result.Succeeded)
18.        {
19.            return Ok();
20.        }
21.        else
22.        {
23.            return GetErrorResult(result);
24.        }
25. 
26.    }
27.    catch (Exception)
28.    {
29. 
30.        throw;
31.    }
32.}


So I set break points at the various stages to capture the token in all 4 stages and then stacked them together to see if I could see a difference, and I did!

The token in it's various stages (it's truncated for readability):
  1. SNLEBi16nGQR7Yob+yMz83Mrq9G8/RkBJGHhCqfwwiGVkhanPH33EOFnHr0QR16u/gdLHQJf
  2. SNLEBi16nGQR7Yob%2byMz83Mrq9G8%2fRkBJGHhCqfwwiGVkhanPH33EOFnHr0QR16u%2fgdLHQJf
  3. SNLEBi16nGQR7Yob yMz83Mrq9G8/RkBJGHhCqfwwiGVkhanPH33EOFnHr0QR16u/gdLHQJf
  4. SNLEBi16nGQR7Yob yMz83Mrq9G8/RkBJGHhCqfwwiGVkhanPH33EOFnHr0QR16u/gdLHQJf

 We can see at Stage 1, the raw code is what it is, it has some non-alphanumeric characters like a plus sign (+) and a forward slash (/) which are expected.  (In Red)

Stage 2 UrlEncodes the token and those non-alphanumeric characters and now encoded.  This too was expected. (In Green)

Stage 3 is in the ConfirmEmail method as the token comes into the method signature.  Ah, notice how it's no longer UrlEncoded!  It almost looks like it's decoded, but not quite.  There's a space where the token in Stage 1 contained a (+).

Stage 4 shows the same thing as Stage 3 after it goes through the UrlDecode method.  3 and 4 are essentially identical, which means that are different from Stage 1.

So I did the simplest of things to make this work.  Here's the new ConfirmEmail method.

01.[Route("confirmemail")]
02.[HttpGet]
03.[AllowAnonymous]
04.public async Task<IHttpActionResult> ConfirmEmail(string userId, string code)
05.{
06.    if (userId == null || code == null)
07.    {
08.        return BadRequest("UserId and/or Code is missing.");
09.    }
10.    try
11.    {
12.        // UrlDecode the token
13.        code = HttpUtility.UrlDecode(code);
14. 
15.        // IMPORTANT STEP!!! The UrlDecode removes all '+' from the
16.        // originally generated token and replaces it with spaces ' '.
17.        // We have to put the + signs back.
18.        code = code.Replace(' ', '+');
19. 
20.        // Now confirm the email and it should work!
21.        var result = await UserManager.ConfirmEmailAsync(userId, code);
22.        if (result.Succeeded)
23.        {
24.            return Ok();
25.        }
26.        else
27.        {
28.            return GetErrorResult(result);
29.        }
30.    }
31.    catch (Exception)
32.    {
33. 
34.        throw;
35.    }
36.}

Line 18 contains the fix!  I just look for any space in the token and replace it with a (+) sign.  That fixed it!  Sheesh!

Now when I click on the email link let's see what I get.



At this point I'm confirmed and I have access to the application.  I can now login successfully!

Let's take a look at the User record in DocumentDB.


We can see now that the "EmailConfirmed" value on line 5 is set to "true".    When I log in I see the nav has changed.

One Last Thing Regarding CORS

Even though I've set CORS to be enabled site-wide in the WebApiConfig file, in your application you may notice that logging in is unsuccessful at first.  If you check F12 tools in the browser you see a message about a missing "Access-Control-Allow-Origin" header.  But why are we still getting this exception if CORS is enabled.

Authenticating (logging in) goes through the ApplicationOAuthProvider class, which is not in the same pipeline as the rest of the application and therefore not affected by the CORS implementation.

To fix this, we just have to add two lines of code to this class in the GrantResourceOwnerCredentials method.

1.public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
2.{
4.    // This article helped me track down the issue that even though CORS is enabled application-wide,
5.    // it still doesn't affect this OWIN component, so we have to enable it here also.
6.    string origins = AppSettingsConfig.CorsPolicyOrigins;
7.    context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new string[] { origins });

There's a link to an article that explains why maybe a little better than I just did.  But once you add this code, users will be able to login successfully!

Summary

Hopefully this article has shown you a method of making Identity work with DocumentDB, and how to get past some gotchas to customize various parts in order to make it work.

Comments

    No comments yet.

 

User Name:
(Required)
Email:
(Required)