From 41d1d6d8fe0b817b0ad1b8e298553827668d3eb6 Mon Sep 17 00:00:00 2001 From: Jeremy Lecour Date: Mon, 24 Jan 2022 23:50:12 +0100 Subject: [PATCH] =?UTF-8?q?pr=C3=A9mices=20d'une=20API=20avec=20authentifi?= =?UTF-8?q?cation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/controllers/api/v1/api_keys_controller.rb | 30 ++++++++++++++++++ app/controllers/api/v1/base_controller.rb | 25 +++++++++++++++ app/controllers/api/v1/checks_controller.rb | 25 +++++++++++++++ .../concerns/api_key_authenticatable.rb | 31 +++++++++++++++++++ app/controllers/concerns/authentication.rb | 23 +++++++------- app/models/api_key.rb | 15 +++++++++ app/models/user.rb | 2 ++ config/credentials.yml.enc | 2 +- config/routes.rb | 10 ++++++ db/migrate/20220124181635_create_api_keys.rb | 15 +++++++++ db/schema.rb | 11 ++++++- 11 files changed, 175 insertions(+), 14 deletions(-) create mode 100644 app/controllers/api/v1/api_keys_controller.rb create mode 100644 app/controllers/api/v1/base_controller.rb create mode 100644 app/controllers/api/v1/checks_controller.rb create mode 100644 app/controllers/concerns/api_key_authenticatable.rb create mode 100644 app/models/api_key.rb create mode 100644 db/migrate/20220124181635_create_api_keys.rb diff --git a/app/controllers/api/v1/api_keys_controller.rb b/app/controllers/api/v1/api_keys_controller.rb new file mode 100644 index 0000000..51f8cd5 --- /dev/null +++ b/app/controllers/api/v1/api_keys_controller.rb @@ -0,0 +1,30 @@ +class Api::V1::ApiKeysController < Api::V1::BaseController + # include ApiKeyAuthenticable + + # # Require API key authentication + # prepend_before_action :authenticate_with_api_key!, only: %i[index destroy] + + def index + render json: current_bearer.api_keys + end + + def create + authenticate_with_http_basic do |email, password| + user = User.find_by email: email + + if user&.authenticate(password) + api_key = user.api_keys.create! token: SecureRandom.hex + + render json: api_key, status: :created and return + end + end + + render status: :unauthorized + end + + def destroy + api_key = current_bearer.api_keys.find(params[:id]) + + api_key.destroy + end + end \ No newline at end of file diff --git a/app/controllers/api/v1/base_controller.rb b/app/controllers/api/v1/base_controller.rb new file mode 100644 index 0000000..b5b204b --- /dev/null +++ b/app/controllers/api/v1/base_controller.rb @@ -0,0 +1,25 @@ +class Api::V1::BaseController < ApplicationController + # before_action :authenticate + + protect_from_forgery with: :null_session + + def ping + render json: { message: "pong" }, status: :ok + end + + private + + def authenticate + authenticate_user_with_token || handle_bad_authentication + end + + def authenticate_user_with_token + authenticate_with_http_token do |token, options| + @user ||= User.find_by(private_api_key: token) + end + end + + def handle_bad_authentication + render json: { message: "Bad credentials" }, status: :unauthorized + end +end \ No newline at end of file diff --git a/app/controllers/api/v1/checks_controller.rb b/app/controllers/api/v1/checks_controller.rb new file mode 100644 index 0000000..be3091b --- /dev/null +++ b/app/controllers/api/v1/checks_controller.rb @@ -0,0 +1,25 @@ +class Api::V1::ChecksController < Api::V1::BaseController + + # POST /checks or /checks.json + def create + @check = Check.new(check_params) + + if @check.save + render json: { message: "created" }, status: :created + else + render json: @check.errors, status: :unprocessable_entity + end + end + + private + # Use callbacks to share common setup or constraints between actions. + def set_check + @check = Check.find(params[:id]) + end + + # Only allow a list of trusted parameters through. + def check_params + params.require(:check).permit(:name, :description, :hostname) + end + +end \ No newline at end of file diff --git a/app/controllers/concerns/api_key_authenticatable.rb b/app/controllers/concerns/api_key_authenticatable.rb new file mode 100644 index 0000000..85f7939 --- /dev/null +++ b/app/controllers/concerns/api_key_authenticatable.rb @@ -0,0 +1,31 @@ +module ApiKeyAuthenticatable + include ActionController::HttpAuthentication::Basic::ControllerMethods + include ActionController::HttpAuthentication::Token::ControllerMethods + + extend ActiveSupport::Concern + + attr_reader :current_api_key + attr_reader :current_bearer + + # Use this to raise an error and automatically respond with a 401 HTTP status + # code when API key authentication fails + def authenticate_with_api_key! + @current_bearer = authenticate_or_request_with_http_token &method(:authenticator) + end + + # Use this for optional API key authentication + def authenticate_with_api_key + @current_bearer = authenticate_with_http_token &method(:authenticator) + end + + private + + attr_writer :current_api_key + attr_writer :current_bearer + + def authenticator(token, options) + @current_api_key = ApiKey.authenticate_by_token token + + current_api_key&.bearer + end + end \ No newline at end of file diff --git a/app/controllers/concerns/authentication.rb b/app/controllers/concerns/authentication.rb index da06243..7013c33 100644 --- a/app/controllers/concerns/authentication.rb +++ b/app/controllers/concerns/authentication.rb @@ -1,49 +1,49 @@ module Authentication extend ActiveSupport::Concern - + included do before_action :current_user helper_method :current_user helper_method :user_signed_in? end - + def authenticate_user! store_location redirect_to login_path, alert: "You need to login to access that page." unless user_signed_in? end - + def login(user) reset_session user.regenerate_session_token session[:current_user_session_token] = user.reload.session_token end - + def forget(user) cookies.delete :remember_token user.regenerate_remember_token end - + def logout user = current_user reset_session user.regenerate_session_token end - + def redirect_if_authenticated redirect_to root_path, alert: "You are already logged in." if user_signed_in? end - + def remember(user) user.regenerate_remember_token cookies.permanent.encrypted[:remember_token] = user.remember_token end - + def store_location session[:user_return_to] = request.original_url if request.get? && request.local? end - + private - + def current_user Current.user ||= if session[:current_user_session_token].present? User.find_by(session_token: session[:current_user_session_token]) @@ -51,9 +51,8 @@ module Authentication User.find_by(remember_token: cookies.permanent.encrypted[:remember_token]) end end - + def user_signed_in? Current.user.present? end end - \ No newline at end of file diff --git a/app/models/api_key.rb b/app/models/api_key.rb new file mode 100644 index 0000000..45c9cfc --- /dev/null +++ b/app/models/api_key.rb @@ -0,0 +1,15 @@ +class ApiKey < ApplicationRecord + encrypts :token, deterministic: true + + belongs_to :bearer, polymorphic: true + + def self.authenticate_by_token!(token) + find_by! token: token + end + + def self.authenticate_by_token(token) + authenticate_by_token! token + rescue ActiveRecord::RecordNotFound + nil + end +end \ No newline at end of file diff --git a/app/models/user.rb b/app/models/user.rb index 9a335a8..015f1b1 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -5,6 +5,8 @@ class User < ApplicationRecord attr_accessor :current_password + has_many :api_keys, as: :bearer + has_secure_password has_secure_token :remember_token has_secure_token :session_token diff --git a/config/credentials.yml.enc b/config/credentials.yml.enc index 8e543ec..6ee31ca 100644 --- a/config/credentials.yml.enc +++ b/config/credentials.yml.enc @@ -1 +1 @@ -LAf4+kWkL2cPoqOeoqqfRaf2Y00zX7cZEAFlZGQkK//YR3Fm2SRb9IeTkKM7Hr57zT+DIXRP5RIA1+p7fa7Bazkg21JGnpMQDbCGTuztA139rUVMcCWoM0tq2S1JnFh3kplxkSC3QLTIEhBcBlbINwXwlKgYcLATE5fR7eFwY3TcSmNnEKuIOliBp1GAs+0ZLDFD1ZR+hGmiOKeIefMzZ8vmoyDGzeEHFKALapLU9fqr+xD/W2AvPd5w30ZUv3bz7yBx4y5NhR1qlvZdR+dFdQK426ObyWfptL4mkc7PJHs47D0S1TtDc8Q7TH4jrxJkfiKU+cWfXcJWDomZVnsfQPocJYg2XPMbstQHRWYOlnVPci5UsEXmzxosjD/I4p1Wb6gBXEa21pxu4abhQraIx3fmzokav8a+hrRC--3aQgyb/0RcdkvuWC--zE2XmPmYd5thg8yPD1YbJA== \ No newline at end of file +tX8j9/8froZx8hSUXJ1zaO6Fmn3jkubOCTvLpVsEGPvTf11VOtyL2LE7Hlgfsx9W0ZEVLlD5cIu09okLO6LpiHUOiTKRxzO/z32c+hT6eZyvCcWRJJewvitfsNxnSL/NN/TN2YgrQC7yEYqR5qQ2oyTFnpyN8VlmyGRJQhjJ5U8DZbPcyFmQ/wO6s05/jxRQU016+/dHG4Pa+7q//I6VHNe9TWsUNo6coVpjGtzyvPdeigfC3l9VoM88bdKuXx9wvQMu0usHhFDLkt5u18AW3A346+2xN+NJegVXH0Hr1NbhVPCNLkSYwy083ismXwisVj1bofENRzt0mf5Yjj8t2KsdtzQ+erXq9wb4scgx5U76hHC8qZXC36Z8S08k9Iq5Z7dECWzafXTiHjqkbjXifn/Zd0PcwGqj93NFOeIreP4SxWWYhic0lte2Ulx2vgLr6KV5U55syDQP5cSAqY8fA1OwcDseUN1O2JGLvLfI17+fjD/rbv5N1mzIV23l9NkyLsHySzyFW05wiPrqH4a2dM8uadhAsfr5g7PdbDfomF3JdVzP5ZNynUh4tzqdlXx6/dfZqcYs+7IWjNHRFrS9vd5D6mwI79QkxaGMVhXCY21IPADcTJI2avDqnJbxcb7cYmtj/zzZF2Qr4pHYHq8=--JXt6Z4gua+B0tGrh--iUcqTF0pAQA4ExORRzEx3w== \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index 4e01725..707624e 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,5 +1,15 @@ Rails.application.routes.draw do + namespace :api do + namespace :v1 do + defaults format: :json do + get '/ping', to: 'base#ping' + resources :api_keys, path: 'api-keys', only: %i[index create destroy] + resources :checks, only: [:create] + end + end + end + # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html root "static_pages#home" diff --git a/db/migrate/20220124181635_create_api_keys.rb b/db/migrate/20220124181635_create_api_keys.rb new file mode 100644 index 0000000..27005d1 --- /dev/null +++ b/db/migrate/20220124181635_create_api_keys.rb @@ -0,0 +1,15 @@ +class CreateApiKeys < ActiveRecord::Migration[7.0] + def up + create_table :api_keys do |t| + t.string :token + t.references :bearer, polymorphic: true, null: false + end + + add_index :api_keys, [:bearer_id, :bearer_type] + add_index :api_keys, :token, unique: true + end + + def down + drop_table :api_keys + end +end diff --git a/db/schema.rb b/db/schema.rb index a0a4b9e..3ef8ea5 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,16 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2022_01_23_074307) do +ActiveRecord::Schema.define(version: 2022_01_24_181635) do + + create_table "api_keys", force: :cascade do |t| + t.string "token" + t.string "bearer_type", null: false + t.integer "bearer_id", null: false + t.index ["bearer_id", "bearer_type"], name: "index_api_keys_on_bearer_id_and_bearer_type" + t.index ["bearer_type", "bearer_id"], name: "index_api_keys_on_bearer" + t.index ["token"], name: "index_api_keys_on_token", unique: true + end create_table "checks", force: :cascade do |t| t.string "name"