# Devise Part 9: Setting Up 302 Redirection vs 401 Unauthorized Handlers

> In part nine, we take a look at how we can set up our API endpoints to return a 401 response if a user is not signed in instead of the default 302 redirect to the login page.

- Canonical: https://dennisokeeffe.com/blog/2022-03-09-part-9-setting-up-302-redirection-vs-401-unauthorized-handlers
- Published: 2022-03-09
- Updated: 2022-03-09
- Tags: ruby, ruby-on-rails, devise

Devise currently handles redirects for us when we are not signed in. This is nice default for our application controller, but what happens when we want to return a **401 Unauthorized** response instead of a 304 redirect?

In this part, we will adjust our Devise initializer to do just that for us in any controller that maps to an `/api/*` route.

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

## Prerequisites

- Catch up with previous parts in this series [Devise with Ruby on Rails 7](https://blog.dennisokeeffe.com/blog/series/devise-with-ruby-on-rails-7).

## 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 8 - ensure you've gone through the previous parts
$ git checkout 8-authorization

# Create a helper
$ mkdir lib/helpers
$ touch lib/helpers/devise_failure_app.rb

# Prepare a folder for our /api/v1 namespace
$ mkdir -p app/controllers/api/v1
```

At this stage, we are ready to write our helper

## Our FailureApp helper

Inside of `lib/helpers/devise_failure_app.rb`, add the following:

```rb
module Helpers
	class DeviseFailureApp < Devise::FailureApp
		def respond
			if request.env['REQUEST_PATH'].start_with?('/api')
				http_auth
			else
				redirect
			end
		end
	end
end
```

This code effectively overrides the default Devise behavior of redirecting to the login page.

Instead, we will return a 401 Unauthorized response for any route that starts with `/api`.

## Updating our Devise initializer

Inside of `config/initializers/devise.rb`, we need to update the `config.warden` manager for the `failure_app`:

```rb
# frozen_string_literal: true
require_relative '../../lib/helpers/devise_failure_app'

# ... rest omitted ...

Devise.setup do |config|
  # ... rest omitted ...

  # ==> Warden configuration
  # If you want to use other strategies, that are not supported by Devise, or
  # change the failure app, you can configure them inside the config.warden block.
  #
  config.warden do |manager|
    # manager.intercept_401 = false
    # manager.default_strategies(scope: :user).unshift :some_external_strategy
    manager.failure_app = Helpers::DeviseFailureApp
  end
end
```

I have omitted a whole bunch of jargon, so ensure you do not remove anything unnecessary and that was already there.

## Updating our routes

To test this, let's update the `config/routes.rb` file so that the documents controller is actually under the `/api/v1` namespace:

```rb
Rails.application.routes.draw do
  devise_for :users,
             controllers: { omniauth_callbacks: 'users/omniauth_callbacks', sessions: 'users/sessions',
                            registrations: 'users/registrations' }
  # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html
  namespace :api do
    namespace :v1 do
      resources :documents, only: %i[index create update destroy show]
    end
  end

  # Defines the root path route ("/")
  resources :users
  resources :home, only: %i[index create]
  resources :session, only: [:index]
  root 'home#index'
end
```

We will need to adjust the controller file too.

## Updating our DocumentsController

Finally, move `app/controllers/documents_controller.rb` to `app/controllers/api/v1/documents_controller.rb`.

We also need to update the class name to `Api::V1::DocumentsController`:

```rb
class Api::V1::DocumentsController < ApplicationController
  include Pundit

  def create
    @doc = Document.new(body: params[:body])
    authorize @doc, :create?
    @doc.save!

    render json: @doc, status: :created
  rescue Pundit::NotAuthorizedError
    render json: { error: 'You are not authorized to create a document' }, status: :unauthorized
  end

  def index
    @docs = Document.all
    render json: @docs
  end

  def update
    @doc = Document.find(params[:id])
    authorize @doc, :update?
    @doc.update!(document_params)
    render json: @doc
  rescue Pundit::NotAuthorizedError
    render json: { error: 'You are not authorized to create a document' }, status: :unauthorized
  end

  def destroy
    @doc = Document.find(params[:id])
    authorize @doc, :destroy?
    @doc.destroy
    render status: :no_content
  rescue Pundit::NotAuthorizedError
    render json: { error: 'You are not authorized to create a document' }, status: :unauthorized
  end

  private

  # Using a private method to encapsulate the permissible parameters
  # is just a good pattern since you'll be able to reuse the same
  # permit list between create and update. Also, you can specialize
  # this method with per-user checking of permissible attributes.
  def document_params
    params.require(:document).permit(:body)
  end
end
```

## Testing our work

Start up the Rails server `bin/rails s`.

In another terminal, I use [`httpie`](https://httpie.io/) to make two requests to check that both the `401` redirect works and so too does our `302` redirect for the `home` page.

```s
$ http GET localhost:3000/api/v1/documents
HTTP/1.1 401 Unauthorized

# ... omitted

You need to sign in or sign up before continuing.

$ http GET localhost:3000/home --header
HTTP/1.1 302 Found
Cache-Control: no-cache
Content-Type: text/html; charset=utf-8
Location: http://localhost:3000/users/sign_in
```

## Updating our tests

We just broke a lot of tests that we wrote in the previous part.

In order to fix this, we just need to do some tweaking.

For consistency, move `spec/controllers/documents_controller_spec.rb` to `spec/controllers/api/v1/documents_controller_spec.rb`.

Then, inside of `spec/controllers/api/v1/documents_controller_spec.rb` change `RSpec.describe DocumentsController` to `RSpec.describe Api::V1::DocumentsController`.

If we run our tests now, we will get two failures:

```s
$ bundler exec rspec
.F....F.

# ... omitted

Finished in 0.35224 seconds (files took 4.38 seconds to load)
8 examples, 2 failures

Failed examples:

rspec ./spec/controllers/api/v1/documents_controller_spec.rb:19 # Api::V1::DocumentsController GET #index unsuccessful responses not authenticated redirects user when they are not logged in
rspec ./spec/controllers/api/v1/documents_controller_spec.rb:78 # Api::V1::DocumentsController GET #show unsuccessful responses redirects user when they are not logged in
```

Both are due to the fact that we are still looking for the redirect response for two of our tests as well as the fact that `request.env['REQUEST_PATH']` is not defined in our tests.

Adjust both tests that assert for the 302 and set the request path and you'll end up with a file like this:

```rb
require 'rails_helper'

RSpec.describe Api::V1::DocumentsController, type: :controller do
  describe 'GET #index' do
    let(:subject) { create(:document) }

    context 'successful responses' do
      login_admin
      it 'returns all posts when user is authorized' do
        get :index, params: { body: subject.body }

        expect(response.status).to eq(200)
        expect(response.parsed_body).to eq([subject.as_json])
      end
    end

    context 'unsuccessful responses' do
      context 'not authenticated' do
        it 'responds with not authorized when they are not logged in' do
          request.headers['REQUEST_PATH'] = api_v1_documents_path
          get :create, params: { body: subject.body }

          expect(response.status).to eq(401)
        end
      end

      context 'not authorized' do
        login_user

        it 'redirects user when they are not logged in' do
          get :create, params: { body: subject.body }

          expect(response.status).to eq(401)
        end
      end
    end
  end

  describe 'GET #show' do
    context 'successful responses' do
      context 'admin' do
        login_admin

        let(:subject) { create(:document, users: [User.first]) }
        let(:document_two) { create(:document) }

        it 'can view their own post' do
          get :show, params: { id: subject.id }

          expect(response.status).to eq(200)
          expect(response.parsed_body).to eq(subject.as_json)
        end

        it 'can view another post' do
          get :show, params: { id: document_two.id }

          expect(response.status).to eq(200)
          expect(response.parsed_body).to eq(document_two.as_json)
        end
      end

      context 'basic user' do
        login_user

        let(:subject) { create(:document, users: [User.first]) }

        it 'can view their own post' do
          get :show, params: { id: subject.id }

          expect(response.status).to eq(200)
          expect(response.parsed_body).to eq(subject.as_json)
        end
      end
    end

    context 'unsuccessful responses' do
      let(:subject) { create(:document) }

      it 'responds with not authorized when they are not logged in' do
        request.headers['REQUEST_PATH'] = api_v1_documents_show_path
        get :create, params: { id: subject.id }

        expect(response.status).to eq(401)
      end

      context 'basic user is not related to document' do
        login_user

        it 'can view their own post' do
          get :show, params: { id: subject.id }

          expect(response.status).to eq(401)
        end
      end
    end
  end
end
```

Re-run our tests and you will see the success:

```s
$ bundler exec rspec
........

Finished in 0.23749 seconds (files took 2 seconds to load)
8 examples, 0 failures
```

## Summary

Today's post demonstrated how we can set up a unauthorized response for our API endpoints and keep the redirect for the others when using our devise authentication.

## Resources and further reading

- [Devise with Ruby on Rails 7](https://blog.dennisokeeffe.com/blog/series/devise-with-ruby-on-rails-7)
- [Source code](https://github.com/okeeffed/demo-rails-7-with-devise-series)
- [StackOverflow answer to redirection](https://stackoverflow.com/questions/55032668/devise-how-to-return-401-unauthorized-without-redirect-if-user-is-not-signed-in)
- [Devise redirect to specific page docs](https://github.com/heartcombo/devise/wiki/How-To:-Redirect-to-a-specific-page-when-the-user-can-not-be-authenticated)

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