21. How to achieve simple and reliable authentication?

Follow part 2, which shows how you connect your React/Javascript front end to endpoints covered here.

First of all, log into your AWS account and navigate to Cognito: https://console.aws.amazon.com/cognito/home

You should be greeted with something similar to this:

For standard authentication, you will want to click ‘Manage User Pools’ and then ‘Create new user pool.

You will then want to fill in some basic details for your user pool:

I will click ‘Step through settings’ in this guide.

I will allow users to sign in using username or email, therefore I check the box for Also allow sign in with verified email address.

For my app, I would like to have the following:

  • username
  • email
  • password
  • phone number
  • picture
  • zone info
  • website

I will not need some of these fields until later, but I may as well setup the congnito up to support them now.

The next few sections I will keep all as default, however I will need to create an app client which will be able to access this user pool

As you can see, I set the refresh token to max allowed. For the time being, my app will not have an automated strategy to fetch this refresh token, so I don’t want to risk putting a small value in here.

Note to disable the Generate client secret. Perhaps you will have it working with the library but at the time of writing it did not seem to be fully supported by the libraries I was using. Here’s a sample StackOverflow answer which suggests to disable it.

After I complete some base functionality and have resources to automate this, I will update it to something smaller.

I will also enable all auth flows:

Once complete, you will be able to review your user pool, then you will be able to check your client details

Here you will need to take note of the client ID and client secret.

For Rails backend, we will use 2 gems to integrate with:

gem 'aws-sdk-cognitoidentityprovider'

You can find a little more detail about them on

https://docs.aws.amazon.com/sdk-for-ruby/v2/api/Aws/CognitoIdentityProvider/Client.html

https://github.com/aws/aws-sdk-ruby,

I was looking to find a guide on using these and came across: https://medium.com/@oscarreciogonzalez/integrate-rails-and-aws-cognito-tutorial-21cc38084055

This documents a lot of what we’re doing here.

Do note that to use the Cognito, we will also need to add some permissions, so go to IAM dashboard in AWS and go to ‘Groups’

Here you will want to ‘Create New Group’ and you can call it ‘CognitoAccess’ like me.

I attached these policies which seemed to have done the trick:

Then just add your AWS user to this group and it should be setup for use.

Now for coding, I created similar code to one in the guide with some differences, first off I create the model to represent Cognito Client: app/models/Cognito/aws_cognito.rb

# frozen_string_literal: true

module Cognito
  class AwsCognito
    attr_reader :client

    def initialize
      @client = @client || Aws::CognitoIdentityProvider::Client.new(region: ENV["AWS_REGION"])  
    end

    def fetch_user(token)
      client.get_user({access_token: token})
    end
    
    def authenticate(user_params)
      user_object = {}
      user_object[:USERNAME] = user_params[:username] if user_params[:username]
      user_object[:EMAIL] = user_params[:email]       if user_params[:email]
      user_object[:PASSWORD] = user_params[:password] if user_params[:password]

      auth_object = {
        user_pool_id: ENV['AWS_COGNITO_POOL_ID'],
        client_id: ENV['AWS_COGNITO_APP_CLIENT_ID'],
        auth_flow: 'ADMIN_NO_SRP_AUTH',
        auth_parameters: user_object
      }

      client.admin_initiate_auth(auth_object)
    end

    def sign_out(access_token)
      client.global_sign_out(access_token: access_token)
    end
    
    def register_user(user_params)
      auth_object = {
        client_id: ENV['AWS_COGNITO_APP_CLIENT_ID'],
        username: user_params[:username] || "",
        password: user_params[:password] || "",
        user_attributes: [
          {
            name: "picture",
            value: "",
          },
          {
            name: "website",
            value: "",
          },
          {
            name: "zoneinfo",
            value: "",
          },
          {
            name: "phone_number",
            value: "",
          },
          {
            name: "email",
            value: user_params[:email] || "",
          },
        ],
      }

      client.sign_up(auth_object)
    end

    def update_user(update_attr) 
      client.update_user_attributes(update_attr)
    end
  end
end

I then create the auth controller: app/controllers/api/v1/auth_controller.rb

# frozen_string_literal: true

module Api
  module V1
    class AuthController < ActionController::API

      def sign_in
        begin
          render json: aws_congito_client.authenticate(user_params).authentication_result.to_hash
        rescue => e
          render status: :bad_request, json: {error: e.to_s}
        end
      end

      def fetch_current_user
        begin
          render json: aws_congito_client.fetch_user(request.headers['Authentication']).to_hash
        rescue => e
          render status: :unauthorized, json: {error: e.to_s}
        end 
      end

      def sign_out
        render json: aws_congito_client.sign_out(request.headers['Authentication']).to_hash
      end

      def register
        begin
          render json: aws_congito_client.register_user(user_params).to_hash
        rescue => e
          render status: :bad_request, json: {error: e.to_s}
        end
      end
      
      private

      def aws_congito_client
        @aws_congito_client ||= Cognito::AwsCognito.new
      end

      def user_params
        params.require(:user).permit(:email, :username, :password)
      end
    end
  end
end

and I register the routes:

namespace :api, defaults: { format: :json } do
    namespace :v1 do
      scope :public do
        post 'sign_in', to: 'auth#sign_in'
        post 'sign_out', to: 'auth#sign_out'
        post 'register', to: 'auth#register'
        get  'current-user', to: 'auth#fetch_current_user'
    end
  end

A lot of these are temporary methods, e.g. I will change ‘sign_out’ to a DELETE type request, but I can sort this a bit later as and when I am integrating with my front end app – for the time being I am just interested in ensuring I have authentication working as expected.

With PostMan, I am able to send a request to register and sign in, and it appears to be working as expected:

Do note that I had to manually enable the user in Cognito UI:

This was the email that I received for confirmation:

I haven’t configured automated confirmations yet. After confirming manually in UI, I am able send a log in request:

Having been able to sign in, I have a bearer token so I will test methods for authentication:

client = Aws::CognitoIdentityProvider::Client.new(region: ENV["AWS_REGION"])

> client.get_user({access_token: token})
 => #<struct Aws::CognitoIdentityProvider::Types::GetUserResponse username="yazoo", user_attributes=[#<struct Aws::CognitoIdentityProvider::Types::AttributeType name="sub", value="487da623-8e65-489d-934c-af2ef586069a">, #<struct Aws::CognitoIdentityProvider::Types::AttributeType name="email_verified", value="false">, #<struct Aws::CognitoIdentityProvider::Types::AttributeType name="phone_number_verified", value="false">, #<struct Aws::CognitoIdentityProvider::Types::AttributeType name="email", value="y.lazarev@hotmail.com">], mfa_options=nil, preferred_mfa_setting=nil, user_mfa_setting_list=nil> 

this is good news, this way I can authenticate my action requests with Cognito by checking that the access_token that will be part of my header requests is valid and corresponds to the user.

I have this hooked up to a controller request so I can also test this with Postman:

For the next part, I will look to have my React front-end link to these endpoints and complete the register – sign in – sign out flow.

Note that the above is just a skeleton for your authentication methods. You will still need to secure your actions behind something like ‘get_user’. e.g. before_action of a secure function, call get user with the authentication header. If the user is not found with the bearer token, you will get unauthorised and it will throw, which is what you want.

Next chapter will show javascript and react methods you can use to connect to these endpoints.