Skip to content
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,15 @@ 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.token_generator = Passwordless::ShortTokenGenerator.new
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.
Expand Down Expand Up @@ -346,6 +355,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.
Expand Down
15 changes: 9 additions & 6 deletions app/models/passwordless/session.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,15 @@ 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)
Passwordless.config.case_insensitive_tokens ? modified_token = token.upcase : modified_token = token
self.token_digest = Passwordless.digest(modified_token)
@token = (modified_token)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should do anything at write time. Rather, let's just upcase in SQL when searching for tokens

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def token=(token)
Passwordless.config.case_insensitive_tokens ? modified_token = token.upcase : modified_token = token
self.token_digest = Passwordless.digest(modified_token)
@token = (modified_token)
def token=(plaintext)
self.token_digest = Passwordless.digest(plaintext)
@token = (plaintext)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we have to do this at write time - we're persisting the token digest, not the token, so I believe we cannot upcase the token digest when searching for tokens.

end

def authenticate(token)
token_digest == Passwordless.digest(token)
Passwordless.config.case_insensitive_tokens ? modified_token = token.upcase : modified_token = token
token_digest == Passwordless.digest(modified_token)
end

def expired?
Expand Down Expand Up @@ -81,8 +83,9 @@ def set_defaults

self.token, self.token_digest = loop {
token = Passwordless.config.token_generator.call(self)
digest = Passwordless.digest(token)
break [token, digest] if token_digest_available?(digest)
Passwordless.config.case_insensitive_tokens ? modified_token = token.upcase : modified_token = token
digest = Passwordless.digest(modified_token)
break [modified_token, digest] if token_digest_available?(digest)
}
end
end
Expand Down
1 change: 1 addition & 0 deletions lib/passwordless/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
26 changes: 26 additions & 0 deletions test/models/passwordless/session_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -69,6 +87,14 @@ def call(_session)
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
old_expires_at = Passwordless.config.expires_at
Expand Down