Skip to content

Latest commit

 

History

History
1005 lines (756 loc) · 36.3 KB

TOKEN.md

File metadata and controls

1005 lines (756 loc) · 36.3 KB

Token-Based Auth in React and Rails with JWT

This is a sample application and walks through one possible auth implementation. It does not cover everything there is to know about auth and is intended as an introduction. Please do not blindly copy/paste the code here. Use this as a guide for setting up auth in a React/Redux application using JSON Web Tokens.


Rails with BCrypt & JWT 🔐

Building Our Server

  • This section will walk through building a rails server. If you have questions about Cors, ActiveModel::Serializer, Postgres, namespacing and versioning our API, and/or general questions about Rails as an api only, refer to this guide.

  • Let's create our app:

rails new backend_project_name --api --database=postgresql
  • We're going to need a few gems in our Gemfile so let's go ahead and add them:
bundle add jwt active_model_serializers
  • If you get a gem not found error, try running gem install on each of these, or manually add them to your Gemfile.

  • Don't forget to uncomment rack-cors and bcrypt from your Gemfile.

  • Finally, run bundle install. Your Gemfile should look something like this:

source 'https://rubygems.org'
git_source(:github) { |repo| "https://github.com/#{repo}.git" }

ruby '2.5.1'

# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
gem 'rails', '~> 5.2.1'
# Use postgresql as the database for Active Record
gem 'pg', '>= 0.18', '< 2.0'
# Use Puma as the app server
gem 'puma', '~> 3.11'
# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
# gem 'jbuilder', '~> 2.5'
# Use Redis adapter to run Action Cable in production
# gem 'redis', '~> 4.0'
# Use ActiveModel has_secure_password
gem 'bcrypt', '~> 3.1.7'

# Use ActiveStorage variant
# gem 'mini_magick', '~> 4.8'

# Use Capistrano for deployment
# gem 'capistrano-rails', group: :development

# Reduces boot times through caching; required in config/boot.rb
gem 'bootsnap', '>= 1.1.0', require: false

# Use Rack CORS for handling Cross-Origin Resource Sharing (CORS), making cross-origin AJAX possible
gem 'rack-cors'


group :development, :test do
  # Call 'byebug' anywhere in the code to stop execution and get a debugger console
  gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
end

group :development do
  gem 'listen', '>= 3.0.5', '< 3.2'
  # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring
  gem 'spring'
  gem 'spring-watcher-listen', '~> 2.0.0'
end


# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]

gem "jwt", "~> 2.1"

gem "active_model_serializers", "~> 0.10.7"
  • Don't forget to enable CORS in your app. Uncomment the following in config/initializers/cors.rb. Don't forget to change the origins from example.com to *

  • Depending on the use-case and needs of our API, we might want to limit access to our app. For example, if our React frontend is deployed to myDankReactApp.com, we might want to limit access to that domain only. If certain endpoints are meant to be public, we can make those available but limit to GET requests, for example.

Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins '*'

    resource '*',
      headers: :any,
      methods: [:get, :post, :put, :patch, :delete, :options, :head]
  end
end
  • You can refer to the rack-cors gem for more information about this file.
  • Please don't forget to change these settings before deploying your app to the internet. Please

Creating Users

  • Run this to set up our User model:
rails g resource User username password_digest bio avatar
rails db:create db:migrate
class User < ApplicationRecord
  has_secure_password
end
  • You might also want to add some validations to your users:
class User < ApplicationRecord
  has_secure_password
  validates :username, uniqueness: { case_sensitive: false }
end

Quick BCrypt Tangent

  • Recall that BCrypt allows us to salt users' plaintext passwords before running them through a hashing function. A hashing function is, basically, a one way function. Similar to putting something in a meat grinder: we cannot feasibly reconstruct something that's been ground up by a meat grinder. We then store these passwords that have been 'digested' by BCrypt in our database. Never ever ever store your users' plaintext passwords in your database. It's bad form and should be avoided at all costs.

  • Let's take a look at some of the functionality provided by BCrypt:

# in rails console
> BCrypt::Password.create('P@ssw0rd')
 => "$2a$10$D0iXNNy/5r2YC5GC4ArGB.dNL6IpUzxH3WjCewb3FM8ciwsHBt0cq"
# in rails console
> salted_pw = BCrypt::Password.create('P@ssw0rd')
  => "$2a$10$YQvJPemUzm8IdCCaHxiOOes6HMEHda/.Hl60cUoYb4X4fncgT8ubG"

> salted_pw.class
  => BCrypt::Password

> salted_pw == 'P@ssw0rd'
  => true
  • BCrypt also provides a method that will take a stringified password_digest and turn it into an instance of BCrypt::Password, allowing us to call the over-written == method.
# in rails console
> sample_digest = User.last.password_digest
  => "$2a$10$SJiIJnmQJ/A4z4fFG5EuE.aOoCjacFuQMVpVzQnhPSJKYLFCoqmWy"

> sample_digest.class
  => String

> sample_digest == 'P@ssword'
 => false

> bcrypt_sample_digest = BCrypt::Password.new(sample_digest)
  => "$2a$10$dw4sYcbLXc8XRX6YGc7ve.ot6LbYevMbSpFQZUaa8tm5NI8cxBPwa"

> bcrypt_sample_digest.class
  => BCrypt::Password

> bcrypt_sample_digest == 'P@ssw0rd'
  => true

mind blown

  • We have no way of storing instances of BCrypt::Password in our database. Instead, we're storing users' password digests as strings. If we were to build our own User#authenticate method using BCrypt, it might look something like this:
class User < ApplicationRecord
  attr_accessor :password

  def authenticate(plaintext_password)
    if BCrypt::Password.new(self.password_digest) == plaintext_password
      self
    else
      false
    end
  end
end
# in rails console
> User.last.authenticate('not my password')
  => false

> User.last.authenticate('P@ssw0rd')
  => #<User id: 21, username: "sylviawoods", password_digest: "$2a$10$dw4sYcbLXc8XRX6YGc7ve.ot6LbYevMbSpFQZUaa8tm...", avatar: nil, created_at: "2018-08-31 02:11:15", updated_at: "2018-08-31 02:11:15", bio: "'Sylvia Woods was an American restaurateur who founded the sould food restaurant Sylvia's in Harlem on Lenox Avenue, New York City in 1962. She published two cookbooks and was an important figure in the community.">
class User < ApplicationRecord
  has_secure_password
end

salt bae

End of BCrypt Tangent


class Api::V1::UsersController < ApplicationController
  def create
    @user = User.create(user_params)
    if @user.valid?
      render json: { user: UserSerializer.new(@user) }, status: :created
    else
      render json: { error: 'failed to create user' }, status: :bad_request
    end
  end

  private
  def user_params
    params.require(:user).permit(:username, :password, :bio, :avatar)
  end
end
class UserSerializer < ActiveModel::Serializer
  attributes :username, :avatar, :bio
end

  • Next let's add the routes we'll need for our server. In config/routes.rb:
Rails.application.routes.draw do
  namespace :api do
    namespace :v1 do
      resources :users, only: [:create]
      post '/login', to: 'auth#create'
      get '/profile', to: 'users#profile'
    end
  end
end

  • Take some time to test this either in Postman or with JavaScript fetch:
fetch('http://localhost:3000/api/v1/users', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    Accept: 'application/json'
  },
  body: JSON.stringify({
    user: {
      username: "sylviawoods",
      password: "whatscooking",
      bio: "Sylvia Woods was an American restaurateur who founded the sould food restaurant Sylvia's in Harlem on Lenox Avenue, New York City in 1962. She published two cookbooks and was an important figure in the community.",
      avatar: "https://upload.wikimedia.org/wikipedia/commons/4/49/Syvia_of_Sylvia%27s_reaturant_N.Y.C_%28cropped%29.jpg"
    }
  })
})
  .then(r => r.json())
  .then(console.log)

Note: if you're using Postman and your formatting is set to "raw and JSON", remember to use double quotes ("") in both keys and values in the request.


Make Sure You Can POST and Create a New User Before Proceeding

intermission


JSON Web Tokens (JWT)

  • Token-based authentication is stateless. We are not storing any information about a logged in user on the server (which also means we don't need a model or table for our user sessions). No stored information means our application can scale and add more machines as necessary without worrying about where a user is logged in. Instead, the client (browser) stores a token and sends that token along with every authenticated request. Instead of storing a plaintext username, or user_id, we can encode user data with JSON Web Tokens (JWT) and store that encoded token client-side.

JWT Auth Flow:

  • Here is the JWT authentication flow for logging in:
    1. An already existing user requests access with their username and password
    2. The app validates these credentials
    3. The app gives a signed token to the client
    4. The client stores the token and presents it with every request. This token is effectively the user's access pass––it proves to our server that they are who they claim to be.
  • JWTs are composed of three strings separated by periods:

    aaaaaaaaaaaaaaa.bbbbbbbbbbbbbbbbbbbbb.ccccccccccccccccccc
    
    • The first part (aaaaaaaaaaaa) is the header

    • The second part (bbbbbbbbbbbb) is the payload - the good stuff, like who this person is, and their id in our database.

    • The third part (ccccccccccccc) is the signature. The signature is a hash of the header and the payload. It is hashed with a secret key, that we will provide (and should store in an environment variable using a gem like Figaro)

    • Head on over to jwt.io and see for yourself:

    JWTs

Encoding and Decoding JWTs

  • Since we've already added gem jwt to our gemfile, let's explore some JWT methods by opening a rails console
    • JWT.encode takes up to three arguments: a payload to encode, an application secret of the user's choice, and an optional third that can be used to specify the hashing algorithm used. Typically, we don't need to show the third. This method returns a JWT as a string.
    • JWT.decode takes three arguments as well: a JWT as a string, an application secret, and––optionally––a hashing algorithm.
#in rails console
>  payload = { beef: 'steak' }

> jwt = JWT.encode(payload, 'boeuf')
=> "eyJhbGciOiJIUzI1NiJ9.eyJiZWVmIjoic3RlYWsifQ._IBTHTLGX35ZJWTCcY30tLmwU9arwdpNVxtVU0NpAuI"

> decoded_hash = JWT.decode(jwt, 'boeuf')
=> [{"beef"=>"steak"}, {"alg"=>"HS256"}]

> data = decoded_hash[0]
=> {"beef"=>"steak"}

Building this functionality into our ApplicationController:

class ApplicationController < ActionController::API
  def encode_token(payload)
    # payload => { beef: 'steak' }
    JWT.encode(payload, 'my_s3cr3t')
    # jwt string: "eyJhbGciOiJIUzI1NiJ9.eyJiZWVmIjoic3RlYWsifQ._IBTHTLGX35ZJWTCcY30tLmwU9arwdpNVxtVU0NpAuI"
  end

  def decoded_token(token)
    # token => "eyJhbGciOiJIUzI1NiJ9.eyJiZWVmIjoic3RlYWsifQ._IBTHTLGX35ZJWTCcY30tLmwU9arwdpNVxtVU0NpAuI"

    JWT.decode(token, 'my_s3cr3t')[0]
    # JWT.decode => [{ "beef"=>"steak" }, { "alg"=>"HS256" }]
    # [0] gives us the payload { "beef"=>"steak" }
  end
end

  • According to the JWT Documentation: Whenever the user wants to access a protected route or resource, the user agent (browser in our case) should send the JWT, typically in the Authorization header using the Bearer schema. The content of the header should look like the following:

    Authorization: Bearer <token>


  • The corresponding fetch request might look like this:
fetch('http://localhost:3000/api/v1/profile', {
  method: 'GET',
  headers: {
    Authorization: `Bearer <token>`
  }
})

  • Knowing this, we can set up our server to anticipate a JWT sent along in request headers, instead of passing the token directly to ApplicationController#decoded_token:
class ApplicationController < ActionController::API
  def encode_token(payload)
    # payload => { beef: 'steak' }
    JWT.encode(payload, 'my_s3cr3t')
    # jwt string: "eyJhbGciOiJIUzI1NiJ9.eyJiZWVmIjoic3RlYWsifQ._IBTHTLGX35ZJWTCcY30tLmwU9arwdpNVxtVU0NpAuI"
  end

  def auth_header
    # { 'Authorization': 'Bearer <token>' }
    request.headers['Authorization']
  end

  def decoded_token
    if auth_header
      token = auth_header.split(' ')[1]
      # headers: { 'Authorization': 'Bearer <token>' }
      begin
        JWT.decode(token, 'my_s3cr3t', true, algorithm: 'HS256')
        # JWT.decode => [{ "beef"=>"steak" }, { "alg"=>"HS256" }]
      rescue JWT::DecodeError
        nil
      end
    end
  end

  • A few things to note about the code above:
    • The Begin/Rescue syntax allows us to rescue out of an exception in Ruby. Let's see an example in a rails console. In the event our server receives and attempts to decode an invalid token:
# in rails console
> invalid_token = "nnnnnnnooooooootttttt.vvvvvvaaaallliiiiidddddd.jjjjjjjwwwwwttttttt"

> JWT.decode(invalid_token, 'my_s3cr3t', true, algorithm: 'HS256')

Traceback (most recent call last):
        1: from (irb):6
JWT::DecodeError (Invalid segment encoding)
  • In other words, if our server receives a bad token, this will raise an exception causing a 500 Internal Server Error. We can account for this by rescuing out of this exception:
# in rails console
> invalid_token = "nnnnnnnooooooootttttt.vvvvvvaaaallliiiiidddddd.jjjjjjjwwwwwttttttt"

> begin JWT.decode(invalid_token, 'my_s3cr3t', true, algorithm: 'HS256')
  rescue JWT::DecodeError
    nil
>  end
 => nil
  • Instead of crashing our server, we simply return nil and keep trucking along.

keep trucking


  • We can then complete our ApplicationController by automatically obtaining the user whenever an authorization header is present:
class ApplicationController < ActionController::API

  def encode_token(payload)
    # don't forget to hide your secret in an environment variable
    JWT.encode(payload, 'my_s3cr3t')
  end

  def auth_header
    request.headers['Authorization']
  end

  def decoded_token
    if auth_header
      token = auth_header.split(' ')[1]
      begin
        JWT.decode(token, 'my_s3cr3t', true, algorithm: 'HS256')
      rescue JWT::DecodeError
        nil
      end
    end
  end

  def current_user
    if decoded_token
      # decoded_token=> [{"user_id"=>2}, {"alg"=>"HS256"}]
      # or nil if we can't decode the token
      user_id = decoded_token[0]['user_id']
      @user = User.find_by(id: user_id)
    end
  end

  def logged_in?
    !!current_user
  end
end
  • Recall that a Ruby object/instance is 'truthy': !!user_instance #=> true and nil is 'falsey': !!nil #=> false. Therefore logged_in? will just return a boolean depending on what our current_user method returns.

  • Finally, let's lock down our application to prevent unauthorized access:
class ApplicationController < ActionController::API
  before_action :authorized

  def encode_token(payload)
    # should store secret in env variable
    JWT.encode(payload, 'my_s3cr3t')
  end

  def auth_header
    # { Authorization: 'Bearer <token>' }
    request.headers['Authorization']
  end

  def decoded_token
    if auth_header
      token = auth_header.split(' ')[1]
      # header: { 'Authorization': 'Bearer <token>' }
      begin
        JWT.decode(token, 'my_s3cr3t', true, algorithm: 'HS256')
      rescue JWT::DecodeError
        nil
      end
    end
  end

  def current_user
    if decoded_token
      user_id = decoded_token[0]['user_id']
      @user = User.find_by(id: user_id)
    end
  end

  def logged_in?
    !!current_user
  end

  def authorized
    render json: { message: 'Please log in' }, status: :unauthorized unless logged_in?
  end
end
  • A few things to note about the code above:
    • before_action :authorized will call the authorized method before anything else happens in our app. This will effectively lock down the entire application. Next we'll augment our UsersController and build our AuthController to allow signup/login.

Updating the UsersController

  • Let's update the UsersController so that it issues a token when users register for our app:
class Api::V1::UsersController < ApplicationController
  skip_before_action :authorized, only: [:create]

  def create
    @user = User.create(user_params)
    if @user.valid?
      @token = encode_token(user_id: @user.id)
      render json: { user: UserSerializer.new(@user), jwt: @token }, status: :created
    else
      render json: { error: 'failed to create user' }, status: :not_acceptable
    end
  end

  private

  def user_params
    params.require(:user).permit(:username, :password, :bio, :avatar)
  end
end
class Api::V1::UsersController < ApplicationController
  skip_before_action :authorized, only: [:create]
end
  • It wouldn't make sense to ask our users to be logged in before they create an account. This circular logic will make it impossible for users to authenticate into the app. How can a user create an account if our app asks them to be logged in or authorized to do so? Skipping the before action 'unlocks' this portion of our app.

omg

  • Try creating a new user again with either postman or fetch and confirm that your server successfully issues a token on signup.

sign me up gif


Implementing Login

  • A token should be issued in two different controller actions: UsersController#create and AuthController#create. Think about what these methods are responsible for––a user signing up for our app for the first time and an already existing user logging back in. In both cases, our server needs to issue a new token🥇.

  • We'll need to create a new controller to handle login: rails g controller api/v1/auth. Next, let's add the following to this newly created AuthController:

class Api::V1::AuthController < ApplicationController
  skip_before_action :authorized, only: [:create]

  def create
    @user = User.find_by(username: user_login_params[:username])
    #User#authenticate comes from BCrypt
    if @user && @user.authenticate(user_login_params[:password])
      # encode token comes from ApplicationController
      token = encode_token({ user_id: @user.id })
      render json: { user: UserSerializer.new(@user), jwt: token }, status: :accepted
    else
      render json: { message: 'Invalid username or password' }, status: :unauthorized
    end
  end

  private

  def user_login_params
    # params { user: {username: 'Chandler Bing', password: 'hi' } }
    params.require(:user).permit(:username, :password)
  end
end
  • We can simply call our ApplicationController#encode_token method, passing the found user's ID in a payload. The newly created JWT can then be passed back along with the user's data. The user data can be stored in our application's state, e.g., React or Redux, while the token can be stored client-side.

  • A few things to keep in mind about the code above:

    • User.find_by({ name: 'Chandler Bing' }) will either return a user instance if that user can be found OR it will return nil if that user is not found.
    • In the event that the user is not found, user = User.find_by(username: params[:username]) will evaluate to nil.
    • Can we call .authenticate on nil? NO!! NoMethodError (undefined method 'authenticate' for nil:NilClass)
    • Ruby, however, is lazy. If Ruby encounters &&, both statements in the expression must evaluate to true. If the statement on the left side evaluates to false, Ruby will not even look at the statement on the right. Let's see an example:
# in irb or a rails console
> true && true
  => true

> true && false
  => false


> true && not_a_variable
  NameError (undefined local variable or method `not_a_variable` for main:Object)

> false && not_a_variable
  => false
  • Let's take another look at our previous example:
@user = User.find_by(username: params[:username])
if @user && @user.authenticate(params[:password])
end
  • If @user is nil, which is falsey, ruby will not even attempt to call @user.authenticate. Without this catch, we'd get a NoMethodError (undefined method 'authenticate' for nil:NilClass).

  • Again, the client should be sending a JWT along with every authenticated request. Refer to this diagram from scotch.io:

scotch.io article on token auth

  • A sample request might look like:
fetch('http://localhost:3000/api/v1/profile', {
  method: 'GET',
  headers: {
    Authorization: `Bearer <token>`
  }
})
  • So, let's update our UsersController so that an authenticated user can access their profile information:
class Api::V1::UsersController < ApplicationController
  skip_before_action :authorized, only: [:create]

  def profile
    render json: { user: UserSerializer.new(current_user) }, status: :accepted
  end

  def create
    @user = User.create(user_params)
    if @user.valid?
      @token = encode_token({ user_id: @user.id })
      render json: { user: UserSerializer.new(@user), jwt: @token }, status: :created
    else
      render json: { error: 'failed to create user' }, status: :not_acceptable
    end
  end

  private

  def user_params
    params.require(:user).permit(:username, :password, :bio, :avatar)
  end
end
  • One final note about the snippet above: ApplicationController calls authorized before any other controller methods are called. If authorization fails, our server will never call UsersController#profile and will instead:
render json: { message: 'Please log in' }, status: :unauthorized

That's It For the Server!


External Resources

Bonus: Google Sign In

Resources:

Google Setup

Follow the steps to create your authorization credentials (just up to step 4 under "Create authorization credentials" - skip the sections below that):

https://developers.google.com/identity/sign-in/web/sign-in

Take note of the client ID - you'll need that later for React and Rails.

Frontend Setup

First let's save the client ID in a .env file so we can access that later. In the root of your React application, create a file called .env and add your Google client ID, like so:

REACT_APP_GOOGLE_CLIENT_ID=739034625712-ads90ik8978gyahbbdf7823asd8213as.apps.googleusercontent.com

Next, install this package:

npm install react-google-login

We'll use this to display a Google sign in button and handle logic for authenticating the user with Google. Update the <Login> component like this:

import React from 'react'
import { GoogleLogin } from 'react-google-login';

class Login extends React.Component {
  state = {
    username: "",
    password: ""
  }

  // new code!
  handleGoogleLogin = (response) => {
    // we'll get a tokenId back from Google on successful login that we'll send to our server to find/create a user
    if (response.tokenId) {
      fetch("http://localhost:3000/google_login", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          "Authorization": `Bearer ${response.tokenId}`
        }
      })
      .then(r => r.json())
      .then(data => {
        console.log(data)
        const { user, token } = data
        // then set that user in state in our App component
        this.props.handleLogin(user)
        // also save the id to localStorage
        localStorage.token = token
      })
    }
  }

  // old code
  handleChange = e => {
    this.setState({ [e.target.name]: e.target.value })
  }

  // old code
  handleSubmit = e => {
    e.preventDefault()
    fetch("http://localhost:3000/login", {
      method: "POST",
      headers: {
        "Content-Type": "application/json"
      },
      body: JSON.stringify(this.state)
    })
      .then(r => r.json())
      .then(data => {
        console.log(data)
        const { user, token } = data
        // then set that user in state in our App component
        this.props.handleLogin(user)
        // also save the id to localStorage
        localStorage.token = token
      })
  }

  render() {
    return (
      <div>
        <form onSubmit={this.handleSubmit}>
          <h1>Login</h1>
          <label>Username</label>
          <input type="text" name="username" autoComplete="off" value={this.state.username} onChange={this.handleChange} />
          <label>Password</label>
          <input type="password" name="password" value={this.state.password} onChange={this.handleChange} autoComplete="current-password" />
          <input type="submit" value="Login" />
        </form>
        <hr />
        <div>
          {/* this is the new component that will help with Google sign in */}
          <GoogleLogin
            clientId={process.env.REACT_APP_GOOGLE_OAUTH_CLIENT_ID}
            buttonText="Login"
            onSuccess={this.handleGoogleLogin}
            onFailure={this.handleGoogleLogin}
            cookiePolicy={'single_host_origin'}
          />
        </div>
      </div>
    )
  }
}

export default Login

That's it for the frontend! The backend will take more work to set up.

Backend Setup

First, we'll need to install a couple gems:

bundle add google-id-token
bundle add dotenv-rails

Next, create a .env file in the root of your project directory and add the Google client ID:

GOOGLE_OAUTH_CLIENT_ID=739034625712-ads90ik8978gyahbbdf7823asd8213as.apps.googleusercontent.com

You should also update your .gitignore file so that your .env file isn't checked into Github:

# add this at the bottom of the file
# .env files
.env*

Next, add a route for handling the Google login request:

# config/routes.rb
post "/google_login", to: "users#google_login"

Then, update your UserController to handle this request:

# app/controllers/user_controller.rb
class UsersController < ApplicationController
  # don't run authorize before google_login, remember - authorized should only run for methods where we expect the user is *already* logged in
  skip_before_action :authorized, only: [:create, :login, :google_login]

  # other methods here...

  def google_login
    # use a helper method to extract the payload from the google token
    payload = get_google_token_payload
    if payload
      # find/create user from payload (this will be a new method in the User model)
      user = User.from_google_signin(payload)

      if user
        # save user_id in token so we can use it in future requests
        token = encode_token({ user_id: user.id })

        # send token and user in response
        render json: { user: UserSerializer.new(user), token: token }
        return
      end
    end
    
    # for invalid requests, send error messages to the front end
    render json: { message: "Could not log in" }, status: :unauthorized
  end

  private

  # helper function to validate the user's token from Google and extract their info
  def get_google_token_payload
    if request.headers["Authorization"]
      # extract the token from the Authorization header
      token_id = request.headers["Authorization"].split(" ")[1]

      # this is the code from the Google auth gem
      validator = GoogleIDToken::Validator.new
      begin

        # check the token_id and return the payload
        # make sure your .env file has a matching key
        validator.check(token_id, ENV["GOOGLE_OAUTH_CLIENT_ID"])
      rescue GoogleIDToken::ValidationError => e
        p "Cannot validate: #{e}"
      end
    end
  end

We'll also add a helper method for creating a new user from the Google payload:

# app/models/user.rb
class User < ApplicationRecord
  has_secure_password

  validates :username, presence: true, uniqueness: { case_sensitive: false }

  def self.from_google_signin(payload)
    # find or create a user based on the email address from the Google payload
    User.where(username: payload["email"]).first_or_create do |new_user|
      new_user.username = payload["email"]
      new_user.avatar = payload["picture"]
      # we need to assign a password to satisfy bcrypt, so generate a random one...
      new_user.password = SecureRandom.base64(15)
    end
  end
  
end

Now, test it out! You should be able to login with your Google account and create a new User instance in the backend with that information.