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: ```javascript #config/auth0-variables.js

module.exports = { clientID: "your Auth0 client id", domain: "your Auth0 domain" - add auth--variables to gitignore - in environment.jsdiff

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 & controllerjavascript #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. }) ```

```javascript

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.handlebars

app/templates/navigation.hbs

javascript

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:javascript

app/templates/application.hbs

{{outlet}}

```

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

```yaml

Auth0

auth0: clientID: auth0 client id domain: auth0 domain secret: auth0 secret audience: api identifier ruby

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:diff 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:diff

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:

```rb

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. ```rb

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 ```diff

app/config/routes.rb

Rails.application.routes.draw do + get "/private", to: "private#private" ... ```

And create a controller: ```rb

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:

```rb ...

config.middleware.insert_before 0, Rack::Cors do allow do origins '' resource '', :headers => :any, :methods => [:get, :post, :options] end end `` Theorigins` 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: ```js 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.