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..ea31f3e8e5 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_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 + nil + end + end + private def user_params diff --git a/app/models/api_credential.rb b/app/models/api_credential.rb index a7736b4133..adcf3b5435 100644 --- a/app/models/api_credential.rb +++ b/app/models/api_credential.rb @@ -37,6 +37,14 @@ def is_refresh_token_expired? refresh_token_expires_at < Time.current end + def revoke_api_token + update_columns(api_token_digest: nil) + end + + def revoke_refresh_token + update_columns(refresh_token_digest: nil) + end + private # Generate unique tokens and hashes them for secure db storage 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 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 diff --git a/spec/models/api_credential_spec.rb b/spec/models/api_credential_spec.rb index 2d4991eee0..99266e3cf3 100644 --- a/spec/models/api_credential_spec.rb +++ b/spec/models/api_credential_spec.rb @@ -100,4 +100,22 @@ expect(api_credential.refresh_token_digest).to eq(Digest::SHA256.hexdigest(refresh_token)) end end + + describe "#revoke_api_token" do + it "sets api token to nil" do + api_credential.return_new_api_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 + api_credential.return_new_refresh_token![:refresh_token] + api_credential.revoke_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..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" @@ -41,4 +41,34 @@ 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(:refresh_token) { create(:api_credential, user: volunteer).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: