Authentication With GraphQL, Relay, and Rails

How to register users and log them in and out in a Relay-on-Rails application

Facebook Login by Photo-Mix is licensed under CC0 Creative Commons

I’ve written previously about my overall approach to doing GraphQL and Relay on Rails, which involves using Rails in API mode on the back-end, and Node.js with Express to serve front-end assets. But no matter how you chose to approach the problem of getting Relay and React working with Rails, you still have to figure out a good way to handle authentication.

In this article, we’ll look at how you register users and log them in and out in a Relay-on-Rails application. This piece covers the back-end implementation only, so you’ll need to know how to implement Relay mutations on your preferred client.

If you want to see how all of this is implemented in an app that boots and works, you can follow along in this repo. In this repo, I took this example app repo and swapped the back-end out with Rails.

The Viewer Query

Before we get into the details of authentication and Relay, we should briefly cover something that you’ll see if you look at any number of Relay examples, especially those provided by Facebook. I’m talking about the viewer query, which is often used as the root query for every query on the site.

Because you took my advice in the previous installment and learned a whole bunch of GraphQL before proceeding, you already know that all GraphQL queries are built on a root node. If your app is a blog, then a root node might be a specific user, and you might then attach a list of posts or even comments to this query.

query {
  user(id: "12345") {
    name
    profilePicture
    emailAddress
    posts {
      edges {
      	node {
      	  id
      	  title
          image
      	}
      }
    }
  }
}

The above Relay query gets some basic info for the author with id 12345, along with the author’s posts.

But often, what a specific visitor sees on a site is directly related to who that visitor is – is she logged in, and if so, what permissions does she have? In other words, for every page on a modern site with a login feature, the identity of the person viewing that page matters.

Facebook and most other Relay apps use a viewer query as the root of all of the other GraphQL queries. Once the identity of this viewer is established, the rest of the query results can be assembled to fit that specific viewer.

query {
  viewer {
    user {
      id
      name
      profilePicture
      emailAddress
    }
    posts {
      edges {
        node {
          id
          title
          image
        }
      }
    }
  }
}

The above simple viewer query assumes that the viewer is a user with an id, and that this user has a list of posts that are visible to him.

Now, you’re probably looking at this query and thinking, “where do we put in the user’s id, so that we know how to retrieve the user’s info and posts?”

The answer, for most Relay apps (my demo app included), is that the viewer’s identity is established and managed outside of the actual GraphQL query, so that when the query is executed the viewer is known and can be included in the query’s context.

What we’ll talk about in the rest of this article is how to establish who the viewer is, and then use that information in our queries.

Out-of-band vs. In-Band

A complete authentication and session management solution for a modern web app consists of the following components, at a minimum:

  1. Registration
  2. Login
  3. Logout
  4. Password Reset

There are two approaches for handling the above in a Relay app: out-of-band and in-band.

To take out-of-band first, this approach involves setting up separate HTTP endpoints for registration and login/logout. Users hit these endpoints to create an account or to create/destroy a session. The calls to the main graphql endpoint that take place during the session are authenticated with cookies or something like a bearer token in the request headers.

Out-of-band session management using something like PassportJS is quite common, and indeed it’s probably more common than attempting to do all of this over Relay itself. In our case, we’re going to do all of this in-band via Relay.

With in-band session management, we’ll be using GraphQL mutations for registration, login, and logout. Most mutations involve changes to the server’s backing datastore, but the session management mutations are special in that they involve changes to the client’s state.

Using Warden for Session Management

The repo that I used as the basis for my project does in-band user and session management via Relay, and it authenticates requests using a JSON Web Token (JWT) stored on the client in a cookie (via cookie-session). There’s a bit of session management middleware that gets the token from the cookie, decodes it, and then makes it available for the server to pass to graphql via the rootValue argument on every request.

  graphQLServer.use(sessionMiddleware)
  graphQLServer.use('/graphql', uploadMiddleWare)

  graphQLServer.use(
    '/graphql',
    graphQLHTTP(({ session, tokenData }) => ({
      graphiql: true,
      pretty: true,
      schema: Schema,
      context: { db: database },
      rootValue: { session, tokenData },
    })),
  )

When porting this app over to Rails, I initially looked at replacing the Express middleware with the devise gem, but ultimately decided against using it.

Devise is awesome, and you should always just use it and never ever roll your own authentication solution. However, Devise brings with it way too much baggage for what I need in this project, and it also assumes a REST API. Even the Devise-based gems that are specifically for Rails APIs are nonetheless thoroughly RESTy, and it in the end I decided it would be more trouble to make those work in my non-REST app than it is to just go down one layer in the stack and use the warden and bcrypt gems that Devise itself uses.

The Warden Rack middleware is fairly easy to use – it sets the cookies and signs them with the app secret, and generally does all of the standard Rails session stuff for you. Here’s how I set it all up.

After adding gem 'warden' to my Gemfile, I created config/initializers/warden.rb and populated it as follows:

Rails.application.config.middleware.insert_after Rack::ETag, Warden::Manager do |manager|
  manager.failure_app = GraphqlController

  manager.serialize_into_session do |viewer|
    viewer.to_h.slice(:id, :role)
  end

  manager.serialize_from_session do |attributes|
    API::Viewer.new(attributes.symbolize_keys)
  end
end

This bit of code inserts the Warden::Manager middleware into the Rack stack, and configures it.

Warden requires you to configure a failure_app, but I’m not actually failing any logins via Warden (more on this in a moment), so this never gets called. I just set it to GraphqlController so Warden doesn’t complain.

The next bit of code serializes the viewer object into the session, i.e. it tells Warden to store the viewer’s id and role in a signed session cookie on the client.

Finally, the serialize_from_session block tells Warden to create a new Viewer object from the id and role that it gets when it reads the session cookie on every request.

Important Note: The id that we’re serializing into and out of the session is not the database primary key field associated with the user record. Rather, it’s a UUID that we generate when the user record is created and store in user.uuid. From the perspective of the API, this is the user’s id, but in the database the id column is a Bigint and is used as the primary key.

In general, the app translates JavaScripty names like creatorId and firstName to the more Rails-friendly user_id and first_name once we leave the API namespace. But we’ll look into that more in another post.

Logging in

Now it’s time to talk about how we actually log in, but first let’s take a look at part of the GraphqlController code:

class GraphqlController < ApplicationController
  
  def execute 
    query = params[:query]
    context = {
      warden: warden,
      viewer: viewer
    }
    result = RailsRelayAuthenticationSchema.execute(query, variables: variables, context: context)
    
    render json: result
  end

  private

  def viewer
    warden.user || API::Viewer.new
  end

  def warden
    request.env['warden']
  end
end

The graphql-ruby gem doesn’t pass a rootValue argument to GraphQL, so we have to put everything into the context object – this is fine, because I was never clear on the point of rootValue as a separate argument, anyway.

You’ll see that I’m putting the actual warden reference into the context – this is necessary because the login and logout mutations will need to talk to that object in order to manipulate the session. Those are the only two GraphQL queries that use context.warden, but it still gets passed in on every request.

Now let’s take a look at the actual LoginMutation:

Mutations::LoginMutation = GraphQL::Relay::Mutation.define do
  # Used to name derived types, eg `"LoginMutation"`:
  name  'Login'

  # Accessible from `inputs` in the resolve function:
  input_field :email, !types.String
  input_field :password, !types.String

  return_field :user, Types::UserType

  resolve ->(object, inputs, ctx) {
    user = API::User.find_by_email(inputs[:email])
    
    if user && user.authenticate(inputs[:password])
      ctx[:warden].set_user(user)
      { user: user }
    else
      GraphQL::ExecutionError.new("Wrong email or password")
    end
  }
end

If you’ve been doing GraphQL in Node.js, then the above will look familiar to you. The graphql-ruby gem is designed to look quite a bit like the corresponding JavaScript implementation. This may rub some Ruby purists the wrong way, but given that most of the work of learning this stack involves turning JavaScript code into Ruby code, it’s a blessing.

This code is very straightforward: if we can find the user via their email address and authenticate them, then we call set_user() on the Warden middleware object, which takes care of serializing the user into a standard Rails session cookie and passing it back to the client. And if we can’t, then we raise a GraphQL::ExecutionError that gets sent back to the client in the standard GraphQL errors vector.

Note that we’re not failing the login via Warden – again, this is not a REST app, so we don’t want to send the visitor to a different endpoint. The non-logged-in-visitor just needs to see an in-band error message transmitted via Relay, which is exactly what GraphQL::ExecutionError gives us. So, as mentioned above, the Warden::Manager.failure_app option goes unused, but we set it because Warden requires it to be set.

Before moving on, let’s take a quick look at User#authenticate:

def authenticate(password_string)
	return self if BCrypt::Password.new(password_digest) == password_string
end

In another part of the app we used bcrypt to digest the user’s original cleartext password at registration, and here we use it to check the submitted password against the digest.

Important Note: It’s important that you check the password as above, because the BCrypt::Pasword#== method is constant-time and designed to be secure against timing attacks. Do not, under any circumstances, cook up your own method for validating the password string – just use the provided bcrypt code.

Logging out

The LogoutMutation is dead simple:

Mutations::LogoutMutation = GraphQL::Relay::Mutation.define do
  name  'Logout'

  return_field :user, Types::UserType

  resolve ->(object, inputs, ctx) {
    ctx[:warden].logout
    { user: ctx[:viewer].user }
  }
end

The call to logout takes care of ending the Rails session – you don’t need to do any extra work, just let Warden do its thing.

Registration

The RegisterMutation touches both the database and the current session, but it’s nonetheless simple:

Mutations::RegisterMutation = GraphQL::Relay::Mutation.define do
  name  'Register'

  # Accessible from `inputs` in the resolve function:
  input_field :email, !types.String
  input_field :password, !types.String
  input_field :firstName, !types.String
  input_field :lastName, !types.String

  return_field :user, Types::UserType

  resolve ->(object, inputs, ctx) {
    existing_user = API::User.find_by(email: inputs[:email])
    
    if existing_user
      GraphQL::ExecutionError.new("Email already taken")
    else
      register = API::Register.call(inputs)

      if register.success?
        ctx[:warden].set_user(register.user)
        { user: register.user }
      else
        GraphQL::ExecutionError.new(register.error)
      end
    end
  }
end

In this code, we use the API::Register interactor to actually create the user in the database, and then we either set the user via Warden or return a GraphQL error.

(This code could be cleaned up by moving the existing_user check into the interactor, and erroring there instead.)

Conclusions and Next Steps

Using Warden and BCrypt to do in-band authentication via Relay is super easy – it’s actually far easier than trying bend Devise to do all of this.

But in ditching Devise, we have to implement our own password recovery functionality. I’ll tackle this in a later installment, and we’ll use Sendgrid’s API instead of the built-in Rails mailers.

Another major thing that needs to be added to this solution is authentication of the front-end server, itself. In other words, with the current implementation anyone can hit our GraphQL endpoint from anywhere. There’s not any rate limiting, nor is there even any any mechanism that we could build on in order to rate-limit viewers who are not logged in. What we want, then, is some sort of token-based API authentication layered on top of the actual user session, so that the front-end server can authorize even non-logged-in requests.

Doing this correctly is more complicated than it may sound. We’ll want to use Warden scopes, and we’ll also want to employ a strategy where we generate a new API token for each request.

Finally, this app needs SSL. We’ll want to force users and the front-end server to connect our GraphQL server over HTTPS, so that’s another project for another post.

Photo of Jon Stokes

Jon is a founder of Ars Technica, and a former Wired editor. When he’s not developing code for Collective Idea clients, he still keeps his foot in the content world via freelancing and the occasional op-ed.

Comments:


Post a Comment

(optional)
(optional — will be included as a link.)
  1. Hello, I’m hachi8833, a tech writer and a translator.

    I’d like to translate the article https://collectiveidea.com/blog/archives/2017/10/23/authentication-with-graphql-relay-and-rails into Japanese and publish on our tech blog https://techracho.bpsinc.jp/ if you’re OK.

    I make sure to indicate the link to original, title, author name in the case.

    Best regards,

    October 30, 2017 at 9:01 AM