Quickstart


What to expect

  • Fully functional API to handle user account
  • Both graphQL and Relay versions
  • Setup with custom user model
  • 20 to 30 minutes

Final code on github.


Start a new Django Project

It's recommended to use virtual env wrapper or virtualenv to create your project inside an isolated python environment. We will use the first.

Create the virtual env

mkvirtualenv graphql-auth-quickstart

Create the Django Project

First install django:

pip install django

Then, create the new project:

django-admin startproject quickstart
cd quickstart

Create the custom user model

Changing to a custom user model mid-project is significantly more difficult. So let's start by adding it now. Run the following:

python manage.py startapp users

Then, create the custom user model:

# users/models.py

from django.contrib.auth.models import AbstractUser

class CustomUser(AbstractUser):

    email = models.EmailField(blank=False, max_length=254, verbose_name="email address")

    USERNAME_FIELD = "username"   # e.g: "username", "email"
    EMAIL_FIELD = "email"         # e.g: "email", "primary_email"

Add it to the settings:

# quickstart/settings.py

INSTALLED_APPS = [
    # ...
    'users'
]

AUTH_USER_MODEL = 'users.CustomUser'

Finally, migrate:

python manage.py migrate

You can customize the mutations to match your custom user model fields, see the dynamic-fields settings.


Setup Graphene and GraphQL JWT

What is Graphene-Django?

Graphene-Django "make it easy to add GraphQL functionality to your Django project".

What is Django-GraphQL-JWT?

Django-GraphQL-JWT is the easiest way to add JSON Web token authentication for Django with GraphQL.

The following instructions are the shameless copy of the Graphene Django installation and the Django GraphQL JWT quickstart.

Installation

pip install graphene-django django-graphql-jwt==0.3.0

This package uses the 0.3.0 version of the django-graphql-jwt. We are working on to support the new 0.3.1 version. You can check the progress here.

Add the url

# quickstart.urls.py

from django.urls import path
from django.views.decorators.csrf import csrf_exempt

from graphene_django.views import GraphQLView

urlpatterns = [
    # ...
    path("graphql", csrf_exempt(GraphQLView.as_view(graphiql=True))),
]

Edit your settings

# quickstart.settings.py

INSTALLED_APPS = [
    # ...
    'django.contrib.staticfiles', # Required for GraphiQL
    'graphene_django',

    # refresh tokens are optional
    'graphql_jwt.refresh_token.apps.RefreshTokenConfig',
]

MIDDLEWARE = [
    # ...
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    # ...
]

GRAPHENE = {
    'SCHEMA': 'quickstart.schema.schema', # this file doesn't exist yet
    'MIDDLEWARE': [
        'graphql_jwt.middleware.JSONWebTokenMiddleware',
    ],
}

AUTHENTICATION_BACKENDS = [
    'graphql_jwt.backends.JSONWebTokenBackend',
    'django.contrib.auth.backends.ModelBackend',
]

GRAPHQL_JWT = {
    "JWT_VERIFY_EXPIRATION": True,

    # optional
    "JWT_LONG_RUNNING_REFRESH_TOKEN": True,
}

If you choose to use the refresh tokens, remember to migrate:

python manage.py migrate

Install Django-GraphQL-Auth

pip install django-graphql-auth
# settings.py

INSTALLED_APPS = [
   # ...
   "graphql_auth",
]

AUTHENTICATION_BACKENDS = [
    # remove this
    # "graphql_jwt.backends.JSONWebTokenBackend",

    # add this
    "graphql_auth.backends.GraphQLAuthBackend",

    # ...
]

Here is an explanation why we are adding this backend.

And make sure your templates configuration has the following:

TEMPLATES = [
    {
        # ...
        'APP_DIRS': True,
    },
]

Run:

python manage.py migrate

Query

Create the schema

Create a file called schema.py next to your settings.py with the following:

# quickstart.schema.py

import graphene

from graphql_auth.schema import UserQuery, MeQuery

class Query(UserQuery, MeQuery, graphene.ObjectType):
    pass

schema = graphene.Schema(query=Query)

Note: you can choose not to include UserQuery or MeQuery depending on your use case.

And add Django-Filter to the installed apps.

django-filter was automatically installed when you installed django-graphql-auth.

INSTALLED_APPS = [
    # ...
    'django_filters'
]

Load fixtures

Before starting to query, let's load some users on the database. Create a new file called users.json in the same directory as manage.py with the following:

Have a look on the fixtures, note that we are creating 4 users and 3 UserStatus. When creating a user, we create a relating UserStatus by default on post_save signal with the following fields:

verified=False
archived=False
secondary_email=None

You can access it on any user:

user.status.[verified | archived | secondary_email]
[
  {
      "model": "users.CustomUser",
      "pk": 1,
      "fields": {
          "password": "pbkdf2_sha256$180000$nFcBtiqGnWN9$hf58wNg77oT1BlNKRdATVVvBIa69+dz22fL1JKOKTaA=",
          "last_login": null,
          "is_superuser": false,
          "username": "user1",
          "first_name": "",
          "last_name": "",
          "email": "user1@email.com",
          "is_staff": false,
          "is_active": true
      }
  },
  {
      "model": "users.CustomUser",
      "pk": 2,
      "fields": {
          "password": "pbkdf2_sha256$180000$nFcBtiqGnWN9$hf58wNg77oT1BlNKRdATVVvBIa69+dz22fL1JKOKTaA=",
          "last_login": null,
          "is_superuser": false,
          "username": "user2",
          "first_name": "",
          "last_name": "",
          "email": "user2@email.com",
          "is_staff": false,
          "is_active": true
      }
  },
  {
      "model": "graphql_auth.userstatus",
      "pk": 2,
      "fields": {
          "user": 2,
          "verified": true,
          "archived": false,
          "secondary_email": null
      }
  },
  {
      "model": "users.CustomUser",
      "pk": 3,
      "fields": {
          "password": "pbkdf2_sha256$180000$nFcBtiqGnWN9$hf58wNg77oT1BlNKRdATVVvBIa69+dz22fL1JKOKTaA=",
          "last_login": null,
          "is_superuser": false,
          "username": "user3",
          "first_name": "",
          "last_name": "",
          "email": "user3@email.com",
          "is_staff": false,
          "is_active": true
      }
  },
  {
      "model": "graphql_auth.userstatus",
      "pk": 3,
      "fields": {
          "user": 3,
          "verified": true,
          "archived": true,
          "secondary_email": null
      }
  },
  {
      "model": "users.CustomUser",
      "pk": 4,
      "fields": {
          "password": "pbkdf2_sha256$180000$nFcBtiqGnWN9$hf58wNg77oT1BlNKRdATVVvBIa69+dz22fL1JKOKTaA=",
          "last_login": null,
          "is_superuser": false,
          "username": "user4",
          "first_name": "",
          "last_name": "",
          "email": "user4@email.com",
          "is_staff": false,
          "is_active": true
      }
  },
  {
      "model": "graphql_auth.userstatus",
      "pk": 4,
      "fields": {
          "user": 4,
          "verified": true,
          "archived": false,
          "secondary_email": "user4_secondary@email.com"
      }
  }
]

run:

python manage.py loaddata users.json

Making your first query

Start the dev server:

python manage.py runserver

Open your browser:

http://127.0.0.1:8000/graphql

This will open the GraphiQL API browser, where you can play with your queries and mutations, also let you explore the schema.

Copy the query below, paste on the GraphiQL interface and hit the play button.

query {
  users {
    edges {
      node {
        username,
        archived,
        verified,
        email,
        secondaryEmail,
      }
    }
  }
}
{
  "data": {
    "users": {
      "edges": [
        {
          "node": {
            "username": "user1",
            "archived": false,
            "verified": false,
            "email": "user1@email.com",
            "secondaryEmail": null
          }
        },
        {
          "node": {
            "username": "user2",
            "archived": false,
            "verified": true,
            "email": "user2@email.com",
            "secondaryEmail": null
          }
        },
        {
          "node": {
            "username": "user3",
            "archived": true,
            "verified": true,
            "email": "user3@email.com",
            "secondaryEmail": null
          }
        },
        {
          "node": {
            "username": "user4",
            "archived": false,
            "verified": true,
            "email": "user4@email.com",
            "secondaryEmail": "user4_secondary@email.com"
          }
        }
      ]
    }
  }
}

Note the edges and node. The UserQuery uses relay to enable the filtering of django-filter.

Query with filters

The UserQuery comes with some default filters:

query {
  users(status_Archived: true){
    edges {
      node {
        username,
        archived,
      }
    }
  }
}
{
  "data": {
    "users": {
      "edges": [
        {
          "node": {
            "username": "user3",
            "archived": true
          }
        }
      ]
    }
  }
}

Take a minute to explore the GraphiQL API browser and query schema on the right upper corner under docs tab.

MeQuery

With MeQuery you can retrieve data for the currently authenticated user:

query {
  me {
    username,
    verified
  }
}
{
  "data": {
    "user": {
      "username": "new_user",
      "verified": true
    }
  }
}

Since this query requires an authenticated user it can only be explored by using the insomnia API client. See the below for more on how to use Insomnia.


Setup Email Backend

The default configuration is to send activation email when registring users, you can set it to False on your settings, but you still need an Email Backend to password reset.

The quickest solution for development is to setup a Console Email Backend, simply add the following to your settings.py.

EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'

Now all emails are sent to the standard output, instead of an actual email and we are ready to continue this guide.


Mutations

Now let's add some mutations to our schema, starting with the registration. On the schema.py add the following:

Register

# quickstart.schema.py

import graphene

from graphql_auth.schema import UserQuery, MeQuery
from graphql_auth import mutations

class AuthMutation(graphene.ObjectType):
   register = mutations.Register.Field()

class Query(UserQuery, MeQuery, graphene.ObjectType):
    pass

class Mutation(AuthMutation, graphene.ObjectType):
   pass

schema = graphene.Schema(query=Query, mutation=Mutation)
# quickstart.schema.py

import graphene

from graphql_auth.schema import UserQuery, MeQuery
from graphql_auth import relay

class AuthMutation(graphene.ObjectType):
   register = relay.Register.Field()

class Query(UserQuery, MeQuery, graphene.ObjectType):
    pass

class Mutation(AuthMutation, graphene.ObjectType):
   pass

schema = graphene.Schema(query=Query, mutation=Mutation)

Take a minute to explore the schema on the documentation tab again.

On your GRAPHQL_JWT["JWT_ALLOW_ANY_CLASSES"] setting, add the following:

GRAPHQL_JWT = {
    #...
    "JWT_ALLOW_ANY_CLASSES": [
        "graphql_auth.mutations.Register",
    ],
}
GRAPHQL_JWT = {
    #...
    "JWT_ALLOW_ANY_CLASSES": [
        "graphql_auth.relay.Register",
    ],
}

Let's try to register a new user:

mutation {
  register(
    email: "new_user@email.com",
    username: "new_user",
    password1: "123456",
    password2: "123456",
  ) {
    success,
    errors,
    token,
    refreshToken
  }
}
{
  "data": {
    "register": {
      "success": false,
      "errors": {
        "password2": [
          {
            "message": "This password is too short. It must contain at least 8 characters.",
            "code": "password_too_short"
          },
          {
            "message": "This password is too common.",
            "code": "password_too_common"
          },
          {
            "message": "This password is entirely numeric.",
            "code": "password_entirely_numeric"
          }
        ]
      },
      "token": null,
      "refreshToken": null
    }
  }
}
mutation {
  register(
    input: {
      email: "new_user@email.com",
      username: "new_user",
      password1: "123456",
      password2: "123456",
    }
  ) {
    success,
    errors,
    token,
    refreshToken
  }
}

Something went wrong! Now you know the response format that you can expect of all mutations.

Let's try again:

mutation {
  register(
    email: "new_user@email.com",
    username: "new_user",
    password1: "supersecretpassword",
    password2: "supersecretpassword",
  ) {
    success,
    errors,
    token,
    refreshToken
  }
}
{
  "data": {
    "register": {
      "success": true,
      "errors": null,
      "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6Im5ld191c2VyMjIiLCJleHAiOjE1ODAxMzUyMTQsIm9yaWdJYXQiOjE1ODAxMzQ5MTR9.lzMjYo_1LO-TMDotySi1VHoC5yLyKr5PWC2l-hdzQ20",
      "refreshToken": "8db1c55b8dbc1f4a24eabe6f5d44dc091a8ca0f7"
    }
  }
}
mutation {
  register(
    input: {
      email: "new_user@email.com",
      username: "new_user",
      password1: "supersecretpassword",
      password2: "supersecretpassword",
    }
  ) {
    success,
    errors,
    token,
    refreshToken
  }
}

Check if the new user is really on the database:

query {
  users (last: 1){
    edges {
      node {
        id,
        username,
        email,
        isActive,
        archived,
        verified,
        secondaryEmail
      }
    }
  }
}
{
  "data": {
    "users": {
      "edges": [
        {
          "node": {
            "id": "VXNlck5vZGU6NQ==",
            "username": "new_user",
            "email": "new_user@email.com",
            "isActive": true,
            "archived": false,
            "verified": false,
            "secondaryEmail": null
          }
        }
      ]
    }
  }
}

There is actually a new user and it is possible to log in (you can change it on the settings), but it is not verified yet.

Save the id of the new user, so we can query it later.

Go to your console and note the email that has been sent. Should be two outputs, html and plain text formats.

Save the token from the url, something like this:

eyJ1c2VybmFtZSI6Im5ld191c2VyIiwiYWN0aW9uIjoiYWN0aXZhdGlvbiJ9:1isoSr:CDwK_fjBSxWj3adC-X16wqzv-Mw

Account Verification

Add the following to the AuthMutation:

# schema.py

class AuthMutation(graphene.ObjectType):
    register = mutations.Register.Field()
    verify_account = mutations.VerifyAccount.Field()
# schema.py

class AuthMutation(graphene.ObjectType):
    register = relay.Register.Field()
    verify_account = relay.VerifyAccount.Field()

Take a minute again to see the changes on your schema.

On your GRAPHQL_JWT["JWT_ALLOW_ANY_CLASSES"] setting, add the following:

GRAPHQL_JWT = {
    #...
    "JWT_ALLOW_ANY_CLASSES": [
        "graphql_auth.mutations.Register",
        "graphql_auth.mutations.VerifyAccount",
    ],
}
GRAPHQL_JWT = {
    #...
    "JWT_ALLOW_ANY_CLASSES": [
        "graphql_auth.relay.Register",
        "graphql_auth.relay.VerifyAccount",
    ],
}

Let's try to verify the account:

mutation {
  verifyAccount(token: "YOUR TOKEN HERE") {
    success,
    errors
  }
}
{
  "data": {
    "verifyAccount": {
      "success": true,
      "errors": null
    }
  }
}
mutation {
  verifyAccount(
    input: {
      token: "<YOUR TOKEN HERE>"
    }
  )
  {
    success,
    errors
  }
}

Check if the user is verified using the id that you have saved early:

query {
  user (id: "<USER ID>"){
    username,
    verified
  }
}
{
  "data": {
    "user": {
      "username": "new_user",
      "verified": true
    }
  }
}

Login

Add the following to the AuthMutation:

# schema.py

class AuthMutation(graphene.ObjectType):
    register = mutations.Register.Field()
    verify_account = mutations.VerifyAccount.Field()
    token_auth = mutations.ObtainJSONWebToken.Field()
# schema.py

class AuthMutation(graphene.ObjectType):
    register = relay.Register.Field()
    verify_account = relay.VerifyAccount.Field()
    token_auth = relay.ObtainJSONWebToken.Field()

And again, on your GRAPHQL_JWT["JWT_ALLOW_ANY_CLASSES"] setting, add the following:

GRAPHQL_JWT = {
    #...
    "JWT_ALLOW_ANY_CLASSES": [
        "graphql_auth.mutations.Register",
        "graphql_auth.mutations.VerifyAccount",
        "graphql_auth.mutations.ObtainJSONWebToken",
    ],
}
GRAPHQL_JWT = {
    #...
    "JWT_ALLOW_ANY_CLASSES": [
        "graphql_auth.relay.Register",
        "graphql_auth.relay.VerifyAccount",
        "graphql_auth.relay.ObtainJSONWebToken",
    ],
}

Let's try to login:

mutation {
  tokenAuth(username: "new_user", password: "supersecretpassword") {
    success,
    errors,
    unarchiving,
    token,
    refreshToken,
    unarchiving,
    user {
      id,
      username,
    }
  }
}
{
  "data": {
    "tokenAuth": {
      "success": true,
      "errors": null,
      "unarchiving": false,
      "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6Im5ld191c2VyIiwiZXhwIjoxNTc5ODk2Njc0LCJvcmlnSWF0IjoxNTc5ODk2Mzc0fQ.pNOkAWPyIanQWrKwvntQqf6asa8pkLuldW12N9nbfOo",
      "refreshToken": "fc1cf50178b7e7923e9580ff73920a04cfeaa9e7",
      "user": {
        "id": "VXNlck5vZGU6NQ==",
        "username": "new_user"
      }
    }
  }
}
mutation {
  tokenAuth(
    input: {
      username: "new_user",
      password: "supersecretpassword"
    }
  ) {
    success,
    errors,
    unarchiving,
    token,
    refreshToken,
    unarchiving,
    user {
      id,
      username,
    }
  }
}

Save this token, we are going to use it to do some protected actions.

The GraphiQL interface that comes with Graphene is great! But to try all features, we need to send this token on the header and the GraphiQL do not support this.


Insomnia API client

We are going to use insomnia API client to send request with authorization header. It is really easy to setup, simple follow the instructions on the site.

Install and come back here!


Update Account

This is the first mutation with login required that we are going to test.

Add the following to the AuthMutation:

# schema.py

class AuthMutation(graphene.ObjectType):
    register = mutations.Register.Field()
    verify_account = mutations.VerifyAccount.Field()
    token_auth = mutations.ObtainJSONWebToken.Field()
    update_account = mutations.UpdateAccount.Field()
# schema.py

class AuthMutation(graphene.ObjectType):
    register = relay.Register.Field()
    verify_account = relay.VerifyAccount.Field()
    token_auth = relay.ObtainJSONWebToken.Field()
    update_account = relay.UpdateAccount.Field()

On the insomnia, create a new request and call it updateAccount. Select the method POST.

On the top of the window, add your graphql url:

http://127.0.0.1:8000/graphql

For the body, select GraphQL Query. Now it works exaclty as the graphiQL.

On the headers pane, create a new header:

  • name: Authorization
  • value: JWT <TOKEN FROM THE LOGIN>

Make the query:

mutation {
  updateAccount(
    firstName: "Joe"
  ) {
    success,
    errors
  }
}
{
  "data": {
    "updateAccount": {
      "success": true,
      "errors": null
    }
  }
}
mutation {
  updateAccount(
    input: {
      firstName: "Joe"
    }
  ) {
    success,
    errors
  }
}

If it fails because of the token (in case you took some time and it has expired), make the login again and get a new token.

Check if it worked:

query {
  user (id: "<USER ID>"){
    username,
    firstName
  }
}
{
  "data": {
    "user": {
      "username": "new_user",
      "firstName": "Joe"
    }
  }
}

Next steps

Full schema

class AuthMutation(graphene.ObjectType):
    register = mutations.Register.Field()
    verify_account = mutations.VerifyAccount.Field()
    resend_activation_email = mutations.ResendActivationEmail.Field()
    send_password_reset_email = mutations.SendPasswordResetEmail.Field()
    password_reset = mutations.PasswordReset.Field()
    password_change = mutations.PasswordChange.Field()
    archive_account = mutations.ArchiveAccount.Field()
    delete_account = mutations.DeleteAccount.Field()
    update_account = mutations.UpdateAccount.Field()
    send_secondary_email_activation =  mutations.SendSecondaryEmailActivation.Field()
    verify_secondary_email = mutations.VerifySecondaryEmail.Field()
    swap_emails = mutations.SwapEmails.Field()

    # django-graphql-jwt inheritances
    token_auth = mutations.ObtainJSONWebToken.Field()
    verify_token = mutations.VerifyToken.Field()
    refresh_token = mutations.RefreshToken.Field()
    revoke_token = mutations.RevokeToken.Field()
class AuthMutation(graphene.ObjectType):
    register = relay.Register.Field()
    verify_account = relay.VerifyAccount.Field()
    resend_activation_email = relay.ResendActivationEmail.Field()
    send_password_reset_email = relay.SendPasswordResetEmail.Field()
    password_reset = relay.PasswordReset.Field()
    password_change = relay.PasswordChange.Field()
    archive_account = relay.ArchiveAccount.Field()
    delete_account = relay.DeleteAccount.Field()
    update_account = relay.UpdateAccount.Field()
    send_secondary_email_activation =  relay.SendSecondaryEmailActivation.Field()
    verify_secondary_email = relay.VerifySecondaryEmail.Field()
    swap_emails = relay.SwapEmails.Field()

    # django-graphql-jwt inheritances
    token_auth = relay.ObtainJSONWebToken.Field()
    verify_token = relay.VerifyToken.Field()
    refresh_token = relay.RefreshToken.Field()
    revoke_token = relay.RevokeToken.Field()

Full Allow Any Classes

GRAPHQL_JWT = {
    #...
    "JWT_ALLOW_ANY_CLASSES": [
        "graphql_auth.mutations.Register",
        "graphql_auth.mutations.VerifyAccount",
        "graphql_auth.mutations.ResendActivationEmail",
        "graphql_auth.mutations.SendPasswordResetEmail",
        "graphql_auth.mutations.PasswordReset",
        "graphql_auth.mutations.ObtainJSONWebToken",
        "graphql_auth.mutations.VerifyToken",
        "graphql_auth.mutations.RefreshToken",
        "graphql_auth.mutations.RevokeToken",
        "graphql_auth.mutations.VerifySecondaryEmail",
    ],
}
GRAPHQL_JWT = {
    #...
    "JWT_ALLOW_ANY_CLASSES": [
        "graphql_auth.relay.Register",
        "graphql_auth.relay.VerifyAccount",
        "graphql_auth.relay.ResendActivationEmail",
        "graphql_auth.relay.SendPasswordResetEmail",
        "graphql_auth.relay.PasswordReset",
        "graphql_auth.relay.ObtainJSONWebToken",
        "graphql_auth.relay.VerifyToken",
        "graphql_auth.relay.RefreshToken",
        "graphql_auth.relay.RevokeToken",
        "graphql_auth.relay.VerifySecondaryEmail",
    ],
}