Skip to content

Commit

Permalink
Merge pull request #857 from barbatus/master
Browse files Browse the repository at this point in the history
Revision of Step 8: Meteor authentication in Angular2.
  • Loading branch information
Urigo committed Nov 10, 2015
2 parents a27f368 + 8cb6107 commit b9dca16
Show file tree
Hide file tree
Showing 3 changed files with 465 additions and 223 deletions.
Expand Up @@ -237,6 +237,11 @@ Then change `main.ts` to run this method on Meteor startup:
{{> DiffBox tutorialName="meteor-angular2-socially" step="3.9"}}

Now run the app and you should see the list of parties on the screen.
If not, please, run

meteor reset

in order to remove all previous parties added before via the terminal.

In the next step, we'll see how to add functionality to our app's UI so that we can add parties on the page.

Expand Down
@@ -1,7 +1,13 @@
{{#template name="tutorialAngular2.step_08.md"}}
{{> downloadPreviousStep stepName="step_07"}}

In this section we'll look at using Meteor Accounts & take a quick detour into using Services in Angular 2.
In this section we'll look at how to implement security for an app
using Meteor and Angular2 API together.

For the Socially app, we are going to add a simple login/password login in order to
allow creation or updating parties for logged-in users only.
Also, we are going to explore how we can restrict access to views based on
user permissions using Angular2 routing API.

# User Accounts

Expand All @@ -12,199 +18,126 @@ 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...

First thing we should do is to remove the 'insecure' package that automatically added to any new Meteor application.
First thing we should do is to remove the "insecure" package that automatically added to any new Meteor application.

The 'insecure' package makes the default behaviour of Meteor collections to permit all.
The "insecure" package makes the default behaviour of Meteor collections to permit all.

By removing that package the default behaviour is changed to deny all.

Execute this command in the command line:

meteor remove insecure

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

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

So first, let's add the 'accounts-password' Meteor package.
So first, let's 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 will also add the 'accounts-ui' package. This package contains all the HTML and CSS we need for the user operation forms.
If Socially weren't an Angular 2 app, our the next step would be to add the "accounts-ui" package, which is a standard Meteor package
that contains all the HTML and CSS we need for the user operation forms.

Later on in this tutorial we will replace those default account-ui forms with custom Angular forms.
But it's a Blaze-related package and will not work in Angular 2.
It's not wrong to say as well that we'd appreciate to use Angular 2 components in Angular 2 where possible.
So, we are going to add "barbatus:ng2-meteor-accounts-ui" instead which is a simple wrapper over standard "accounts-ui" that
provides Blaze LoginButtons view as a Angular 2 component and, besides, does some necessary cleanup behind the scene.

meteor add accounts-ui
meteor add barbatus:ng2-meteor-accounts-ui

Now let's add the accounts-ui template ( <code ng-non-bindable>&#123;&#123;> loginButtons &#125;&#125;</code> ) into our app, into index.html.
Let's add the `<accounts-ui>` component to the right of the party addition button in the PartiesForm template:

Unfortunately, Angular 2 doesn't currently play well with Blaze. This means that any Blaze component must be placed outside of our root app.
{{> DiffBox tutorialName="meteor-angular2-socially" step="8.1"}}

Note: An Angular 2 component version of `loginButtons` is in the works, and can be found [here](https://github.com/ShMcK/Angular2-Meteor-Demos/tree/master/ng2-accounts-ui).
Then import all dependencies:

For now, we can add `loginButtons` to our `index.html`, which will still work.
{{> DiffBox tutorialName="meteor-angular2-socially" step="8.2"}}

{{> DiffBox tutorialName="angular2-meteor" step="8.1"}}
Now run the code, you'll see a login link to the right of the "add" button. Click on the link and create an account,
then try to log in and log out.

Run the code, create an account, login, logout...
That's it! As you can see it's very easy to add basic login support with the help of Meteor.

## Meteor.allow()
## Parties.allow()

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

Let's go to the model folder and change the file to look like this:

{{> DiffBox tutorialName="angular2-meteor" step="8.3"}}

## Mongo Commands (insert, update, remove)

The [collection.allow Meteor function](http://docs.meteor.com/#/full/allow) defines the permissions that the client needs to write directly to the collection (like we did until now).

In each callback of action type (insert, update, remove) the functions should return true if they think the operation should be allowed.
Otherwise they should return false, or nothing at all (undefined).

The available callbacks are:

* insert(userId, doc)

The user userId wants to insert the document doc into the collection. Return true if this should be allowed.

doc will contain the _id field if one was explicitly set by the client, or if there is an active transform. You can use this to prevent users from specifying arbitrary _id fields.

* update(userId, doc, fieldNames, modifier)

The user userId wants to update a document doc. (doc is the current version of the document from the database, without the proposed update.) Return true to permit the change.

fieldNames is an array of the (top-level) fields in doc that the client wants to modify, for example ['name', 'score'].

modifier is the raw Mongo modifier that the client wants to execute; for example, {$set: {'name.first': "Alice"}, $inc: {score: 1}}.

Only Mongo modifiers are supported (operations like $set and $push). If the user tries to replace the entire document rather than use $-modifiers, the request will be denied without checking the allow functions.

* remove(userId, doc)
{{> DiffBox tutorialName="meteor-angular2-socially" step="8.3"}}

The user userId wants to remove doc from the database. Return true to permit this.
What we did is we've just added security to our app using only 10 lines of code.

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](http://docs.meteor.com/#/full/allow).

In our example:

* insert - only if the user who makes the insert is the party owner.
* update - only if the user who makes the update is the party owner.
* remove - only if the user who deletes the party is the party owner.


## Meteor.userId()
## Meteor.user()

OK, right now none of the parties has an owner so we can't change any of them.

So let's add the following simple code to define an owner for each party that gets created.

Let's take our current user's id and set it as the owner id of the party we are creating.

If you're using TypeScript, you'll have to adjust your IParty interface:

{{> DiffBox tutorialName="angular2-meteor" step="8.4"}}

Change the code for the add button in `parties-form.ts` to to also insert a user:

{{> DiffBox tutorialName="angular2-meteor" step="8.5"}}

Do the same to `party-details.ts`:

{{> DiffBox tutorialName="angular2-meteor" step="8.6"}}


So first we set the new party's owner to our current user's id and then push it to the parties collection like before.

Now, start the app in 2 different browsers and login with 2 different users.

Test editing and removing your own parties, and try to do the same for parties owned by another user.
Let's take our current user's ID and set it as the owner id of the party we are creating.

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

This could get dangerously repetitive. This is an opportunity to see how services work in Angular 2.
For now we are going to keep it simple in this app and allow every logged-in users to change a party.
Change the code for the add button in `parties-form.ts` to save user ID as well. Also,
it'd be useful to add an alert promting user to log in if she wants to add or update a party:

## Angular 2 Services
{{> DiffBox tutorialName="meteor-angular2-socially" step="8.4"}}

Let's create a `PartyService` for handling party changes:
Now, do the user check in the `party-details.ts`:

{{> DiffBox tutorialName="angular2-meteor" step="8.7"}}
{{> DiffBox tutorialName="meteor-angular2-socially" step="8.5"}}

Looking at this code later, it may not be clear what these parameters are, so using types can help.
Calling each time `Meteor.user()` or `Meteor.userId()` might seems bulky.
Also, you will likely want to use these functions in some component template.

{{> DiffBox tutorialName="angular2-meteor" step="8.8"}}
How can you simply life here? You can try out "barbatus:ng2-meteor-accounts" package, which
wraps around all Meteor Accounts API (login with password and social logins functionality)
and exports two services for the usage in Angular 2. Besides that, it has two convenient annotations: `InjectUser` and `RequireUser`.
The second one we'll touch a bit a later, but the first one is exactly what we need.

Finally, let's refactor our party object calls into a function.
If you place `InjectUser` above the PartyDetails it will inject a new user property to it:

We can use a function in the service to create our clean party objects.
__`client/parties-form/parties-form.ts`:__
...

{{> DiffBox tutorialName="angular2-meteor" step="8.9"}}
import {InjectUser} from 'meteor-accounts';

The resulting service should look like this:

_{{> DiffBox tutorialName="angular2-meteor" step="8.10"}}

Much cleaner.

> Note: these methods are still client-side and thus insecure. We'll talk about Meteor methods & calls later.
## Injecting an Angular 2 Service

This process should get easier, in fact, it should look this:

constructor(public partyService:PartyService) {
add() {
if (this.partiesForm.valid) {
partyService.add(this.partiesForm.value);
...

A note about the constructor syntax here:

* public - binds this.partyService to partyService (you can also use 'private', though it acts the same)
* partyService - the optional alias we use for PartyService
* PartyService - the target we are injecting into the class

Unfortunately, the syntax listed above isn't currently working. Keep in mind, Angular 2 is under development. To get things working, we can use the `Inject` annotation instead.

In `parties-list.ts`, `party-details.ts` and `party-form.ts` follow the instructions below.

- `import {Inject} from 'angular2/angular2';`
- `import {PartyService} from 'client/lib/party-service';`
- In the component, add `viewBindings: [PartyService]`

```
@Component({
selector: 'parties-list',
viewBindings: [PartyService]
selector: 'parties-form'
})
```

4. Inject partyService into the constructor and set `this.partyService` to the injected.

```
constructor(@Inject(PartyService) partyService:PartyService) {
this.partyService = partyService;
```

5. Access the service through `this.partyService` in your methods.

```
this.partyService.remove(party._id)
```

I hope this syntax will clean up in the future.

In the end, it should look like this:

{{> DiffBox tutorialName="angular2-meteor" step="8.11"}}

@View({
templateUrl: 'client/parties-form/parties-form.html',
directives: [FORM_DIRECTIVES, AccountsUI]
})
@InjectUser()
export class PartiesForm {
constructor() {
...
console.log(this.user);
}
...
}

# Challenge
Call `this.user` and you will see, it returns same object as `Meteor.user()`.
New property is reactive and can be used in any template, for example:

Inject the PartyService into the three places it's needed.
__`client/parties-form/parties-form.html`:__

If you have difficulties, please checkout steps [8.12](https://github.com/ShMcK/ng2-socially-tutorial/commit/84379e8326e7bb392d9aebea5fd25dd782a6b684) or [8.13](https://github.com/ShMcK/ng2-socially-tutorial/commit/b87c18b8b61a026d0d2f44691c946c2c158e03b2) in the tutorial-repo.
<div *ng-if="!user">Please, log in to change party</div>
<form [ng-form-model]="partiesForm" #f="form" (submit)="addParty(f.value)">
...
</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 `ng-if` attribute, and
will disappear otherwise. Don't forget to import `NgIf` dependency in the component.

# Social login

Expand All @@ -217,8 +150,6 @@ To do this, we simply need to add the right packages in the console:

Now run the app. when you will first press the login buttons of the social login, meteor will show you a wizard that will help you define your app.

{{> DiffBox tutorialName="angular2-meteor" step="8.14"}}

You can also skip the wizard and configure it manually like the explanation here: [http://docs.meteor.com/#meteor_loginwithexternalservice](http://docs.meteor.com/#meteor_loginwithexternalservice)

There are more social login services you can use:
Expand All @@ -231,42 +162,73 @@ There are more social login services you can use:
* Weibo
* Meteor developer account

# Routing Permissions

# Authentication With Routers
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 was denied early on.

Now that we prevented authorized users from changing parties they don't own,
there is no reason for them to go into the party details page.
This can be easily done again with help of "barbatus:ng2-meteor-accounts" package
that has simple `RequireUser` annotation. Just place it above `PartyDetails`
and you will see that, if a user is not logged-in to the system, she won't be able to access that route.
Let's add package and then implement restricted access:

We can easily prevent them from going into that view using our routes.
meteor add barbatus:ng2-meteor-accounts

We are going to use the `canActivate` router hook.
{{> DiffBox tutorialName="meteor-angular2-socially" step="8.6"}}

{{> DiffBox tutorialName="angular2-meteor" step="8.15"}}
Now log out and try to click on any party link. See, links don't work!

Now, if a user is not logged in to the system, it won't be able to access that route.
But what about more sophisticated access? Say, let's prevent from going into the PartyDetails view for those
who don't own that particular party.

The call to `Meteor.userId()` will return null if none is found, and will return a value if it is found, thus allowing the route to be activated.
It's easy implement in Angular 2 as well using `@CanActivate` annotation.
BTW, `RequireUser` itself is just a simple inheritor of `@CanActivate`.
Let's add `checkPermissions` function, where we get the current route's `partyId` parameter
and check if the corresponding party's owner is the same as currently logged-in.
And then pass it in `@CanActivate` attribute:

__`client/parties-form/parties-form.ts`:__

We could even add a helpful alert here.
import {CanActivate, ComponentInstruction} from 'angular2/router';

{{> DiffBox tutorialName="angular2-meteor" step="8.16"}}
function checkPermissions(instruction: ComponentInstruction) {
var partyId = instruction.params('partyId');
var party = Parties.findOne(partyId);
return (party && party.owner == Meteor.userId());
}

Component({
selector: 'party-details'
})
@View({
templateUrl: 'client/party-details/party-details.html',
directives: [RouterLink, FORM_DIRECTIVES]
})
@CanActivate(checkPermissions)
export class PartyDetails {
...
}

* NOTE: This approach is currently not working. *
Now log in, then add new party, log out and click on the party link.
Nothing happens meaning that access is restricted.

We don't need to handle any redirect because the route request was denied early on.
> 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.
on the top of the routes file, let's add these lines:
> 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 secure application!

Please note it is possible for someone with malicious intent to override your route configuration on the client.
As that is where we the user is authenticated, they can remove the check to get access.
We've added two poweful features to our app:

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.
- "accounts-ui" package that comes with features like user login, logout, registration
and complete UI supporting them
- restricted access to the party details page for logged-in users only

{{/template}}

0 comments on commit b9dca16

Please sign in to comment.