Fork me on GitHub
Note: The Socially 2 tutorial is no longer maintained. Instead, we have a new, better and comprehensive tutorial, integrated with our WhatsApp clone tutorial: WhatsApp clone tutorial with Ionic 2, Angular 2 and Meteor (we just moved all features and steps, and implemented them better!)

Users & Authentication

Note: If you skipped ahead to this section, click here to download a zip of the tutorial at this point.

In this section we'll look at how to:

  • Implement security for an app using Meteor and Angular 2 API
  • Setup user accounts in meteor using email and password
  • Restrict access to views based on user permissions

Removing Insecure

Right now, our app is publishing all parties to all clients, allowing any client to change those parties. The changes are then reflected back to all the other clients automatically.

This is super powerful and easy, but what about security? We don't want any user to be able to change any party...

For quick and easy setup, Meteor automatically includes a package called insecure. As the name implies, the packages provides a default behavior to Meteor collections allowing all reads and writes.

The first thing we should do is to remove the "insecure" package. By removing that package, the default behavior is changed to deny all.

Execute this command in the command line:

$ meteor remove insecure
> insecure removed from your project

Let's try to change the parties array or a specific party. Nothing's working.

That's because now we have to write an explicit security rule for each operation we want to make on the Mongo collection.

We can assume we will allow a user to alter data if any of the following are true:

  • the user is logged in
  • the user created the party
  • the user is an admin

User Accounts

One of Meteor's most powerful packages is the Meteor accounts system.

Add the "accounts-password" Meteor package. It's a very powerful package for all the user operations you can think of: login, signup, change password, password recovery, email confirmation and more.

$ meteor add accounts-password

Now we are going to add angular2-meteor-accounts-ui which is a package that contains all the HTML and CSS we need for the user operation forms.

$ meteor npm install --save angular2-meteor-accounts-ui

Because Angular 2 works with modules, we need to import this package's module into our:

9.2 Import AccountsModule client/imports/app/app.module.ts
2
3
4
5
6
7
8
 
13
14
15
16
17
18
19
20
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router';
import { AccountsModule } from 'angular2-meteor-accounts-ui';
 
import { AppComponent } from './app.component';
import { routes } from './app.routes';
...some lines skipped...
    BrowserModule,
    FormsModule,
    ReactiveFormsModule,
    RouterModule.forRoot(routes),
    AccountsModule
  ],
  declarations: [
    AppComponent,

Let's add the <login-buttons> tag below of the party form in the PartiesList's template:

1
2
3
4
5
6
<div>
  <parties-form></parties-form>
  <login-buttons></login-buttons>
 
  <ul>
    <li *ngFor="let party of parties | async">

Run the code, you'll see a login link below the form. Click on the link and then "create account" to sign up. Try to log in and log out.

That's it! As you can see, it's very easy to add basic login support with the help of the Meteor accounts package.

Parties.allow()

Now that we have our account system, we can start defining our security rules for the parties.

Let's go to the "collection" folder and specify what actions are allowed:

9.5 Add Parties collection security both/collections/parties.collection.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { MongoObservable } from 'meteor-rxjs';
import { Meteor } from 'meteor/meteor';
 
import { Party } from '../models/party.model';
 
export const Parties = new MongoObservable.Collection<Party>('parties');
 
function loggedIn() {
  return !!Meteor.user();
}
 
Parties.allow({
  insert: loggedIn,
  update: loggedIn,
  remove: loggedIn
});

In only 10 lines of code we've specified that inserts, updates and removes can only be completed if a user is logged in.

The callbacks passed to the Parties.allow are executed on the server only. The client optimistically assumes that any action (such as removal of a party) will succeed, and reverts the action as soon as the server denies permission. If you want to learn more about those parameters passed into Parties.allow or how this method works in general, please, read the official Meteor docs on allow.

Meteor.user()

Let's work on ensuring only the party creator (owner) can change the party data.

First we must define an owner for each party that gets created. We do this by taking our current user's ID and setting it as the owner ID of the created party.

Meteor's base accounts package provides two reactive functions that we are going to use, Meteor.user() and Meteor.userId().

For now, we are going to keep it simple in this app and allow every logged-in user to change a party. It'd be useful to add an alert prompting the user to log in if she wants to add or update a party.

Change the click handler of the "Add" button in the parties-form.component.ts, addParty:

1
2
3
4
5
6
 
26
27
28
29
30
31
32
33
34
35
36
import { Component, OnInit } from '@angular/core';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { Meteor } from 'meteor/meteor';
 
import { Parties } from '../../../../both/collections/parties.collection';
 
...some lines skipped...
  }
 
  addParty(): void {
    if (!Meteor.userId()) {
      alert('Please log in to add a party');
      return;
    }
 
    if (this.addForm.valid) {
      Parties.insert(this.addForm.value);
 

Now, change it to save the user ID as well:

32
33
34
35
36
37
38
    }
 
    if (this.addForm.valid) {
      Parties.insert(Object.assign({}, this.addForm.value, { owner: Meteor.userId() }));
 
      this.addForm.reset();
    }

Notice that you'll need to update the Party interface in the party.interface.ts definition file with the optional new property: owner?: string:

9.8 Define owner in Party type both/models/party.model.ts
4
5
6
7
8
  name: string;
  description: string;
  location: string;
  owner?: string; 
}

Let's verify the same logic for updating a party:

9.9 Check access to update a party client/imports/app/parties/party-details.component.ts
1
2
3
4
5
6
7
 
34
35
36
37
38
39
40
41
42
43
44
import { Component, OnInit, OnDestroy } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Subscription } from 'rxjs/Subscription';
import { Meteor } from 'meteor/meteor'; 
 
import 'rxjs/add/operator/map';
 
...some lines skipped...
  }
 
  saveParty() {
    if (!Meteor.userId()) {
      alert('Please log in to change this party');
      return;
    }
    
    Parties.update(this.party._id, {
      $set: {
        name: this.party.name,

canActivate

CanActivate is a one of three guard types in the new router. It decides if a route can be activated.

Now you can specify if a component can be accessed only when a user is logged in using the canActivate property in the router definition.

9.10 Require user to access PartyDetails client/imports/app/app.routes.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { Route } from '@angular/router';
import { Meteor } from 'meteor/meteor';
 
import { PartiesListComponent } from './parties/parties-list.component';
import { PartyDetailsComponent } from './parties/party-details.component';
 
export const routes: Route[] = [
  { path: '', component: PartiesListComponent },
  { path: 'party/:partyId', component: PartyDetailsComponent, canActivate: ['canActivateForLoggedIn'] }
];
 
export const ROUTES_PROVIDERS = [{
  provide: 'canActivateForLoggedIn',
  useValue: () => !! Meteor.userId()
}];

We created a new provider called canActivateForLoggedIn that contains a boolean value with login state.

As you can see we specified only the name of that provider inside canActivate property.

It's worth mentioning that guards can receive more than one provider.

Now, we only need to declare this provider in our NgModule:

9.11 Add ROUTES_PROVIDERS client/imports/app/app.module.ts
5
6
7
8
9
10
11
 
20
21
22
23
24
25
26
27
28
import { AccountsModule } from 'angular2-meteor-accounts-ui';
 
import { AppComponent } from './app.component';
import { routes, ROUTES_PROVIDERS } from './app.routes';
import { PARTIES_DECLARATIONS } from './parties';
 
@NgModule({
...some lines skipped...
    AppComponent,
    ...PARTIES_DECLARATIONS
  ],
  providers: [
    ...ROUTES_PROVIDERS
  ],
  bootstrap: [
    AppComponent
  ]

InjectUser

If you place @InjectUser above the PartiesFormComponent it will inject a new user property:

client/imports/app/parties/parties-form.component.ts:

import { InjectUser } from 'angular2-meteor-accounts-ui';
import { Meteor } from 'meteor/meteor';
import { OnInit } from '@angular/core';
import template from './parties-form.component.html';

@Component({
  selector: 'parties-form',
  template,
})
@InjectUser('user')
export class PartiesFormComponent implements OnInit {
  user: Meteor.User;

  ngOnInit() {
    console.log(this.user);
  }
}

Call this.user and you will see that it returns the same object as Meteor.user(). The new property is reactive and can be used in any template, for example:

client/imports/app/parties/parties-form.component.html:

<div *ngIf="!user">Please, log in to change party</div>
<form ...>
  ...
</form>

As you can see, we've added a label "Please, login to change party" that is conditioned to be shown if user is not defined with help of an ngIf attribute, and will be hidden otherwise.

Routing Permissions

Let's imagine now that we allow to see and change party details only for logged-in users. An ideal way to implement this would be to restrict redirecting to the party details page when someone clicks on a party link. In this case, we don't need to check access manually in the party details component itself because the route request is denied early on.

This can be easily done again with help of CanActivate property. You can do this with the PartyDetailsComponent, just like we did previous steps earlier with the PartiesFormComponent.

Now log out and try to click on any party link. See, links don't work!

But what about more sophisticated access? Say, let's prevent access into the PartyDetails view for those who don't own that particular party.

It could be done inside of a component using canActivate method.

Let's add a canActivate method and CanActivate interface, where we get the current route's partyId parameter and check if the corresponding party's owner is the same as the currently logged-in user.

client/imports/app/parties/party-details.component.ts:

import { CanActivate } from '@angular/router';
import template from './party-details.component.html';

@Component({
  selector: 'party-details',
  template
})
export class PartyDetails implements CanActivate {
  // ...

  canActivate() {
    const party = Parties.findOne(this.partyId);
    return (party && party.owner == Meteor.userId());
  }
}

Now log in, then add a new party, log out and click on the party link. Nothing happens meaning that access is restricted to party owners.

Please note it is possible for someone with malicious intent to override your routing restrictions on the client. You should never restrict access to sensitive data, sensitive areas, using the client router only.

This is the reason we also made restrictions on the server using the allow/deny functionality, so even if someone gets in they cannot make updates. While this prevents writes from happening from unintended sources, reads can still be an issue. The next step will take care of privacy, not showing users parties they are not allowed to see.

Summary

Amazing, only a few lines of code and we have a much more secure application!

We've added two powerful features to our app:

  • the "accounts-ui" package that comes with features like user login, logout, registration and complete UI supporting them;
  • restricted access to the party details page, with access available for logged-in users only.