# Devise Part 4: Authentication With A Separate Frontend

> In part four, we add in a separate Next.js project into the mix and demonstrate how to organize authentication for our main Rails API.

- Canonical: https://dennisokeeffe.com/blog/2022-03-07-part-4-authenticated-with-a-separate-frontend
- Published: 2022-03-07
- Updated: 2022-03-07
- Tags: ruby, ruby-on-rails, devise

In the [previous post (part three)](https://blog.dennisokeeffe.com/blog/2022-03-07-part-3-updating-our-devise-views-with-tailwind-css), we added in Tailwind to our setup to improve our default Devise views.

This part will create a new Next.js project and add in (rudimentary) authentication.

In our case, we will be continuing on with the default authentication setup, although I may write another part towards the end that uses [devise_auth_token](https://github.com/lynndylanhurley/devise_token_auth) which would work better for mobile apps, etc.

Source code can be found [here](https://github.com/okeeffed/demo-rails-7-with-devise-series).

## Prerequisites

1. Basic familiarity with setting up a [new Rails project](https://guides.rubyonrails.org/getting_started.html).
2. Not entire required to follow along, but we will be using [Next.js](https://nextjs.org/) in this demo.

## Getting started

We will be working from the source code found [here](https://github.com/okeeffed/demo-rails-7-with-devise-series).

```s
# Clone and change into the project
$ git clone https://github.com/okeeffed/demo-rails-7-with-devise-series
$ cd demo-rails-7-with-devise-series

# Continue from part 2 - warning that Redis required
$ git checkout 3-adding-tailwind

# Add the necessary gems
$ bundler add rack-cors

# Create a sessions controller to pass back an initial CSRF token
$ bin/rails g controller session index

# Add our file required for cors config
$ touch config/initializers/cors.rb
```

We add in `rack-cors` as we will need to update our Rails API to allow cross-origin requests.

## Setting up our Next.js project

From the root directory of the project, let's create a new project called `next-frontend`.

```s
# Create a new Next.js application
$ npx create-next-app@latest --ts next-frontend

# Create required files
$ mkdir next-frontend/lib
$ touch next-frontend/pages/another.tsx next-frontend/lib/axios.ts

# We'll use the axios package for sending requests
$ npm --prefix next-frontend install axios
```

Update `next-frontend/package.json` to have the `dev` script start on port 4000 by updating it to `next dev -p 4000"`.

Once that is done, we will update our `bin/dev` script to start Next.js at the same time:

```s
web: bin/rails server -p 3000
css: bin/rails tailwindcss:watch
next: npm --prefix next-frontend run dev
```

> Note: The `prefix` flag enable us to run an npm script in another directory.

## Updating our files in our Next.js project

I won't go into too many details about what is happening here - I am hoping that you understand Next.js.

The general gist of what we want to do is update the page at `localhost:4000` to have a basic login form with a button to test an endpoint and see if we can authenticate.

We will also add a button to go to the `localhost:4000/another` route from that page.

To do so, update the code at `next-frontend/pages/index.tsx` to:

```tsx
import type { NextPage } from "next";
import Head from "next/head";
import Link from "next/link";
import styles from "../styles/Home.module.css";
import axios from "../lib/axios";
import * as React from "react";

const Home: NextPage = () => {
  const onSubmit = async (e: React.SyntheticEvent) => {
    try {
      e.preventDefault();
      console.log("submit");

      const target = e.target as typeof e.target & {
        email: { value: string };
        password: { value: string };
      };

      const { data } = await axios.post("http://localhost:3000/users/sign_in", {
        user: {
          email: target.email.value,
          password: target.password.value,
          remember_me: 0,
        },
      });

      window.alert("Sign in successful");
    } catch (err) {
      window.alert("Sign in failed!");
    }
  };

  // This will always cause an error with this code,
  // but we will be signed out so ignore the error.
  const onSignOut = async (e: React.SyntheticEvent) => {
    e.preventDefault();

    try {
      await axios.delete("http://localhost:3000/users/sign_out");
    } catch (e) {
      window.alert("Signed out!");
    }
  };

  const testEndpoint = async () => {
    try {
      const { data } = await axios.post("http://localhost:3000/home");
      window.alert(JSON.stringify(data));
    } catch (e) {
      window.alert((e as Error).message);
    }
  };

  return (
    <div className={styles.container}>
      <Head>
        <title>Create Next App</title>
        <meta name="description" content="Generated by create next app" />
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <main className={styles.main}>
        <form onSubmit={onSubmit}>
          <div>
            <input name="email" type="text" placeholder="Email" />
          </div>
          <div>
            <input name="password" type="password" placeholder="Password" />
          </div>
          <div>
            <button type="submit">Sign in</button>
          </div>
        </form>
        <button onClick={onSignOut}>Sign out</button>
        <button onClick={testEndpoint}>Test Endpoint</button>

        <Link href="/another">Go to /another</Link>
      </main>
    </div>
  );
};

export default Home;
```

Another is similar to home but without the login. It is just to test whether or not we understand that you session is still active.

```tsx
import type { NextPage } from "next";
import Head from "next/head";
import Link from "next/link";
import styles from "../styles/Home.module.css";
import axios from "../lib/axios";

const Another: NextPage = () => {
  const testEndpoint = async () => {
    try {
      const { data } = await axios.post("http://localhost:3000/home");
      window.alert(JSON.stringify(data));
    } catch (e) {
      // please ignore my forced typecasting
      window.alert((e as Error).message);
    }
  };

  return (
    <div className={styles.container}>
      <Head>
        <title>Create Next App</title>
        <meta name="description" content="Generated by create next app" />
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <main className={styles.main}>
        <button onClick={testEndpoint}>Test Endpoint</button>
        <Link href="/">Go to home page</Link>
      </main>
    </div>
  );
};

export default Another;
```

## Passing the CSRF Token with our requests

In the above code, we are importing `axios` from `next-frontend/lib/axios.ts`.

We are yet to fill in that code. Add the following:

```ts
import axios from "axios";

axios.defaults.xsrfCookieName = "CSRF-TOKEN";
axios.defaults.xsrfHeaderName = "X-CSRF-Token";
axios.defaults.withCredentials = true;

export default axios;
```

Here we are setting a default of axios to always pass our CSRF token under the head `X-CSRF-Token` header which grabs the value from the cookie we have stored under `CSRF-TOKEN`.

We now need to setup our Rails API to give us that CSRF token on each request.

## Updating our Rails API

Back in our application controller, let's update our code to set the `CSRF-TOKEN` on each request:

```rb
class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception

  before_action :set_csrf_cookie
  before_action :authenticate_user!

  private

  def set_csrf_cookie
    cookies['CSRF-TOKEN'] = form_authenticity_token
  end
end
```

This will ensure that we can get the token from the cookie and pass it to our axios requests.

That being said, we already generated the controller earlier. Head to `app/controllers/session_controller.rb` and update the code to this:

```rb
class SessionController < ApplicationController
  skip_before_action :authenticate_user!

  def index; end
end
```

We are essentially bypassing the authentication and just sending the CSRF token for this route.

To use the route, you will need to add `resources :session, only: [:index]` to our `config/routes.rb` file.

## Updating our app to grab the initial CSRF token

We need to update Next.js to grab the initial token.

We can do so in this contrived solution by updating `next-frontend/pages/_app.tsx`:

```tsx
import "../styles/globals.css";
import type { AppProps } from "next/app";
import { useEffect } from "react";
import { useRouter } from "next/router";
import axios from "../lib/axios";

function MyApp({ Component, pageProps }: AppProps) {
  const router = useRouter();

  useEffect(() => {
    axios.get("http://localhost:3000/session");
  }, [router.asPath]);

  return <Component {...pageProps} />;
}

export default MyApp;
```

We are nearly there! The last thing that we need to do is configure our CORS policy.

## Configuring CORS

We installed the `rack-cors` gem during the setup here, so we need to enable it to allow both `localhost:3000` AND `localhost:4000` to access our resources.

We can do so by updating `config/initializers/cors.rb` to this:

```rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins 'localhost:3000', 'localhost:4000'

    resource '*',
             headers: :any,
             methods: %i[get post put patch delete options head],
             credentials: true
  end
end
```

## Turning off the forgery protection origin check

Lastly, we need to create an API route that we want to bypass the forgery protection for.

Since this is a contrived post and we are using it to test our CSRF token cross-origin, we can do so by updating our `config/application.rb` file to the following:

```rb
require_relative 'boot'

require 'rails/all'

# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)

module DemoRails7WithDeviseSeries
  class Application < Rails::Application
    # Initialize configuration defaults for originally generated Rails version.
    config.load_defaults 7.0

    # Configuration for the application, engines, and railties goes here.
    #
    # These settings can be overridden in specific environments using the files
    # in config/environments, which are processed later.
    #
    # config.time_zone = "Central Time (US & Canada)"
    # config.eager_load_paths << Rails.root.join("extras")

    # Since we will be using other origins
    config.action_controller.forgery_protection_origin_check = false
  end
end
```

This will allow our Next.js application to make requests to our API from `localhost:4000` if the CSRF token is present.

## Updating our Home controller

Lastly, we set up our Next.js app to ping the POST endpoint `localhost:3000/home`.

Although this is a contrived example we are making, we need to update the home controller to respond to this request.

Add the create method to the home controller `app/controllers/home_controller.rb`:

```rb
class HomeController < ApplicationController
  def index; end

  def create
    render json: { message: 'Welcome to the API' }
  end
end
```

Finally, update the routes config files `config/routes.rb` to this:

```rb
Rails.application.routes.draw do
  get 'session/index'
  devise_for :users
  # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html

  resources :session, only: [:index]
  resources :home, only: %i[index create]

  # Defines the root path route ("/")
  root 'home#index'
end
```

## Testing our CSRF token

We can start the Rails app and Next.js app by running `bin/dev` from the root in a terminal.

Open up our Next.js app at `localhost:4000` and you will see the following:

![Next.js home page](https://cdn.dennisokeeffe.com/assets/2022-03-07-part-4-authenticated-with-a-separate-frontend/1-home-page.webp)

Sign in, and a popup will let you know we've signed in:

![Sign in success](https://cdn.dennisokeeffe.com/assets/2022-03-07-part-4-authenticated-with-a-separate-frontend/2-signed-in.webp)

> Note: Because of the contrived-nature of the code, the endpoint will also return 200 on a failed sign-in but you can check this on the network settings or note a failure if the next part does not return as expected.

If we click out test endpoint button, we get the JSON that we expected back:

![Expected response](https://cdn.dennisokeeffe.com/assets/2022-03-07-part-4-authenticated-with-a-separate-frontend/3-authed-response.webp)

If we now selected "sign out" and hit our test endpoint button without signing in, we will get a 422 error.

![422 error](https://cdn.dennisokeeffe.com/assets/2022-03-07-part-4-authenticated-with-a-separate-frontend/4-after-sign-out.webp)

In order to test the `/another` endpoint, you will need to refresh the page (bad design by me with the CSRF token being destroyed when we signed out). If you do follow to `/another` after successfully signing in, you'll note the API continues to work.

## Summary

Today's post demonstrated how to set up CORS and CSRF protection in Rails with a remote Next.js frontend on another domain.

With some code (prone to errors), we were able to demonstrate how we can sign in and out of this remote app.

The example itself was quite contrived and it is worth going over it with a grain of salt: there are a lot of improvements to be made and potential risks to double-check.

There is an alternative that uses an opaque token with the [`devise_auth_token`](https://github.com/lynndylanhurley/devise_token_auth) gem that I will demonstrate in another part.

## Resources and further reading

- [Rails getting started](https://guides.rubyonrails.org/getting_started.html)
- [Source code](https://github.com/okeeffed/demo-rails-7-with-devise-series)
- [`devise_auth_token`](https://github.com/lynndylanhurley/devise_token_auth)

_Photo credit: [`kevinzk429`](https://unsplash.com/@kevinzk429)_
