This document explains steps to authenticate users by OAuth using Facebook as OAuth provider. When all steps in this document are completed, the entire repository will look like the repository at this point in the history [ github | gitlab ].
Before trying steps of this document, OAuth provider setting should be completed and tested. Read Setting Up Facebook Login to learn how to setup OAuth by Facebook.
This document focuses on ReactJS side and GraphQL. Only for this section,
Insomnia will be used to test a sign in sequence by GraphQL mutations.
So far, the docs have used GraphiQL
, which is an awesome IDE with nice code completion
and schema documentation. However, unfortunately, it doesn't provide a feature to set HTTP header.
The OAuth flow in this document is called a client side flow. The ReactJS (client) will
initiate the OAuth flow and get access_token
first. Then, a token based authentication
starts. The access_token
will be added to the HTTP request header,
Authorization: Bearer ACCESS_TOKEN
. The GraphQL controller on the Rails side gets the
access_token
and asks facebook to check who is making the request.
-
Install react-facebook-login package
Not like Rails side, the ReactJS side doesn't have a standard way to add OAuth feature. The 'react-facebook-login' package is an easy and relatively well used tool, so it was chosen here.
- Add
react-facebook-login
packageyarn add react-facebook-login
- Add
-
FacebookLoginButton
component-
Create a file
app/javascript/packs/components/FacebookLoginButton.jsx
The value of YOURAPPID is the APP ID we can see on facebook developer website. Read Setting Up Facebook Login for details.
import React, { Component } from 'react' import FacebookLogin from 'react-facebook-login' class FacebookLoginButton extends Component { responseFacebook = (response) => { console.log(response) window.localStorage.setItem('_textblog:access_token', response.accessToken) window.localStorage.setItem('_textblog:uid', response.id) window.localStorage.setItem('_textblog:name', response.name) window.localStorage.setItem('_textblog:email', response.email) } componentClicked = (e) => { console.log(e) } render() { return ( <FacebookLogin appId="YOURAPPID" autoLoad={true} fields="id,name,email" onClick={this.componentClicked} callback={this.responseFacebook} /> ) } } export default FacebookLoginButton
Above implementation is buggy, not nice one but works. For now, let's leave this naive code and focus on OAuth stuff.
-
-
Add
FacebookLoginButton
in the React app- Edit
app/javascript/packs/hello_react.jsx
.... import FacebookLoginButton from './components/FacebookLoginButton' .... <ApolloProvider client={client}> <div>Hello {props.name}!</div> <FacebookLoginButton/> <UserList/> </ApolloProvider> ....
- Edit
-
Test "Login With Facebook" Button
-
Make a HTTP request to
http://localhost:3000/
"Login With Facebook" Button shows up. -
Open JavaScript console and click the button The
responseFacebook
function prints the response in the JavaScript console. Open the JavaScript console and click the button. The response looks like: -
Check localStorage Some of the response parameters are saved in localStorage. Type
window.localStorage
in the JavaScript console to see those. -
Take memo the value of access_token The access_token will be used to test GrapQL mutations. Save the value.
-
-
Get the access token in GraphQL
This and following sections don't use the client side React app. Tests will be done on GraphQL IDE. GraphQL IDE is now switched to Insominia to set Authorization HTTP request header. Interaction will be done between
GraphQLController
and Insomina.-
Install
Koala
gemAfter the access token is pulled out from HTTP request header, the server app wants to verify who is making a request. To check this, the server should ask OAuth provider passing the access token.
The
Koala
gem is library to hit Facebook Graph API endpoint for this kind of jobs.Add gem in
Gemfile
gem 'koala', '~> 2.4'
bundle install
-
Create a helper class
To avoid controller code messy, also, to make it testable, the code to get the access token related will go to the helper.
rails g helper Graphql
Edit
app/helpers/graphql_helper.rb
module GraphqlHelper def get_api(request) auth_header = request.headers["authorization"] if auth_header values = auth_header.split(' ') access_token = values.length == 2 && values.first == 'Bearer' ? values.last : nil koala = access_token ? Koala::Facebook::API.new(access_token) : nil return {access_token: access_token, koala: koala} else return {access_token: nil, koala: nil} end end end
-
Add
get_api
method call inGraphqlController
Edit
app/controllers/graphql_controller.rb
and add two lines:include GraphqlHelper
andcontext[:api] = get_api(request)
class GraphqlController < ApplicationController include GraphqlHelper skip_before_action :verify_authenticity_token def execute variables = ensure_hash(params[:variables]) query = params[:query] operation_name = params[:operationName] context = { # Query context goes here, for example: # current_user: current_user, } context[:api] = get_api(request) result = TextblogSchema.execute(query, variables: variables, context: context, operation_name: operation_name) render json: result .... ....
-
-
Update GraphQL schemas
-
Add
signInUser
mutationA new mutation to sign-in user will be added here. Since queries don't need user authentication, the access token verification is performed in mutation's resolve method.
Firstly, correctness of authorization header is checked. Then, the resolver asks Facebook for a user information. If an error is raised, the error message will be returned as a GraphQL object. The error is returned for various reasons. For example, a user was logged out from Facebook, a session was expired, or wrong access token was given (mostly copy&paste error during testing).
Edit
app/graphql/types/mutation_type.rb
and add:signInUser
field.Types::MutationType = GraphQL::ObjectType.define do name "Mutation" field :signInUser, Types::UserType do resolve -> (obj, args, ctx) { access_token, koala = ctx[:api][:access_token], ctx[:api][:koala] if access_token.nil? raise GraphQL::ExecutionError.new('Authorization request header is missing.') end if koala.nil? raise GraphQL::ExecutionError.new('Authorization request header is invalid.') end begin me = koala.get_object('me', {'fields': 'id,name,email'}) # this line may raise error User.where(provider: 'facebook', uid: me['id']).first_or_create do |user| user.name = me['name'] user.email = me['email'] end rescue => e GraphQL::ExecutionError.new(e.message) end } end .... ....
-
Update
user_type
schemaWhile going over Adding OAuth,
provider
anduid
columns were added to User model. To make it consistent, add those in GraphQL schema.Edit
app/graphql/types/user_type.rb
and addprovider
anduid
fields.Types::UserType = GraphQL::ObjectType.define do name "User" field :id, !types.ID field :provider, !types.String field :uid, !types.ID field :name, !types.String field :email, !types.String end
-
-
Test GraphQL mutation on Insominia
-
Run Insominia
For the first time, Insominia show the window like this:
-
Create a new request
On OSX, hit command-n key or click + (plus) button on the left pane, then create a new request.
HTTP method to make GraphQL is always POST. This can be checked by typing
rails routes
.graphql POST /graphql(.:format)
-
Write URL and select GraphQL
Write POST URL,
http://localhost:3000/graphql
. Also, selectGraphQL
from Body dropdown. -
Select Bearer from Auth dropdown
-
Write a mutation
mutation { signInUser { id provider uid name email } }
-
Set valid/invalid access token in Bearer or leave it blank
In the section 4 of this document, the access token was shown in JavaScript console. This is the one to test. If the valid access token is set in Bearer and click "Send", a user information will be returned.
Additionally, if the user is not on the database, the user will be created. This can be checked by requesting http://localhost:3000/ or Rails console.
If Bearer is blank or has invalid token, the result is an error message.
-
For now, textblog app got an user authentication feature by GraphQL. Next topic is about Creating Top Page.