Story Details for articles
5 - DocumentDB - Golf Tracker Article - A word on Identity
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.
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.
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.
In this class I had to just change a single line of code, from this:
... to this.
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.
In addition, I added an EmailService class in this file that would handle sending emails for confirmation of tokens.
Don't forget to update the web.config smtp settings.
I just modified two lines in the ConfigureAuth method.
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.
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.
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.
I had specific requirements for both Registration and Login procedures, that I will outline here.
- Enter credentials in sign up form
- Save information to database (DocumentDB)
- Require email confirmation via email message
- Perform email address validation from email message link
- Enter credentials
- Make sure email address is confirmed, otherwise display a message and link to validate email
- 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.
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.
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.
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.
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.
Invalid TokenSo 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.
- Register - line 22 - as the raw token from the creation method
- Register - line 25 - as the UrlEncoded(code) token
- ConfirmEmail - line 4 - as it comes into the method, in whatever state it's in
- ConfirmEmail - line 13 - as the token is UrlDecoded(code)
The AccountController/ConfirmEmail method.
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):
- SNLEBi16nGQR7Yob yMz83Mrq9G8/RkBJGHhCqfwwiGVkhanPH33EOFnHr0QR16u/gdLHQJf
- 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.
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.
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!
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.