From 90ce4342e409b0513833d62853663146577cd498 Mon Sep 17 00:00:00 2001 From: Mikkel Malmberg Date: Fri, 16 Jun 2023 12:11:54 +0200 Subject: [PATCH] Store encrypted tokens --- .../passwordless/sessions_controller.rb | 4 ++-- app/models/passwordless/session.rb | 14 +++++++++++--- ...71104221735_create_passwordless_sessions.rb | 2 +- lib/passwordless.rb | 5 +++++ lib/passwordless/token_digest.rb | 18 ++++++++++++++++++ test/dummy/db/schema.rb | 2 +- test/fixtures/passwordless/sessions.yml | 6 +++--- test/integration/navigation_test.rb | 7 +++---- test/models/passwordless/session_test.rb | 2 ++ test/passwordless_test.rb | 8 +++++++- test/token_digest_test.rb | 13 +++++++++++++ 11 files changed, 66 insertions(+), 15 deletions(-) create mode 100644 lib/passwordless/token_digest.rb create mode 100644 test/token_digest_test.rb diff --git a/app/controllers/passwordless/sessions_controller.rb b/app/controllers/passwordless/sessions_controller.rb index 8a411a4..d69c364 100644 --- a/app/controllers/passwordless/sessions_controller.rb +++ b/app/controllers/passwordless/sessions_controller.rb @@ -31,7 +31,7 @@ def create end end - flash[:notice] = I18n.t('passwordless.sessions.create.email_sent_if_record_found') + flash[:notice] = I18n.t("passwordless.sessions.create.email_sent_if_record_found") redirect_to(sign_in_path) end @@ -119,7 +119,7 @@ def find_authenticatable def passwordless_session @passwordless_session ||= Session.find_by!( authenticatable_type: authenticatable_classname, - token: params[:token] + token_digest: Passwordless.digest(params[:token]) ) end end diff --git a/app/models/passwordless/session.rb b/app/models/passwordless/session.rb index f4da5ba..3912472 100644 --- a/app/models/passwordless/session.rb +++ b/app/models/passwordless/session.rb @@ -16,12 +16,16 @@ class Session < ApplicationRecord :expires_at, :user_agent, :remote_addr, - :token, + :token_digest, presence: true ) before_validation :set_defaults + # save the token in memory so we can put it in emails but only save the + # hashed version in the database + attr_accessor :token + scope( :available, lambda { where("expires_at > ?", Time.current) } @@ -61,9 +65,13 @@ def available? def set_defaults self.expires_at ||= Passwordless.expires_at.call self.timeout_at ||= Passwordless.timeout_at.call - self.token ||= loop { + + return if self.token || self.token_digest + + self.token, self.token_digest = loop { token = Passwordless.token_generator.call(self) - break token unless Session.find_by(token: token) + digest = Passwordless.digest(token) + break [token, digest] unless Session.find_by(token_digest: digest) } end end diff --git a/db/migrate/20171104221735_create_passwordless_sessions.rb b/db/migrate/20171104221735_create_passwordless_sessions.rb index 2b4546d..4b0c913 100644 --- a/db/migrate/20171104221735_create_passwordless_sessions.rb +++ b/db/migrate/20171104221735_create_passwordless_sessions.rb @@ -14,7 +14,7 @@ def change t.datetime(:claimed_at) t.text(:user_agent, null: false) t.string(:remote_addr, null: false) - t.string(:token, null: false) + t.string(:token_digest, null: false) t.timestamps end diff --git a/lib/passwordless.rb b/lib/passwordless.rb index be196f3..08a2f29 100644 --- a/lib/passwordless.rb +++ b/lib/passwordless.rb @@ -3,10 +3,15 @@ require "active_support" require "passwordless/errors" require "passwordless/engine" +require "passwordless/token_digest" require "passwordless/url_safe_base_64_generator" # The main Passwordless module module Passwordless + def self.digest(token) + TokenDigest.new(token).digest + end + mattr_accessor(:parent_mailer) { "ActionMailer::Base" } mattr_accessor(:default_from_address) { "CHANGE_ME@example.com" } mattr_accessor(:token_generator) { UrlSafeBase64Generator.new } diff --git a/lib/passwordless/token_digest.rb b/lib/passwordless/token_digest.rb new file mode 100644 index 0000000..107d967 --- /dev/null +++ b/lib/passwordless/token_digest.rb @@ -0,0 +1,18 @@ +module Passwordless + class TokenDigest + ALGORITHM = "SHA256" + + def initialize(str) + @str = str + end + + def digest + key = self.class.key() + OpenSSL::HMAC.hexdigest(ALGORITHM, key, @str) + end + + def self.key + @key ||= ActiveSupport::KeyGenerator.new(Rails.application.secret_key_base).generate_key("passwordless") + end + end +end diff --git a/test/dummy/db/schema.rb b/test/dummy/db/schema.rb index e719ae5..e227d0a 100644 --- a/test/dummy/db/schema.rb +++ b/test/dummy/db/schema.rb @@ -19,7 +19,7 @@ t.datetime "claimed_at", precision: nil t.text "user_agent", null: false t.string "remote_addr", null: false - t.string "token", null: false + t.string "token_digest", null: false t.datetime "created_at", precision: nil, null: false t.datetime "updated_at", precision: nil, null: false t.index ["authenticatable_type", "authenticatable_id"], name: "authenticatable" diff --git a/test/fixtures/passwordless/sessions.yml b/test/fixtures/passwordless/sessions.yml index 6514b0f..2ed5f1e 100644 --- a/test/fixtures/passwordless/sessions.yml +++ b/test/fixtures/passwordless/sessions.yml @@ -5,19 +5,19 @@ one: expires_at: 2017-11-04 23:17:35 user_agent: MyText remote_addr: MyString - token: MyString + token_digest: MyString two: timeout_at: 2017-11-04 23:17:35 expires_at: 2017-11-04 23:17:35 user_agent: MyText remote_addr: MyString - token: MyString + token_digest: MyString john: timeout_at: 2017-11-04 23:17:35 expires_at: 2017-11-04 23:17:35 user_agent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.62 Safari/537.36" remote_addr: 127.0.0.1 - token: 3veDCdFw4VedgZtZZkCJ7um7rsc2bFNCZGB51Iih024 + token_digest: 3veDCdFw4VedgZtZZkCJ7um7rsc2bFNCZGB51Iih024 authenticatable: john (User) diff --git a/test/integration/navigation_test.rb b/test/integration/navigation_test.rb index 9165f75..2f0a5ce 100644 --- a/test/integration/navigation_test.rb +++ b/test/integration/navigation_test.rb @@ -25,9 +25,7 @@ class NavigationTest < ActionDispatch::IntegrationTest # Submit form post( "/users/sign_in", - params: { - passwordless: {email: alice.email} - }, + params: {passwordless: {email: alice.email}}, headers: {"HTTP_USER_AGENT" => "Mosaic v.1"} ) @@ -42,9 +40,10 @@ class NavigationTest < ActionDispatch::IntegrationTest assert_equal 1, ActionMailer::Base.deliveries.count email = ActionMailer::Base.deliveries.first assert_equal alice.email, email.to.first + token = email.body.match(/sign_in\/([\w\-_]+)/)[1] # Expect mail body to include session link - token_sign_in_path = "/users/sign_in/#{user_session.token}" + token_sign_in_path = "/users/sign_in/#{token}" assert email.body.to_s.include?(token_sign_in_path) # Follow link, Expect redirect to /secret path which has been unsuccessfully diff --git a/test/models/passwordless/session_test.rb b/test/models/passwordless/session_test.rb index 98f3516..1aada86 100644 --- a/test/models/passwordless/session_test.rb +++ b/test/models/passwordless/session_test.rb @@ -46,6 +46,7 @@ def create_session(attrs = {}) refute_nil session.expires_at refute_nil session.timeout_at refute_nil session.token + refute_nil session.token_digest end test("with a custom token generator") do @@ -62,6 +63,7 @@ def call(_session) session.validate assert_equal "ALWAYS ME", session.token + assert_equal Passwordless.digest("ALWAYS ME"), session.token_digest Passwordless.token_generator = old_generator end diff --git a/test/passwordless_test.rb b/test/passwordless_test.rb index 2bec0fc..a86b248 100644 --- a/test/passwordless_test.rb +++ b/test/passwordless_test.rb @@ -3,6 +3,12 @@ require "test_helper" module Passwordless - class Test < ActiveSupport::TestCase + class PasswordlessTest < ActiveSupport::TestCase + test(".digest(str)") do + a = Passwordless.digest("string") + b = Passwordless.digest("string") + + assert_equal a, b + end end end diff --git a/test/token_digest_test.rb b/test/token_digest_test.rb new file mode 100644 index 0000000..c686d57 --- /dev/null +++ b/test/token_digest_test.rb @@ -0,0 +1,13 @@ +require "test_helper" + +module Passwordless + class DigestTest < ActiveSupport::TestCase + test("generates a digest") do + a = TokenDigest.new("mystring").digest + b = TokenDigest.new("mystring").digest + + assert a.length > 10 + assert_equal a, b + end + end +end