Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Case insensitive tokens #249

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
8 changes: 5 additions & 3 deletions app/models/passwordless/session.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ def token=(plaintext)
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 +82,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
Loading