<title type="html">How to implement Rails API authentication with Devise and Doorkeeper</title>
<link href="https://rubyyagi.com/rails-api-authentication-devise-doorkeeper/" rel="alternate" type="text/html" title="How to implement Rails API authentication with Devise and Doorkeeper"/>
<published>2020-12-06T00:00:00+00:00</published>
<updated>2020-12-06T00:00:00+00:00</updated>
<id>https://rubyyagi.com/rails-api-authentication-devise-doorkeeper</id>
<content type="html" xml:base="https://rubyyagi.com/rails-api-authentication-devise-doorkeeper/"><p>Most of the time when we implement API endpoints on our Rails app, we want to limit access of these API to authorized users only, thereās a few strategy for authenticating user through API, ranging from a <a href="https://github.com/gonzalo-bulnes/simple_token_authentication">simple token authentication</a> to a fullblown OAuth provider with JWT.</p>
<p>In this tutorial, we will implement an OAuth provider for API authentication on the same Rails app we serve the user, using Devise and <a href="https://github.com/doorkeeper-gem/doorkeeper">Doorkeeper</a> gem.</p>
<p>After this tutorial, you would be able to implement Devise sign in/sign up on Rails frontend, and Doorkeeper OAuth (login, register) on the API side for mobile app client, or a separate frontend client like React etc.</p>
<p>This tutorial assume that you have some experience using Devise and your Rails app will both have a frontend UI and API for users to register and sign in. We can also use Doorkeeper to allow third party to create their own OAuth application on our own Rails app platform, but that is out of the scope of this article, as this article will focus on creating our own OAuth application for self consumption only.</p>
<p><strong>Table of contents</strong></p>
<ol>
<li><a href="#scaffold-a-model">Scaffold a model</a></li>
<li><a href="#setup-devise-gem">Setup Devise gem</a></li>
<li><a href="#setup-doorkeeper-gem">Setup Doorkeeper gem</a></li>
<li><a href="#customize-doorkeeper-configuration">Customize Doorkeeper configuration</a></li>
<li><a href="#create-your-own-oauth-application">Create your own OAuth application</a></li>
<li><a href="#how-to-login--logout-and-refresh-token-using-api">How to login , logout and refresh token using API</a></li>
<li><a href="#create-api-controllers-that-require-authentication">Create API controllers that require authentication</a></li>
<li><a href="#create-an-endpoint-for-user-registration">Create an endpoint for user registration</a></li>
<li><a href="#revoke-user-token-manually">Revoke user token manually</a></li>
<li><a href="#references">References</a></li>
</ol>
<h2 id="scaffold-a-model">Scaffold a model</h2>
<p>Letās start with some scaffolding so we can have a model, controller and view for CRUD, you can skip this section if you already have an existing Rails app.</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">rails</span> <span class="n">g</span> <span class="n">scaffold</span> <span class="n">bookmarks</span> <span class="n">title</span><span class="ss">:string</span> <span class="n">url</span><span class="ss">:string</span>
</code></pre></div></div>
<p>then in <strong>routes.rb</strong> , set the root path to ābookmarks#indexā. Devise requires us to set a root path in routes to work.</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># config/routes.rb</span>
<span class="n">root</span> <span class="s1">'bookmarks#index'</span>
</code></pre></div></div>
<p>Now we have a sample CRUD Rails app, we can move on to the next step.</p>
<h2 id="setup-devise-gem">Setup Devise gem</h2>
<p>Add devise gem in the <strong>Gemfile</strong> :</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Gemfile</span>
<span class="c1"># ...</span>
<span class="n">gem</span> <span class="s1">'devise'</span><span class="p">,</span> <span class="s1">'~> 4.7.3'</span>
</code></pre></div></div>
<p>and run <code class="language-plaintext highlighter-rouge">bundle install</code> to install it.</p>
<p>Next, run the Devise installation generator :</p>
<p><code class="language-plaintext highlighter-rouge">rails g devise:install</code></p>
<p>Then we create the user model (or any other model name you are using like admin, staff etc) using Devise :</p>
<p><code class="language-plaintext highlighter-rouge">rails g devise User</code></p>
<p>You can customize the devise features you want in the generated migration file, and also in the User model file.</p>
<p>Then run <code class="language-plaintext highlighter-rouge">rake db:migrate</code> to create the users table.</p>
<p>Now we have the Devise user set up, we can add <strong>authenticate_user!</strong> to <strong>bookmarks_controller.rb</strong> so only logged in users can view the controller now.</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># app/controllers/bookmarks_controller.rb</span>
<span class="k">class</span> <span class="nc">BookmarksController</span> <span class="o"><</span> <span class="no">ApplicationController</span>
<span class="n">before_action</span> <span class="ss">:authenticate_user!</span>
<span class="c1"># ....</span>
<span class="k">end</span>
</code></pre></div></div>
<p>Next we will move to the main part, which is setting up authentication for the API using Doorkeeper gem.</p>
<h2 id="setup-doorkeeper-gem">Setup Doorkeeper gem</h2>
<p>Add doorkeeper gem in the <strong>Gemfile</strong> :</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Gemfile</span>
<span class="c1"># ...</span>
<span class="n">gem</span> <span class="s1">'doorkeeper'</span><span class="p">,</span> <span class="s1">'~> 5.4.0'</span>
</code></pre></div></div>
<p>and run <code class="language-plaintext highlighter-rouge">bundle install</code> to install it.</p>
<p>Next, run the Doorkeeper installation generator :</p>
<p><code class="language-plaintext highlighter-rouge">rails g doorkeeper:install</code></p>
<p>This will generate the configuration file for Doorkeeper in <strong>config/initializers/doorkeeper.rb</strong>, which we will customize later.</p>
<p>Next, run the Doorkeeper migration generator :</p>
<p><code class="language-plaintext highlighter-rouge">rails g doorkeeper:migration</code></p>
<p>This will generate a migration file for Doorkeeper in <strong>db/migrate/ā¦_create_doorkeeper_tables.rb</strong> .</p>
<p>We will customize the migration file as we wonāt need all the tables / attributes generated.</p>
<p>Open the <strong>ā¦._create_doorkeeper_tables.rb</strong> migration file, then edit to make it look like below :</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># frozen_string_literal: true</span>
<span class="k">class</span> <span class="nc">CreateDoorkeeperTables</span> <span class="o"><</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Migration</span><span class="p">[</span><span class="mf">6.0</span><span class="p">]</span>
<span class="k">def</span> <span class="nf">change</span>
<span class="n">create_table</span> <span class="ss">:oauth_applications</span> <span class="k">do</span> <span class="o">|</span><span class="n">t</span><span class="o">|</span>
<span class="n">t</span><span class="p">.</span><span class="nf">string</span> <span class="ss">:name</span><span class="p">,</span> <span class="ss">null: </span><span class="kp">false</span>
<span class="n">t</span><span class="p">.</span><span class="nf">string</span> <span class="ss">:uid</span><span class="p">,</span> <span class="ss">null: </span><span class="kp">false</span>
<span class="n">t</span><span class="p">.</span><span class="nf">string</span> <span class="ss">:secret</span><span class="p">,</span> <span class="ss">null: </span><span class="kp">false</span>
<span class="c1"># Remove `null: false` if you are planning to use grant flows</span>
<span class="c1"># that doesn't require redirect URI to be used during authorization</span>
<span class="c1"># like Client Credentials flow or Resource Owner Password.</span>
<span class="n">t</span><span class="p">.</span><span class="nf">text</span> <span class="ss">:redirect_uri</span>
<span class="n">t</span><span class="p">.</span><span class="nf">string</span> <span class="ss">:scopes</span><span class="p">,</span> <span class="ss">null: </span><span class="kp">false</span><span class="p">,</span> <span class="ss">default: </span><span class="s1">''</span>
<span class="n">t</span><span class="p">.</span><span class="nf">boolean</span> <span class="ss">:confidential</span><span class="p">,</span> <span class="ss">null: </span><span class="kp">false</span><span class="p">,</span> <span class="ss">default: </span><span class="kp">true</span>
<span class="n">t</span><span class="p">.</span><span class="nf">timestamps</span> <span class="ss">null: </span><span class="kp">false</span>
<span class="k">end</span>
<span class="n">add_index</span> <span class="ss">:oauth_applications</span><span class="p">,</span> <span class="ss">:uid</span><span class="p">,</span> <span class="ss">unique: </span><span class="kp">true</span>
<span class="n">create_table</span> <span class="ss">:oauth_access_tokens</span> <span class="k">do</span> <span class="o">|</span><span class="n">t</span><span class="o">|</span>
<span class="n">t</span><span class="p">.</span><span class="nf">references</span> <span class="ss">:resource_owner</span><span class="p">,</span> <span class="ss">index: </span><span class="kp">true</span>
<span class="c1"># Remove `null: false` if you are planning to use Password</span>
<span class="c1"># Credentials Grant flow that doesn't require an application.</span>
<span class="n">t</span><span class="p">.</span><span class="nf">references</span> <span class="ss">:application</span><span class="p">,</span> <span class="ss">null: </span><span class="kp">false</span>
<span class="n">t</span><span class="p">.</span><span class="nf">string</span> <span class="ss">:token</span><span class="p">,</span> <span class="ss">null: </span><span class="kp">false</span>
<span class="n">t</span><span class="p">.</span><span class="nf">string</span> <span class="ss">:refresh_token</span>
<span class="n">t</span><span class="p">.</span><span class="nf">integer</span> <span class="ss">:expires_in</span>
<span class="n">t</span><span class="p">.</span><span class="nf">datetime</span> <span class="ss">:revoked_at</span>
<span class="n">t</span><span class="p">.</span><span class="nf">datetime</span> <span class="ss">:created_at</span><span class="p">,</span> <span class="ss">null: </span><span class="kp">false</span>
<span class="n">t</span><span class="p">.</span><span class="nf">string</span> <span class="ss">:scopes</span>
<span class="c1"># The authorization server MAY issue a new refresh token, in which case</span>
<span class="c1"># *the client MUST discard the old refresh token* and replace it with the</span>
<span class="c1"># new refresh token. The authorization server MAY revoke the old</span>
<span class="c1"># refresh token after issuing a new refresh token to the client.</span>
<span class="c1"># @see https://tools.ietf.org/html/rfc6749#section-6</span>
<span class="c1">#</span>
<span class="c1"># Doorkeeper implementation: if there is a `previous_refresh_token` column,</span>
<span class="c1"># refresh tokens will be revoked after a related access token is used.</span>
<span class="c1"># If there is no `previous_refresh_token` column, previous tokens are</span>
<span class="c1"># revoked as soon as a new access token is created.</span>
<span class="c1">#</span>
<span class="c1"># Comment out this line if you want refresh tokens to be instantly</span>
<span class="c1"># revoked after use.</span>
<span class="n">t</span><span class="p">.</span><span class="nf">string</span> <span class="ss">:previous_refresh_token</span><span class="p">,</span> <span class="ss">null: </span><span class="kp">false</span><span class="p">,</span> <span class="ss">default: </span><span class="s2">""</span>
<span class="k">end</span>
<span class="n">add_index</span> <span class="ss">:oauth_access_tokens</span><span class="p">,</span> <span class="ss">:token</span><span class="p">,</span> <span class="ss">unique: </span><span class="kp">true</span>
<span class="n">add_index</span> <span class="ss">:oauth_access_tokens</span><span class="p">,</span> <span class="ss">:refresh_token</span><span class="p">,</span> <span class="ss">unique: </span><span class="kp">true</span>
<span class="n">add_foreign_key</span><span class="p">(</span>
<span class="ss">:oauth_access_tokens</span><span class="p">,</span>
<span class="ss">:oauth_applications</span><span class="p">,</span>
<span class="ss">column: :application_id</span>
<span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>
<p>The modification I did on the migration file :</p>
<ol>
<li>Remove <strong>null: false</strong> on the <strong>redirect_uri</strong> for oauth_applications table. As we are using the OAuth for API authentication, we wonāt need to redirect the user to a callback page (like after you sign in with Google / Apple / Facebook on an app, they will redirect to a page usually).</li>
<li>Remove the creation of table <strong>oauth_access_grants</strong> , along with its related index and foreign key.</li>
</ol>
<p>The OAuth application table is used to keep track of the application we created to use for authentication. For example, we can create three application, one for Android app client, one for iOS app client and one for React frontend, this way we can know which clients the users are using. If you only need one client (eg: web frontend), it is fine too.</p>
<p>Hereās an example of Github OAuth applications :</p>
<p><img src="https://rubyyagi.s3.amazonaws.com/16-rails-api-authentication-devise-doorkeeper/example_oauth.png" alt="github oauth" /></p>
<p>Next, run <code class="language-plaintext highlighter-rouge">rake db:migrate</code> to add these tables into database.</p>
<p>Next, we will customize the Doorkeeper configuration.</p>
<h2 id="customize-doorkeeper-configuration">Customize Doorkeeper configuration</h2>
<p>Open <strong>config/initializers/doorkeeper.rb</strong> , and edit the following.</p>
<p>Comment out or remove the block for <strong>resource_owner_authenticator</strong> at the top of the file.</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">#config/initializers/doorkeeper.rb</span>
<span class="no">Doorkeeper</span><span class="p">.</span><span class="nf">configure</span> <span class="k">do</span>
<span class="c1"># Change the ORM that doorkeeper will use (requires ORM extensions installed).</span>
<span class="c1"># Check the list of supported ORMs here: https://github.com/doorkeeper-gem/doorkeeper#orms</span>
<span class="n">orm</span> <span class="ss">:active_record</span>
<span class="c1"># This block will be called to check whether the resource owner is authenticated or not.</span>
<span class="c1"># resource_owner_authenticator do</span>
<span class="c1"># raise "Please configure doorkeeper resource_owner_authenticator block located in #{__FILE__}"</span>
<span class="c1"># Put your resource owner authentication logic here.</span>
<span class="c1"># Example implementation:</span>
<span class="c1"># User.find_by(id: session[:user_id]) || redirect_to(new_user_session_url)</span>
<span class="c1"># end</span>
</code></pre></div></div>
<p>The <strong>resouce_owner_authenticator</strong> block is used to get the authenticated user information or redirect the user to login page from OAuth, for example like this Twitter OAuth page :</p>
<p><img src="https://rubyyagi.s3.amazonaws.com/16-rails-api-authentication-devise-doorkeeper/example_oauth_redirect.png" alt="twitter oauth example" /></p>
<p>As we are going to exchange OAuth token by using user login credentials (email + password) on the API, we donāt need to implement this block, so we can comment it out.</p>
<p>To tell Doorkeeper we are using user credentials to login, we need to implement the <strong>resource_owner_from_credentials</strong> block like this :</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">#config/initializers/doorkeeper.rb</span>
<span class="no">Doorkeeper</span><span class="p">.</span><span class="nf">configure</span> <span class="k">do</span>
<span class="c1"># Change the ORM that doorkeeper will use (requires ORM extensions installed).</span>
<span class="c1"># Check the list of supported ORMs here: https://github.com/doorkeeper-gem/doorkeeper#orms</span>
<span class="n">orm</span> <span class="ss">:active_record</span>
<span class="c1"># This block will be called to check whether the resource owner is authenticated or not.</span>
<span class="c1"># resource_owner_authenticator do</span>
<span class="c1"># raise "Please configure doorkeeper resource_owner_authenticator block located in #{__FILE__}"</span>
<span class="c1"># Put your resource owner authentication logic here.</span>
<span class="c1"># Example implementation:</span>
<span class="c1"># User.find_by(id: session[:user_id]) || redirect_to(new_user_session_url)</span>
<span class="c1"># end</span>
<span class="n">resource_owner_from_credentials</span> <span class="k">do</span> <span class="o">|</span><span class="n">_routes</span><span class="o">|</span>
<span class="no">User</span><span class="p">.</span><span class="nf">authenticate</span><span class="p">(</span><span class="n">params</span><span class="p">[</span><span class="ss">:email</span><span class="p">],</span> <span class="n">params</span><span class="p">[</span><span class="ss">:password</span><span class="p">])</span>
<span class="k">end</span>
<span class="c1"># ...</span>
</code></pre></div></div>
<p>This will allow us to send the user email and password to the /oauth/token endpoint to authenticate user.</p>
<p>Then we need to implement the <strong>authenticate</strong> class method on the <strong>app/models/user.rb</strong> model file.</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># app/models/user.rb</span>
<span class="k">class</span> <span class="nc">User</span> <span class="o"><</span> <span class="no">ApplicationRecord</span>
<span class="c1"># Include default devise modules. Others available are:</span>
<span class="c1"># :confirmable, :lockable, :timeoutable, :trackable and :omniauthable</span>
<span class="n">devise</span> <span class="ss">:database_authenticatable</span><span class="p">,</span> <span class="ss">:registerable</span><span class="p">,</span>
<span class="ss">:recoverable</span><span class="p">,</span> <span class="ss">:rememberable</span><span class="p">,</span> <span class="ss">:validatable</span>
<span class="n">validates</span> <span class="ss">:email</span><span class="p">,</span> <span class="ss">format: </span><span class="no">URI</span><span class="o">::</span><span class="no">MailTo</span><span class="o">::</span><span class="no">EMAIL_REGEXP</span>
<span class="c1"># the authenticate method from devise documentation</span>
<span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">authenticate</span><span class="p">(</span><span class="n">email</span><span class="p">,</span> <span class="n">password</span><span class="p">)</span>
<span class="n">user</span> <span class="o">=</span> <span class="no">User</span><span class="p">.</span><span class="nf">find_for_authentication</span><span class="p">(</span><span class="ss">email: </span><span class="n">email</span><span class="p">)</span>
<span class="n">user</span><span class="o">&</span><span class="p">.</span><span class="nf">valid_password?</span><span class="p">(</span><span class="n">password</span><span class="p">)</span> <span class="p">?</span> <span class="n">user</span> <span class="p">:</span> <span class="kp">nil</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>
<p>You can read more on the authenticate method on <a href="https://github.com/heartcombo/devise/wiki/How-To:-Find-a-user-when-you-have-their-credentials">Deviseās github Wiki page</a>.</p>
<p>Next, enable password grant flow in <strong>config/initializers/doorkeeper.rb</strong> , this will allow us to send the user email and password to the /oauth/token endpoint and get OAuth token in return.</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Doorkeeper</span><span class="p">.</span><span class="nf">configure</span> <span class="k">do</span>
<span class="n">orm</span> <span class="ss">:active_record</span>
<span class="n">resource_owner_from_credentials</span> <span class="k">do</span> <span class="o">|</span><span class="n">_routes</span><span class="o">|</span>
<span class="no">User</span><span class="p">.</span><span class="nf">authenticate</span><span class="p">(</span><span class="n">params</span><span class="p">[</span><span class="ss">:email</span><span class="p">],</span> <span class="n">params</span><span class="p">[</span><span class="ss">:password</span><span class="p">])</span>
<span class="k">end</span>
<span class="c1"># enable password grant</span>
<span class="n">grant_flows</span> <span class="sx">%w[password]</span>
<span class="c1"># ....</span>
</code></pre></div></div>
<p>You can search for āgrant_flowsā in this file, and uncomment and edit it.</p>
<p>Next, insert <strong>allow_blank_redirect_uri true</strong> into the configuration, so that we can create OAuth application with blank redirect URL (user wonāt get redirected after login, as we are using API).</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Doorkeeper</span><span class="p">.</span><span class="nf">configure</span> <span class="k">do</span>
<span class="n">orm</span> <span class="ss">:active_record</span>
<span class="n">resource_owner_from_credentials</span> <span class="k">do</span> <span class="o">|</span><span class="n">_routes</span><span class="o">|</span>
<span class="no">User</span><span class="p">.</span><span class="nf">authenticate</span><span class="p">(</span><span class="n">params</span><span class="p">[</span><span class="ss">:email</span><span class="p">],</span> <span class="n">params</span><span class="p">[</span><span class="ss">:password</span><span class="p">])</span>
<span class="k">end</span>
<span class="n">grant_flows</span> <span class="sx">%w[password]</span>
<span class="n">allow_blank_redirect_uri</span> <span class="kp">true</span>
<span class="c1"># ....</span>
</code></pre></div></div>
<p>As the OAuth application we create is for our own use (not third part), we can skip authorization.</p>
<p>Insert <strong>skip_authorization</strong> into the configuration like this :</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Doorkeeper</span><span class="p">.</span><span class="nf">configure</span> <span class="k">do</span>
<span class="n">orm</span> <span class="ss">:active_record</span>
<span class="n">resource_owner_from_credentials</span> <span class="k">do</span> <span class="o">|</span><span class="n">_routes</span><span class="o">|</span>
<span class="no">User</span><span class="p">.</span><span class="nf">authenticate</span><span class="p">(</span><span class="n">params</span><span class="p">[</span><span class="ss">:email</span><span class="p">],</span> <span class="n">params</span><span class="p">[</span><span class="ss">:password</span><span class="p">])</span>
<span class="k">end</span>
<span class="n">grant_flows</span> <span class="sx">%w[password]</span>
<span class="n">allow_blank_redirect_uri</span> <span class="kp">true</span>
<span class="n">skip_authorization</span> <span class="k">do</span>
<span class="kp">true</span>
<span class="k">end</span>
<span class="c1"># ....</span>
</code></pre></div></div>
<p>The authorization we skipped is something like this :</p>
<p><img src="https://rubyyagi.s3.amazonaws.com/16-rails-api-authentication-devise-doorkeeper/oauth_authorization.png" alt="authorization" /></p>
<p>As we skipped authorization, user wonāt need to click the āauthorizeā button to interact with our API.</p>
<p>Optionally, if you want to enable refresh token mechanism in OAuth, you can insert the <strong>use_refresh_token</strong> into the configuration. This would allow the client app to request a new access token using the refresh token when the current access token is expired.</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Doorkeeper</span><span class="p">.</span><span class="nf">configure</span> <span class="k">do</span>
<span class="n">orm</span> <span class="ss">:active_record</span>
<span class="n">resource_owner_from_credentials</span> <span class="k">do</span> <span class="o">|</span><span class="n">_routes</span><span class="o">|</span>
<span class="no">User</span><span class="p">.</span><span class="nf">authenticate</span><span class="p">(</span><span class="n">params</span><span class="p">[</span><span class="ss">:email</span><span class="p">],</span> <span class="n">params</span><span class="p">[</span><span class="ss">:password</span><span class="p">])</span>
<span class="k">end</span>
<span class="n">grant_flows</span> <span class="sx">%w[password]</span>
<span class="n">allow_blank_redirect_uri</span> <span class="kp">true</span>
<span class="n">skip_authorization</span> <span class="k">do</span>
<span class="kp">true</span>
<span class="k">end</span>
<span class="n">use_refresh_token</span>
<span class="c1"># ...</span>
</code></pre></div></div>
<p>With this, we have finished configuring Doorkeeper authentication for our API.</p>
<p>Next, we will add the doorkeeper route in <strong>routes.rb</strong> , this will add the /oauth/* routes.</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Rails</span><span class="p">.</span><span class="nf">application</span><span class="p">.</span><span class="nf">routes</span><span class="p">.</span><span class="nf">draw</span> <span class="k">do</span>
<span class="n">use_doorkeeper</span> <span class="k">do</span>
<span class="n">skip_controllers</span> <span class="ss">:authorizations</span><span class="p">,</span> <span class="ss">:applications</span><span class="p">,</span> <span class="ss">:authorized_applications</span>
<span class="k">end</span>
<span class="c1"># ...</span>
<span class="k">end</span>
</code></pre></div></div>
<p>As we donāt need the app authorization, we can skip the authorizations and authorized_applications controller. We can also skip the applications controller, as users wonāt be able to create or delete OAuth application.</p>
<p>Next, we need to create our own OAuth application manually in the console so we can use it for authentication.</p>
<h2 id="create-your-own-oauth-application">Create your own OAuth application</h2>
<p>Open up rails console, <code class="language-plaintext highlighter-rouge">rails console</code></p>
<p>Then create an OAuth application using this command :</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Doorkeeper</span><span class="o">::</span><span class="no">Application</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span><span class="ss">name: </span><span class="s2">"Android client"</span><span class="p">,</span> <span class="ss">redirect_uri: </span><span class="s2">""</span><span class="p">,</span> <span class="ss">scopes: </span><span class="s2">""</span><span class="p">)</span>
</code></pre></div></div>
<p>You can change the name to any name you want, and leave redirect_uri and scopes blank.</p>
<p><img src="https://rubyyagi.s3.amazonaws.com/16-rails-api-authentication-devise-doorkeeper/create_client.png" alt="create_client" /></p>
<p>This will create a record in the <strong>oauth_applications</strong> table. Keep note that the <strong>uid</strong> attribute and <strong>secret</strong> attribute, these are used for authentication on API later, uid = client_id and secret = client_secret.</p>
<p>For production use, you can create a database seed for initial creation of the OAuth applications in <strong>db/seeds.rb</strong> :</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># db/seeds.rb</span>
<span class="c1"># if there is no OAuth application created, create them</span>
<span class="k">if</span> <span class="no">Doorkeeper</span><span class="o">::</span><span class="no">Application</span><span class="p">.</span><span class="nf">count</span><span class="p">.</span><span class="nf">zero?</span>
<span class="no">Doorkeeper</span><span class="o">::</span><span class="no">Application</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span><span class="ss">name: </span><span class="s2">"iOS client"</span><span class="p">,</span> <span class="ss">redirect_uri: </span><span class="s2">""</span><span class="p">,</span> <span class="ss">scopes: </span><span class="s2">""</span><span class="p">)</span>
<span class="no">Doorkeeper</span><span class="o">::</span><span class="no">Application</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span><span class="ss">name: </span><span class="s2">"Android client"</span><span class="p">,</span> <span class="ss">redirect_uri: </span><span class="s2">""</span><span class="p">,</span> <span class="ss">scopes: </span><span class="s2">""</span><span class="p">)</span>
<span class="no">Doorkeeper</span><span class="o">::</span><span class="no">Application</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span><span class="ss">name: </span><span class="s2">"React"</span><span class="p">,</span> <span class="ss">redirect_uri: </span><span class="s2">""</span><span class="p">,</span> <span class="ss">scopes: </span><span class="s2">""</span><span class="p">)</span>
<span class="k">end</span>
</code></pre></div></div>
<p>Then run <code class="language-plaintext highlighter-rouge">rake db:seed</code> to create these applications.</p>
<p><strong>Doorkeeper::Application</strong> is just a namespaced model name for the <strong>oauth_applications</strong> table, you can perform ActiveRecord query as usual :</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># client_id of the application</span>
<span class="no">Doorkeeper</span><span class="o">::</span><span class="no">Application</span><span class="p">.</span><span class="nf">find_by</span><span class="p">(</span><span class="ss">name: </span><span class="s2">"Android client"</span><span class="p">).</span><span class="nf">uid</span>
<span class="c1"># client_secret of the application</span>
<span class="no">Doorkeeper</span><span class="o">::</span><span class="no">Application</span><span class="p">.</span><span class="nf">find_by</span><span class="p">(</span><span class="ss">name: </span><span class="s2">"Android client"</span><span class="p">).</span><span class="nf">secret</span>
</code></pre></div></div>
<p>Now we have Doorkeeper application set up, we can try to login user in the next section.</p>
<h2 id="how-to-login--logout-and-refresh-token-using-api">How to login , logout and refresh token using API</h2>
<p>We will need a user created to be able to login / logout them using the OAuth endpoints, you can register a dummy user on the devise web UI if you havenāt already (eg: localhost:3000/users/sign_up) or create one via the rails console.</p>
<p>The HTTP requests below can either send attributes using JSON format or URL-Encoded form.</p>
<h3 id="login">Login</h3>
<p>To login the user on the OAuth endpoint, we need to send a HTTP POST request to <strong>/oauth/token</strong>, with <strong>grant_type</strong>, <strong>email</strong>, <strong>password</strong>, <strong>client_id</strong> and <strong>client_secret</strong> attributes.</p>
<p><img src="https://rubyyagi.s3.amazonaws.com/16-rails-api-authentication-devise-doorkeeper/login_oauth.png" alt="login oauth" /></p>
<p>As we are using password in exchange for OAuth access and refresh token, the <strong>grant_type</strong> value should be <strong>password</strong>.</p>
<p><strong>email</strong> and <strong>password</strong> is the login credential of the user.</p>
<p><strong>client_id</strong> is the <strong>uid</strong> of the Doorkeeper::Application (OAuth application) we created earlier, with this we can identify which client the user has used to log in.</p>
<p><strong>client_secret</strong> is the <strong>secret</strong> of the Doorkeeper::Application (OAuth application) we created earlier.</p>
<p>On successful login attempt, the API will return <strong>access_token</strong>, <strong>refresh_token</strong>, <strong>token_type</strong>, <strong>expires_in</strong> and <strong>created_at</strong> attributes.</p>
<p>We can then use <strong>access_token</strong> to call protected API that requires user authentication.</p>
<p><strong>refresh_token</strong> can be used to generate and retrieve a new access token after the current access_token has expired.</p>
<p><strong>expires_in</strong> is the time until expiry for the access_token, starting from the UNIX timestamp of <strong>created_at</strong>, the default value is 7200 (seconds), which is around 2 hours.</p>
<h3 id="logout">Logout</h3>
<p>To log out a user, we can revoke the access token, so that the same access token cannot be used anymore.</p>
<p>To revoke an access token, we need to send a HTTP POST request to <strong>/oauth/revoke</strong>, with <strong>token</strong>, <strong>client_id</strong> and <strong>client_secret</strong> attributes.</p>
<p>Other than these attributes, we also need to set Authorization header for the HTTP request to use Basic Auth, using <strong>client_id</strong> value for the <strong>username</strong> and <strong>client_password</strong> value for the <strong>password</strong>. (According to <a href="https://github.com/doorkeeper-gem/doorkeeper/issues/1412#issuecomment-631938006">this reply</a> in Doorkeeper gem repository)</p>
<p><img src="https://rubyyagi.s3.amazonaws.com/16-rails-api-authentication-devise-doorkeeper/logout_auth.png" alt="logout auth" /></p>
<p><img src="https://rubyyagi.s3.amazonaws.com/16-rails-api-authentication-devise-doorkeeper/logout_body.png" alt="logout body" /></p>
<p>After revoking a token, the token record will have a <strong>revoked_at</strong> column filled :</p>
<p><img src="https://rubyyagi.s3.amazonaws.com/16-rails-api-authentication-devise-doorkeeper/revoked_token.png" alt="revoked_at" /></p>
<h3 id="refresh-token">Refresh token</h3>
<p>To retrieve a new access token when the current access token is (almost) expired, we can send a HTTP POST to <strong>/oauth/token</strong> , it is the same endpoint as login, but this time we are using ārefresh_tokenā as the value for <strong>grant_type</strong>, and is sending the value of refresh token instead of login credentials.</p>
<p>To refresh a token, we need to send <strong>grant_type</strong>, <strong>refresh_token</strong>, <strong>client_id</strong> and <strong>client_secret</strong> attributes.</p>
<p><strong>grant_type</strong> needs to be equal to ā<strong>refresh_token</strong>ā here as we are using refresh token to authenticate.</p>
<p><strong>refresh_token</strong> should be the refresh token value you have retrieved during login.</p>
<p><strong>client_id</strong> is the <strong>uid</strong> of the Doorkeeper::Application (OAuth application) we created earlier.</p>
<p><strong>client_secret</strong> is the <strong>secret</strong> of the Doorkeeper::Application (OAuth application) we created earlier.</p>
<p><img src="https://rubyyagi.s3.amazonaws.com/16-rails-api-authentication-devise-doorkeeper/refresh_token.png" alt="refresh_token" /></p>
<p>On successful refresh attempt, the API return a new access_token and refresh_token, which we can use to call protected API that requires user authentication.</p>
<h2 id="create-api-controllers-that-require-authentication">Create API controllers that require authentication</h2>
<p>Now that we have user authentication set up, we can now create API controllers that require authentication.</p>
<p>For this, I recommend creating a base API application controller, then subclass this controller for controllers that require authentication.</p>
<p>Create a base API application controller (<strong>application_controller.rb</strong>) and place it in <strong>app/controllers/api/application_controller.rb</strong> .</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># app/controllers/api/application_controller.rb</span>
<span class="k">module</span> <span class="nn">Api</span>
<span class="k">class</span> <span class="nc">ApplicationController</span> <span class="o"><</span> <span class="no">ActionController</span><span class="o">::</span><span class="no">API</span>
<span class="c1"># equivalent of authenticate_user! on devise, but this one will check the oauth token</span>
<span class="n">before_action</span> <span class="ss">:doorkeeper_authorize!</span>
<span class="kp">private</span>
<span class="c1"># helper method to access the current user from the token</span>
<span class="k">def</span> <span class="nf">current_user</span>
<span class="vi">@current_user</span> <span class="o">||=</span> <span class="no">User</span><span class="p">.</span><span class="nf">find_by</span><span class="p">(</span><span class="ss">id: </span><span class="n">doorkeeper_token</span><span class="p">[</span><span class="ss">:resource_owner_id</span><span class="p">])</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>
<p>The API application subclasses from <a href="https://api.rubyonrails.org/classes/ActionController/API.html">ActionController::API</a>, which is a lightweight version of ActionController::Base, and does not contain HTML layout and templating functionality (we dont need it for API anyway), and it doesnāt have CORS protection.</p>
<p>We add the <strong>doorkeeper_authorize!</strong> method in the before_action callback, as this will check if the user is authenticated with a valid token before calling methods in the controller, this is similar to the authenticate_user! method on devise.</p>
<p>We also add a <strong>current_user</strong> method to get the current user object, then we can attach the current user on some modelās CRUD action.</p>
<p>As example of a protected API controller, letās create a bookmarks controller to retrieve all bookmarks.</p>
<p><strong>app/controllers/api/bookmarks_controller.rb</strong></p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># app/controllers/api/bookmarks_controller.rb</span>
<span class="k">module</span> <span class="nn">Api</span>
<span class="k">class</span> <span class="nc">BookmarksController</span> <span class="o"><</span> <span class="no">Api</span><span class="o">::</span><span class="no">ApplicationController</span>
<span class="k">def</span> <span class="nf">index</span>
<span class="vi">@bookmarks</span> <span class="o">=</span> <span class="no">Bookmark</span><span class="p">.</span><span class="nf">all</span>
<span class="n">render</span> <span class="ss">json: </span><span class="p">{</span> <span class="ss">bookmarks: </span><span class="vi">@bookmarks</span> <span class="p">}</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>
<p>The bookmarks controller will inherit from the base API application controller we created earlier, and it will return all the bookmarks in JSON format.</p>
<p>Donāt forget to add the route for it in <strong>routes.rb</strong> :</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Rails</span><span class="p">.</span><span class="nf">application</span><span class="p">.</span><span class="nf">routes</span><span class="p">.</span><span class="nf">draw</span> <span class="k">do</span>
<span class="c1"># ....</span>
<span class="n">namespace</span> <span class="ss">:api</span> <span class="k">do</span>
<span class="n">resources</span> <span class="ss">:bookmarks</span><span class="p">,</span> <span class="ss">only: </span><span class="sx">%i[index]</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>
<p>Then we can retrieve the bookmarks by sending a HTTP GET request to <strong>/api/bookmarks</strong>, with the userās access token in the Authorization Header (Authorization: Bearer [User Access Token])</p>
<p><img src="https://rubyyagi.s3.amazonaws.com/16-rails-api-authentication-devise-doorkeeper/get_bookmarks_authorized.png" alt="get boomarks api authorization" /></p>
<p>To access protected API controllers, we will need to include the Authorization HTTP header, with the values of āBearer [User Access Token]ā.</p>
<h2 id="create-an-endpoint-for-user-registration">Create an endpoint for user registration</h2>
<p>It would be weird if we only allow user registration through website, we would also need to add an API endpoint for user to register an account .</p>
<p>For this, letās create a users controller and place it in <strong>app/controllers/api/users_controller.rb</strong>.</p>
<p>The <strong>create</strong> action will create an user account from the supplied email and password.</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">module</span> <span class="nn">Api</span>
<span class="k">class</span> <span class="nc">UsersController</span> <span class="o"><</span> <span class="no">Api</span><span class="o">::</span><span class="no">ApplicationController</span>
<span class="n">skip_before_action</span> <span class="ss">:doorkeeper_authorize!</span><span class="p">,</span> <span class="ss">only: </span><span class="sx">%i[create]</span>
<span class="k">def</span> <span class="nf">create</span>
<span class="n">user</span> <span class="o">=</span> <span class="no">User</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">email: </span><span class="n">user_params</span><span class="p">[</span><span class="ss">:email</span><span class="p">],</span> <span class="ss">password: </span><span class="n">user_params</span><span class="p">[</span><span class="ss">:password</span><span class="p">])</span>
<span class="n">client_app</span> <span class="o">=</span> <span class="no">Doorkeeper</span><span class="o">::</span><span class="no">Application</span><span class="p">.</span><span class="nf">find_by</span><span class="p">(</span><span class="ss">uid: </span><span class="n">params</span><span class="p">[</span><span class="ss">:client_id</span><span class="p">])</span>
<span class="k">return</span> <span class="n">render</span><span class="p">(</span><span class="ss">json: </span><span class="p">{</span> <span class="ss">error: </span><span class="s1">'Invalid client ID'</span><span class="p">},</span> <span class="ss">status: </span><span class="mi">403</span><span class="p">)</span> <span class="k">unless</span> <span class="n">client_app</span>
<span class="k">if</span> <span class="n">user</span><span class="p">.</span><span class="nf">save</span>
<span class="c1"># create access token for the user, so the user won't need to login again after registration</span>
<span class="n">access_token</span> <span class="o">=</span> <span class="no">Doorkeeper</span><span class="o">::</span><span class="no">AccessToken</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span>
<span class="ss">resource_owner_id: </span><span class="n">user</span><span class="p">.</span><span class="nf">id</span><span class="p">,</span>
<span class="ss">application_id: </span><span class="n">client_app</span><span class="p">.</span><span class="nf">id</span><span class="p">,</span>
<span class="ss">refresh_token: </span><span class="n">generate_refresh_token</span><span class="p">,</span>
<span class="ss">expires_in: </span><span class="no">Doorkeeper</span><span class="p">.</span><span class="nf">configuration</span><span class="p">.</span><span class="nf">access_token_expires_in</span><span class="p">.</span><span class="nf">to_i</span><span class="p">,</span>
<span class="ss">scopes: </span><span class="s1">''</span>
<span class="p">)</span>
<span class="c1"># return json containing access token and refresh token</span>
<span class="c1"># so that user won't need to call login API right after registration</span>
<span class="n">render</span><span class="p">(</span><span class="ss">json: </span><span class="p">{</span>
<span class="ss">user: </span><span class="p">{</span>
<span class="ss">id: </span><span class="n">user</span><span class="p">.</span><span class="nf">id</span><span class="p">,</span>
<span class="ss">email: </span><span class="n">user</span><span class="p">.</span><span class="nf">email</span><span class="p">,</span>
<span class="ss">access_token: </span><span class="n">access_token</span><span class="p">.</span><span class="nf">token</span><span class="p">,</span>
<span class="ss">token_type: </span><span class="s1">'bearer'</span><span class="p">,</span>
<span class="ss">expires_in: </span><span class="n">access_token</span><span class="p">.</span><span class="nf">expires_in</span><span class="p">,</span>
<span class="ss">refresh_token: </span><span class="n">access_token</span><span class="p">.</span><span class="nf">refresh_token</span><span class="p">,</span>
<span class="ss">created_at: </span><span class="n">access_token</span><span class="p">.</span><span class="nf">created_at</span><span class="p">.</span><span class="nf">to_time</span><span class="p">.</span><span class="nf">to_i</span>
<span class="p">}</span>
<span class="p">})</span>
<span class="k">else</span>
<span class="n">render</span><span class="p">(</span><span class="ss">json: </span><span class="p">{</span> <span class="ss">error: </span><span class="n">user</span><span class="p">.</span><span class="nf">errors</span><span class="p">.</span><span class="nf">full_messages</span> <span class="p">},</span> <span class="ss">status: </span><span class="mi">422</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="kp">private</span>
<span class="k">def</span> <span class="nf">user_params</span>
<span class="n">params</span><span class="p">.</span><span class="nf">permit</span><span class="p">(</span><span class="ss">:email</span><span class="p">,</span> <span class="ss">:password</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">generate_refresh_token</span>
<span class="kp">loop</span> <span class="k">do</span>
<span class="c1"># generate a random token string and return it, </span>
<span class="c1"># unless there is already another token with the same string</span>
<span class="n">token</span> <span class="o">=</span> <span class="no">SecureRandom</span><span class="p">.</span><span class="nf">hex</span><span class="p">(</span><span class="mi">32</span><span class="p">)</span>
<span class="k">break</span> <span class="n">token</span> <span class="k">unless</span> <span class="no">Doorkeeper</span><span class="o">::</span><span class="no">AccessToken</span><span class="p">.</span><span class="nf">exists?</span><span class="p">(</span><span class="ss">refresh_token: </span><span class="n">token</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>
<p>As the user doesnāt have an account at this point, we want to exempt this action from requiring authentication information, so we added the line <strong>skip_before_action :doorkeeper_authorize!, only: %i[create]</strong> at the top. This will make the <strong>create</strong> method to skip running the <strong>doorkeeper_authorize!</strong> before_action method we defined in the base API controller, and the client app can call the user account creation API endpoint without authentication information.</p>
<p>We then create an AccessToken on successful user registration using <strong>Doorkeeper::AccessToken.create()</strong> and return it in the HTTP response, so the user wonāt need to login right after registration.</p>
<p>Remember to add a route for ths user registration action in <strong>routes.rb</strong> :</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Rails</span><span class="p">.</span><span class="nf">application</span><span class="p">.</span><span class="nf">routes</span><span class="p">.</span><span class="nf">draw</span> <span class="k">do</span>
<span class="c1"># ....</span>
<span class="n">namespace</span> <span class="ss">:api</span> <span class="k">do</span>
<span class="n">resources</span> <span class="ss">:users</span><span class="p">,</span> <span class="ss">only: </span><span class="sx">%i[create]</span>
<span class="n">resources</span> <span class="ss">:bookmarks</span><span class="p">,</span> <span class="ss">only: </span><span class="sx">%i[index]</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>
<p>Then we can call create user API by sending a HTTP POST request containing the userās <strong>email</strong>, <strong>password</strong> and <strong>client_id</strong> to <strong>/api/users</strong> like this :</p>
<p><img src="https://rubyyagi.s3.amazonaws.com/16-rails-api-authentication-devise-doorkeeper/oauth_registration.png" alt="create user API" /></p>
<p>The <strong>client_id</strong> is used to identify which client app the user is using for registration.</p>
<h1 id="revoke-user-token-manually">Revoke user token manually</h1>
<p>Say if you suspect a userās access token has been misused or abused, you can revoke them manually using this function :</p>
<p><strong>Doorkeeper::AccessToken.revoke_all_for(application_id, resource_owner)</strong></p>
<p>application_id is the <strong>id</strong> of the Doorkeeper::Application (OAuth application) we want to revoke the user from.</p>
<p>resource_owner is the <strong>user object</strong>.</p>
<p>Example usage:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">client_app</span> <span class="o">=</span> <span class="no">Doorkeeper</span><span class="o">::</span><span class="no">Application</span><span class="p">.</span><span class="nf">find_by</span><span class="p">(</span><span class="ss">name: </span><span class="s1">'Android client'</span><span class="p">)</span>
<span class="n">user</span> <span class="o">=</span> <span class="no">User</span><span class="p">.</span><span class="nf">find</span><span class="p">(</span><span class="mi">7</span><span class="p">)</span>
<span class="no">Doorkeeper</span><span class="o">::</span><span class="no">AccessToken</span><span class="p">.</span><span class="nf">revoke_all_for</span><span class="p">(</span><span class="n">client_app</span><span class="p">.</span><span class="nf">id</span> <span class="p">,</span> <span class="n">user</span><span class="p">)</span>
</code></pre></div></div>
<h2 id="references">References</h2>
<p><a href="https://github.com/heartcombo/devise/wiki/How-To:-Find-a-user-when-you-have-their-credentials">Devise wiki - How To: Find a user when you have their credentials</a></p>
<p><a href="https://github.com/doorkeeper-gem/doorkeeper/wiki/Using-Resource-Owner-Password-Credentials-flow">Doorkeeper wiki - Using Resource Owner Password Credentials flow</a></p>
<script async="" data-uid="d862c2871b" src="https://rubyyagi.ck.page/d862c2871b/index.js"></script></content>
<summary type="html">Most of the time when we implement API endpoints on our Rails app, we want to limit access of these API to authorized users only, thereās a few strategy for authenticating user through API, ranging from a simple token authentication to a fullblown OAuth provider with JWT.</summary>