datacite/levriero

View on GitHub
app/models/concerns/authenticable.rb

Summary

Maintainability
B
4 hrs
Test Coverage
F
41%
module Authenticable
  extend ActiveSupport::Concern

  require "jwt"
  require "base64"

  included do
    # encode JWT token using SHA-256 hash algorithm
    def encode_token(payload)
      # replace newline characters with actual newlines
      private_key = OpenSSL::PKey::RSA.new(ENV["JWT_PRIVATE_KEY"].to_s.gsub(
                                             '\n', "\n"
                                           ))
      JWT.encode(payload, private_key, "RS256")
    rescue JSON::GeneratorError => e
      Rails.logger.error "JSON::GeneratorError: #{e.message} for #{payload}"
      nil
    end

    # decode JWT token using SHA-256 hash algorithm
    def decode_token(token)
      public_key = OpenSSL::PKey::RSA.new(ENV["JWT_PUBLIC_KEY"].to_s.gsub('\n',
                                                                          "\n"))
      payload = (JWT.decode token, public_key, true,
                            { algorithm: "RS256" }).first

      # check whether token has expired
      return {} unless Time.now.to_i < payload["exp"].to_i

      payload
    rescue JWT::DecodeError => e
      Rails.logger.error "JWT::DecodeError: #{e.message} for #{token}"
      {}
    rescue OpenSSL::PKey::RSAError => e
      public_key = ENV["JWT_PUBLIC_KEY"].presence || "nil"
      Rails.logger.error "OpenSSL::PKey::RSAError: #{e.message} for #{public_key}"
      {}
    end

    # basic auth
    def encode_auth_param(username: nil, password: nil)
      return nil unless username.present? && password.present?

      ::Base64.strict_encode64("#{username}:#{password}")
    end

    # basic auth
    def decode_auth_param(username: nil, password: nil)
      return {} unless username.present? && password.present?

      user = if username.include?(".")
               Client.where(symbol: username.upcase).first
             else
               Provider.unscoped.where(symbol: username.upcase).first
             end

      return {} unless user && secure_compare(user.password,
                                              encrypt_password_sha256(password))

      uid = username.downcase

      get_payload(uid: uid, user: user)
    end

    def get_payload(uid: nil, user: nil)
      roles = {
        "ROLE_ADMIN" => "staff_admin",
        "ROLE_ALLOCATOR" => "provider_admin",
        "ROLE_DATACENTRE" => "client_admin",
      }
      payload = {
        "uid" => uid,
        "role_id" => roles.fetch(user.role_name, "user"),
        "name" => user.name,
        "email" => user.contact_email,
      }

      if uid.include? "."
        payload["provider_id"] = uid.split(".", 2).first
        payload["client_id"] = uid
      elsif uid != "admin"
        payload["provider_id"] = uid
      end

      payload
    end

    # constant-time comparison algorithm to prevent timing attacks
    # from Devise
    def secure_compare(a, b)
      return false if a.blank? || b.blank? || a.bytesize != b.bytesize

      l = a.unpack "C#{a.bytesize}"

      res = 0
      b.each_byte { |byte| res |= byte ^ l.shift }
      res.zero?
    end
  end

  module ClassMethods
    # encode token using SHA-256 hash algorithm
    def encode_token(payload)
      # replace newline characters with actual newlines
      private_key = OpenSSL::PKey::RSA.new(ENV["JWT_PRIVATE_KEY"].to_s.gsub(
                                             '\n', "\n"
                                           ))
      JWT.encode(payload, private_key, "RS256")
    rescue OpenSSL::PKey::RSAError => e
      Rails.logger.error e.inspect

      nil
    end

    # basic auth
    def encode_auth_param(username: nil, password: nil)
      return nil unless username.present? && password.present?

      ::Base64.strict_encode64("#{username}:#{password}")
    end

    # generate JWT token
    def generate_token(attributes = {})
      payload = {
        uid: attributes.fetch(:uid, "0000-0001-5489-3594"),
        name: attributes.fetch(:name, "Josiah Carberry"),
        email: attributes.fetch(:email, nil),
        provider_id: attributes.fetch(:provider_id, nil),
        client_id: attributes.fetch(:client_id, nil),
        role_id: attributes.fetch(:role_id, "staff_admin"),
        iat: Time.now.to_i,
        exp: Time.now.to_i + attributes.fetch(:exp, 30),
      }.compact

      encode_token(payload)
    end

    def get_payload(uid: nil, user: nil)
      roles = {
        "ROLE_ADMIN" => "staff_admin",
        "ROLE_ALLOCATOR" => "provider_admin",
        "ROLE_DATACENTRE" => "client_admin",
      }
      payload = {
        "uid" => uid,
        "role_id" => roles.fetch(user.role_name, "user"),
        "name" => user.name,
        "email" => user.contact_email,
      }

      if uid.include? "."
        payload["provider_id"] = uid.split(".", 2).first
        payload["client_id"] = uid
      elsif uid != "admin"
        payload["provider_id"] = uid
      end

      payload
    end
  end
end