Add access token authentication via HTTP AUTHORIZATION bearer header
This commit is contained in:
committed by
Stanko K.R.
parent
ea697b7143
commit
a81c681a8d
@@ -46,7 +46,7 @@ module Authentication
|
|||||||
end
|
end
|
||||||
|
|
||||||
def resume_session
|
def resume_session
|
||||||
if session = find_session_by_cookie
|
if session = find_session_by_cookie || find_or_start_session_by_bearer_token
|
||||||
set_current_session session
|
set_current_session session
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -55,12 +55,30 @@ module Authentication
|
|||||||
Session.find_signed(cookies.signed[:session_token])
|
Session.find_signed(cookies.signed[:session_token])
|
||||||
end
|
end
|
||||||
|
|
||||||
def request_authentication
|
def find_or_start_session_by_bearer_token
|
||||||
if Current.account.present?
|
if request_authorized_by_bearer_token?
|
||||||
session[:return_to_after_authenticating] = request.url
|
Identity::AccessToken.find_by(token: authorization_bearer_token)&.session
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
redirect_to_login_url
|
def request_authorized_by_bearer_token?
|
||||||
|
request.authorization.to_s.starts_with? "Bearer"
|
||||||
|
end
|
||||||
|
|
||||||
|
def authorization_bearer_token
|
||||||
|
request.authorization.to_s.split(" ", 2).second
|
||||||
|
end
|
||||||
|
|
||||||
|
def request_authentication
|
||||||
|
if request_authorized_by_bearer_token?
|
||||||
|
head :unauthorized
|
||||||
|
else
|
||||||
|
if Current.account.present?
|
||||||
|
session[:return_to_after_authenticating] = request.url
|
||||||
|
end
|
||||||
|
|
||||||
|
redirect_to_login_url
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def after_authentication_url
|
def after_authentication_url
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
class Identity < ApplicationRecord
|
class Identity < ApplicationRecord
|
||||||
include Joinable, Transferable
|
include Joinable, Transferable
|
||||||
|
|
||||||
|
has_many :access_tokens, dependent: :destroy
|
||||||
has_many :magic_links, dependent: :destroy
|
has_many :magic_links, dependent: :destroy
|
||||||
has_many :sessions, dependent: :destroy
|
has_many :sessions, dependent: :destroy
|
||||||
has_many :users, dependent: :nullify
|
has_many :users, dependent: :nullify
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
class Identity::AccessToken < ApplicationRecord
|
||||||
|
belongs_to :identity
|
||||||
|
|
||||||
|
has_secure_token
|
||||||
|
enum :permission, %w[ read write ].index_by(&:itself), default: :read
|
||||||
|
|
||||||
|
def session
|
||||||
|
identity.sessions.find_or_create_by! user_agent: session_user_agent
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
# Overload the user_agent identification for access token session reuse.
|
||||||
|
# This allows us to easily reuse a single session record per access token.
|
||||||
|
def session_user_agent
|
||||||
|
"access-token-#{id}"
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
class CreateIdentityAccessTokens < ActiveRecord::Migration[8.2]
|
||||||
|
def change
|
||||||
|
create_table :identity_access_tokens, id: :uuid do |t|
|
||||||
|
t.uuid :identity_id, null: false
|
||||||
|
t.string :token
|
||||||
|
t.string :permission
|
||||||
|
t.text :description
|
||||||
|
|
||||||
|
t.timestamps
|
||||||
|
|
||||||
|
t.index ["identity_id"], name: "index_access_token_on_identity_id"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
Generated
+10
@@ -321,6 +321,16 @@ ActiveRecord::Schema[8.2].define(version: 2025_12_05_010536) do
|
|||||||
t.index ["email_address"], name: "index_identities_on_email_address", unique: true
|
t.index ["email_address"], name: "index_identities_on_email_address", unique: true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
create_table "identity_access_tokens", id: :uuid, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||||
|
t.datetime "created_at", null: false
|
||||||
|
t.text "description"
|
||||||
|
t.uuid "identity_id", null: false
|
||||||
|
t.string "permission"
|
||||||
|
t.string "token"
|
||||||
|
t.datetime "updated_at", null: false
|
||||||
|
t.index ["identity_id"], name: "index_access_token_on_identity_id"
|
||||||
|
end
|
||||||
|
|
||||||
create_table "magic_links", id: :uuid, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
create_table "magic_links", id: :uuid, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||||
t.string "code", null: false
|
t.string "code", null: false
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
|
|||||||
@@ -321,6 +321,16 @@ ActiveRecord::Schema[8.2].define(version: 2025_12_05_010536) do
|
|||||||
t.index ["email_address"], name: "index_identities_on_email_address", unique: true
|
t.index ["email_address"], name: "index_identities_on_email_address", unique: true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
create_table "identity_access_tokens", id: :uuid, force: :cascade do |t|
|
||||||
|
t.datetime "created_at", null: false
|
||||||
|
t.text "description", limit: 65535
|
||||||
|
t.uuid "identity_id", null: false
|
||||||
|
t.string "permission", limit: 255
|
||||||
|
t.string "token", limit: 255
|
||||||
|
t.datetime "updated_at", null: false
|
||||||
|
t.index ["identity_id"], name: "index_access_token_on_identity_id"
|
||||||
|
end
|
||||||
|
|
||||||
create_table "magic_links", id: :uuid, force: :cascade do |t|
|
create_table "magic_links", id: :uuid, force: :cascade do |t|
|
||||||
t.string "code", limit: 255, null: false
|
t.string "code", limit: 255, null: false
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
require "test_helper"
|
||||||
|
|
||||||
|
class ApiTest < ActionDispatch::IntegrationTest
|
||||||
|
test "authenticate with valid access token" do
|
||||||
|
get boards_path(format: :json), env: bearer_token_env(identity_access_tokens(:jasons_api_token).token)
|
||||||
|
assert_response :success
|
||||||
|
end
|
||||||
|
|
||||||
|
test "fail to authenticate with invalid access token" do
|
||||||
|
get boards_path(format: :json), env: bearer_token_env("nonsense")
|
||||||
|
assert_response :unauthorized
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def bearer_token_env(token)
|
||||||
|
{ "HTTP_AUTHORIZATION" => "Bearer #{token}" }
|
||||||
|
end
|
||||||
|
end
|
||||||
+5
@@ -0,0 +1,5 @@
|
|||||||
|
jasons_api_token:
|
||||||
|
identity: jason
|
||||||
|
token: 018cf1425682700098f24f0799e3fe20
|
||||||
|
description: My Superscript
|
||||||
|
permission: read
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
require "test_helper"
|
||||||
|
|
||||||
|
class Identity::AccessTokenTest < ActiveSupport::TestCase
|
||||||
|
test "only one session at the time" do
|
||||||
|
assert_changes -> { Session.count }, +1 do
|
||||||
|
identity_access_tokens(:jasons_api_token).session
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_no_changes -> { Session.count } do
|
||||||
|
identity_access_tokens(:jasons_api_token).session
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
Reference in New Issue
Block a user