Home Archives Search Feed GitHub Projects About


Introduction

For a recent project I wanted to use Auth0 authentication with a Ruby on Rails JSON API backend and a React Native (Expo) frontend that has persistent users on the backend. There were a lot of resources around these topics, but none that I found provided a comprehensive look into what was necessary for what I wanted to accomplish.

Note: This assumes some general knowledge of Rails, React Native and JWT. Also, I’m not claiming this follows best security practices - there is plenty of documentation for that on the Auth0 website as well as other resources on the web.

Create App Scaffolds

API

This is an arbitrary scaffold for a JSON API.

class PagesController < ApplicationController

  def private
    render json: {status: "This is private!"}
  end
end

Add an insecure CORS configuration to application.rb

# ...

config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins '*'
    resource '*',
      headers: :any,
      methods: %i(get post put patch delete options head)
  end
end

# ...

Cool, that’s all we need to do for the API right now.

Client

Note: it’s important to use $ expo login before or after creating the mobile app, as it creates a URL at https://auth.expo.io/*username*/*projectname*, that we redirect to later for authentication.

Set up Auth0

First we need to create and Auth0 Application and API. You can think of the Application as representing the React Native app, and the API as representing the Rails API. We’re going to use the default email/password login, however if you add other Auth0 Connections like Facebook/Google login they will work out of the box with this strategy.

Set up Auth0 Application

  1. After signing up, navigate to the Auth0 dashboard and go to Create Application
  2. Name your application and choose Native as the application type
  3. Go to settings, note the domain, client ID, and client secret.
  4. In allowed callback URLS, add https://auth.expo.io/*username*/*projectname* - where username is your Expo username, and project name is your Expo project name.

Set up Auth0 API

  1. Go to APIs > Create API
  2. Choose a name and identifier (doesn’t have to be a URL, it just represents your API).
  3. You don’t have to change any settings here, but you can add a permission eg. read:private_scoped to play around with scopes later.

Set up Client App

We’ll set up a basic auth flow using react-navigation as described in their docs. We’ll also test our connection to Auth0, initially retrieving an ID Token from their servers. An ID Token provides information about the authenticated, but won’t provide access to resources on our API. We’ll be using Access Tokens later on, so I wanted to make the distinction now.

We’ll use [https://github.com/expo/auth0-example/blob/master/App.js] as a base. For simplicity, I’m putting all of the code into one file. There’s a lot going on here, so I’ve added some comments.

// App.js

import { AuthSession } from "expo";
import jwtDecode from "jwt-decode";
import React from "react";
import {
  ActivityIndicator,
  AsyncStorage,
  StyleSheet,
  Text,
  View
} from "react-native";
import { TouchableOpacity } from "react-native-gesture-handler";
import { createAppContainer, createSwitchNavigator } from "react-navigation";

// Authentication Constants
const auth0ClientId = "*INSERT AUTH0 CLIENT ID*"; // eg. BFwBIoJC333YiZHYJPbAj1JBoFh0LW6Q'
const auth0Domain = "*INSERT AUTH0 DOMAIN"; // eg. https://yourdomain.auth0.com

// Helper method, transforms object of params into URL query params
const toQueryString = params => {
  return (
    "?" +
    Object.entries(params)
      .map(
        ([key, value]) =>
          `${encodeURIComponent(key)}=${encodeURIComponent(value)}`
      )
      .join("&")
  );
};

// Initial screen, determines if we have an token and routes us accordingly
class AuthLoading extends React.Component {
  constructor(props) {
    super(props);
    this._bootstrapAsync();
  }

  _bootstrapAsync = async () => {
    const idToken = await AsyncStorage.getItem("idToken");

    if (!idToken) {
      this.props.navigation.navigate("SignIn");
    } else {
      this.props.navigation.navigate("Authenticated");
    }
  };

  render() {
    return (
      <View style={styles.container}>
        <ActivityIndicator />
      </View>
    );
  }
}

class SignIn extends React.Component {
  // launches browser to authenticate and get jwt token
  _loginAsync = async () => {
    const redirectUrl = AuthSession.getRedirectUrl(); // gets expo redirect URL (https://auth.expo.io/*username*/*app*)
    console.log(redirectUrl);

    const queryParams = toQueryString({
      client_id: auth0ClientId,
      redirect_uri: redirectUrl,
      response_type: "id_token",
      scope: "openid profile", // requesting `openid profile` scope, ie. basic openid information about user
      nonce: "nonce"
    });
    const authUrl = `${auth0Domain}/authorize` + queryParams;
    const response = await AuthSession.startAsync({ authUrl }); // launches browser, performs authentication
    if (response.type === "success") {
      this._handleResponseAsync(response.params);
    }
  };

  _handleResponseAsync = async response => {
    const idToken = response.id_token;
    const decoded = jwtDecode(idToken);
    const { name } = decoded;
    console.warn(name);
    await AsyncStorage.setItem("idToken", idToken);
    this.props.navigation.navigate("Authenticated");
  };

  render() {
    return (
      <View style={styles.container}>
        <TouchableOpacity onPress={this._loginAsync}>
          <Text>Log in with Auth0</Text>
        </TouchableOpacity>
      </View>
    );
  }
}

class Authenticated extends React.Component {
  render() {
    return (
      <View style={styles.container}>
        <Text>{`We're authenticated!`}</Text>
      </View>
    );
  }
}

export default createAppContainer(
  createSwitchNavigator(
    {
      AuthLoading: AuthLoading,
      SignIn: SignIn,
      Authenticated: Authenticated
    },
    {
      initialRouteName: "AuthLoading"
    }
  )
);

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#fff",
    alignItems: "center",
    justifyContent: "center"
  }
});

Running the app should allow you to log in and see the authenticated message. While this works to show a successful connection to Auth0, we need to make some changes to connect to our API.

Set up API

Reference: [https://auth0.com/docs/quickstart/backend/rails/01-authorization]

Let’s create a new file at app/lib/json_web_token.rb (we’re creating a lib folder in the app folder so it’s auto-loaded by Rails). The purpose of this file to is verify the JWT access token that we’ll be passing in. It doesn’t need to communicate with Auth0 to do this aside from grabbing the JSON Web Key Set (JWKS) which we can cache. Note: Auth0’s docs say it’s fine to cache the JWKS, though upon decode failure it should refetch and retry decoding once just to make sure it hasn’t expired.

# app/lib/json_web_token.rb
require 'net/http'
require 'uri'

class JsonWebToken
  def self.verify(token)
    JWT.decode(token, nil,
               true,
               algorithm: 'RS256',
               iss: '*YOUR DOMAIN*',
               verify_iss: true,
               aud: '*YOUR API IDENTIFER*',
               verify_aud: true) do |header|
                 @@jwks_hash ||= jwks_hash
                 @@jwks_hash[header['kid']]
               end
  rescue StandardError
    JWT.decode(token, nil,
               true,
               algorithm: 'RS256',
               iss: '*YOUR DOMAIN*',
               verify_iss: true,
               aud: '*YOUR API IDENTIFER*',
               verify_aud: true) do |header|
      jwks_hash[header['kid']]
    end
  end

  def self.jwks_hash
    jwks_raw = Net::HTTP.get URI('*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

Now to actually use this we can add a controller concern, that would look something like this:

# app/controllers/concerns/secured.rb
module Secured
  extend ActiveSupport::Concern

  included do
    before_action :authenticate_request!
  end

  private

  def authenticate_request!
    @auth_payload, @auth_header = auth_token
    @current_user = User.find_or_create_by(uid: @auth_payload["sub"])

  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

This adds a before_action to a controller that grabs the Authorization header from a request and attempts to decode it using the JsonWebToken class we created earlier. If it succeeds, it sets @current_user based off of the uid from the authorization payload - and then allows the controller action to fire normally - otherwise it’ll render an unauthorized status message.

If you check the official Auth0 docs for this, you can see how to use scopes to limit users based off permissions, but I’m skipping that in this guide.

Now if we add Secured to our PagesController and do a request we should see that we get an unauthorized response:

# app/controllers/pages_controller.rb
class PagesController < ApplicationController
  include Secured
  def private
    render json: {status: "This is private!"}
  end
end

Let’s test it…

Insomnia TestInsomnia Test

Great, now let’s authorize our mobile client.

Authorization on Client

Initially when we logged in, we were only requesting an identification token from Auth0, which provides some basic OpenID profile information. What we need to verify in our Rails server is an access token. We can request an access token instead of an identification token by changing the response_type field in our queryParams object to token. We also need to add an audience key and add the API identifier as the value. So it ends up looking like this:

// App.js

const queryParams = toQueryString({
  client_id: auth0ClientId,
  redirect_uri: redirectUrl,
  response_type: "token",
  scope: "openid profile", // requesting `openid profile` scope, ie. basic openid information about user
  nonce: "nonce",
  audience: "*YOUR API IDENTIFIER*"
});

Then we can change _handleResponseAsync to grab the access_token and save that instead:

// App.js

_handleResponseAsync = async response => {
  const accessToken = response.access_token;
  await AsyncStorage.setItem("accessToken", accessToken);
  this.props.navigation.navigate("Authenticated");
};

We should also change _bootstrapAsync in the AuthLoading screen to check for an access token:

// App.js

_bootstrapAsync = async () => {
  const accessToken = await AsyncStorage.getItem("accessToken");

  if (!accessToken) {
    this.props.navigation.navigate("SignIn");
  } else {
    this.props.navigation.navigate("Authenticated");
  }
};

Okay so now we’re authenticating and getting an access token from Auth0 and saving it to our client. Let’s try modifying our Authenticated screen to use it. First, let’s change to a class and do a request on componentDidMount against our private endpoint.

// App.js
class Authenticated extends React.Component {
  state = {
    data: {}
  };

  async componentDidMount() {
    try {
      const res = await fetch("http://localhost:3000/pages/private");
      const json = await res.json();
      this.setState({
        data: json
      });
    } catch (e) {
      console.warn(e);
    }
  }

  render() {
    const { data } = this.state;

    return (
      <View style={styles.container}>
        <Text>{`We're authenticated!`}</Text>
        <Text>{data.errors}</Text>
      </View>
    );
  }
}

As expected we are getting that unauthorized error. But let’s see what happens when we pass in the access token in an authorization header field.

// App.js

 async componentDidMount() {
    try {
      const accessToken = await AsyncStorage.getItem("accessToken");
      const res = await fetch("http://localhost:3000/pages/private", {
        headers: {
          Authorization: accessToken
        }
      });
      const json = await res.json();

      this.setState({
        data: json
      });
    } catch (e) {
      console.warn(e);
    }
  }

Auth SuccessAuth Success

Nice, we now have access and we’re getting the user id from the database in Rails. From here you can build out the rest of your app as usual. Please let me know if you have any questions, or think I missed something. Thanks for reading!

Posted on November 1, 2019






Previous post →