diff --git a/README.md b/README.md index d684185..4d935ac 100644 --- a/README.md +++ b/README.md @@ -296,6 +296,14 @@ end Passwordless will keep generating tokens until it finds one that hasn't been used yet. So be sure to use some kind of method where matches are unlikely. +If your app should not distinguish between lowercase and uppercase letters in tokens, `Passwordless.config.case_insensitive_tokens` may be set to `true`. + +```ruby +Passwordless.configure do |config| + config.case_insensitive_tokens = true # allows `abc123` and `AbC123` to match `ABC123` +end +``` + ### Timeout and Expiry The _timeout_ is the time by which the generated token and magic link is invalidated. After this the token cannot be used to sign in to your app and the user will need to request a new token. @@ -346,6 +354,16 @@ class User < ApplicationRecord end ``` +## Testing + +To run tests locally on a fork of this repository: + +``` +$ bundle +$ bin/rails db:create RAILS_ENV=test +$ bin/rails test +``` + ## Test helpers To help with testing, a set of test helpers are provided. diff --git a/app/models/passwordless/session.rb b/app/models/passwordless/session.rb index f9c6d39..2eb3b8a 100644 --- a/app/models/passwordless/session.rb +++ b/app/models/passwordless/session.rb @@ -32,12 +32,14 @@ class Session < ApplicationRecord # hashed version in the database attr_reader :token - def token=(plaintext) - self.token_digest = Passwordless.digest(plaintext) - @token = (plaintext) + def token=(token) + token = token.upcase if Passwordless.config.case_insensitive_tokens + self.token_digest = Passwordless.digest(token) + @token = token end def authenticate(token) + token = token.upcase if Passwordless.config.case_insensitive_tokens token_digest == Passwordless.digest(token) end @@ -81,6 +83,7 @@ def set_defaults self.token, self.token_digest = loop { token = Passwordless.config.token_generator.call(self) + token = token.upcase if Passwordless.config.case_insensitive_tokens digest = Passwordless.digest(token) break [token, digest] if token_digest_available?(digest) } diff --git a/lib/passwordless/config.rb b/lib/passwordless/config.rb index 7b0c472..65b84f9 100644 --- a/lib/passwordless/config.rb +++ b/lib/passwordless/config.rb @@ -32,6 +32,7 @@ class Configuration option :parent_mailer, default: "ActionMailer::Base" option :restrict_token_reuse, default: true option :token_generator, default: ShortTokenGenerator.new + option :case_insensitive_tokens, default: false option :combat_brute_force_attacks, default: !Rails.env.test? option :expires_at, default: lambda { 1.year.from_now } diff --git a/test/models/passwordless/session_test.rb b/test/models/passwordless/session_test.rb index 3acbecc..119dcf3 100644 --- a/test/models/passwordless/session_test.rb +++ b/test/models/passwordless/session_test.rb @@ -16,6 +16,24 @@ class SessionTest < ActiveSupport::TestCase refute session.authenticate("no") end + test("authenticate with case insensitive tokens") do + Passwordless.config.case_insensitive_tokens = true + session = create_session(token: "hi123") + + assert session.authenticate("hi123") + assert session.authenticate("Hi123") + assert session.authenticate("HI123") + refute session.authenticate("no123") + + Passwordless.config.case_insensitive_tokens = false + session = create_session(token: "hi123") + + assert session.authenticate("hi123") + refute session.authenticate("Hi123") + refute session.authenticate("HI123") + refute session.authenticate("no123") + end + test("#expired?") do expired_session = create_session(expires_at: 1.hour.ago) @@ -68,6 +86,14 @@ def call(_session) assert_equal "hi", session.token assert_equal Passwordless.digest("hi"), session.token_digest end + + test("setting token manually when case insensitive") do + Passwordless.config.case_insensitive_tokens = true + session = Session.new(token: "hi") + assert_equal "hi".upcase, session.token + assert_equal Passwordless.digest("hi".upcase), session.token_digest + Passwordless.config.case_insensitive_tokens = false + end test("with a custom expire at function") do custom_expire_at = Time.parse("01-01-2100").utc