Introduction

You’ve likely heard about the new kid on the block; GraphQL. It has many advantages over traditional REST APIs such as the ability to request only what you need for a resource or list of resources, no need for versioning changes, and better stability. Due to these advantages and others, companies like GitHub, Yelp, and The New York Times have begun to implement GraphQL for both external and internal use. If you have an existing Rails app that uses Devise for authentication and you’d like to move towards having a Single Page App (SPA), Vue and GraphQL work seamlessly together to round out your stack.

After much searching, I was not able to find a good article that focused on how to implement authentication with Rails, Vue, GraphQL and Devise altogether. Thus, I’d like to provide a step-by-step guide on how to get going with simple Token/Bearer Authentication inclusive of test coverage.

The technologies used in this article are as follows:

  1. Ruby on Rails (RoR)
  2. GraphQL (for Ruby)
  3. Devise
  4. VueJS
  5. Apollo (for Vue)
  6. Vuex (optional)

My examples and explanations assume that you have a RoR application already set up with GraphQL. If not, check out the GraphQL-Ruby Getting Started guide and be sure to install GraphiQL as well to help experiment with and debug your queries and mutations. I’ll also assume that you have VueJS set up either with Webpacker or as a standalone application. To get started quickly with an existing RoR application, check out the Webpacker installation guide. Lastly, if you’re not already familiar with GraphQL, Vue, and Apollo, start by checking out the Helpful Tools section below, to save yourself a lot of debugging headache.

Accompanying this article is a demo app hosted on Heroku with code accessible on GitHub. Follow along with the full examples to fully comprehend the steps explained within this article.

Rails Configuration

Devise

Relevant code: routes.rb, devise.rb, user.rb

If you do not have Devise set up already, check out the Devise Getting Started guide. The following examples will use the User model but feel free to name your model whatever you’d like. The only custom configuration you’ll need that is different than the defaults is the removal of the devise session routes. This is because we will be using mutations for all user session interactions.

# config/routes.rb

devise_for :users, skip: :sessions

Once your app is set up with Devise, follow the steps to set up Devise::TokenAuthenticatable, a plugin extracted from Devise which allows you to use tokens for authentication.

I put the Devise::TokenAuthenticatable.setup block into my config/initializers/devise.rb initializer and left everything commented out besides config.should_ensure_authentication_token = true.

should_ensure_authentication_token ensures that any model with devise :token_authenticatable, in this case User, will have an auth token set before saving, ie. handling the need to add before_save :ensure_authentication_token to your User model. This is important because a User should always have an auth token. When a user signs out, we’ll simply generate a new token that will be provided the next time the user signs back in.

User and GraphQL Ids

Relevant code: user_spec.rb, interface.rb, interface_spec.rb

I always like to ensure my application is working how it’s configured so I threw a spec around the should_ensure_authentication_token behavior.

If you have an app with an existing users table and only needed to add the authentication_token field, you can write a simple script to backfill all existing users.

User.find_each { |user| user.reset_authentication_token! }

I’ve also created a GraphQL::Interface concern which is included by all models that are exposed via GraphQL. The interface defines gql_id and find_by_gql_id which use the GraphQL::Schema::UniqueWithinType module to create id hashes and look up objects by those hashed ids.

GraphqlController

Relevant code: graphql_controller.rb, requests/graphql_spec.rb

With Devise and Devise::TokenAuthenticatable set up, your app will now assign a current_user variable in your controllers when it receives a request with Authorization: Bearer <token> in its headers. The current_user is an instance of the User model.

GraphQL provides context around queries and mutations being executed which, in simple terms, is an object containing metadata needed during execution. To use our new current_user variable, we’ll add it to the context in the GraphqlController.

Authorize User Helper

Relevant code: base_mutation.rb, base_mutation_spec.rb

To ensure that a user is signed in before changing any data, regardless of any other permissions, add the authorize_user method to the Mutations::BaseMutation class. Now you can call authorize_user at the top of any mutation like so.

# app/graphql/mutations/my_first_mutation.rb

def resolve(**args)
  authorize_user
  # perform some change
end

UserType and Authentication Token

Relevant code: user_type.rb, user_type_spec.rb

Set up a UserType and be sure to expose the authentication_token. You’ll need to ensure that only the owner of the token can request it in any queries exposing the user. To do so, you can check the current_user’s gql_id against the UserType being requested.

At this point, you have all the building blocks in place to do authentication on the backend and interact with the User model. Next, we’ll set up the frontend configuration to handle authentication with every request.

Vue Configuration

Relevant code: apolloProvider.js

Apollo is a set of tools to help interface with GraphQL from the frontend. If you have not already set it up on your Vue app, follow the Vue Apollo Installation guide. Similarly to Axios or any other network-interfacing library, you must configure your requests to point to the right domain, handle CSRF, and set up your authentication method.

Since the example in this article uses Webpacker, my domain is the same for both the Vue frontend and Rails backend. The browser’s current origin can be fetched to be used as the host with a call to window.location.origin.

The CSRF token is set in the application layout out-of-the-box with Rails. You should find the csrf_meta_tags in the head of your app/views/layouts/application.html. We’ll fetch that value and pass it along with every request as the X-CSRF-Token in the headers.

Lastly, we’ll use localStorage to set and fetch the auth token. Note that using cookies is perfectly acceptable as well. In this example, app-level names are stored in an appConstant.js file and in this case AUTH_TOKEN_KEY is set to 'authToken'.

With that configuration in place, the file is imported into the application configuration and added to the Vue instance.

// app/javascript/packs/application.js

document.addEventListener('DOMContentLoaded', () => {
  new Vue({
    el: '#vg-application',
    name: 'AppRoot',
    apolloProvider,
    router,
    store: createStore(),
    render: h => h(Application),
  });
});

Now every request via Apollo will be dressed up with the headers you need to populate current_user on the backend. Next step is to allow users to register.

Register User

Rails Backend

Relevant code: register_user.rb, register_user_spec.rb, mutation_result.rb, mutation_result_spec.rb, mutation_type.rb

Now the fun begins! We’ll now wire frontend and backend together with a mutation that will create a User and return their basic information as well as their authenticationToken. As long as this token remains the same between what is stored in the database and what’s stored in localStorage, the user’s session will be authenticated.

The mutation itself is pretty straight forward but it’s crucial to note that the current_user in the context is being set upon successful User creation. This is important because there was no user signed in when the register user request was made. We’ll need one set when we’re asking for the authenticationToken in the response because the line we've added in UserType, which would otherwise not return a token.

You may also notice MutationResult which is a very thin wrapper to the hash of return fields defined on Mutations::BaseMutation. They provide some useful defaults to DRY up and standardize the results of all mutations.

Finally, as with any mutation, define it as a field in the Types::MutationType GraphQL type.

Vue Frontend

Relevant code: registerUser.js, sign_up/new.vue, userStore.js

To prevent bloat in components and provide reusability, the registerUser mutation is stored in the app/javascript/mutations directory. It takes in the Apollo instance and all required fields and returns the promise from apollo.mutate.

The mutation is then imported into the new sign up component and can be called upon click such as a form submission. If you are using Vuex, this is where you can then set your user object in the store. The most important line in the resolution of the mutation’s promise is localStorage.setItem(AUTH_TOKEN_KEY, user.authenticationToken);. This localStorage value is what will now be passed along with every apollo request and reset upon signing out.

Sign In

Relevant code: sign_in.rb, sign_in_spec.rb, signIn.js, sign_in/new.vue

The only pieces worth noting in the SignIn mutation are the two calls to the Devise methods; find_for_database_authentication and valid_password? both of which are straight forward. The methods work together to ensure you have a valid email and password combination on an existing User. Again, context[:current_user] is explicitly set during sign in so that the authenticationToken on the UserType can be returned.

The Vue side is nearly identical to the register user mutation and component. After a successful response is received, the auth token is set in localStorage and page navigation is triggered.

Sign Out

Relevant code: sign_out.rb, sign_out_spec.rb, signOut.js, navbar.vue

Sign out is a bit different than the other mutations because it is where the token gets reset. The SignOut mutation takes the current_user from the context and calls reset_authentication_token! which generates and persists a new auth token for the user. Unlike the result of other mutations, the token should not be returned here.

On the frontend, the localStorage auth token is cleared and, if using Vuex, the user object in your store needs to be reset as well. Subsequent requests with the old token will now result in the “User not signed in” error.

Restricting Access

Relevant code: applicationSettings.js, home/show.vue

You can now add a global computed property to check if the user is signed in. This can be used to toggle content on the page as well as redirect users that should not have access to a page at all. Keep in mind that you still need to build a permissions system on the backend to restrict access to any resources served over GraphQL or any other APIs you may have.

Conclusion

That’s it! You now have a fully working RoR + VueJS app with authentication that you can extend with permissions and roles. You can also access current_user in the context of any query or mutation to both protect sensitive data and fetch data related to the signed in user. Head over to the demo app and code to see the full example.

Helpful Tools

The absolute must-have tools for working with Vue and Apollo are the Chrome Vue Tool extension and Chrome Apollo Tool extension. Both provide so much insight into the state of your components and how your code is behaving that you will have little use for debugger, console.log, or even much of the standard Chrome Dev Tools features.

If you’re not already familiar with GraphiQL web interface, head over to http://localhost:3000/graphiql to try out your queries and mutations. This tool provides an interface that is very useful in debugging as well as dynamic documentation of all your types and mutations and a history of the actions you’ve performed.

Finally, at the time of writing this, there is no way to attach headers to your GraphQL requests with the GraphiQL tool in the browser. To do so, download Altair for a more full-featured GraphQL client that will allow you to add your Bearer <token> headers while testing.

Sound interesting? We’re hiring. Apply here or send me a message at jklein@doximity.com if you want to learn more about how we’re building the largest network for clinicians ever.


Huge thanks to Rob Malko, Fabio Rhem and Bruno Miranda for editing, and Hannah Frank for the epic illustrations.

Be sure to follow @doximity_tech if you'd like to be notified about new blog posts.