Quick introduction
Check these links first:
What is JWT?
https://en.wikipedia.org/wiki/JSON_Web_Token
What is GraphQL?
https://graphql.org/learn/
Let’s say you are working on some GraphQL based API, you already have devise and you want to implement JWT authentication.
As usual we can start from some gems.
Adding gems to your project
gem 'devise'
gem 'devise-token_authenticatable'
Next, we should define UserType with some basic fields. Let’s add authentication_token field and authentication_token method with special check for current user in context.
module Types
class UserType < Types::BaseObject
graphql_name "User"
implements GraphQL::Types::Relay::Node
global_id_field :id
field :phone, String, null: false
delegate :phone, to: :object
field :lastName, String, null: false
delegate :last_name, to: :object
field :firstName, String, null: false
delegate :first_name, to: :object
field :email, String, null: false
field :authentication_token, String, null: false
def authentication_token
if object.gql_id != context[:current_user]&.gql_id
raise GraphQL::UnauthorizedFieldError,
"Unable to access authentication_token"
end
object.authentication_token
end
end
end
Next step - migration, let’s add fields and index for Devise::TokenAuthenticatable gem.
class DeviseCreateUsers < ActiveRecord::Migration[6.0]
def change
create_table :users do |t|
# Custom
t.string :first_name, null: false
t.string :last_name, null: false
t.string :phone, null: false
## Database authenticatable
t.string :email, null: false, default: ""
t.string :encrypted_password, null: false, default: ""
## Recoverable
t.string :reset_password_token
t.datetime :reset_password_sent_at
## Rememberable
t.datetime :remember_created_at
## Trackable
t.integer :sign_in_count, default: 0, null: false
t.datetime :current_sign_in_at
t.datetime :last_sign_in_at
t.inet :current_sign_in_ip
t.inet :last_sign_in_ip
t.text :authentication_token
t.datetime :authentication_token_created_at
t.timestamps null: false
end
add_index :users, :email, unique: true
add_index :users, :reset_password_token, unique: true
add_index :users, :authentication_token, unique: true
add_index :users, :phone, unique: true
end
end
Frontend app
Now, we can implement token related work on our Vue app. SignIn component with call to Apollo client and set token to localStorage in your browser.
<script>
import Alerts from '@/components/shared/alerts';
import _get from 'lodash/get';
import signIn from '@/mutations/signIn';
import { AUTH_TOKEN_KEY } from '@/configuration/appConstants';
import { mapMutations } from 'vuex';
export default {
name: 'SignIn',
components: { Alerts },
data() {
return {
errors: [],
form: {},
};
},
methods: {
...mapMutations(['signIn']),
handleSignIn() {
signIn({
apollo: this.$apollo,
...this.form,
}).then(response => _get(response, 'data.signIn', {}))
.then(response => {
if(response.success) {
const user = response.user;
this.signIn(user);
localStorage.setItem(AUTH_TOKEN_KEY, user.authenticationToken);
this.$router.push({ name: 'home' });
} else {
this.errors = this.errorMessages(response.data.signIn.errors);
}
}).catch(error => {
this.errors = [error];
});
},
},
};
</script>
Same thing here SignUp client, and setting AUTH_TOKEN_KEY with authenticationToken token.
<script>
import Alerts from '../../shared/alerts';
import _get from 'lodash/get';
import signUp from '@/mutations/registerUser';
import { AUTH_TOKEN_KEY } from '@/configuration/appConstants';
import { mapMutations } from 'vuex';
export default {
name: 'SignUp',
components: { Alerts },
data() {
return {
errors: [],
form: {},
};
},
methods: {
...mapMutations(['signIn']),
handleSignUp() {
signUp({
apollo: this.$apollo,
...this.form,
}).then(response => _get(response, 'data.registerUser', {}))
.then(response => {
if(response.success) {
const user = response.user;
this.signIn(user); // using the Vuex store
localStorage.setItem(AUTH_TOKEN_KEY, user.authenticationToken);
this.$router.push({ name: 'home' });
} else {
this.errors = this.errorMessages(response.data.registerUser.errors);
}
}).catch(error => {
this.errors = [error];
});
},
},
};
</script>
Let’s create a SignIn mutation which receive email and password as params.
import gql from 'graphql-tag';
const mutation = gql`
mutation signIn($email: String!, $password: String!) {
signIn(input: { email: $email, password: $password }) {
user {
id
firstName
lastName
authenticationToken
}
success
errors
}
}
`;
export default function signIn({
apollo,
email,
password,
}) {
return apollo.mutate({
mutation,
variables: {
email,
password,
},
});
}
And same thing for registerUser mutation, with user registration params.
import gql from 'graphql-tag';
const mutation = gql`
mutation registerUser(
$phone: String!,
$firstName: String!,
$lastName: String!,
$email: String!,
$password: String!,
) {
registerUser(input: {
phone: $phone,
firstName: $firstName,
lastName: $lastName,
email: $email,
password: $password,
}) {
user {
id
phone
firstName
lastName
email
authenticationToken
}
success
errors
}
}
`;
export default function({
apollo,
phone,
firstName,
lastName,
email,
password,
}) {
return apollo.mutate({
mutation,
variables: {
phone,
firstName,
lastName,
email,
password,
},
});
}
Backend
Okay, next steps on backend side (Rails) and CORS configuration, simple thing and nothing special.
Rails.application.config.middleware.insert_before 0, Rack::Cors do
allow do
origins 'http://localhost:8080'
resource '*',
headers: :any,
expose: ['access-token', 'expiry', 'token-type', 'Authorization'],
credentials: true,
methods: [:get, :post, :put, :patch, :delete, :options, :head]
end
end
SignOut mutation. On this step - just check for current_user and return MutationResult.
module Mutations
class SignOut < Mutations::BaseMutation
graphql_name "SignOut"
field :user, Types::UserType, null: false
def resolve
user = context[:current_user]
if user.present?
success = user.reset_authentication_token!
MutationResult.call(
obj: { user: user },
success: success,
errors: user.errors
)
else
GraphQL::ExecutionError.new("User not signed in")
end
end
end
end
RegisterUser mutation - set current_user in a context and return MutationResult.
module Mutations
class RegisterUser < Mutations::BaseMutation
graphql_name "RegisterUser"
argument :phone, String, required: true
argument :first_name, String, required: true
argument :last_name, String, required: true
argument :email, String, required: true
argument :password, String, required: true
field :user, Types::UserType, null: false
def resolve(args)
user = User.create!(args)
context[:current_user] = user
MutationResult.call(
obj: { user: user },
success: user.persisted?,
errors: user.errors
)
rescue ActiveRecord::RecordInvalid => invalid
GraphQL::ExecutionError.new(
"Invalid Attributes for #{invalid.record.class.name}: " \
"#{invalid.record.errors.full_messages.join(', ')}"
)
end
end
end
Done! Now we have working user login/register/logout workflow based on GraphQL and JWT.