Real-World Angular Series, Part 2b: Authentication and Data Modeling
Learn how to construct the back-end of your Angular application, using MongoDB to call data, and set up authentication and authorization protocols.
Join the DZone community and get the full member experience.
Join For FreeAdmin Authorization
For our RSVP app, only users with admin
privileges should be able to create, edit, and delete events. All other users will only be able to RSVP to these events. In order to implement this, we'll need to assign and then utilize user roles in both our Node.js API and our Angular app.
First, let's take a look at the steps involved:
- Use Auth0 Rules to establish user roles and then add them to the ID (client) and access (API) tokens.
- Implement middleware in our Node.js API to ensure only
admin
users can access certain API routes. - Use the role information in the Angular app to restrict access to certain routes and features.
Let's get started!
Use an Auth0 Rule for Admin Authorization
Rules are JavaScript functions that Auth0 executes each time a user authenticates. They provide an easy way to extend authentication functionality.
The first step is to log into your Auth0 dashboard and create a new Rule. Select the "Set roles to a user" rule template:
This opens up a JavaScript template. We only want to assign an admin
role to our own account at this time. It'd be a good idea to change the name of this rule so we can see at a glance what it does. I changed the name of the rule to Set admin role for me
. We can easily modify line 6
of the template where it checks the user's email for indexOf()
a specific email domain.
I'll change this to my full Google email address because that is the OAuth account to which I want to assign the admin
role. I'm also using social connections with Facebook and Twitter, and it's important to keep in mind that some connections don't return an email
field. Therefore, I need to modify the rule so that it checks that user.email
exists before using indexOf()
. I'll modify the addRolesToUser()
function to use the following:
if (user.email && user.email.indexOf('[MY_FULL_GOOGLE_ACCOUNT_EMAIL]') > -1) {
Replace [MY_FULL_GOOGLE_ACCOUNT_EMAIL]
with your own credentials. The rule template should then look like this:
When finished, click the "Save" button at the bottom of the page.
Create an Auth0 Rule to Add Claims to Tokens
Now we'll create a second rule that will add this metadata from the Auth0 database to the ID token that is returned to the Angular app upon successful authentication, as well as the access token that is sent to the API to authorize endpoints.
Create another new Rule in your Auth0 dashboard. This time, as we won't be using an existing template, click the "empty rule" button:
We added app_metadata
with a roles
array to our users in the previous rule, but since this isn't part of the OpenID standard claims, we need to add custom claims in order to include roles data in the ID and access tokens.
In the JavaScript template, enter a name for this rule, such as Add user role to tokens
. Then add the following code:
function (user, context, callback) {
var namespace = 'http://myapp.com/roles';
var userRoles = user.app_metadata.roles;
context.idToken[namespace] = userRoles;
context.accessToken[namespace] = userRoles;
callback(null, user, context);
}
The namespace
identifier can be any non-Auth0 HTTP or HTTPS URL and does not have to point to an actual resource. Auth0 enforces this recommendation from OIDC regarding additional claims and will silently exclude any claims that do not have a namespace.
The key for our custom claim will be http://myapp.com/roles
. This is how we'll retrieve theroles
array from the ID and access tokens in our Angular app and Node API. Our rule assigns the Auth0 user's app_metadata.roles
to this property.
When finished, click the "Save" button to save this rule.
Sign In With Admin Account
The next thing we need to do is sign in with our intended admin user. This will trigger the rules to execute and the app metadata will be added to our targeted account. Then the roles data will also be available in the tokens whenever the user logs in.
Since we've implemented login in our Angular app already, all we need to do is sign in with the account we specified in our Set admin role for me
rule. Visit your Angular app in the browser at http://localhost:4200 and click the "Log In" link we added in the header.
Once you've logged in, you can check to verify that the appropriate role was added to your user account in the Auth0 Dashboard Users section. Find the user you just logged in with and click the name to view details. This user's Metadata section should now look like this:
Admin Middleware in Node API
Now that we have role support with our authentication, we can use this to protect API routes that require administrator access.
Open the server config.js
file and add a NAMESPACE
property with the namespace we used when creating our Add user role to tokens
rule:
// server/config.js
module.exports = {
...,
NAMESPACE: 'http://myapp.com/roles'
};
We can now implement middleware that will verify that a user is authenticated and has admin privileges to access API endpoints.
Make the following additions to the server api.js
file:
// server/api.js
...
module.exports = function(app, config) {
// Authentication middleware
const jwtCheck = jwt({
...
});
// Check for an authenticated admin user
const adminCheck = (req, res, next) => {
const roles = req.user[config.NAMESPACE] || [];
if (roles.indexOf('admin') > -1) {
next();
} else {
res.status(401).send({message: 'Not authorized for admin access'});
}
}
...
Our Add user role to tokens
rule added the following key/value pair to our ID and access tokens:
"http://myapp.com/roles": ["admin"]
The express-jwt package adds the decoded token to req.user
by default. The adminCheck
middleware finds this property and looks for a value of admin
in the array. If found, the request proceeds. If not, a 401 Unauthorized
status is returned with a short error message.
Now our API is set up to handle admin
roles.
Admin Authorization in Angular App
We also want the front-end to know if the user is an admin
or not, so let's update our AuthService
to get and store this information.
First, we need to store the namespace in our AUTH_CONFIG
. Open the auth.config.ts
file and add a NAMESPACE
key:
// src/app/auth/auth.config.ts
...
interface AuthConfig {
...,
NAMESPACE: string;
};
export const AUTH_CONFIG: AuthConfig = {
...,
NAMESPACE: 'http://myapp.com/roles'
};
Now that we have the namespace stored, let's add support for storing admin status in the auth.service.ts
file:
// src/app/auth/auth.service.ts
...
export class AuthService {
...
isAdmin: boolean;
...
constructor(private router: Router) {
// If authenticated, set local profile property,
// admin status, and update login status subject.
// If token is expired but user data still in localStorage, log out
if (this.tokenValid) {
this.userProfile = JSON.parse(localStorage.getItem('profile'));
this.isAdmin = localStorage.getItem('isAdmin') === 'true';
this.setLoggedIn(true);
}
}
...
private _setSession(authResult, profile) {
// Save session data and update login status subject
...
this.isAdmin = this._checkAdmin(profile);
localStorage.setItem('isAdmin', this.isAdmin.toString());
this.setLoggedIn(true);
}
private _checkAdmin(profile) {
// Check if the user has admin role
const roles = profile[AUTH_CONFIG.NAMESPACE] || [];
return roles.indexOf('admin') > -1;
}
logout() {
// Ensure all auth items removed from localStorage
...
localStorage.removeItem('isAdmin');
// Reset local properties, update loggedIn$ stream
this.userProfile = undefined;
this.isAdmin = undefined;
this.setLoggedIn(false);
}
...
First, we'll add a new property called isAdmin: boolean
. This will store the user's admin status so we can use it in the front-end.
In the constructor, if the user is authenticated, we'll look for an isAdmin
key in local storage. Local storage stores values as strings, so we'll cast it as a boolean.
Next, we'll update the _setSession()
function. After setting the local userProfile
property, we'll use a private _checkAdmin()
method to determine whether the user has admin
in their roles.
Finally, we'll remove isAdmin
data from local storage and the service in the logout()
method.
We now have the ability to check whether or not a user has admin privileges on the client side.
We now have admin authorization set up on both our API and in our Angular app. We'll do a lot more with this as we develop our application!
Planning App Features
We have our database, Angular app, authentication, and secured Node API structurally ready for further development. Now it's time to do some feature planning and data modeling. It's vitally important to plan an application's data structure before diving straight into writing endpoints and business logic.
Let's consider our RSVP app's intended features at a high level, then we'll extrapolate what our database schema models should look like in order to bring these features to life.
Events
- The main listing of public events available on the homepage with search feature; this should only show future events.
- A full listing of all events (public, private, future, past) available for admins.
- Detail view of the event allows authenticated users to RSVP and to view who else has RSVPed.
- Events can only be created, edited, and deleted by admins.
- Deleting an event should also delete all RSVPs for that event.
- Events can be listed publicly on the homepage, or excluded from the listing and accessed directly with a link.
- Event RSVPs should be retrieved by event ID.
Event Fields
- Event ID (automatically generated by MongoDB, also serves as a direct link).
- The title of the event.
- Location.
- Start date and time.
- End date and time.
- Description.
- Public listing vs. requires a link to view.
RSVPs
- Any authenticated user can RSVP for an event that is in the future, either via a direct link or from the homepage listing.
- Users cannot add or edit an RSVP for an event that has ended.
- Users can update their own existing RSVP responses, but not delete them (RSVPs are deleted if the associated event is deleted).
RSVP Fields
- RSVP ID (automatically generated by MongoDB).
- User ID.
- Name.
- Event ID.
- Attending/Not Attending.
- The number of additional (+1) guests (only applicable if attending).
- Comments.
Users
- Users should be able to view a list of all their own RSVPs in their profile.
- User data handled through Auth0 authentication and profile retrieval; users aren't stored in MongoDB.
- Users are associated with their RSVPs by user ID.
- In order to edit an RSVP, the user's ID must be verified with the user ID in the RSVP.
- Admin users can perform CRUD operations on events.
Data Modeling
We now have an idea about what features our events and RSVPs need to support. Let's create both the server and client-side models necessary to support our application.
Create Schema
First, we'll create the necessary schema to leverage our database. Create a new folder in theserver
directory called models
. In this folder, add a file called Event.js
and a file calledRsvp.js
. These will contain our Event and RSVP models. We're using mongoose for MongoDB object modeling. Each mongoose schema maps to a MongoDB collection and defines the shape of the documents within that collection.
We'll start with Event.js
. Open this file and add the following event schema:
// server/models/Event.js
/*
|--------------------------------------
| Event Model
|--------------------------------------
*/
const mongoose = require('mongoose');
// FIX promise deprecation warning:
// https://github.com/Automattic/mongoose/issues/4291
mongoose.Promise = global.Promise;
const Schema = mongoose.Schema;
const eventSchema = new Schema({
title: { type: String, required: true },
location: { type: String, required: true },
startDatetime: { type: Date, required: true },
endDatetime: { type: Date, required: true },
description: String,
viewPublic: { type: Boolean, required: true }
});
module.exports = mongoose.model('Event', eventSchema);
This schema maps to our outlined features for events.
Now let's write the RSVP schema in the Rsvp.js
file:
// server/models/Rsvp.js
/*
|--------------------------------------
| Rsvp Model
|--------------------------------------
*/
const mongoose = require('mongoose');
// FIX promise deprecation warning:
// https://github.com/Automattic/mongoose/issues/4291
mongoose.Promise = global.Promise;
const Schema = mongoose.Schema;
const rsvpSchema = new Schema({
userId: { type: String, required: true },
name: { type: String, required: true },
eventId: { type: String, required: true },
attending: { type: Boolean, required: true },
guests: Number,
comments: String
});
module.exports = mongoose.model('Rsvp', rsvpSchema);
Now we need to require our models in the API. Open the server api.js
file and add the following:
// server/api.js
/*
|--------------------------------------
| Dependencies
|--------------------------------------
*/
...
const Event = require('./models/Event');
const Rsvp = require('./models/Rsvp');
...
We'll use these models to retrieve data from MongoDB in our endpoints, but first, let's model the data on the front-end as well.
Add Models to Our Angular App
We'll also add event and RSVP models in the front-end to define the shape of the data we expect to retrieve when making API calls. Create two new class files with the CLI:
$ ng g class core/models/event.model
$ ng g class core/models/rsvp.model
Let's add the event model code in event.model.ts
:
// src/app/core/models/event.model.ts
export class EventModel {
constructor(
public title: string,
public location: string,
public startDatetime: Date,
public endDatetime: Date,
public viewPublic: boolean,
public description?: string,
public _id?: string,
) { }
}
We're naming the models EventModel
(and RsvpModel
) to avoid conflicts with the existing Event
constructors if your editor or IDE uses intelligent code completion. Optional members must be listed after required members. The _id
property is optional because it only exists if retrieving data from the database, but not if we're creating new records.
Now add the RSVP model in rsvp.model.ts
:
// src/app/core/models/rsvp.model.ts
export class RsvpModel {
constructor(
public userId: string,
public name: string,
public eventId: string,
public attending: boolean,
public guests?: number,
public comments?: string,
public _id?: string
) { }
}
Create and Seed Collections in MongoDB
In order to query the database, we first need to create the necessary collections and provide a little bit of seed data. There are a couple of ways we could do this: either through mLab or in MongoBooster (with the Mongo shell), which we set up earlier. Let's use MongoBooster because this method can be used with any MongoDB database, not just mLab.
Create Collections
Open your MongoBooster app to the mLab connection we created during our MongoDB Setup.
Once you've connected, right-click the database in the left sidebar and select "Create Collection..." When prompted, enter the collection name events
. Click "Ok" and then create a second collection called rsvps
. You should now have two empty collections listed in your database.
Add Event Seed Documents
Now we'll add some documents to the events
collection for seed data. Right-click the collection name in the sidebar and select "Insert Documents..." The Mongo shell will open with db.events.insert([{}])
prompting you to add data. Replace this with the following:
db.events.insert([{
"title": "Test Event Past",
"location": "Home",
"startDatetime": ISODate("2017-05-04T18:00:00.000-04:00"),
"endDatetime": ISODate("2017-05-04T20:00:00.000-04:00"),
"viewPublic": true
}, {
"title": "MongoBooster Test",
"location": "Seattle, WA",
"startDatetime": ISODate("2017-08-12T20:00:00.000-04:00"),
"endDatetime": ISODate("2017-08-13T10:00:00.000-04:00"),
"viewPublic": true
}, {
"title": "Bob's Private Event",
"location": "Bob's House",
"startDatetime": ISODate("2017-10-05T12:30:00.000-04:00"),
"endDatetime": ISODate("2017-10-05T14:30:00.000-04:00"),
"viewPublic": false
}])
When finished, click "Run" in the top bar. A console tab and a result tab should appear. You can then double-click on the events
collection again to see your new documents listed. They should each have an _id
property containing the automatically-generated object ID and should look something like this:
Add RSVP Seed Documents
Let's add a few documents to the rsvps
collection as well. We'll assign the RSVPs to the seed events, so make sure you copy the object ID string (for the RSVP eventId
) from a couple of event documents. You can do this by right-clicking an event document and selecting "View Document..." You can then copy the string from the "_id" : ObjectId(string)
in the JSON Viewer.
We also need to assign RSVPs to user IDs. You can get specific user IDs by going to the Auth0 Dashboard Users and copying the user_id
from the Identity Provider Attributes of whichever accounts you'd like to associate RSVPs with. Then when you log into the app with any of those accounts, the seed data RSVPs will be associated with the authentication service's userProfile.sub
property.
Using this information, we can create some seed data for RSVPs. Let's add a couple of documents to the rsvps
collection in our MongoDB database:
db.rsvps.insert([{
"userId": "<IDP>|<USER_ID>",
"eventId": "<EVENT_OBJECT_ID_STRING>",
"attending": true,
"guests": 3,
"comments": "Really looking forward to this!"
}, {
"userId": "<IDP>|<USER_ID>",
"eventId": "<EVENT_OBJECT_ID_STRING>",
"attending": false,
"comments": "Regretfully, I can't make it."
}, {
"userId": "<IDP>|<USER_ID>",
"eventId": "<EVENT_OBJECT_ID_STRING>",
"attending": true,
"guests": 2
}])
Replace the information in angle brackets < >
with user_id
s from Auth0 and event IDs from the data we entered previously. This will set up the appropriate relationships.
The database should then look something like this in MongoBooster:
MongoBooster makes it simple to manipulate collections and documents as well as query the database with both the Mongo shell and a GUI. It's a handy tool to have at your disposal for any MongoDB project, and particularly useful if you need to work with a database that is not hosted on your local machine.
We now have some seed documents to work with so we can get our API and Angular app up and running with data available right off the bat.
Summary
In Part 2 of our Real-World Angular Series, we've covered authentication and authorization, feature planning, and data modeling for our MEAN stack application. In the next part of the tutorial series, we'll tackle fetching data from the database with a Node API and how to display data with Angular, complete with filtering and sorting.
Published at DZone with permission of Kim Maida, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments