GraphQL interface on top of the Devise Token Auth (DTA) gem.
- GraphqlDevise
- Table of Contents
- Introduction
- Installation
- Usage
- Mounting Auth Schema on a Separate Route
- Mounting Operations Into Your Own Schema
- Available Mount Options
- Available Operations
- Configuring Model
- Email Reconfirmation
- Customizing Email Templates
- I18n
- Authenticating Controller Actions
- Making Requests
- More Configuration Options
- GraphQL Interpreter
- Using Alongside Standard Devise
- Future Work
- Contributing
- License
Graphql-Devise heavily relies on two gems:
- Devise Token Auth (DTA)
- Devise (which is a DTA dependency)
This gem provides a GraphQL interface on top of DTA which is designed for REST APIs. Features like token management, token expiration and everything up until using the actual GraphQL schema is still controlled by DTA. For that reason the gem's generator invokes DTA and Devise generators and creates initializer files for each one of them.
We strongly recommend getting familiar with DTA documentation to use this gem to its full potential. More configuration details available in configuration section
Add this line to your application's Gemfile:
gem 'graphql_devise'And then execute:
$ bundleGraphql Devise generator will execute Devise and Devise Token Auth generators to setup the gems in your project. You can customize them to your needs using their initializer files(one per gem) as usual.
$ bundle exec rails generate graphql_devise:installThe generator accepts 2 params:
user_class: Model name in whichDevisemodules will be included. This uses afind or createstrategy. Defaults toUser.mount_path: Path in which the dedicated graphql schema for devise will be mounted. Defaults to/graphql_auth.
The option mount is available starting from v0.12.0. This option will allow you to mount the operations in your own schema instead of a dedicated one. When this option is provided mount_path param is not used.
To configure the gem to use a separate schema, the generator will use user_class and mount_path params.
The route will be mounted in config/routes.rb. For instance the executing:
$ bundle exec rails g graphql_devise:install Admin api/authWill do the following:
- Execute
Deviseinstall generator - Execute
Devise Token Authinstall generator withAdminandapi/authas params- Find or create
Adminmodel - Add
devisemodules toAdminmodel - Other changes that you can find here
- Find or create
- Add the route to
config/routes.rbmount_graphql_devise_for 'Admin', at: 'api/auth'
Admin could be any model name you are going to be using for authentication,
and api/auth could be any mount path you would like to use for auth.
- Remember that by default this gem mounts a completely separate GraphQL schema on a separate controller in the route provided by the
atoption in themount_graphql_devise_formethod in theconfig/routes.rbfile. If noatoption is provided, the route will be/graphql_auth. - Avoid passing the
--mountoption or the gem will try to use an existing schema.
To configure the gem to use your own GQL schema use the --mount option.
For instance the executing:
$ bundle exec rails g graphql_devise:install Admin --mount MySchemaWill do the following:
- Execute
Deviseinstall generator - Execute
Devise Token Authinstall generator withAdminandapi/authas params- Find or create
Adminmodel - Add
devisemodules toAdminmodel - Other changes that you can find here
- Add
SchemaPluginto the specified schema.
- Find or create
- When using the
--mountoption themount_pathparams is ignored. - The generator will look for your schema under
app/graphql/directory. We are expecting the name of the file is the same as the as the one passed in the mount option transformed withunderscore. In the example, passingMySchema, will try to find the fileapp/graphql/my_schema.rb. - You can actually mount a resource's auth schema in a separate route and in your app's schema at the same time, but that's probably not a common scenario.
GraphqlDevise operations can be used in two ways:
- Using a separate schema via
mount_graphql_devise_forhelper in the routes file. - Using your own schema by adding a plugin in the class.
Creating a separate schema is the default option, the generator will do that by default.
You can mount this gem's GraphQL auth schema in your routes file like this:
# config/routes.rb
Rails.application.routes.draw do
mount_graphql_devise_for(
'User',
at: 'api/v1',
authenticatable_type: Types::MyCustomUserType,
operations: {
login: Mutations::Login
},
skip: [:sign_up],
additional_mutations: {
# generates mutation { adminUserSignUp }
admin_user_sign_up: Mutations::AdminUserSignUp
},
additional_queries: {
# generates query { publicUserByUuid }
public_user_by_uuid: Resolvers::UserByUuid
}
)
endThis can be done using the generator or manually.
The second argument of the mount_graphql_devise method is a hash of options where you can
customize how the queries and mutations are mounted into the schema. For a list of available
options go here
Starting with v0.12.0 you can now mount the GQL operations provided by this gem into your
app's main schema.
# app/graphql/dummy_schema.rb
class DummySchema < GraphQL::Schema
# It's important that this line goes before setting the query and mutation type on your
# schema in graphql versions < 1.10.0
use GraphqlDevise::SchemaPlugin.new(
query: Types::QueryType,
mutation: Types::MutationType,
resource_loaders: [
GraphqlDevise::ResourceLoader.new('User', only: [:login, :confirm_account])
]
)
mutation(Types::MutationType)
query(Types::QueryType)
endThe example above describes just one of the possible scenarios you might need.
The second argument of the GraphqlDevise::ResourceLoader initializer is a hash of
options where you can customize how the queries and mutations are mounted into the schema.
For a list of available options go here.
It's important to use the plugin in your schema before assigning the mutation and query type to
it in graphql versions < 1.10.0. Otherwise the auth operations won't be available.
You can provide as many resource loaders as you need to the resource_loaders option, and each
of those will be loaded into your schema. These are the options you can initialize the
SchemaPlugin with:
query: This param is mandatory unless you skip all queries via the resource loader options. This should be the sameQueryTypeyou provide to thequerymethod in your schema.mutation: This param mandatory unless you skip all mutations via the resource loader options. This should be the sameMutationTypeyou provide to themutationmethod in your schema.resource_loaders: This is an optional array ofGraphqlDevise::ResourceLoaderinstances. Here is where you specify the operations that you want to load into your app's schema. If no loader is provided, no operations will be added to your schema, but you will still be able to authenticate queries and mutations selectively. More on this in the controller authentication section.authenticate_default: This is a boolean value which istrueby default. This value defines what is the default behavior for authentication in your schema fields.truemeans every root level field requires authentication unless specified otherwise using theauthenticate: falseoption on the field.falsemeans your root level fields won't require authentication unless specified otherwise using theauthenticate: trueoption on the field.unauthenticated_proc: This param is optional. Here you can provide a proc that receives one argument (field name) and is called whenever a field that requires authentication is called without an authenticated resource. By default aGraphQL::ExecutionErrorwill be raised if authentication fails. This will provide a GQL like error message on the response.
Both the mount_graphql_devise_for method and the GraphqlDevise::ResourceLoader class
take the same options. So, wether you decide to mount this gem in a separate route
from your main application's schema or you use our GraphqlDevise::SchemaPlugin to load
this gem's auth operation into your schema, these are the options you can provide as a hash.
# Using the mount method in your config/routes.rb file
mount_graphql_devise_for('User', {})
# Providing options to a GraphqlDevise::ResourceLoader
GraphqlDevise::ResourceLoader.new('User', {})at: Route where the GraphQL schema will be mounted on the Rails server. In this example your API will have these two routes:POST /api/v1/graphql_authandGET /api/v1/graphql_auth. If this option is not specified, the schema will be mounted at/graphql_auth. This option only works if you are using the mount method.operations: Specifying this is optional. Here you can override default behavior by specifying your own mutations and queries for every GraphQL operation. Check available operations in this file mutations and queries. All mutations and queries are built so you can extend default behavior just by extending our default classes and yielding your customized code after callingsuper, example here.authenticatable_type: By default, the gem will add anauthenticatablefield to every mutation and anauthenticatabletype to every query. Gem will try to useTypes::<model>Typeby default, so in our example you could defineTypes::UserTypeand every query and mutation will use it. But, you can override this type with this option like in the example.skip: An array of the operations that should not be available in the authentication schema. All these operations are symbols and should belong to the list of available operations in the gem.only: An array of the operations that should be available in the authentication schema. Theskipandonlyoptions are mutually exclusive, an error will be raised if you pass both to the mount method.additional_mutations: Here you can add as many mutations as you need, for those features that don't fully match the provided default mutations and queries. You need to provide a hash to this option, and each key will be the name of the mutation on the schema. Also, the value provided must be a valid mutation. This is similar to what you can accomplish with devise_scope.additional_queries: Here you can add as many queries as you need, for those features that don't fully match the provided default mutations and queries. You need to provide a hash to this option, and each key will be the name of the query on the schema. Also, the value provided must be a valid Resolver. This is also similar to what you can accomplish with devise_scope.
Additional mutations and queries will be added to the schema regardless
of other options you might have specified like skip or only.
Additional queries and mutations is usually a good place for other
operations on your schema that require no authentication (like sign_up).
Also by adding them through the mount method, your mutations and
resolvers can inherit from our base mutation
or base resolver
respectively, to take advantage of some of the methods provided by devise
just like with devise_scope
The following is a list of the symbols you can provide to the operations, skip and only options of the mount method:
:login
:logout
:sign_up
:update_password
:send_password_reset
:confirm_account
:check_password_tokenJust like with Devise and DTA, you need to include a module in your authenticatable model, so with our example, your user model will have to look like this:
# app/models/user.rb
class User < ApplicationRecord
devise :database_authenticatable,
:registerable,
:recoverable,
:rememberable,
:trackable,
:lockable,
:validatable,
:confirmable
# including after calling the `devise` method is important.
include GraphqlDevise::Concerns::Model
endThe install generator can do this for you if you specify the user_class option.
See Installation for details.
DTA and Devise support email reconfirmation. When the confirmable module is added to your
resource, an email is sent to the provided email address when the signUp mutation is used.
You can also use this gem so every time a user updates the email field, a new email gets sent
for the user to confirm the new email address. Only after clicking on the confirmation link,
the email will be updated on the database to use the new value.
In order to use this feature there are a couple of things to setup first:
- Make user your model includes the
:confirmablemodule. - Add an
unconfirmed_emailString column to your resource's table.
After that is done, you simply need to call a different update method on your resource,
update_with_email. This method behaves exactly the same as ActiveRecord's update method
if the previous steps are not performed, or if you are not updating the email attribute.
It is also mandatory to provide two additional attributes when email will change or an error
will be raised:
schema_url: The full url where your GQL schema is mounted. You can get this value from the controller available in the context of your mutations and queries like this:
context[:controller].full_url_without_paramsconfirmation_success_url: This the full url where you want users to be redirected after the email has changed successfully (usually a front-end url). This value is mandatory unless you have setdefault_confirm_success_urlin your devise_token_auth initializer.
So, it's up to you where you require confirmation of changing emails. Here's an example on how you might do this. And also a demonstration on the method usage:
user.update_with_email(
name: 'New Name',
email: '[email protected]',
schema_url: 'http://localhost:3000/graphql',
confirmation_success_url: 'https://google.com'
)The approach of this gem is a bit different from DeviseTokenAuth. We have placed our templates in app/views/graphql_devise/mailer,
so if you want to change them, place yours on the same dir structure on your Rails project. You can customize these two templates:
app/views/graphql_devise/mailer/confirmation_instructions.html.erbapp/views/graphql_devise/mailer/reset_password_instructions.html.erb
The main reason for this difference is just to make it easier to have both Standard Devise and this gem running at the same time.
Check these files to see the available helper methods you can use in your views.
GraphQL Devise supports locales. For example, the graphql_devise.confirmations.send_instructions locale setting supports the %{email} variable in case you would like to include it in the resend confirmation instructions for the user. Take a look at our locale file to see all of the available messages.
Keep in mind that if your app uses multiple locales, you should set the I18n.locale accordingly. You can learn how to do this here.
When mounting the operation is in you own schema instead of a dedicated one, you will need to authenticate users in your controllers, just like in DTA. There are 2 alternatives to accomplish this.
For this you will need to call authenticate_<model>! in a before_action controller hook.
In our example our model is User, so it would look like this:
# app/controllers/my_controller.rb
class MyController < ApplicationController
include GraphqlDevise::Concerns::SetUserByToken
before_action :authenticate_user!
def my_action
result = DummySchema.execute(params[:query], context: current_user: current_user)
render json: result unless performed?
end
endThe install generator can include the concern in you application controller. If authentication fails for a request, execution will halt and a REST error will be returned since the request never reaches your GQL schema.
For this you will need to add the GraphqlDevise::SchemaPlugin to your schema as described
here.
# app/controllers/my_controller.rb
class MyController < ApplicationController
include GraphqlDevise::Concerns::SetUserByToken
def my_action
result = DummySchema.execute(params[:query], context: graphql_context(:user))
render json: result unless performed?
end
endThe graphql_context method receives a symbol identifying the resource you are trying
to authenticate. So if you mounted the 'User' resource, the symbol is :user. You can use
this snippet to find the symbol for more complex scenarios
resource_klass.to_s.underscore.tr('/', '_').to_sym. graphql_context can also take an
array of resources if you mounted more than one into your schema. The gem will try to
authenticate a resource for each element on the array until it finds one.
Internally in your own mutations and queries a key current_resource will be available in
the context if a resource was successfully authenticated or nil otherwise.
Keep in mind that sending multiple values to the graphql_context method means that depending
on who makes the request, the context value current_resource might contain instances of the
different models you might have mounted into the schema.
Please note that by using this mechanism your GQL schema will be in control of what queries are restricted to authenticated users and you can only do this at the root level fields of your GQL schema. Configure the plugin as explained here so this can work.
In you main app's schema this is how you might specify if a field needs to be authenticated or not:
module Types
class QueryType < Types::BaseObject
# user field used the default set in the Plugin's initializer
field :user, resolver: Resolvers::UserShow
# this field will never require authentication
field :public_field, String, null: false, authenticate: false
# this field requires authentication
field :private_field, String, null: false, authenticate: true
end
endRemember to check performed? before rendering the result of the graphql operation. This is required because some operations perform a redirect and without this check you will get a AbstractController::DoubleRenderError.
Here is a list of the available mutations and queries assuming your mounted model is User.
| Operation | Description | Example |
|---|---|---|
| login | This mutation has a second field by default. credentials can be fetched directly on the mutation return type.Credentials are still returned in the headers of the response. |
userLogin(email: String!, password: String!): UserLoginPayload |
| logout | userLogout: UserLogoutPayload | |
| signUp | The parameter confirmSuccessUrl is optional unless you are using the confirmable plugin from Devise in your resource's model. If you have confirmable set up, you will have to provide it unless you have config.default_confirm_success_url set in config/initializers/devise_token_auth.rb. |
userSignUp(email: String!, password: String!, passwordConfirmation: String!, confirmSuccessUrl: String): UserSignUpPayload |
| sendResetPassword | userSendResetPassword(email: String!, redirectUrl: String!): UserSendReserPasswordPayload | |
| updatePassword | The parameter currentPassword is optional if you have config.check_current_password_before_update set to false (disabled by default) on your generated config/initializers/devise_token_aut.rb or if the resource model supports the recoverable Devise plugin and the resource's allow_password_change attribute is set to true (this is done in the userCheckPasswordToken query when you click on the sent email's link). |
userUpdatePassword(password: String!, passwordConfirmation: String!, currentPassword: String): UserUpdatePasswordPayload |
| resendConfirmation | The UserResendConfirmationPayload will return the authenticatable resource that was sent the confirmation instructions but also has a message: String! that can be used to notify a user what to do after the instructions were sent to them |
userResendConfirmation(email: String!, redirectUrl: String!): UserResendConfirmationPayload |
| Operation | Description | Example |
|---|---|---|
| confirmAccount | Performs a redirect using the redirectUrl param |
userConfirmAccount(confirmationToken: String!, redirectUrl: String!): User |
| checkPasswordToken | Performs a redirect using the redirectUrl param |
userCheckPasswordToken(resetPasswordToken: String!, redirectUrl: String): User |
The reason for having 2 queries is that these 2 are going to be accessed when clicking on
the confirmation and reset password email urls. There is no limitation for making mutation
requests using the GET method on the Rails side, but looks like there might be a limitation
on the Apollo Client.
We will continue to build better docs for the gem after this first release, but in the mean time you can use our specs to better understand how to use the gem. Also, the dummy app used in our specs will give you a clear idea on how to configure the gem on your Rails application.
As mentioned in the introduction there are many configurations that will change how this gem behaves. You can change this values on the initializer files generated by the installer.
The generated initializer file config/initializers/devise_token_auth.rb has all the available options documented
as comments. You can also use
DTA's docs as a reference.
In this section the most important configurations will be highlighted.
- change_headers_on_each_request: This configurations defaults to
false. This will allow you to store the credentials for as long as the token life_span permits. And you can send the same credentials in each request. Setting this totruemeans that tokens will change on each request you make, and the new values will be returned in the headers. So your client needs to handle this. - batch_request_buffer_throttle: When change_headers_on_each_request is set to true, you might still want your credentials to be valid more than once as you might send parallel request. The duration you set here will determine how long the same credentials work after the first request is received.
- token_lifespan: This configuration takes a duration and you can set it to a value like
1.month,2.weeks,1.hour, etc.
Note: Remember this gem adds a layer on top of DTA, so some configurations might not apply.
The generated initializer file config/initializers/devise_token_auth.rb has all the available options documented
as comments. You can also use
Devise's docs as a reference.
In this section the most important configurations will be highlighted.
- password_length: You can change this value to validate password length on sign up and password update (must enable the validatable module).
- mailer_sender: Set it to a string with the sender's email address like
'[email protected]'. - case_insensitive_keys: Setting a value like
[:email]will make email field case insensitive on login, sign up, etc. - email_regexp: You can customize the regex that will validate the format of email addresses (must enable the validatable module).
Note: Remember this gem adds a layer on top of Devise, so some configurations might not apply.
GraphQL-Ruby >= 1.9.0 includes a new runtime module which you may use for your schema.
Eventually, it will become the default. You can read more about it
here.
This gem supports schemas using the interpreter and it is recommended as it introduces several improvements which focus mainly on performance.
The DeviseTokenAuth gem allows experimental use of the standard Devise gem to be configured at the same time, for more information you can check this answer here.
This gem supports the same and should be easier to handle email templates due to the fact we don't override standard Devise templates.
We will continue to improve the gem and add better docs.
- Make sure this gem can correctly work alongside DTA and the original Devise gem.
- Improve DOCS.
- Add support for unlockable and other Devise modules.
- Add feature specs for confirm account and reset password flows.
Bug reports and pull requests are welcome on GitHub at https://github.com/graphql-devise/graphql_devise.
The gem is available as open source under the terms of the MIT License.