Skip to content

Commit

Permalink
Merge pull request #6216 from 7riumph/feature/6207
Browse files Browse the repository at this point in the history
Feature/6207 Endpoint api/v1/sign_in now outputs randomized tokens in response upon sign-in
  • Loading branch information
FireLemons authored Feb 19, 2025
2 parents 5e2a32e + 778758e commit ede9e67
Show file tree
Hide file tree
Showing 22 changed files with 305 additions and 35 deletions.
1 change: 1 addition & 0 deletions .allow_skipping_tests
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ models/application_record.rb
models/concerns/CasaCase/validations.rb
models/concerns/by_organization_scope.rb
models/concerns/roles.rb
models/concerns/api.rb
models/fund_request.rb
notifications/base_notifier.rb
notifications/delivery_methods/sms.rb
Expand Down
20 changes: 18 additions & 2 deletions app/blueprints/api/v1/session_blueprint.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
class Api::V1::SessionBlueprint < Blueprinter::Base
identifier :id
field :user do |user|
{
id: user.id,
display_name: user.display_name,
email: user.email,
refresh_token_expires_at: user.api_credential&.refresh_token_expires_at,
token_expires_at: user.api_credential&.token_expires_at
}
end

fields :id, :display_name, :email, :token
field :api_token do |user|
token = user.api_credential
token.return_new_api_token![:api_token]
end

field :refresh_token do |user|
token = user.api_credential
token.return_new_refresh_token![:refresh_token]
end
end
6 changes: 3 additions & 3 deletions app/controllers/api/v1/base_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ class Api::V1::BaseController < ActionController::API
before_action :authenticate_user!, except: [:create]

def authenticate_user!
token, options = ActionController::HttpAuthentication::Token.token_and_options(request)
api_token, options = ActionController::HttpAuthentication::Token.token_and_options(request)
user = User.find_by(email: options[:email])
if user && token && ActiveSupport::SecurityUtils.secure_compare(user.token, token)
if user && api_token && ActiveSupport::SecurityUtils.secure_compare(user.api_credential.api_token_digest, Digest::SHA256.hexdigest(api_token))
@current_user = user
else
render json: {message: "Wrong password or email"}, status: 401
render json: {message: "Incorrect email or password."}, status: 401
end
end

Expand Down
2 changes: 1 addition & 1 deletion app/controllers/api/v1/users/sessions_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ def create
if @user
render json: Api::V1::SessionBlueprint.render(@user), status: 201
else
render json: {message: "Wrong password or email"}, status: 401
render json: {message: "Incorrect email or password."}, status: 401
end
end

Expand Down
78 changes: 78 additions & 0 deletions app/models/api_credential.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
require "digest"

class ApiCredential < ApplicationRecord
belongs_to :user

before_save :generate_api_token
before_save :generate_refresh_token

# Securely confirm/deny that Hash in db is same as current users token Hash
def authenticate_api_token(api_token)
Digest::SHA256.hexdigest(api_token) == api_token_digest
end

def authenticate_refresh_token(refresh_token)
Digest::SHA256.hexdigest(refresh_token) == refresh_token_digest
end

# Securely generate and then return new tokens
def return_new_api_token!
new_token = generate_api_token
update_column(:api_token_digest, api_token_digest)
{api_token: new_token}
end

def return_new_refresh_token!
new_token = generate_refresh_token
update_column(:refresh_token_digest, refresh_token_digest)
{refresh_token: new_token}
end

# Verifying token has or has not expired
def is_api_token_expired?
token_expires_at < Time.current
end

def is_refresh_token_expired?
refresh_token_expires_at < Time.current
end

private

# Generate unique tokens and hashes them for secure db storage
def generate_api_token
new_api_token = SecureRandom.hex(18)
self.api_token_digest = Digest::SHA256.hexdigest(new_api_token)
new_api_token
end

def generate_refresh_token
new_refresh_token = SecureRandom.hex(18)
self.refresh_token_digest = Digest::SHA256.hexdigest(new_refresh_token)
new_refresh_token
end
end

# == Schema Information
#
# Table name: api_credentials
#
# id :bigint not null, primary key
# api_token_digest :string
# refresh_token_digest :string
# refresh_token_expires_at :datetime
# token_expires_at :datetime
# created_at :datetime not null
# updated_at :datetime not null
# user_id :bigint not null
#
# Indexes
#
# index_api_credentials_on_api_token_digest (api_token_digest) UNIQUE WHERE (api_token_digest IS NOT NULL)
# index_api_credentials_on_refresh_token_digest (refresh_token_digest) UNIQUE WHERE (refresh_token_digest IS NOT NULL)
# index_api_credentials_on_user_id (user_id)
#
# Foreign Keys
#
# fk_rails_... (user_id => users.id)
#
1 change: 0 additions & 1 deletion app/models/casa_admin.rb
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ def change_to_supervisor!
# reset_password_sent_at :datetime
# reset_password_token :string
# sign_in_count :integer default(0), not null
# token :string
# type :string
# unconfirmed_email :string
# created_at :datetime not null
Expand Down
13 changes: 13 additions & 0 deletions app/models/concerns/api.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
module Api
extend ActiveSupport::Concern
included do
has_one :api_credential, dependent: :destroy
after_create :initialize_api_credentials
end

private

def initialize_api_credentials
create_api_credential unless api_credential
end
end
1 change: 0 additions & 1 deletion app/models/supervisor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,6 @@ def recently_unassigned_volunteers
# reset_password_sent_at :datetime
# reset_password_token :string
# sign_in_count :integer default(0), not null
# token :string
# type :string
# unconfirmed_email :string
# created_at :datetime not null
Expand Down
3 changes: 1 addition & 2 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@
# model for all user roles: volunteer supervisor casa_admin inactive
class User < ApplicationRecord
include Roles
include Api
include ByOrganizationScope
include DateHelper

before_save :normalize_phone_number
after_create :skip_email_confirmation_upon_creation
after_create :create_preference_set
before_update :record_previous_email
has_secure_token :token, length: 36

validates_with UserValidator

Expand Down Expand Up @@ -217,7 +217,6 @@ def normalize_phone_number
# reset_password_sent_at :datetime
# reset_password_token :string
# sign_in_count :integer default(0), not null
# token :string
# type :string
# unconfirmed_email :string
# created_at :datetime not null
Expand Down
1 change: 0 additions & 1 deletion app/models/volunteer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,6 @@ def cases_where_contact_made_in_days(num_days = CONTACT_MADE_IN_DAYS_NUM)
# reset_password_sent_at :datetime
# reset_password_token :string
# sign_in_count :integer default(0), not null
# token :string
# type :string
# unconfirmed_email :string
# created_at :datetime not null
Expand Down
5 changes: 5 additions & 0 deletions db/migrate/20250207080433_remove_token_from_users.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class RemoveTokenFromUsers < ActiveRecord::Migration[7.2]
def change
safety_assured { remove_column :users, :token, :string }
end
end
19 changes: 19 additions & 0 deletions db/migrate/20250207080511_create_api_credentials.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
class CreateApiCredentials < ActiveRecord::Migration[7.2]
def change
create_table :api_credentials do |t|
t.references :user, null: false, foreign_key: true
t.string :api_token
t.string :refresh_token
t.datetime :token_expires_at, default: -> { "NOW() + INTERVAL '7 hours'" }
t.datetime :refresh_token_expires_at, default: -> { "NOW() + INTERVAL '30 days'" }

t.string :api_token_digest
t.string :refresh_token_digest

t.timestamps
end

add_index :api_credentials, :api_token_digest, unique: true, where: "api_token_digest IS NOT NULL"
add_index :api_credentials, :refresh_token_digest, unique: true, where: "refresh_token_digest IS NOT NULL"
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
class RemovePlainTextTokensFromApiCredentials < ActiveRecord::Migration[7.2]
def change
safety_assured { remove_column :api_credentials, :api_token, :string }
safety_assured { remove_column :api_credentials, :refresh_token, :string }
end
end
17 changes: 15 additions & 2 deletions db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema[7.2].define(version: 2024_10_17_050129) do
ActiveRecord::Schema[7.2].define(version: 2025_02_08_160513) do
# These are extensions that must be enabled in order to support this database
enable_extension "pg_stat_statements"
enable_extension "plpgsql"
Expand Down Expand Up @@ -89,6 +89,19 @@
t.index ["reset_password_token"], name: "index_all_casa_admins_on_reset_password_token", unique: true
end

create_table "api_credentials", force: :cascade do |t|
t.bigint "user_id", null: false
t.datetime "token_expires_at", default: -> { "(now() + 'PT7H'::interval)" }
t.datetime "refresh_token_expires_at", default: -> { "(now() + 'P30D'::interval)" }
t.string "api_token_digest"
t.string "refresh_token_digest"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["api_token_digest"], name: "index_api_credentials_on_api_token_digest", unique: true, where: "(api_token_digest IS NOT NULL)"
t.index ["refresh_token_digest"], name: "index_api_credentials_on_refresh_token_digest", unique: true, where: "(refresh_token_digest IS NOT NULL)"
t.index ["user_id"], name: "index_api_credentials_on_user_id"
end

create_table "banners", force: :cascade do |t|
t.bigint "casa_org_id", null: false
t.bigint "user_id", null: false
Expand Down Expand Up @@ -683,7 +696,6 @@
t.string "unconfirmed_email"
t.string "old_emails", default: [], array: true
t.boolean "receive_reimbursement_email", default: false
t.string "token"
t.boolean "monthly_learning_hours_report", default: false, null: false
t.datetime "date_of_birth"
t.index ["casa_org_id"], name: "index_users_on_casa_org_id"
Expand All @@ -700,6 +712,7 @@
add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id"
add_foreign_key "additional_expenses", "case_contacts"
add_foreign_key "addresses", "users"
add_foreign_key "api_credentials", "users"
add_foreign_key "banners", "casa_orgs"
add_foreign_key "banners", "users"
add_foreign_key "casa_case_emancipation_categories", "casa_cases"
Expand Down
4 changes: 2 additions & 2 deletions lib/tasks/deployment/20230822145532_populate_api_tokens.rake
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ namespace :after_party do
puts "Running deploy task 'populate_api_tokens'" unless Rails.env.test?

# Put your task implementation HERE.
User.where(token: nil).each do |user|
User.find_each do |user|
user.update(receive_sms_notifications: false) if user.phone_number.blank?
user.regenerate_token
user.api_credential || user.create_api_credential
end

# Update task as completed. If you remove the line below, the task will
Expand Down
9 changes: 9 additions & 0 deletions spec/factories/api_credential.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
FactoryBot.define do
factory :api_credential do
association :user
api_token_digest { Digest::SHA256.hexdigest(SecureRandom.hex(18)) }
refresh_token_digest { Digest::SHA256.hexdigest(SecureRandom.hex(18)) }
token_expires_at { 1.hour.from_now }
refresh_token_expires_at { 1.day.from_now }
end
end
5 changes: 4 additions & 1 deletion spec/factories/users.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@
case_assignments { [] }
phone_number { "" }
confirmed_at { Time.now }
token { "verysecuretoken" }

after(:create) do |user|
create(:api_credential, user: user)
end

trait :inactive do
type { "Volunteer" }
Expand Down
Loading

0 comments on commit ede9e67

Please sign in to comment.