Modern web applications have an explicit separation between the server and the client. Clients use AngularJS, ReactJS, EmberJS, and others. Servers use NodeJS, Java, and, .NET. Microsoft’s .NET platform is a strong, battle-proven server-side framework, and AngularJS is arguably the most popular client-side framework. They work seamlessly together, but adding solid, secure authentication can seem daunting. Leveraging Stormpath makes it easier than you’d think!
Get the Base Application
If you want to follow along with this article, you can clone the master branch from Github or download it from here. If you just want to see the finished project, you can clone the finished
branch, or download the finished app from here.
1. Get Your Stormpath Credentials
Login to your Stormpath Account and there is a link to Manage API Keys under Developer Tools on the right-hand side of the admin home page (If this is a new Stormpath account, the link will say “Create API Key”). At the bottom of the page there is an API Keys section, just click the “Create API Key” button. After confirming with the modal dialog, a file named apiKey-[APIKEYID].properties should automatically be downloaded to your computer. Hang on to that, and we’ll use it later.
2. Add Stormpath to Your ASP.NET Core WebAPI
Visual Studio 2015
Open the Visual Studio Package Manager console and enter the following command:
1 2 |
PM> Install-Package Stormpath.AspNetCore |
Visual Studio Code
If you aren’t using Visual Studio, you can also edit project.json
file add the following line to the dependencies
section:
1 2 |
"Stormpath.AspNetCore": "*" |
Then run dotnet restore
(Chances are, VS Code will prompt you with a “Restore” button at the top of the IDE).
Adding the Stormpath Middleware
Open the Startup.cs
file in the root of your project. This file is required for ASP.NET Core applications and is similar to Global.asax file in ASP.NET. At the top of the file, add this using statement:
1 2 |
using Stormpath.AspNetCore; |
Then, edit the ConfigureServices
and Configure
methods as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
public void ConfigureServices(IServiceCollection services) { services.AddStormpath(); // Add framework services. services.AddMvc(); } public void Configure(IApplicationBuilder app, IHostingEnvironment env) { // This is required to serve static files like .html or .js app.UseStaticFiles(); // make sure this line comes after app.UseStaticFiles(); app.UseDefaultFiles(); // serves up our wwwroot files app.UseStaticFiles(); // allows serving of static files app.UseStormpath(); app.UseMvc(); // sets MVC routes for webapi } |
3. Add Your Stormpath Credentials to Your Application
Now we’ll install our Stormpath credentials into the application. Create a file in the root of the application and name it stormpath.yaml
. Use the API key and secret contained in the apiKey.properties
file you downloaded, as well as the Stormpath Application href to fill in the values in the YAML file:
1 2 3 4 5 6 7 8 9 |
--- application: href: "https://api.stormpath.com/v1/applications/<application_id>" client: apiKey: id: "<id_found_in_file>" secret: "<secret_found_in_file>" |
Configuration via a YAML file is simple and straightforward, but it’s important to not check this file into public source control, as it would expose your API key and secret. For production, Stormpath strongly recommends you to store these values as environment variables, STORMPATH_APPLICATION_HREF
, STORMPATH_CLIENT_APIKEY_ID
and STORMPATH_CLIENT_APIKEY_SECRET
respectively. Read the documentation on Environment Variables here.
4. Add Stormpath to the AngularJS App
You can use your package manager of choice (Bower or NPM) for client-side dependencies. For simplicity’s sake, we’re going to get the reference directly from Github using Rawgit.
Add a link reference in your index.html
file in the root of your client app to the Stormpath Angular SDK and templates files.
1 2 3 |
<script src="//cdn.rawgit.com/stormpath/stormpath-sdk-angularjs/1.0.0/dist/stormpath-sdk-angularjs.min.js"></script> <script src="//cdn.rawgit.com/stormpath/stormpath-sdk-angularjs/1.0.0/dist/stormpath-sdk-angularjs.tpls.min.js"></script> |
Then add the references to the angular app module:
1 2 |
angular.module('ToDoApp', ['ngCookies', 'ngResource', 'ngSanitize', 'ui.router', 'stormpath', 'stormpath.templates']) |
5. Add Login and Register to the Angular App
Next, we’ll add some menu items in the index.html
file:
1 2 3 4 |
<li><a ui-sref="login" if-not-user>Login</a></li> <li><a ui-sref="register" if-not-user>Register</a></li> <li><a ui-sref="home" sp-logout if-user>Logout</a></li> |
And the routes that go with them:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
.state('register', { url: '/register', templateUrl: '/app/auth/views/register.view.html' }) .state('login', { url: '/login', templateUrl: '/app/auth/views/login.view.html' }) .state('todo', { url: '/todo', templateUrl: '/app/todo/views/todo.view.html', sp: { authenticate: true } }) |
We’ve added a login and register route to the application’s router, and we’ve updated the todo
route to only be accessible by logged-in users.
The “Logout” route is completely handled by Stormpath, but we’ll need some handlers for the “Login” and “Register” routes. I’ve created a folder called auth
to put these controllers in. First, the controllers:
login.controller.js
1 2 3 4 5 6 7 |
(function(){ function loginController(){} angular.module('ToDoApp') .controller('LoginController', [loginController]); }()) |
register.controller.js
1 2 3 4 5 6 7 |
(function(){ function registerController(){} angular.module('ToDoApp') .controller('RegisterController', [registerController]); }()) |
Also, don’t forget to at the script references to your index.html
page:
1 2 3 |
<script src="app/auth/controllers/login.controller.js"></script> <script src="app/auth/controllers/register.controller.js"></script> |
You’ll notice, both controllers are essentially empty. That’s because the real magic happens in the views:
login.view.html
1 2 3 4 |
<section ng-controller="LoginController"> <div sp-login-form></div> </section> |
register.view.html
1 2 3 4 |
<section ng-controller="RegisterController"> <div sp-registration-form post-login-state="todo"></div> </section> |
We used a few directives here that come with the Stormpath Angular SDK. The sp-login-form
and sp-registration-form
create their respective forms, and the post-login-state
tells the application where to go once the registration process is done and the application logs us in (assuming auto-login is enabled in the Stormpath Management UI).
6. Configure the Angular App
There are some things that need to be configured to make the Angular/.NET/Stormpath combination work seamlessly. First, we need to set up the routing hash and the configuration for the registration form’s POST behavior:
(Don’t forget to inject the $locationProvider and STORMPATH_CONFIG into the config function for the application)
1 2 3 |
$locationProvider.html5Mode(true); STORMPATH_CONFIG.FORM_CONTENT_TYPE = 'application/json'; |
For html5Mode
to work in Angular, you must specify the base URL in the index.html
file:
1 2 3 4 |
<head> <meta charset="utf-8"> <base href="/"> <!-- This tells the application what the base url is for the app --> |
This turns on HTML5-type routing for the Angular application, and the STORMPATH_CONFIG
line tells the Angular SDK to POST
the registration form as application/json
. By default, the Angular SDK sends the registration information as application/x-www-form-urlencoded
, but the ASP.NET SDK expects JSON to be posted. This line just makes sure they’re on the same page as far as Content-Type
headers.
Lastly, we’ll configure how the application behaves after login and after logout:
1 2 |
.run(['$stormpath', '$rootScope', '$state', initializer]); |
1 2 3 4 5 6 7 8 9 10 11 12 13 |
function initializer($stormpath, $rootScope, $state) { // Finally, configure the login state and the default state after login $stormpath.uiRouter({ loginState: 'login', defaultPostLoginState: 'todo' }); // Bind the logout event $rootScope.$on('$sessionEnd', function () { $state.transitionTo('login'); }); } |
This code lets the application know that we want users to be routed to the todo
route once someone has successfully logged in and that we want them to be routed to the login
routed when they log out.
7. Adding Authorization to the Server Side
Now that we have the client side authenticating users, we need to make sure the server side only returns ToDos for the currently logged in user. First, we need to make sure that when the user requests their ToDos, they’re authenticated by adding the Authorize
attribute to the TodoController
:
1 2 3 4 5 6 7 |
[Authorize] [Route("api/[controller]")] public class TodosController : Controller { // implementation ... } |
Then we just make sure the queries filter by the currently logged in user and attach the current user to our ToDos when they’re being added.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
[HttpGet] public IActionResult Get() { return Ok(_context.Todos.Where(x=>x.User == User.Identity.Name).ToList()); } [HttpGet("{id}", Name = "GetTodo")] public IActionResult GetById(int id) { var todo = _context.Todos.SingleOrDefault(t => t.User == User.Identity.Name && t.Id == id); if(todo == null) { return NotFound($"No todo with an Id of {id} was found."); } return Ok(todo); } public IActionResult Post([FromBody] Todo todo) { if (string.IsNullOrEmpty(todo.Description)) { return BadRequest("There must be a description in the todo."); } todo.User = User.Identity.Name; _context.Entry(todo).State = todo.Id > 0 ? EntityState.Modified : EntityState.Added; _context.SaveChanges(); return CreatedAtRoute("GetTodo", new { id = todo.Id }, todo); } [HttpDelete("{id}")] public IActionResult Delete(int id) { var todo = _context.Todos.FirstOrDefault(t => t.User == User.Identity.Name && t.Id == id); if (todo == null) { return NotFound($"No todo with an Id of {id} was found for the current user."); } _context.Todos.Remove(todo); _context.SaveChanges(); return Ok(); } |
That’s it! When you fire up the application and try to navigate to the todo
page, you should be redirected to the login
route, and it should look something like this:
You should now only see ToDos for the currently logged in user, and when you add a ToDo, it should be saved as a ToDo for that user!
—
Excited to learn more about ASP.NET Core, or user authentication with Stormpath? Check out these resources:
And as always, hit me up in the comments below, or on Twitter @leebrandt with questions!