DEV Community

loading...
Cover image for Ruby on Rails + Auth0: Authenticating your API with an external authentication service

Ruby on Rails + Auth0: Authenticating your API with an external authentication service

rwehresmann profile image Rodrigo Walter Ehresmann ・8 min read

Introduction

In 2019 I wrote a post with this same subject and an extra tool: Knock.

With a few modifications in the gem source code, we could easily integrate Auth0 into our Rails API, but that on Rails 5. Rails 6 brought 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.

Configuring Auth0 Application

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

Alt Text

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

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

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

Calling Auth0 API

Everything will be created under the class Auth0, and it'll be using HTTP gem 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 and Management API v2 for relevant documentation and further instructions.

So let's signup an user.

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
Enter fullscreen mode Exit fullscreen mode

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" 
Enter fullscreen mode Exit fullscreen mode

There is nothing special about the request itself, but you can be wondering about the connection param. Auth0 connection 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 the created user:

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
Enter fullscreen mode Exit fullscreen mode

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"}

Enter fullscreen mode Exit fullscreen mode

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:

{
  "iss": "",
  "sub": "",
  "aud": "",
  "iat": ,
  "exp": ,
  "azp": "",
  "scope": "",
  "gty": ""
}
Enter fullscreen mode Exit fullscreen mode

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:

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
Enter fullscreen mode Exit fullscreen mode

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:

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
Enter fullscreen mode Exit fullscreen mode

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"}> 

Enter fullscreen mode Exit fullscreen mode

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.

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"}] 
Enter fullscreen mode Exit fullscreen mode

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, and the class below is what you can use as a decoder for the Auth0 JWT:

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

Enter fullscreen mode Exit fullscreen mode

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 and Management API 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 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.

Discussion (0)

Forem Open with the Forem app