Auth0 + Angular 2 + Rails 5 Authentication

What we’re going to do

This post shows how to get Auth0 + Angular 2 + Rails 5 authentication working. It’s not meant to be a comprehensive guide, just an intro. But it’s a pretty complete intro.

I’ll show you how to get Auth0 plugged into an Angular 2 app, and then show you how to get that Auth0/Angular combo talking to the Rails back-end.

I’m assuming you’re comfortable with Rails and that you’ve at least touched an Angular 2 app before. I provide the code to keep your Angular 2 test suite from breaking but I’m not worried so much about the Rails side, since I assume you can handle keeping that in order yourself.

Configuring Auth0

First we’ll get our Auth0 account set up. Carry out the following steps:

Go to auth0.com and create an account

In the “manage” area, go to Client and click “CREATE CLIENT”

For Name, enter “HomeLibrary: Development”

Under “Choose a client type”, choose “Single Page Web Applications”

Click Create

On the next screen where it asks “What technology are you using for your web app?”, choose Angular 2

First, clone my HomeLibrary Seed Project and cd into the client directory. (The client directory is where the Angular application lives.)

Shell

1

2

$git clonehttps://github.com/jasonswett/home-library-seed

$cdhome-library-seed/client

Next, create an authentication branch.

Shell

1

git checkout-bauthentication

There are two NPM libraries we’ll need in order to get authentication working: angular2-jwt and auth0-lock.

Shell

1

$npm install--save angular2-jwt auth0-lock

Now put your Auth0 domain and Auth0 client ID into your Angular environment file. (Client ID can be found by going to the Clients section in Auth0, then your app, then the Settings tab.)

Free Guide

Getting Started﻿ ﻿with Angular and Rails

Get an Angular/Rails app up and running in as little as 20 minutes

src/environments/environment.ts

JavaScript

1

2

3

4

5

6

7

8

9

10

11

12

13

14

// src/environments/environment.ts

// The file contents for the current environment will overwrite these during build.

// The build system defaults to the dev environment which uses `environment.ts`, but if you do

// `ng build --env=prod` then `environment.prod.ts` will be used instead.

// The list of which env maps to which file can be found in `angular-cli.json`.

export constenvironment={

production:false,

auth0:{

clientID:'your_client_id',

domain:'yourdomain.auth0.com'

}

};

Next we simply pull these values from the environment file into auth.config.ts.

src/app/auth.config.ts

JavaScript

1

2

3

4

5

6

7

8

9

10

11

12

13

// src/app/auth.config.ts

import{environment}from'../environments/environment';

interfaceAuthConfiguration{

clientID:string,

domain:string

}

export const myConfig:AuthConfiguration={

clientID:environment.auth0.clientID,

domain:environment.auth0.domain

};

We’ll create an Auth service that looks like this. What follows is mostly copy-and-pasted from Auth0’s example, with one critical difference: email is included in the auth params, meaning Auth0 will give us not only the user’s Auth0 ID (which is fairly meaningless to us on its own) but also the user’s email address.

I think it’s nice to be able to store the user’s email address in our database. That way if you happen to look at the user table on its own, the data has some meaning by itself without having to cross-reference Auth0, which could be tedious. I also feel like having a user table with no human-readable identifiers would be a really error-prone situation. It’s nice to be able to know what you’re looking at at a glance.

src/app/auth.service.ts

Shell

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

//src/app/auth.service.ts

import{Injectable}from'@angular/core';

import{tokenNotExpired}from'angular2-jwt';

import{myConfig}from'./auth.config';

let Auth0Lock=require('auth0-lock').default;

@Injectable()

exportclassAuth{

//Configure Auth0

lock=newAuth0Lock(myConfig.clientID,myConfig.domain,{

auth:{

params:{scope:'openid email'}

}

});

constructor(){

this.lock.on('authenticated',(authResult)=>{

localStorage.setItem('id_token',authResult.idToken);

});

}

public login(){

//Call the show method todisplay the widget.

this.lock.show();

};

public authenticated(){

//Check ifthere'san unexpired JWT

//It searches foran item inlocalStorage with key=='id_token'

returntokenNotExpired();

};

public logout(){

//Remove token from localStorage

localStorage.removeItem('id_token');

};

}

We want to show a Log In button on the home page, and in order to do that, we need to add an auth object to our AppComponent.

src/app/app.component.ts

Shell

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

//src/app/app.component.ts

import{Component}from'@angular/core';

import{Auth}from'./auth.service';

@Component({

selector:'app-root',

templateUrl:'./app.component.html',

styleUrls:['./app.component.css'],

providers:[Auth]

})

exportclassAppComponent{

title='HomeLibrary';

constructor(private auth:Auth){}

}

Here what we add to the template to make the Log In (and Log Out) button show up.

Connecting the Rails back end

The first step is to create a User model. We’ll give it an email and an auth0_id_string. (I didn’t want to call it auth0_id because that might lead a person to guess, incorrectly, that auth0_id is a foreign key to some value in another table.)

We also need to add before_action :authenticate_user to BooksController.

app/controllers/books_controller.rb

Ruby

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

46

47

48

49

50

51

52

53

54

# app/controllers/books_controller.rb

classBooksController<ApplicationController

before_action:set_book,only:[:show,:update,:destroy]

before_action:authenticate_user

# GET /books

defindex

@books=Book.all

renderjson:@books

end

# GET /books/1

defshow

renderjson:@book

end

# POST /books

defcreate

@book=Book.new(book_params)

if@book.save

renderjson:@book,status::created,location:@book

else

renderjson:@book.errors,status::unprocessable_entity

end

end

# PATCH/PUT /books/1

defupdate

if@book.update(book_params)

renderjson:@book

else

renderjson:@book.errors,status::unprocessable_entity

end

end

# DELETE /books/1

defdestroy

@book.destroy

end

private

# Use callbacks to share common setup or constraints between actions.

defset_book

@book=Book.find(params[:id])

end

# Only allow a trusted parameter "white list" through.

defbook_params

params.require(:book).permit(:name)

end

end

And since Auth0 factors our Client ID and Client Secret into the JWT, Knock will need to know our Client ID and Client Secret for comparison purposes. If the JWT from Auth0 doesn’t contain the Client ID and Client we’re expecting, then we shouldn’t consider the JWT valid.

config/secrets.yml

YAML

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

# Be sure to restart your server when you modify this file.

# Your secret key is used for verifying the integrity of signed cookies.

# If you change this key, all old signed cookies will become invalid!

# Make sure the secret is at least 30 characters and all random,

# no regular words or you'll be exposed to dictionary attacks.

# You can use `rails secret` to generate a secure secret key.

# Make sure the secrets in this file are kept private

# if you're sharing your code publicly.

development:

secret_key_base: your_secret_key_base

auth0_client_id: your_auth0_client_id

auth0_client_secret: your_auth0_client_secret

test:

secret_key_base: your_test_secret_key_base

# Do not keep production secrets in the repository,

# instead read values from the environment.

production:

secret_key_base: <%=ENV["SECRET_KEY_BASE"]%>

Lastly on the Rails side, we’ll need to add a from_token_payload to the User model. Knock expects this function to be defined. When we receive our Auth0 JWT, Knock will decode it and call from_token_payload, passing in the decoded values from the JWT.

app/models/user.rb

Ruby

1

2

3

4

5

6

7

8

9

10

# app/models/user.rb

classUser<ApplicationRecord

defself.from_token_payload(payload)

find_or_create_by(

email:payload['email'],

auth0_id_string:payload['sub']

)

end

end

Adding BookService

Our Angular app needs a way to hit the /api/books endpoint. We’ll create a BookService for this purpose.

Shell

1

2

$cdclient

$ng generate service book

Put an HTTP request in BookService that hits http://localhost:3000/api/books.json. Note that we’re using AuthHttp rather than Angular’s built-in Http library.

src/app/book.service.ts

JavaScript

1

2

3

4

5

6

7

8

9

10

11

12

13

14

// src/app/book.service.ts

import{Injectable}from'@angular/core';

import{AuthHttp}from'angular2-jwt';

@Injectable()

exportclassBookService{

constructor(private authHttp:AuthHttp){}

getList(){

returnthis.authHttp.get('http://localhost:3000/api/books.json');

}

}

After the user is authenticated on the front-end, we want to call bookService.getList(). This will force an authentication to happen in Rails and a new user will be created if one doesn’t already exist with that email.

src/app/auth.service.ts

JavaScript

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

// src/app/auth.service.ts

import{Injectable}from'@angular/core';

import{tokenNotExpired}from'angular2-jwt';

import{myConfig}from'./auth.config';

import{BookService}from'./book.service';

let Auth0Lock=require('auth0-lock').default;

@Injectable()

exportclassAuth{

// Configure Auth0

lock=newAuth0Lock(myConfig.clientID,myConfig.domain,{

auth:{

params:{scope:'openid email'}

}

});

constructor(private bookService:BookService){

this.lock.on('authenticated',(authResult)=>{

localStorage.setItem('id_token',authResult.idToken);

this.bookService.getList().subscribe();

});

}

publiclogin(){

// Call the show method to display the widget.

this.lock.show();

};

publicauthenticated(){

// Check if there's an unexpired JWT

// It searches for an item in localStorage with key == 'id_token'

returntokenNotExpired();

};

publiclogout(){

// Remove token from localStorage

localStorage.removeItem('id_token');

};

}

We’ll need to add BookService and AUTH_PROVIDERS to our AppModule.

JavaScript

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

// client/src/app/app.module.ts

import{BrowserModule}from'@angular/platform-browser';

import{NgModule}from'@angular/core';

import{FormsModule}from'@angular/forms';

import{HttpModule}from'@angular/http';

import{AUTH_PROVIDERS}from'angular2-jwt';

import{AppComponent}from'./app.component';

import{BookService}from'./book.service';

@NgModule({

declarations:[

AppComponent

],

imports:[

BrowserModule,

FormsModule,

HttpModule

],

providers:[AUTH_PROVIDERS,BookService],

bootstrap:[AppComponent]

})

exportclassAppModule{}

Now complete the following steps:

Go to http://localhost:4200, click Log In and authenticate.

Start a rails console and do User.all

You should see a single user record containing your email address. This tells you that authentication is working.