From 8e03865328ed229aa04d63ac01ba810f6232a6e3 Mon Sep 17 00:00:00 2001 From: xihai01 Date: Mon, 24 Feb 2025 15:33:18 -0500 Subject: [PATCH 1/6] add rspec tests and generate new swagger file --- spec/models/api_credential_spec.rb | 16 ++++++++++ spec/requests/api/v1/users/sessions_spec.rb | 33 +++++++++++++++++++++ spec/swagger_helper.rb | 6 ++++ swagger/v1/swagger.yaml | 31 ++++++++++++++++++- 4 files changed, 85 insertions(+), 1 deletion(-) diff --git a/spec/models/api_credential_spec.rb b/spec/models/api_credential_spec.rb index 2d4991eee0..d1b633ca5a 100644 --- a/spec/models/api_credential_spec.rb +++ b/spec/models/api_credential_spec.rb @@ -100,4 +100,20 @@ expect(api_credential.refresh_token_digest).to eq(Digest::SHA256.hexdigest(refresh_token)) end end + + describe "#revoke_token" do + it "sets api token to nil" do + api_token = api_credential.return_new_api_token![:api_token] + api_credential.revoke_token(api_token) + + expect(api_credential.api_token_digest).to be_nil + end + + it "sets refresh token to nil" do + refresh_token = api_credential.return_new_refresh_token![:refresh_token] + api_credential.revoke_token(refresh_token) + + expect(api_credential.refresh_token_digest).to be_nil + end + end end diff --git a/spec/requests/api/v1/users/sessions_spec.rb b/spec/requests/api/v1/users/sessions_spec.rb index fd80dafa42..99864a9bd6 100644 --- a/spec/requests/api/v1/users/sessions_spec.rb +++ b/spec/requests/api/v1/users/sessions_spec.rb @@ -41,4 +41,37 @@ end end end + + path "/api/v1/users/sign_out" do + delete "Signs out a user" do + tags "Sessions" + produces "application/json" + parameter name: :Authorization, in: :header, type: :string, required: true + + let(:casa_org) { create(:casa_org) } + let(:volunteer) { create(:volunteer, casa_org: casa_org) } + let(:api_credential) { create(:api_credential, user: volunteer) } + let(:refresh_token) { api_credential.return_new_refresh_token![:refresh_token] } + + response "200", "user signed out" do + let(:Authorization) { "Bearer #{refresh_token}" } + schema "$ref" => "#/components/schemas/sign_out" + run_test! do |response| + expect(response.content_type).to eq("application/json; charset=utf-8") + expect(response.body).to eq({message: "Signed out successfully."}.to_json) + expect(response.status).to eq(200) + end + end + + response "401", "unauthorized" do + let(:Authorization) { "Bearer foo" } + schema "$ref" => "#/components/schemas/sign_out" + run_test! do |response| + expect(response.content_type).to eq("application/json; charset=utf-8") + expect(response.body).to eq({message: "An error occured when signing out."}.to_json) + expect(response.status).to eq(401) + end + end + end + end end diff --git a/spec/swagger_helper.rb b/spec/swagger_helper.rb index 145c05b27b..737dee2680 100644 --- a/spec/swagger_helper.rb +++ b/spec/swagger_helper.rb @@ -42,6 +42,12 @@ properties: { message: {type: :string} } + }, + sign_out: { + type: :object, + properties: { + message: {type: :string} + } } } }, diff --git a/swagger/v1/swagger.yaml b/swagger/v1/swagger.yaml index 627992bfc9..c746369fa3 100644 --- a/swagger/v1/swagger.yaml +++ b/swagger/v1/swagger.yaml @@ -19,7 +19,7 @@ components: type: string email: type: string - api_token_expires_at: + token_expires_at: type: datetime refresh_token_expires_at: type: datetime @@ -28,6 +28,11 @@ components: properties: message: type: string + sign_out: + type: object + properties: + message: + type: string paths: "/api/v1/users/sign_in": post: @@ -61,6 +66,30 @@ paths: required: - email - password + "/api/v1/users/sign_out": + delete: + summary: Signs out a user + tags: + - Sessions + parameters: + - name: Authorization + in: header + required: true + schema: + type: string + responses: + '200': + description: user signed out + content: + application/json: + schema: + "$ref": "#/components/schemas/sign_out" + '401': + description: unauthorized + content: + application/json: + schema: + "$ref": "#/components/schemas/sign_out" servers: - url: https://{defaultHost} variables: From c787435478fd19def66554d9aea1600c3e3468cc Mon Sep 17 00:00:00 2001 From: xihai01 Date: Mon, 24 Feb 2025 16:31:05 -0500 Subject: [PATCH 2/6] add helper function in api_credential table to clear tokens --- app/models/api_credential.rb | 13 +++++++++++++ spec/models/api_credential_spec.rb | 8 ++++++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/app/models/api_credential.rb b/app/models/api_credential.rb index a7736b4133..a010fe44fe 100644 --- a/app/models/api_credential.rb +++ b/app/models/api_credential.rb @@ -37,6 +37,19 @@ def is_refresh_token_expired? refresh_token_expires_at < Time.current end + # clear tokens + # token argument takes in two strings: api_token and refresh_token + def revoke_token(token) + if (token == "api_token") + update_columns(api_token_digest: nil) + elsif (token == "refresh_token") + update_columns(refresh_token_digest: nil) + else + return nil + end + return token + end + private # Generate unique tokens and hashes them for secure db storage diff --git a/spec/models/api_credential_spec.rb b/spec/models/api_credential_spec.rb index d1b633ca5a..fe4884d7b4 100644 --- a/spec/models/api_credential_spec.rb +++ b/spec/models/api_credential_spec.rb @@ -104,16 +104,20 @@ describe "#revoke_token" do it "sets api token to nil" do api_token = api_credential.return_new_api_token![:api_token] - api_credential.revoke_token(api_token) + api_credential.revoke_token("api_token") expect(api_credential.api_token_digest).to be_nil end it "sets refresh token to nil" do refresh_token = api_credential.return_new_refresh_token![:refresh_token] - api_credential.revoke_token(refresh_token) + api_credential.revoke_token("refresh_token") expect(api_credential.refresh_token_digest).to be_nil end + + it "returns nil if token is not found" do + expect(api_credential.revoke_token("invalid_token")).to be_nil + end end end From bf2d43d8be0a1b8f4955f2bf567380c4ad99fbe7 Mon Sep 17 00:00:00 2001 From: xihai01 Date: Wed, 26 Feb 2025 11:43:00 -0500 Subject: [PATCH 3/6] add sign out route to controller --- app/controllers/api/v1/base_controller.rb | 2 +- .../api/v1/users/sessions_controller.rb | 16 ++++++++++++++++ config/routes.rb | 2 +- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/app/controllers/api/v1/base_controller.rb b/app/controllers/api/v1/base_controller.rb index 485827894f..ae1fc239fa 100644 --- a/app/controllers/api/v1/base_controller.rb +++ b/app/controllers/api/v1/base_controller.rb @@ -1,6 +1,6 @@ class Api::V1::BaseController < ActionController::API rescue_from ActiveRecord::RecordNotFound, with: :not_found - before_action :authenticate_user!, except: [:create] + before_action :authenticate_user!, except: [:create, :destroy] def authenticate_user! api_token, options = ActionController::HttpAuthentication::Token.token_and_options(request) diff --git a/app/controllers/api/v1/users/sessions_controller.rb b/app/controllers/api/v1/users/sessions_controller.rb index 5c36f9b1fa..fc948a94ab 100644 --- a/app/controllers/api/v1/users/sessions_controller.rb +++ b/app/controllers/api/v1/users/sessions_controller.rb @@ -8,6 +8,22 @@ def create end end + def destroy + # fetch refresh token from request header + refresh_token = request.headers["Authorization"]&.split(" ")&.last + # find user's api credentials by refresh token + api_credential = ApiCredential.find_by(refresh_token_digest: Digest::SHA256.hexdigest(refresh_token)) + # set api and refresh tokens to nil; otherwise render 401 + if api_credential + api_credential.revoke_token("api_token") + api_credential.revoke_token("refresh_token") + render json: {message: "Signed out successfully."}, status: 200 + else + render json: {message: "An error occured when signing out."}, status: 401 + return + end + end + private def user_params diff --git a/config/routes.rb b/config/routes.rb index a97cd7fb04..7344e21169 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -250,7 +250,7 @@ namespace :v1 do namespace :users do post "sign_in", to: "sessions#create" - # get 'sign_out', to: 'sessions#destroy' + delete "sign_out", to: "sessions#destroy" end end end From bb13f87b236d7c21ad84bb293f0f6854e2f82f47 Mon Sep 17 00:00:00 2001 From: xihai01 Date: Wed, 26 Feb 2025 12:37:53 -0500 Subject: [PATCH 4/6] seed api credential with token data in dev --- db/seeds.rb | 1 + db/seeds/api_credential_data.rb | 6 ++++++ 2 files changed, 7 insertions(+) create mode 100644 db/seeds/api_credential_data.rb diff --git a/db/seeds.rb b/db/seeds.rb index f6d70dce47..27b3ba1900 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -147,3 +147,4 @@ def create_org_related_data(db_populator, casa_org, options) Rails.logger.error { "Caught error during db seed emancipation_options_prune, continuing. Message: #{e}" } end load(Rails.root.join("db/seeds/placement_data.rb")) +load(Rails.root.join("db/seeds/api_credential_data.rb")) diff --git a/db/seeds/api_credential_data.rb b/db/seeds/api_credential_data.rb new file mode 100644 index 0000000000..b1a946cdbc --- /dev/null +++ b/db/seeds/api_credential_data.rb @@ -0,0 +1,6 @@ +ApiCredential.destroy_all +users = User.all + +users.each do |user| + ApiCredential.create!(user: user, api_token_digest: Digest::SHA256.hexdigest(SecureRandom.hex(18)), refresh_token_digest: Digest::SHA256.hexdigest(SecureRandom.hex(18))) +end From fad485784492c58740a8e50a91f371b843d90e28 Mon Sep 17 00:00:00 2001 From: xihai01 Date: Wed, 26 Feb 2025 16:21:24 -0500 Subject: [PATCH 5/6] address changes: separate revoke_tokens --- .../api/v1/users/sessions_controller.rb | 4 ++-- app/models/api_credential.rb | 17 ++++++----------- spec/models/api_credential_spec.rb | 12 +++++------- 3 files changed, 13 insertions(+), 20 deletions(-) diff --git a/app/controllers/api/v1/users/sessions_controller.rb b/app/controllers/api/v1/users/sessions_controller.rb index fc948a94ab..1044db0fd3 100644 --- a/app/controllers/api/v1/users/sessions_controller.rb +++ b/app/controllers/api/v1/users/sessions_controller.rb @@ -15,8 +15,8 @@ def destroy api_credential = ApiCredential.find_by(refresh_token_digest: Digest::SHA256.hexdigest(refresh_token)) # set api and refresh tokens to nil; otherwise render 401 if api_credential - api_credential.revoke_token("api_token") - api_credential.revoke_token("refresh_token") + api_credential.revoke_api_token + api_credential.revoke_refresh_token render json: {message: "Signed out successfully."}, status: 200 else render json: {message: "An error occured when signing out."}, status: 401 diff --git a/app/models/api_credential.rb b/app/models/api_credential.rb index a010fe44fe..adcf3b5435 100644 --- a/app/models/api_credential.rb +++ b/app/models/api_credential.rb @@ -37,17 +37,12 @@ def is_refresh_token_expired? refresh_token_expires_at < Time.current end - # clear tokens - # token argument takes in two strings: api_token and refresh_token - def revoke_token(token) - if (token == "api_token") - update_columns(api_token_digest: nil) - elsif (token == "refresh_token") - update_columns(refresh_token_digest: nil) - else - return nil - end - return token + def revoke_api_token + update_columns(api_token_digest: nil) + end + + def revoke_refresh_token + update_columns(refresh_token_digest: nil) end private diff --git a/spec/models/api_credential_spec.rb b/spec/models/api_credential_spec.rb index fe4884d7b4..fbecc6c04b 100644 --- a/spec/models/api_credential_spec.rb +++ b/spec/models/api_credential_spec.rb @@ -101,23 +101,21 @@ end end - describe "#revoke_token" do + describe "#revoke_api_token" do it "sets api token to nil" do api_token = api_credential.return_new_api_token![:api_token] - api_credential.revoke_token("api_token") + api_credential.revoke_api_token expect(api_credential.api_token_digest).to be_nil end + end + describe "#revoke_refresh_token" do it "sets refresh token to nil" do refresh_token = api_credential.return_new_refresh_token![:refresh_token] - api_credential.revoke_token("refresh_token") + api_credential.revoke_refresh_token expect(api_credential.refresh_token_digest).to be_nil end - - it "returns nil if token is not found" do - expect(api_credential.revoke_token("invalid_token")).to be_nil - end end end From 8711ff09ae1d3b48e4da1ab2b920e6b07ec7654c Mon Sep 17 00:00:00 2001 From: xihai01 Date: Wed, 26 Feb 2025 16:53:32 -0500 Subject: [PATCH 6/6] lint files --- .../api/v1/users/sessions_controller.rb | 2 +- spec/models/api_credential_spec.rb | 4 ++-- spec/requests/api/v1/users/sessions_spec.rb | 17 +++++++---------- 3 files changed, 10 insertions(+), 13 deletions(-) diff --git a/app/controllers/api/v1/users/sessions_controller.rb b/app/controllers/api/v1/users/sessions_controller.rb index 1044db0fd3..ea31f3e8e5 100644 --- a/app/controllers/api/v1/users/sessions_controller.rb +++ b/app/controllers/api/v1/users/sessions_controller.rb @@ -20,7 +20,7 @@ def destroy render json: {message: "Signed out successfully."}, status: 200 else render json: {message: "An error occured when signing out."}, status: 401 - return + nil end end diff --git a/spec/models/api_credential_spec.rb b/spec/models/api_credential_spec.rb index fbecc6c04b..99266e3cf3 100644 --- a/spec/models/api_credential_spec.rb +++ b/spec/models/api_credential_spec.rb @@ -103,7 +103,7 @@ describe "#revoke_api_token" do it "sets api token to nil" do - api_token = api_credential.return_new_api_token![:api_token] + api_credential.return_new_api_token![:api_token] api_credential.revoke_api_token expect(api_credential.api_token_digest).to be_nil @@ -112,7 +112,7 @@ describe "#revoke_refresh_token" do it "sets refresh token to nil" do - refresh_token = api_credential.return_new_refresh_token![:refresh_token] + api_credential.return_new_refresh_token![:refresh_token] api_credential.revoke_refresh_token expect(api_credential.refresh_token_digest).to be_nil diff --git a/spec/requests/api/v1/users/sessions_spec.rb b/spec/requests/api/v1/users/sessions_spec.rb index 99864a9bd6..0ec44df3e8 100644 --- a/spec/requests/api/v1/users/sessions_spec.rb +++ b/spec/requests/api/v1/users/sessions_spec.rb @@ -1,6 +1,9 @@ require "swagger_helper" RSpec.describe "sessions API", type: :request do + let(:casa_org) { create(:casa_org) } + let(:volunteer) { create(:volunteer, casa_org: casa_org) } + path "/api/v1/users/sign_in" do post "Signs in a user" do tags "Sessions" @@ -15,9 +18,6 @@ required: %w[email password] } - let(:casa_org) { create(:casa_org) } - let(:volunteer) { create(:volunteer, casa_org: casa_org) } - response "201", "user signed in" do let(:user) { {email: volunteer.email, password: volunteer.password} } schema "$ref" => "#/components/schemas/login_success" @@ -46,15 +46,12 @@ delete "Signs out a user" do tags "Sessions" produces "application/json" - parameter name: :Authorization, in: :header, type: :string, required: true + parameter name: :authorization, in: :header, type: :string, required: true - let(:casa_org) { create(:casa_org) } - let(:volunteer) { create(:volunteer, casa_org: casa_org) } - let(:api_credential) { create(:api_credential, user: volunteer) } - let(:refresh_token) { api_credential.return_new_refresh_token![:refresh_token] } + let(:refresh_token) { create(:api_credential, user: volunteer).return_new_refresh_token![:refresh_token] } response "200", "user signed out" do - let(:Authorization) { "Bearer #{refresh_token}" } + let(:authorization) { "Bearer #{refresh_token}" } schema "$ref" => "#/components/schemas/sign_out" run_test! do |response| expect(response.content_type).to eq("application/json; charset=utf-8") @@ -64,7 +61,7 @@ end response "401", "unauthorized" do - let(:Authorization) { "Bearer foo" } + let(:authorization) { "Bearer foo" } schema "$ref" => "#/components/schemas/sign_out" run_test! do |response| expect(response.content_type).to eq("application/json; charset=utf-8")