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:
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 include
d 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 import
ed 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 import
ed 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.