Ruby on Rails + Auth0: Authenticating your API with an external authentication service
Tags: ruby, auth0, api, authentication
Published: 2021-03-01 01:30:44

## Introduction

In 2019 I wrote a [post](https://medium.com/@rwehresmann/ruby-on-rails-auth0-knock-7f114ed87e4b) with this same subject and an extra tool: [Knock](https://github.com/nsarno/knock). 

With a few modifications in the gem source code, we could easily integrate [Auth0](https://auth0.com/) into our Rails API, but that on Rails 5. Rails 6 brought [Zeitwerk](https://github.com/fxn/zeitwerk) code loader together, which makes it harder to perform the alterations suggested in my previous post.

With those problems in mind, I came to the conclusion that is easier to drop Knock off this task and do it yourself. It isn't a difficult task but requires some understanding of Auth0 API. In this post, I'll show you one way you can do it.

### What We are Going to Build

An integration with Auth0 allowing our users to signup and login with a username and password, plus the functionality of update the user password.

The users need to login into our Rails API, but to our API perform any action in Auth0, like update a password, the API itself must have access to the access token. To do so we'll use the [Client Credentials Flow](https://auth0.com/docs/flows/client-credentials-flow).

![](https://images.ctfassets.net/cdy7uua7fh8z/2waLvaQdM5Fl5ZN5xUrF2F/8c5ddae68ac8dd438cdeb91fe1010fd1/auth-sequence-client-credentials.png)

## Configuring Auth0 Application

**1)** Select the *Machine to Machine Applications* and create it;

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/h892d3qij2e35y0yphlo.png)

**2)** In the next step, select *Auth0 Management API* as your API, and be sure to select the necessary scopes to allow us to update the user password as proposed in the introduction of this post. You can click in *Authorize* to proceed;

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qe2q815wy96qso2ey8b2.png)

**3)** On the application page, go to *Show Advanced Settings*. In the *Grant Types* tab, be sure to select the grants we'll use to authorize our API to communicate with Auth0 and allow the user-password registering process.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/i7na81qojnwna15g9t3i.png)

If furtherly you wanna allow new features in your API, like delete users and email confirmation, for instance, the scopes presented in step 2 are the place to go. If you wanna implement other authorization flow, like refresh tokens, for instance, the grants presented in step 3 are the place to go.

With the Auth0 application set, you're ready to proceed with your Rails API.

## Rails API

### Setting your credentials/ENV

You'll need to make available in your Rails API some information from Auth0. To do so, you can use rails credentials, `.env`, or anything else you feel comfortable using:

  1. Domain
  2. Client ID
  3. Client Secret
  4. Your JWKS (JSON Web Key Set)
  5. Audience

The first three ones you can find on your Auth0 application page. The JWKS are available in the following URL (you should copy the whole JSON): `https://[YOUR-DOMAIN]/.well-known/jwks.json` (i.e., https://rwehresmann.auth0.com/.well-known/jwks.json). The *audience* is named as *API Identifier* and you can find it in the *APIs* tab of your application.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/u22gu2aodr88jydk7z11.png)

### Calling Auth0 API

Everything will be created under the class `Auth0`, and it'll be using [HTTP gem](https://github.com/httprb/http) to perform the quests, but feel free to decide over your code organization and tools. 

About the Auth0 API, you can check the [Authentication API](https://auth0.com/docs/api/authentication) and [Management API v2](https://auth0.com/docs/api/management/v2) for relevant documentation and further instructions.

So let's [signup](https://auth0.com/docs/api/authentication#signup) an user.

```ruby
class Auth0
  @@credentials = Rails.application.credentials.dig(:auth0)

    def self.signup(email, password)    
    HTTP.headers(accept: 'application/json').post(
      "https://#{@@credentials.dig(:domain)}/dbconnections/signup",
      form: {
        client_id: @@credentials.dig(:client_id),
        email: email,
        password: password,
        connection: 'Username-Password-Authentication',
      }
    )
  end
end
```

Calling it in the `rails console`:

```
3.0.0 :001 > response = Auth0.signup('devto@mail.com', 'supersecrectpasswd')
 => #<HTTP::Response/1.1 200 OK {"Date"=>"Sun, 28 Feb 2021 20:35:28 GMT", ... 
3.0.0 :002 > user_id = JSON.parse(response.to_s)['_id']
 => "603bfe90c23beb006ad972d6" 
```

There is nothing special about the request itself, but you can be wondering about the `connection` param. [Auth0 connection](https://auth0.com/docs/identityproviders) is simply the source of the user and in this case, the source is a database. The `Username-Password-Authentication` is a database created by default in your account.

The `_id` represents the id of the user inside Auth0.

Now we'll [log in](https://auth0.com/docs/api/authentication#resource-owner-password) the created user:

```ruby
class Auth0
  @@credentials = Rails.application.credentials.dig(:auth0)

...

  def self.login(email, password)
    HTTP.headers(accept: 'application/x-www-form-urlencoded').post(
      "https://#{@@credentials.dig(:domain)}/oauth/token",
      form: {
        grant_type: 'password',
        username: email,
        password: password,
        audience: @@credentials.dig(:audience),
        client_id: @@credentials.dig(:client_id),
        client_secret: @@credentials.dig(:client_secret),
      }
    )
  end
end
```

Calling it in the `rails console`:

```
3.0.0 :003 > response = Auth0.login('devto@mail.com', 'supersecrectpasswd')
 => #<HTTP::Response/1.1 200 OK {"Date"=>"Sun, 28 Feb 2021 20:38:12 GMT", ... 
3.0.0 :004 > puts response.to_s
 => {"access_token":"eyJhbGc...","scope":"read:current_user update:current_user_metadata delete:current_user_metadata create:current_user_metadata create:current_user_device_credentials delete:current_user_device_credentials update:current_user_identities","expires_in":86400,"token_type":"Bearer"}

```

The `grant_type` is `password` because we configured before that the user login would be through username and password. 

The `access_token` (shortened it in the example) returned in the response is the JWT (JSON Web Token). Where is necessary, the users will use this token to perform actions in your Rails API. Once the JWT is decoded, you'll have the following payload structure:

```json
{
  "iss": "",
  "sub": "",
  "aud": "",
  "iat": ,
  "exp": ,
  "azp": "",
  "scope": "",
  "gty": ""
}
```

The `sub` (subject) is represented by the ID of the user in your Auth0 application. This ID is already returned in the signup call and is a good idea to persist it. The following flow could be implemented to identify the current user:

  1. A user access your rails API sending you his JWT;
  2. You decode the JWT and get the user id (sub);
  3. You query for the user that has this id, and he is your current user.

To finish, let's [change the user password](https://auth0.com/docs/api/management/v2#!/Users/patch_users_by_id):

```ruby
class Auth0
  @@credentials = Rails.application.credentials.dig(:auth0)

...

  def self.update_password(token, auth0_user_id, new_password)
    HTTP.auth("Bearer #{token}")
      .headers(accept: 'application/json')
      .patch(
        "https://#{@@credentials.dig(:domain)}/api/v2/users/#{auth0_user_id}",
        json: {
          client_id: @@credentials.dig(:client_id),
          password: new_password,
          connection: 'Username-Password-Authentication',
        }
      )
  end
end
```

There are two details to pay attention to about this method:
 
**1)** The endpoint used has the finality to *update a user*, not only a password. There are many attributes we can update and you can check the documentation to get further details. We'll be updating only the password because this is everything we need in the demonstration built in this post;

**2)** We need to inform a token. And what is this token about? It is the *Client Credentials Flow* we discussed earlier, that will provide us a token that will allow our Rails application to perform actions in the Auth0 application (limited by the scopes we defined).

So let's implement the [client credentials flow](https://auth0.com/docs/api/authentication#client-credentials-flow):

```ruby
class Auth0
  @@credentials = Rails.application.credentials.dig(:auth0)

...

  def self.login_server
    HTTP.headers(accept: 'application/x-www-form-urlencoded').post(
      "https://#{@@credentials.dig(:domain)}/oauth/token",
      form: {
        grant_type: 'client_credentials',
        audience: @@credentials.dig(:audience),
        client_id: @@credentials.dig(:client_id),
        client_secret: @@credentials.dig(:client_secret),
      }
    )
  end
end
```

Testing everything in the `rails console`:

```
3.0.0 :006 > response = Auth0.login_server
 => "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Ik5USXhRVUUyTTBJeE1qSkVOM...
3.0.0 :013 > Auth0.update_password(token, "auth0|#{user_id}", 'newsupersecretpas
swd')
 => #<HTTP::Response/1.1 200 OK {"Date"=>"Sun, 28 Feb 2021 20:58:21 GMT", "Content-Type"=>"application/json; charset=utf-8", "Transfer-Encoding"=>"chunked", "Connection"=>"close", "Set-Cookie"=>"__cfduid=d47242c0ca577209101634ebb26a907ba1614545901; expires=Tue, 30-Mar-21 20:58:21 GMT; path=/; domain=.auth0.com; HttpOnly; SameSite=Lax; Secure", "Cf-Ray"=>"628d1029fc00d07a-CWB", "Cache-Control"=>"no-cache", "Strict-Transport-Security"=>"max-age=31536000", "Vary"=>"origin,accept-encoding", "Cf-Cache-Status"=>"DYNAMIC", "Cf-Request-Id"=>"088c086e400000d07a70897000000001", "Expect-Ct"=>"max-age=604800, report-uri=\"https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct\"", "Ot-Baggage-Auth0-Request-Id"=>"628d1029fc00d07a", "Ot-Tracer-Sampled"=>"true", "Ot-Tracer-Spanid"=>"2f5c2d4c6a8d5ee6", "Ot-Tracer-Traceid"=>"4003510336be279a", "X-Content-Type-Options"=>"nosniff", "X-Ratelimit-Limit"=>"10", "X-Ratelimit-Remaining"=>"9", "X-Ratelimit-Reset"=>"1614545902", "Server"=>"cloudflare", "Alt-Svc"=>"h3-27=\":443\"; ma=86400, h3-28=\":443\"; ma=86400, h3-29=\":443\"; ma=86400"}> 

```

And it's done! As you can see, we used `"auth0|#{user_id}"` (all the examples of this section run in the same terminal session, and `user_id` comes from later when we signup). The Auth0 user ID we must inform is always in the format `provider|ID`. We didn't use any external provider when signed up, and because of that, the provider is Auth0 itself.

### Decoding the JWT

Decoding JWTs is simple whit the help of an already implemented solution. In this section, I'll be using the [ruby-jwt](https://github.com/jwt/ruby-jwt).

How to decode will depend on how it was encoded first. For instance:

```
3.0.0 :001 > JWT.encode({ sub: 'my user id' }, nil, 'none')
 => "eyJhbGciOiJub25lIn0.eyJzdWIiOiJteSB1c2VyIGlkIn0." 
3.0.0 :002 > jwt = JWT.encode({ sub: 'my user id' }, nil, 'none')
 => "eyJhbGciOiJub25lIn0.eyJzdWIiOiJteSB1c2VyIGlkIn0." 
3.0.0 :003 > JWT.decode(jwt, nil, false)
 => [{"sub"=>"my user id"}, {"alg"=>"none"}] 
```

Quite simple. With Auth0, however, the JWT is signed, so there are a few parameters we need to inform to decode it successfully. You can read more about JWT in general [here](https://auth0.com/learn/json-web-tokens/), and the class below is what you can use as a decoder for the Auth0 JWT: 

```ruby
class JwtDecoder
  def initialize(access_token)
    @access_token = access_token
  end

  def call
    JWT.decode(
      @access_token, 
      nil,
      true, # Verify the signature of this token
      algorithm: 'RS256',
      iss: "https://#{credentials.fetch(:domain)}/",        
      verify_iss: true,
      aud: credentials.fetch(:audience),
      verify_aud: true
    ) do |header|
      jwks_hash[header['kid']]
    end.first
  end

  private

  def jwks_hash
    jwks_raw = credentials.fetch(:jwks).to_json
    jwks_keys = Array(JSON.parse(jwks_raw)['keys'])
    Hash[
      jwks_keys.map do |key| 
        [
          key['kid'],
          OpenSSL::X509::Certificate.new(Base64.decode64(key['x5c'].first)).public_key
        ]
      end
    ]
  end

  def credentials
    @credentials ||= Rails.application.credentials.dig(:auth0)
  end
end

```

## Conclusion

This post showed how to implement a complete authorization flow with username and password in your Rails API using Auth0 as your authentication service.

With the configurations showed in the Auth0 section, you can easily extend your implementation and start using other features from Auth0 just by authorizing new scopes. Auth0 docs are a bit messy, but remember that most of the information necessary to use their API you will found in [Authentication API](https://auth0.com/docs/api/authentication) and [Management API v2](https://auth0.com/docs/api/management/v2).

Rails API is our context, but it wasn't in the scope of this post to build an API from step 0. In my first [post](https://medium.com/@rwehresmann/ruby-on-rails-auth0-knock-7f114ed87e4b) about this subject, Knock provided the helpers we are used to having like `current_user` and `authenticate_user`. However, once you have the Auth0 access methods, know how to get the JWT and decode it, like shown in the Rails API section, there is no mystery:

  * `authenticate_user` is nothing more than decode the JWT, get its sub (subject, the id from the user in Auth0), and search for the user in your database;
  * `current_user` is nothing more than get the user in the authentication process and make it a helper for your controllers.

You have full flexibility to implement those methods the way more suit your application.

***

This is it! If you have any comments or suggestions, don't hold back, let me know.

***

Options if you like my content and would like to support me directly (never required, but much appreciated):

BTC address: `bc1q5l93xue3hxrrwdjxcqyjhaxfw6vz0ycdw2sg06`

[![buy me a coffee](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z1n0hai66hv33dphoiic.png)](https://www.buymeacoffee.com/rwehresmann)