loading...

Creating an Ember SPA with Auth0 authentication with a Rails api

dschlauderaff profile image David Schlauderaff ・6 min read

I am creating a cookbook organization/meal planner app. I've been working with Ember for a while, but the backend of the app I work on professionally is mostly a black box. I call api's with the authorization process that's already in place. I wanted to set up my own graphql api using Rails. Getting Auth0 to talk to both applications has been a real head-scratcher. There are not a lot (any) tutorials that I could find that just gave you the steps to follow so that it just works.

For the past few nights working on this, I've had so many tabs open to different bits of documentation, blogs, and Stack Overflow questions that my browser has been regularly crashing. Here is what I did to pull it all together.

Setup Auth0

Setting up Auth0 was relatively painless: sign up/login, click on the create application button from the dashboard, choose Single Page Application. Unfortunately, there is no quickstart for Ember. Name the app, set the allowed callback URL: http://localhost:4200 (this is all in development mode for now) and allowed logout URL: http://localhost:4200

Once the application is created, the app's domain, client Id and client secret are available in the settings page of the application.

Next, set up the api application. Again, quite easy, just provide a name and an identifier. The identifier will be used in the applications as the API Audience key.

Configuring Ember

Create a new app:
$ ember new no-stories

Remove the ember-welcome-page.

Install the ember-simple-auth-auth0 add-on:
$ ember install ember-simple-auth-auth0

Configure auth0 add-on:

  • auth0 configuration variables:
 #config/auth0-variables.js

  module.exports = {
    clientID: "your Auth0 client id",
    domain: "your Auth0 domain"
  • add auth--variables to gitignore
    • in environment.js
#config/environment.js
+  const AUTH_CONFIG = require('./auth0-variables')

  module.exports = function(environment) {
   let ENV = {
     ...
+   'ember-simple-auth: {
+     authenticationRoute: 'login',
+     auth0: {
+       clientId: AUTH_CONFIG.clientID, 
+       domain: AUTH_CONFIG.domain,
+       logoutReturnToURL: '/',
+       audience: 'your API Audience key',
+       enableImpersonation: false,
+       silentAuth: {}
+     }
+   },
    ...
  • application route & controller
 #routes/application.js
 import  Route  from  '@ember/routing/route'
import  RSVP  from  'rsvp'
import  ApplicationRouteMixin  from  'ember-simple-auth-auth0/mixins/application-route-mixin'

export  default  Route.extend(ApplicationRouteMixin, {
  beforeSessionExpired() {
    // Do custom async logic here, e.g. notify
    // the user that they are about to be logged out.

    return  RSVP.resolve()
}

// Do other application route stuff here. All hooks provided by
// ember-simple-auth's ApplicationRouteMixin, e.g. sessionInvalidated(),
// are supported and work just as they do in basic ember-simple-auth.
})
#controllers/application.js

import  Controller  from  '@ember/controller'
import { inject  as  service } from  '@ember/service'

export  default  Controller.extend({
  session:  service(),

  actions: {
    login() {
      const  authOptions  = {
        responseType:  'token id_token',
        scope:  'openid email profile',
        audience:  'API audience key'
      }

      this.session.authenticate(
        'authenticator:auth0-universal',
        authOptions,
        (err, email) => {
          alert(`Email link sent to ${email}`)
        }
      )
    },

    logout() {
      this.session.invalidate()
    }
  }
})

Next, create a simple navigation component to display login/logout button. The styles are from ember-tachyon-shim.

#app/templates/navigation.hbs
<header  class="bg-black-90 fixed w-100 ph3 pv3 pv4-ns ph4-m ph5-l">
  <nav  class="f6 fw6 ttu tracked">
    {{#if  session.isAuthenticated}}
      <a  href="#"  class="link dim white dib mr3"  {{action  "logout"}}>
        Logout
      </a>
    {{else}}
      <a  href="#"  class="link dim white dib mr3"  {{action  "login"}}>
        Login
      </a>
    {{/if}}
    <a  class="link dim white dib mr3"  href="#"  title="Home">
      Placeholder
    </a>
    <a  class="link dim white dib"  href="#"  title="Contact">
      Contact
    </a>
  </nav>
</header>
#app/components/navigation.js
import  Component  from  '@ember/component'
import { inject  as  service } from  '@ember/service'

export  default  Component.extend({
  session:  service(),

  actions: {
    login() {
      this.login()
    },

    logout() {
      this.logout()
    }
  }
})

Plug the navigation component into the application template:

#app/templates/application.hbs
<Navigation @login={{action  "login"}} @logout={{action  "logout"}} />
<div  class="main">
  {{outlet}}
</div>

By this point, the application can authenticate through Auth0 by clicking the login button, and be able to log this.session.data.authenticated, which should contain a lot of information, particularly two json web tokens: accessToken and idToken.

Setup the Rails api

Setting up the rails app was relatively straightforward. I was able to follow Auth0's rails documentation with only a few tweaks because I'm using Rails 6. Also, the rack-cors gem needs to be configured, which is not addressed at all in the Auth0 documentation that I saw. Here are the steps:

$ rails new my-api --api

Adding the Auth0 config values to credentials.yml.enc:
$ EDITOR="code --wait" rails credentials:edit will open a tab in VS Code to the decrypted credentials file

# Auth0
auth0:
  clientID: auth0 client id
  domain: auth0 domain
  secret: auth0 secret
  audience: api identifier
# lib/json_web_token.rb

# frozen_string_literal: true
require 'net/http'
require 'uri'

class JsonWebToken
  def self.verify(token)
    JWT.decode(token, nil,
               true, # Verify the signature of this token
               algorithm: 'RS256',
               iss: 'https://YOUR_DOMAIN/',
               verify_iss: true,
               aud: Rails.application.secrets.auth0_api_audience,
               verify_aud: true) do |header|
      jwks_hash[header['kid']]
    end
  end

  def self.jwks_hash
    jwks_raw = Net::HTTP.get URI("https://YOUR_DOMAIN/.well-known/jwks.json")
    jwks_keys = Array(JSON.parse(jwks_raw)['keys'])
    Hash[
      jwks_keys
      .map do |k|
        [
          k['kid'],
          OpenSSL::X509::Certificate.new(
            Base64.decode64(k['x5c'].first)
          ).public_key
        ]
      end
    ]
  end
end

In my version, I have changed the jwks_raw assignment from a straight request to a cache, to cut down on the number of requests sent to the auth0 server:

def self.jwks_hash
- jwks_raw - Net::HTTP.get URI("https//YOUR_DOMAIN/.well-known/jwks.json")
+ jwks_raw = Rails.cache.fetch("JWKS_HASH", exires_in: 10.hours) do
+   Net::HTTP.get URI("https://#{Rails.application.credentials[:auth0][:domain]}.well-known/jwks.json")
+ end 
  jwks_keys = Array(JSON.parse(jwks_raw)['keys'])
  ...

Doing this requires updating config/environments/development.rb to store items in memory:

#config/environments/development.rb

...
# Run rails dev:cache to toggle caching.
if  Rails.root.join('tmp', 'caching-dev.txt').exist?
  config.cache_store = :memory_store 
  config.public_file_server.headers = {
    'Cache-Control' => "public, max-age=#{2.days.to_i}"
  }
else
  config.action_controller.perform_caching =  false

- config.cache_store = :null_store 
+ config.cache_store = :memory_store
end
...

Next I define a Secured concern:

# app/controllers/concerns/secured.rb

# frozen_string_literal: true
module Secured
  extend ActiveSupport::Concern

  included do
    before_action :authenticate_request!
  end

  private

  def authenticate_request!
    auth_token
  rescue JWT::VerificationError, JWT::DecodeError
    render json: { errors: ['Not Authenticated'] }, status: :unauthorized
  end

  def http_token
    if request.headers['Authorization'].present?
      request.headers['Authorization'].split(' ').last
    end
  end

  def auth_token
    JsonWebToken.verify(http_token)
  end
end

The next section of the Auth0 documentation is about validating scopes. I included this because I intend to use it eventually, but for this stage of the project, I'm only concerned with the /private route, with no scope associated.


  SCOPES = {
    '/private' => nil,
    '/private-scoped'    => ['read:messages']
  }

  private

  def authenticate_request!
    @auth_payload, @auth_header = auth_token

    render json: { errors: ['Insufficient scope'] }, status: :unauthorized unless scope_included
  rescue JWT::VerificationError, JWT::DecodeError
    render json: { errors: ['Not Authenticated'] }, status: :unauthorized
  end

  def scope_included
    if SCOPES[request.env['PATH_INFO']] == nil
      true
    else
      # The intersection of the scopes included in the given JWT and the ones in the SCOPES hash needed to access
      # the PATH_INFO, should contain at least one element
      (String(@auth_payload['scope']).split(' ') & (SCOPES[request.env['PATH_INFO']])).any?
    end
  end

To smoke test that it is actually working as intended, I add a /private route to the app/config/routes.rb

#app/config/routes.rb
Rails.application.routes.draw do
+ get "/private", to: "private#private"
...

And create a controller:

# app/controllers/private_controller.rb

# frozen_string_literal: true
class PrivateController < ActionController::API
  include Secured

  def private
    render json: 'Hello from a private endpoint! You need to be authenticated to see this.'
  end
end

Lastly, the rack-cors gem need to be configured to allow requests from the ember app:
In the gemfile, uncomment the rack-cors gem and run bundle install. Then in app/config/application.rb:

...

config.middleware.insert_before 0, Rack::Cors  do
  allow do
    origins '*'
    resource '*', :headers => :any, :methods => [:get, :post, :options]
  end
end

The origins is overly-permissive at this point, and I'll want to tighten it up later, but for now I'm only concerned with getting it up and running.

The moment of truth

In the Ember app, I generate a smoke test route:
$ ember g route private-test

And import the ember-fetch add-on:
$ ember install ember-fetch

I set up my test in the app/routes/private-test.js file:

import  Route  from  '@ember/routing/route'
import  ApplicationRouteMixin  from  'ember-simple-auth-auth0/mixins/application-route-mixin'
import { inject  as  service } from  '@ember/service'
import  fetch  from  'fetch'

export  default  Route.extend(ApplicationRouteMixin, {
  session:  service(),

  model() {
    return  fetch('http://localhost:3000/private', {
      method:  'GET',
      cache:  false,
      headers: {
        Authorization:  `Bearer ${this.session.data.authenticated.accessToken}`,
        'Access-Control-Allow-Origin':  '*'
      }
    }).then(response  => {
      console.log(response)
    })
  }
})

With everything in place, start both servers and the flow should look like this:

  1. localhost:4200/ - click the "Login" button
  2. Redirected to the Auth0 login page
  3. Enter credentials
  4. Returned to localhost:4200/
  5. Navigate to localhost:4200/private-test
  6. In the developer tools, the api response will be logged out.

The response isn't very pretty, and you need to have the network tab open to actually see the "Hello from a private endpoint!" string, but the authentication is working, and the ember and rails applications can talk to each other through Auth0.

My eventual goals for this application is to set the api up as a graphql api. There are a lot of things that can be better organized in this proof-of-concept code, such as the headers should probably be added somewhere besides the individual routes. When I finally got the authenticated response, I felt I needed to write it down asap before I forgot everything I did.

Discussion

pic
Editor guide
Collapse
vhiairrassary profile image
Victor Hiairrassary

It seems there is a typo: expires_in in Rails.cache.fetch("JWKS_HASH", exires_in: 10.hours) instead of exires_in.