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.

What We are Going to Build

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.

What Do We Need

  • 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!

Configuring an application in 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.

Generating the Rails Project

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.

Setting ENV Variables

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.

Starting With Knock

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.

Creating Wrappers For Sign In And Sign Out Calls

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.

Changing Knock Implementation

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.

Authentication in Rails API

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.

Extra Tip: Debugging Errors inside Knock

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.

Conclusion

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.