Ruby on Rails + Auth0 + Knock: User authentication through an external authentication service

This article is also available in Portuguese here.

If you’re using Rails 6, check it out this post where Auth0 is integrated without using the Knock gem.

A common and shared characteristic in web applications is to use external services for authentication. There are many good reasons to justify this choice, like the toolbox offered by these services (email verification, MFA, Passwordless Authentication, etc).

Integrating these services can be a challenge. This is the subject of this article, where we’re gonna integrate an authentication service in a Ruby on Rails API-only.

In this post we’re gonna create a Ruby on Rails API-only to integrate with the Auth0 authentication service using as support the Knock gem. This includes a simple way to sign in and sign up in our application using the Auth0 endpoints and afterward exploring how to authenticate our API using Knock. Discussions about the best authentication strategies are outside this article’s scope.

The choice of Auth0 is because it’s a popular and widely used authentication service, and has an attractive free plan (even for applications that handle a few thousand users).

On the other hand, Knock was implemented to support integration with Auth0, streamlining the developer’s work. Very little information about how to do that is available, which brings an unnecessary complexity for the task and increases the interest on write an article detailing the process.

  • A free account in Auth0;
  • Ruby (2.6.3);
  • Ruby on Rails (5.2.3).

Other necessary gems are gonna be listed throughout the article. The indicated versions of Ruby and RoR were used during the development of this article. However, it’ll probably work with other versions too.

Let’s get started!

First, it’s necessary to create an account on Auth0.

On the Applications menu option, we create a new one selecting Machine to Machine Application option. The application type determines which parameters are configurable.

You’ll be asked to choose an API and to select the allowed scopes for the application. For the purpose of this article, select the default Auth0 Management API and allow all the scopes.

P.S.: Allow all scopes isn’t a good practice, though. In a real situation, have in mind that you need to understand your use case and choose the most restrictive scopes possible.

We’re gonna use the classic authentication through a password. Usually, this option is disabled by default, so be sure to activate it accessing your application page > Settings > Advanced Settings > Grant Types > selecting the Password checkbox and subsequently saving your alteration.

The Rails application gonna be API-only and can be generated running in your shell rails _5.2.3_ new one-bit-auth. As good practice, don’t forget to specify in Gemfile the Ruby version that is gonna be used:

Create the database running in your shell rake db:migrate.

To present the logged user it’s necessary to create the User model. We can do that running rails g model User. The generated migration should be as follow:

Now we create the users table running rake db:migrate. Include the user.rb following:

The field auth0_uid is where the user ID from Auth0 is gonna be stored.

To handle ENV variables I’m gonna use the dotenv-rails gem, setting them in .env file, that must be created in the application root directory. Therefore, for the development environment include in your Gemfile the following:

As good practice for security, include your .env in your .gitignore. Now, we’re gonna set the necessary ENV variables. In your .env file, include:

AUTH0_AUDIENCE="https://[your-audience].auth0.com/api/v2/"
AUTH0_CLIENT_SECRET="[your-client-secret]"
AUTH0_RSA_DOMAIN="https://[your-domain]/.well-known/jwks.json"
AUTH0_CLIENT_ID="[your-client-id]"
AUTH0_DOMAIN="[your-domain]"

With the exception of AUTH0_RSA_DOMAIN, these pieces of information can be found on the page of the application created in Auth0, at the tab Settings (AUTH0_CLIENT_SECRET, AUTH0_CLIENT_ID and AUTH0_CLIENT_ID) and API (AUTH0_AUDIENCE, identified there as API IDENTIFIER). We’re gonna use the application’s default API, called Auth0 Management API.

Your AUTH0_RSA_DOMAIN is on Auth0 default path /.well-know/jwks.json. For instance, https://rhehresmann.auth0.com/.well-known/jwks.json.

First, include Knock in your Gemfile:

The http gem is there as my personal choice to handle HTTP requests that are gonna done in the next steps. Subsequently, we install the gems with bundle install and generate the file knock.rb running rails generate knock:install.

This is Knock’s configurations file and brings together some comments, mentioning default Knock’s configurations and alternative ones. Without going further, it’s interesting to highlight that this gem hasn’t been receiving updates in the last years and some examples there are outdated.

These are the configurations that we’re gonna use:

Going into detail:

  • config.token_secret_signature_key: it’s mandatory to use the signature key of the application created on Auth0, with the exception of test environments. Normally we don’t want to do external requests in these environments, so I kept the Knock default configuration for them, which is to access whatever is present in Rails’ credentials;
  • config.token_audience: its’s mandatory to use the API identification of the application created in Auth0;
  • config.token_signature_algorithm: Knock default configuration brings the algorithm HS256, however in Auth0 is being used the RS256;
  • config.token_public_key: RS256 algorithm is asymmetric, which means it uses a private and public key. The public key can be found on the domain defined in the ENV variable, and the rest of the code is only about extract the public key from there.

I choose to create the signup.rb and signin.rb in the following folders structure: app/lib/auth0. The implementation of both classes is simple, where we only need to inform email and password to perform its actions.

You can check further details about the requests made with the http gem in their documentation. Also, you can check more about Auth0's endpoints here. Now let’s test these classes on rails console:

  • Sign up:
> response = Auth0::Signup.perform('onebitauth@mail.com', '123')
=> #<HTTP::Response/1.1 200 OK {"Date"=>"Thu, 02 May 2019 01:08:56 GMT", "Content-Type"=>"application/json; charset=utf-8", "Content-Length"=>"87", "Connection"=>"close", "X-Auth0-Requestid"=>"7a13741ce42e6586c57a", "X-Ratelimit-Limit"=>"50", "X-Ratelimit-Remaining"=>"49", "X-Ratelimit-Reset"=>"1556759338", "Cache-Control"=>"private, no-store, no-cache, must-revalidate, post-check=0, pre-check=0", "Strict-Transport-Security"=>"max-age=15724800", "X-Robots-Tag"=>"noindex, nofollow, nosnippet, noarchive"}>
2.6.3 :003 > response.to_s
=> "{\"_id\":\"5cca4328bf80c910f5e7be2c\",\"email_verified\":false,\"email\":\"onebitauth@mail.com\"}"

Perfect, the user was created! You can also check this information in your Auth0 account.

In a real situation, you would probably create something that would use our signup method, adding some logic to create the user in your database after the signup, saving the ID returned by Auth0. In this article I’ll not create such a method, so let’s create this manually. For instance:

> User.create!(auth0_uid: 'auth0|5cca4328bf80c910f5e7be2c', email: 'onebitauth@mail.com')

P.S.: Don’t forget to use the email and ID returned for you instead of the one used in the example above!

Pay attention to the ID format of the example. What represents the user’s ID in Auth0 is gonna follow the format provider|ID. If in doubt, it’s possible to check this information on the user’s page on Auth0, searching for user_id on the section Identity Provider Attributes.

  • Sign in:
> response = Auth0::Signin.perform('onebitauth@mail.com', '123')
=> #<HTTP::Response/1.1 200 OK {"Date"=>"Thu, 02 May 2019 01:13:20 GMT", "Content-Type"=>"application/json", "Content-Length"=>"2533", "Connection"=>"close", "X-Auth0-Requestid"=>"f83e1823d05513008635", "X-Ratelimit-Limit"=>"100", "X-Ratelimit-Remaining"=>"99", "X-Ratelimit-Reset"=>"1556760465", "Cache-Control"=>"private, no-store, no-cache, must-revalidate, post-check=0, pre-check=0", "Pragma"=>"no-cache", "Strict-Transport-Security"=>"max-age=15724800", "X-Robots-Tag"=>"noindex, nofollow, nosnippet, noarchive"}>
2.6.3 :005 > response.to_s
=> "{\"access_token\":\"Bn4f9oWN1eBw\",\"id_token\":\"cHe3Fvsg\",\"scope\":\"openid profile email address phone 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\"}"

P.S.: For matters of readability, I replaced the generated token for a shorter sequence of characters.

It worked! Now we have in hands the authentication token that is gonna be used in the authentication process of the Rails API.

When we’re developing any kind of authentication inside an application, it’s common to use a method called current_user, that returns the current user logged in. Knock has this method implemented in its source code, however, it’s necessary to change some details of it to make our integration with Auth0 work. In knock.rb, we’ll include the following:

The key points to understand this alteration:

  • Have in mind that in the implementation described in this article, entity_class.find_by is gonna be equivalent to User.find_by(key_to_find => @payload['sub]);
  • @payload will contain information extracted from the token (JWT), and sub (subject) is the entity of who the token belongs, normally represented by an ID (explanations about JWT are beyond this article’s scope, but you can check out more information in articles like this);
  • Knock::AuthToken.class_eval allow us to rewrite the method entity_for of the module Knock::AuthToken, and it’s important to mention that these alterations need to be done before the include of this module, like in this article where we do inside an initializer.

Now we have Knock ready to work with Auth0 inside our Rails application. The original implementation of the method entity_for can be found here. Note that the purpose of this alteration is basically only to do an find_by(auth0_uid: @payload["sub"]) instead of using the method find. That’s why we don’t desire to search by the User model ID field, but the auth0_uid instead.

Finally, the last step of our integration: putting everything to work together. Let’s start adding Knock’s authentication module in application_controller.rb:

Let’s create the home_controller.rb, where we’re gonna add the Knock callback to authenticate the user:

Before we can test the authentication process, we need to include a route in routes.rb for the action created above:

We ended the necessary implementation, and now we’re ready to test. The HTTP requests for the Rails API are gonna be done with Linux curl library. However, the same can be done with any other options, like the software Postman.

First, I’m gonna call our index action without post any further information:

$ curl http://localhost:3000/index

The returned HTTP status was 401 Unauthorized. Now let’s sign in the user with the wrapper created before:

> Auth0::Signin.new('onebitauth@mail.com', '123')

In sequence, call the index URL informing the returned token in header:

$ curl -H 'Accept: application/json' -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ik5USXhRVUUyTTBJeE1qSkVOMFl5T0VJek9FRkdSRE14TkVRME1qbERSamMxTnprNFFUTTBOQSJ9.eyJpc3MiOiJodHRwczovL3J3ZWhyZXNtYW5uLmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHw1Y2NjZWQ3NzU1YTdkNzEwZjU1ODUwYTciLCJhdWQiOlsiaHR0cHM6Ly9yd2VocmVzbWFubi5hdXRoMC5jb20vYXBpL3YyLyIsImh0dHBzOi8vcndlaHJlc21hbm4uYXV0aDAuY29tL3VzZXJpbmZvIl0sImlhdCI6MTU1NjkzNDEyMCwiZXhwIjoxNTU3MDIwNTIwLCJhenAiOiJpeTZHYXR2UWtUUnVTSktLV2NhZ1J2aU5MaThsakxYSyIsInNjb3BlIjoib3BlbmlkIHByb2ZpbGUgZW1haWwgYWRkcmVzcyBwaG9uZSByZWFkOmN1cnJlbnRfdXNlciB1cGRhdGU6Y3VycmVudF91c2VyX21ldGFkYXRhIGRlbGV0ZTpjdXJyZW50X3VzZXJfbWV0YWRhdGEgY3JlYXRlOmN1cnJlbnRfdXNlcl9tZXRhZGF0YSBjcmVhdGU6Y3VycmVudF91c2VyX2RldmljZV9jcmVkZW50aWFscyBkZWxldGU6Y3VycmVudF91c2VyX2RldmljZV9jcmVkZW50aWFscyB1cGRhdGU6Y3VycmVudF91c2VyX2lkZW50aXRpZXMiLCJndHkiOiJwYXNzd29yZCJ9.nrVTy_9rgTi0GVfmPUMrmQcj8jkIzWF0NuZA-X-4SrwiD3Oeodt0UITEdFa-rsM34LSlazN8JglHpgR181MpOXGBPSIkoaJPSX6Scu0wJUxTsZ7G3M3cNoZ9p_Pk_XL-pj2TartimUdsbWZLIdwyRmnzxJ0ePAujlez429gwEwzIjnW38KoVQznTx8_HBkziK4iK5-MHzpdr1rXINhwzz9dWsto-1C88kMx5bQ-rsdTNRG-zi3tLkE9vBFswgg5CDr_A19_V1hFN0-h56oMn6kP3IOr7cEEhxXR15zs7U9zJDvmXKOW-POUCrcR9IikjGGSktMamMz8I9SBrs1oBXg" http://localhost:3000/index
User onebitauth@mail.com is logged in.

This is it! The defined message to be returned when the user is logged in was returned.

The implementation of Knock used in this article doesn’t give us any clues of why the error 401 was returned from Auth0. Of course, the most obvious situation is easy to deduct: may the user isn’t logged in at all. However, there are some silent errors that happen and are cumbersome to find out, like the use of the wrong signature algorithm. We can make a little change in one of the Knock’s methods to debug these kinds of errors. In your knock.rb you may include the following:

Don’t worry about understanding everything that’s written there. Only a ruby begin rescue structure was removed, forcing to raise an error when something wrong happens calling Knock::AuthToken.

This post exemplified how to integrate with a few steps a Rails API with Auth0 authentication’s service, using as support the Knock gem. The lack of documentation exemplifying how to use this gem with a service like Auth0 can discourage its use with this purpose, or even frustrate a try. Having the knowledge that some points of the Knock source code need to be changed to do such integration can make the task even scarier.

However, with the information provided through this article, this task becomes relatively simple and delivers the Knock gem in a functional state to be used with Auth0. Therefore, the implementation made here can be used like a step-by-step to easily integrate Auth0 in your projects.

The application’s source code is available on my GitHub.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store