Coder Social home page Coder Social logo

mikker / passwordless Goto Github PK

View Code? Open in Web Editor NEW
1.2K 13.0 85.0 356 KB

πŸ— Authentication for your Rails app without the icky-ness of passwords

License: MIT License

Ruby 88.59% HTML 8.90% JavaScript 1.34% CSS 1.18%
ruby rails passwordless authentication engine

passwordless's Introduction

Passwordless

CI Rubygems codecov

Add authentication to your Rails app without all the icky-ness of passwords. Magic link authentication, if you will. We call it passwordless.


Installation

Add to your bundle and copy over the migrations:

$ bundle add passwordless
$ bin/rails passwordless_engine:install:migrations

Upgrading

See Upgrading to Passwordless 1.0 for more details.

Usage

Passwordless creates a single model called Passwordless::Session, so it doesn't come with its own user model. Instead, it expects you to provide one, with an email field in place. If you don't yet have a user model, check out the wiki on creating the user model.

Enable Passwordless on your user model by pointing it to the email field:

class User < ApplicationRecord
  # your other code..

  passwordless_with :email # <-- here! this needs to be a column in `users` table

  # more of your code..
end

Then mount the engine in your routes:

Rails.application.routes.draw do
  passwordless_for :users

  # other routes
end

Getting the current user, restricting access, the usual

Passwordless doesn't give you current_user automatically. Here's how you could add it:

class ApplicationController < ActionController::Base
  include Passwordless::ControllerHelpers # <-- This!

  # ...

  helper_method :current_user

  private

  def current_user
    @current_user ||= authenticate_by_session(User)
  end

  def require_user!
    return if current_user
    save_passwordless_redirect_location!(User) # <-- optional, see below
    redirect_to root_path, alert: "You are not worthy!"
  end
end

Et voilΓ :

class VerySecretThingsController < ApplicationController
  before_action :require_user!

  def index
    @things = current_user.very_secret_things
  end
end

Providing your own templates

To make Passwordless look like your app, override the bundled views by adding your own. You can manually copy the specific views that you need or copy them to your application with rails generate passwordless:views.

Passwordless has 2 action views and 1 mailer view:

# the form where the user inputs their email address
app/views/passwordless/sessions/new.html.erb
# the form where the user inputs their just received token
app/views/passwordless/sessions/show.html.erb
# the email with the token and magic link
app/views/passwordless/mailer/sign_in.text.erb

See the bundled views.

Registering new users

Because your User record is like any other record, you create one like you normally would. Passwordless provides a helper method to sign in the created user after it is saved – like so:

class UsersController < ApplicationController
  include Passwordless::ControllerHelpers # <-- This!
  # (unless you already have it in your ApplicationController)

  def create
    @user = User.new(user_params)

    if @user.save
      sign_in(create_passwordless_session(@user)) # <-- This!
      redirect_to(@user, flash: { notice: 'Welcome!' })
    else
      render(:new)
    end
  end

  # ...
end

URLs and links

By default, Passwordless uses the resource name given to passwordless_for to generate its routes and helpers.

passwordless_for :users
  # <%= users_sign_in_path %> # => /users/sign_in

passwordless_for :users, at: '/', as: :auth
  # <%= auth_sign_in_path %> # => /sign_in

Also be sure to specify ActionMailer's default_url_options.host and tell the routes as well:

# config/application.rb for example:
config.action_mailer.default_url_options = {host: "www.example.com"}
routes.default_url_options[:host] ||= "www.example.com"

Configuration

To customize Passwordless, create a file config/initializers/passwordless.rb.

The default values are shown below. It's recommended to only include the ones that you specifically want to modify.

Passwordless.configure do |config|
  config.default_from_address = "[email protected]"
  config.parent_controller = "ApplicationController"
  config.parent_mailer = "ActionMailer::Base"
  config.restrict_token_reuse = false # Can a token/link be used multiple times?
  config.token_generator = Passwordless::ShortTokenGenerator.new # Used to generate magic link tokens.

  config.expires_at = lambda { 1.year.from_now } # How long until a signed in session expires.
  config.timeout_at = lambda { 10.minutes.from_now } # How long until a token/magic link times out.

  config.redirect_back_after_sign_in = true # When enabled the user will be redirected to their previous page, or a page specified by the `destination_path` query parameter, if available.
  config.redirect_to_response_options = {} # Additional options for redirects.
  config.success_redirect_path = '/' # After a user successfully signs in
  config.failure_redirect_path = '/' # After a sign in fails
  config.sign_out_redirect_path = '/' # After a user signs out

  config.paranoid = false # Display email sent notice even when the resource is not found.
end

Delivery method

By default, Passwordless sends emails. See Providing your own templates. If you need to customize this further, you can do so in the after_session_save callback.

In config/initializers/passwordless.rb:

Passwordless.configure do |config|
  config.after_session_save = lambda do |session, request|
    # Default behavior is
    # Passwordless::Mailer.sign_in(session).deliver_now

    # You can change behavior to do something with session model. For example,
    # SmsApi.send_sms(session.authenticatable.phone_number, session.token)
  end
end

Token generation

By default Passwordless generates short, 6-digit, alpha numeric tokens. You can change the generator using Passwordless.config.token_generator to something else that responds to call(session) eg.:

Passwordless.configure do |config|
  config.token_generator = lambda do |session|
    "probably-stupid-token-#{session.user_agent}-#{Time.current}"
  end
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.

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.

The expiry is the expiration time of the session of a logged in user. Once this is expired, the user is signed out.

Note: Passwordless' session relies on Rails' own session and so will never live longer than that.

To configure your Rails session, in config/initializers/session_store.rb:

Rails.application.config.session_store :cookie_store,
  expire_after: 1.year,
  # ...

Redirection after sign-in

By default Passwordless will redirect back to where the user wanted to go if it knows where that is -- so you'll have to help it. Passwordless::ControllerHelpers provide a method:

class ApplicationController < ActionController::Base
  include Passwordless::ControllerHelpers # <-- Probably already have this!

  # ...

  def require_user!
    return if current_user
    save_passwordless_redirect_location!(User) # <-- this one!
    redirect_to root_path, alert: "You are not worthy!"
  end
end

This can also be turned off with Passwordless.config.redirect_back_after_sign_in = false.

Looking up the user

By default Passwordless uses the passwordless_with column to case insensitively fetch the user resource.

You can override this by defining a class method fetch_resource_for_passwordless in your user model. This method will be called with the down-cased, stripped email and should return an ActiveRecord instance.

class User < ApplicationRecord
  def self.fetch_resource_for_passwordless(email)
    find_or_create_by(email: email)
  end
end

Test helpers

To help with testing, a set of test helpers are provided.

If you are using RSpec, add the following line to your spec/rails_helper.rb:

require "passwordless/test_helpers"

If you are using TestUnit, add this line to your test/test_helper.rb:

require "passwordless/test_helpers"

Then in your controller, request, and system tests/specs, you can utilize the following methods:

passwordless_sign_in(user) # signs you in as a user
passwordless_sign_out # signs out user

Security considerations

There's no reason that this approach should be less secure than the usual username/password combo. In fact this is most often a more secure option, as users don't get to choose the horrible passwords they can't seem to stop using. In a way, this is just the same as having each user go through "Forgot password" on every login.

But be aware that when everyone authenticates via emails, the way you send those mails becomes a weak spot. Email services usually provide a log of all the mails you send so if your email delivery provider account is compromised, every user in the system is as well. (This is the same for "Forgot password".) Reddit was once compromised using this method.

Ideally you should set up your email provider to not log these mails. And be sure to turn on non-SMS 2-factor authentication if your provider supports it.

Alternatives

  • OTP JWT -- Passwordless JSON Web Tokens

License

MIT

passwordless's People

Contributors

andreaskundig avatar avinoth avatar bcasci avatar dependabot-preview[bot] avatar dependabot-support avatar dependabot[bot] avatar digerata avatar fermion avatar henrikbjorn avatar joesouthan avatar kevinbongart avatar lucianghinda avatar luiscobot avatar madogiwa0124 avatar mftaff avatar mikker avatar nickhammond avatar olimart avatar olleolleolle avatar paulrbr avatar petergoldstein avatar puffo avatar pusewicz avatar rickychilcott avatar robinsk1 avatar stephenson avatar uysim avatar yenshirak avatar ykpythemind avatar yshmarov avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

passwordless's Issues

Make sessions (actually) expire

Sessions don't expire right now. They get expires_at set to a year in the future but we don't really use that anywhere. It's meant to make the cookies expire at some point, forcing the user to sign in again. Probably a good idea.

(NB: Expiry is different than timeout. Timeout is sign in using this token before. Expiry is sign user out automatically after)

Now, we could do this by setting an expiry on the cookie itself. But I'm not sure. I think I'd like to check it programmatically, so we can alert the user of the reason they need to sign in again, e.g Your session has expired, please sign in again. I think the best place to do this for now is in the sessions controller.

Alternative passwordless_with options

Hi @mikker, I'm looking to replace my use of devise with a passwordless option, and this gem is looking really promising. Thanks for the great work! I have a question about capabilities. Does this library support the use case of multiple options for matching on the authentication? What I mean is, let's say I have both email and phone attribute on my User table, and I want to let my users enter either their email OR phone to receive their magic link (via email and SMS respectively). Would this just be a matter of declaring both as passwordless_with, like:

class User < ApplicationRecord
  passwordless_with :email
  passwordless_with :phone
end

or is it more complicated than this?

Another question, how easy would it be to support a temporary code sort of auth, instead of magic links? If I were to add SMS support, I'd prefer to send a 6 digit code which then the user would enter into the app to get their session, rather than a magic link, because they might be trying to sign in on a different device than their phone.

Thanks again for this great gem!

Dependabot can't resolve your Ruby dependency files

Dependabot can't resolve your Ruby dependency files.

As a result, Dependabot couldn't update your dependencies.

The error Dependabot encountered was:

Bundler::VersionConflict with message: Bundler could not find compatible versions for gem "url":
  In Gemfile:
    codecov (>= 0, <= 0.1.14) was resolved to 0.1.14, which depends on
      url

Could not find gem 'url', which is required by gem 'codecov (>= 0, <= 0.1.14)', in any of the sources.

If you think the above is an error on Dependabot's side please don't hesitate to get in touch - we'll do whatever we can to fix it.

You can mention @dependabot in the comments below to contact the Dependabot team.

Create auth token links for email

I'm not sure if there is a preferred way of doing this or not. But what I want to be able to do is send an email to a user that wasn't user initiated that automatically signs them in. For example I want to email the user to prompt them to update a post and I want that link to be authenticated so if they aren't signed in they don't have to go through the sign in flow and make multiple trips to their email.

My idea was to use an auth_token in the url that is also checked in current_user.

Example:

1. Initiate Email Link

user = User.first # some user I am emailing
session = Passwordless::Session.create(
  remote_addr: 'generated_on_users_behalf',
  user_agent: 'generated_on_users_behalf',
  authenticatable: user
)
url = "https://example.com/posts/new?auth_token=#{session.token}

2. Link send via email

<a href="https://example.com/posts/new?auth_token=xxxxxxxxxx">Update Post</a>

3. Lookup via session or auth token

def current_user
  @current_user ||= authenticate_by_session(User) || authenticate_by_token(params[:auth_token])
end

def authenticate_by_token(token)
  return if token.blank?

  # For brute force, first checks if token is blank so this doesn't slow down every request
  BCrypt::Password.create(token)
  session = Passwordless::Session.find_by(token: token)
  return if session.blank?

  sign_in session
  session.authenticatable
end

What I like about this is you can auth in with an auth token in any action that uses require_user! and once rails signs them in with the auth token then they will be signed in and future requests will short circuit to the authenticate_by_session like normal.

This may be the preferred way of doing this, Im just not sure if there was a better way or a way it could be built into the project going forward. Open to suggestions and feedback, thanks for building a great package, really enjoying it! πŸ˜ƒ

Store the generated tokens as hashed (BCrypt)

Hello, I'd like to use this library but I'm concerned about the fact that the tokens are stored in plain text in the database. For their duration (by default, one hour) they are essentially passwords. I would have expected them to be hashed or at least encrypted before being stored in the database, like a password, no?

I imagine it's less of an issue because they're short-lived, but I guess if an attacker has read access to the database and initiates a login of one of the users, the attacker can get the newly generated token and login as that user. If it were hashed or encrypted, the attacker would need write access to the database and know what hashing/encryption algorithm is used.

I imagine I'm missing something, but passwordless is a pretty close analogue to password reset tokens, and devise encrypts those.

I'd love to know why that's not the case in this library, because otherwise it looks great!

Support for model that is not named "User"

I did not find the answer of this on the docs (I might have overlooked it).
I am trying to use passwordless, I do not have a model name User, the model used in the authentication is named Teacher.

So in the routes.rb, I got:
passwordless_for :teachers

but when I access to /teachers/sign_in/my-token I get:

NameError in Passwordless::SessionsController#show
uninitialized constant Passwordless::SessionsController::User

Is there a way to use a model NOT named User? if so what I am missing?

Note that the view /teachers/sign_in works as expected.

Thanks!

Don't isolate namespace

Isolating namespace adds a bit of confusion around the routes. For a gem like ours this probably adds more trouble than benefit.

Support I18n

Hey man,
I have been reading up on how to add I18n support to a gem, and mostly things seem pretty straightforward.
The main issue I have come up with is how to provide an easy way for someone using the gem to customize the YAML files - similar to how devise loads devise.en.yml into the main app's config/locales directory.

It seems like this happens in the gem's engine, but I haven't been able to find an example or documentation on how to do that. I have a feeling it is quite simple πŸ˜„

Have you done something like this before? Do you know of a resource I can turn to?

Thanks! I am looking forward to creating a PR for this, as soon as I sort out what steps are required

Add routes examples to README

It was a bit unclear how routes work in passwordless. I added passwordless_for :companies and expected something like company_sign_in_url.

$ rails routes command shows the following: sign_in GET /sign_in(.:format) passwordless/sessions#new. Only after a few hours of experimenting and reading through the source I realized I had to write companies.sign_in_path. Would be nice if we added some examples on generated routes.

Using an authenticatable with ids that are not ints doesn't work

We are trying to set up passwordless (which looks sweet and exactly what we need!) with an authenticable model that uses uuid for its id. Because the type of authenticatable_id in the passwordless_sessions is set to bigint, this results in our uuids being cast to integers, which leads to sessions not working, because the authenticatable id no longer matches an existing authenticatable.

I looked through the documentation but did not see a way to configure passwordless to use a uuid string type for its authenticatable_id. We could migrate passwordless_sessions to use a string for its authenticatable_id column, but that feels a little dirty, and I don't know if it will break anything else.

Is there a way to configure passwordless to use uuids as the authenticatable id?

Thx in advance. πŸ™

Save session.id in cookie instead of user.id to allow expiration

When we authenticate_by_cookie we find the user by the saved user_id in the cookies. This means we don't actually know when their session expires.

Instead we'll save the session.id and look up the user from that. That way we'll know if the session has expired too. We might even want to check for this on every sign-in-required request?

Errors on Rails 6 API Application

Trying to log in but I get the following error:

#<ActionController::RoutingError: undefined method `helper' for Passwordless::ApplicationController:Class>

Does this gem work on Rails 6 and/or API only applications?

Allow to config parent mailer

if we do like this

Password.setup do |config|
   config.parent_mailer = "Application"
end

And our mailer inherit from that parent_mailer
So we can allow user to custom options and layout by their base on rails config
We can also remove this remove this Passwordless.default_from_address = "[email protected]"

It got this concept from devise. If you are ok I can help

How to set routes with namespaces?

I'm trying to add passwordless routes to a API with a v1 namespace attached to my Users model, but running into errors:

routes.rb:

Rails.application.routes.draw do
  concern :api_base do
    passwordless_for :users, at: '/'

    resources :users, only: [:create]
  end

  namespace :v1 do
    concerns :api_base
  end
end

Response:

POST http://127.0.0.1:3000/v1/sign_in

HTTP/1.1 500 Internal Server Error
Content-Type: application/json; charset=UTF-8
X-Request-Id: 4da1b92e-4761-4e68-b0fc-b414b8b8203f
X-Runtime: 0.186245
Content-Length: 21664

{
  "status": 500,
  "error": "Internal Server Error",
  "exception": "#<NoMethodError: undefined method `users_path' for #<Module:0x0000558ec65c08f0>>",
  "traces": {
    "Application Trace": [],
    "Framework Trace": [
      {
        "exception_object_id": 32260,
        "id": 0,
        "trace": "actionpack (6.0.2.2) lib/action_dispatch/routing/mapper.rb:687:in `block in define_generate_prefix'"
      },
      {
        "exception_object_id": 32260,
        "id": 1,
        "trace": "actionpack (6.0.2.2) lib/action_dispatch/routing/routes_proxy.rb:40:in `token_sign_in_url'"
      },
      {
        "exception_object_id": 32260,
        "id": 2,
        "trace": "passwordless (fd1fef1088da) app/mailers/passwordless/mailer.rb:14:in `magic_link'"
      },
      {
        "exception_object_id": 32260,
        "id": 3,
        "trace": "actionpack (6.0.2.2) lib/abstract_controller/base.rb:196:in `process_action'"
      },
      {
        "exception_object_id": 32260,
        "id": 4,
        "trace": "actionpack (6.0.2.2) lib/abstract_controller/callbacks.rb:42:in `block in process_action'"
      },
      {
        "exception_object_id": 32260,
        "id": 5,
        "trace": "activesupport (6.0.2.2) lib/active_support/callbacks.rb:101:in `run_callbacks'"
      },
      {
        "exception_object_id": 32260,
        "id": 6,
        "trace": "actionpack (6.0.2.2) lib/abstract_controller/callbacks.rb:41:in `process_action'"
      },
      {
        "exception_object_id": 32260,
        "id": 7,
        "trace": "actionpack (6.0.2.2) lib/abstract_controller/base.rb:136:in `process'"
      },
      {
        "exception_object_id": 32260,
        "id": 8,
        "trace": "actionmailer (6.0.2.2) lib/action_mailer/rescuable.rb:25:in `block in process'"
      },
      {
        "exception_object_id": 32260,
        "id": 9,
        "trace": "actionmailer (6.0.2.2) lib/action_mailer/rescuable.rb:17:in `handle_exceptions'"
      },
      {
        "exception_object_id": 32260,
        "id": 10,
        "trace": "actionmailer (6.0.2.2) lib/action_mailer/rescuable.rb:24:in `process'"
      },
      {
        "exception_object_id": 32260,
        "id": 11,
        "trace": "actionview (6.0.2.2) lib/action_view/rendering.rb:39:in `process'"
      },
      {
        "exception_object_id": 32260,
        "id": 12,
        "trace": "actionmailer (6.0.2.2) lib/action_mailer/base.rb:637:in `block in process'"
      },
      {
        "exception_object_id": 32260,
        "id": 13,
        "trace": "activesupport (6.0.2.2) lib/active_support/notifications.rb:180:in `block in instrument'"
      },
      {
        "exception_object_id": 32260,
        "id": 14,
        "trace": "activesupport (6.0.2.2) lib/active_support/notifications/instrumenter.rb:24:in `instrument'"
      },
      {
        "exception_object_id": 32260,
        "id": 15,
        "trace": "activesupport (6.0.2.2) lib/active_support/notifications.rb:180:in `instrument'"
      },
      {
        "exception_object_id": 32260,
        "id": 16,
        "trace": "actionmailer (6.0.2.2) lib/action_mailer/base.rb:636:in `process'"
      },
      {
        "exception_object_id": 32260,
        "id": 17,
        "trace": "actionmailer (6.0.2.2) lib/action_mailer/message_delivery.rb:123:in `block in processed_mailer'"
      },
      {
        "exception_object_id": 32260,
        "id": 18,
        "trace": "actionmailer (6.0.2.2) lib/action_mailer/message_delivery.rb:122:in `tap'"
      },
      {
        "exception_object_id": 32260,
        "id": 19,
        "trace": "actionmailer (6.0.2.2) lib/action_mailer/message_delivery.rb:122:in `processed_mailer'"
      },
      {
        "exception_object_id": 32260,
        "id": 20,
        "trace": "actionmailer (6.0.2.2) lib/action_mailer/message_delivery.rb:113:in `deliver_now'"
      },
      {
        "exception_object_id": 32260,
        "id": 21,
        "trace": "passwordless (fd1fef1088da) lib/passwordless.rb:24:in `block (2 levels) in <module:Passwordless>'"
      },
      {
        "exception_object_id": 32260,
        "id": 22,
        "trace": "passwordless (fd1fef1088da) app/controllers/passwordless/sessions_controller.rb:28:in `create'"
      },
      {
        "exception_object_id": 32260,
        "id": 23,
        "trace": "actionpack (6.0.2.2) lib/action_controller/metal/basic_implicit_render.rb:6:in `send_action'"
      },
      {
        "exception_object_id": 32260,
        "id": 24,
        "trace": "actionpack (6.0.2.2) lib/abstract_controller/base.rb:196:in `process_action'"
      },
      {
        "exception_object_id": 32260,
        "id": 25,
        "trace": "actionpack (6.0.2.2) lib/action_controller/metal/rendering.rb:30:in `process_action'"
      },
      {
        "exception_object_id": 32260,
        "id": 26,
        "trace": "actionpack (6.0.2.2) lib/abstract_controller/callbacks.rb:42:in `block in process_action'"
      },
      {
        "exception_object_id": 32260,
        "id": 27,
        "trace": "activesupport (6.0.2.2) lib/active_support/callbacks.rb:101:in `run_callbacks'"
      },
      {
        "exception_object_id": 32260,
        "id": 28,
        "trace": "actionpack (6.0.2.2) lib/abstract_controller/callbacks.rb:41:in `process_action'"
      },
      {
        "exception_object_id": 32260,
        "id": 29,
        "trace": "actionpack (6.0.2.2) lib/action_controller/metal/rescue.rb:22:in `process_action'"
      },
      {
        "exception_object_id": 32260,
        "id": 30,
        "trace": "actionpack (6.0.2.2) lib/action_controller/metal/instrumentation.rb:33:in `block in process_action'"
      },
      {
        "exception_object_id": 32260,
        "id": 31,
        "trace": "activesupport (6.0.2.2) lib/active_support/notifications.rb:180:in `block in instrument'"
      },
      {
        "exception_object_id": 32260,
        "id": 32,
        "trace": "activesupport (6.0.2.2) lib/active_support/notifications/instrumenter.rb:24:in `instrument'"
      },
      {
        "exception_object_id": 32260,
        "id": 33,
        "trace": "activesupport (6.0.2.2) lib/active_support/notifications.rb:180:in `instrument'"
      },
      {
        "exception_object_id": 32260,
        "id": 34,
        "trace": "actionpack (6.0.2.2) lib/action_controller/metal/instrumentation.rb:32:in `process_action'"
      },
      {
        "exception_object_id": 32260,
        "id": 35,
        "trace": "actionpack (6.0.2.2) lib/action_controller/metal/params_wrapper.rb:245:in `process_action'"
      },
      {
        "exception_object_id": 32260,
        "id": 36,
        "trace": "activerecord (6.0.2.2) lib/active_record/railties/controller_runtime.rb:27:in `process_action'"
      },
      {
        "exception_object_id": 32260,
        "id": 37,
        "trace": "actionpack (6.0.2.2) lib/abstract_controller/base.rb:136:in `process'"
      },
      {
        "exception_object_id": 32260,
        "id": 38,
        "trace": "actionpack (6.0.2.2) lib/action_controller/metal.rb:191:in `dispatch'"
      },
      {
        "exception_object_id": 32260,
        "id": 39,
        "trace": "actionpack (6.0.2.2) lib/action_controller/metal.rb:252:in `dispatch'"
      },
      {
        "exception_object_id": 32260,
        "id": 40,
        "trace": "actionpack (6.0.2.2) lib/action_dispatch/routing/route_set.rb:51:in `dispatch'"
      },
      {
        "exception_object_id": 32260,
        "id": 41,
        "trace": "actionpack (6.0.2.2) lib/action_dispatch/routing/route_set.rb:33:in `serve'"
      },
      {
        "exception_object_id": 32260,
        "id": 42,
        "trace": "actionpack (6.0.2.2) lib/action_dispatch/journey/router.rb:49:in `block in serve'"
      },
      {
        "exception_object_id": 32260,
        "id": 43,
        "trace": "actionpack (6.0.2.2) lib/action_dispatch/journey/router.rb:32:in `each'"
      },
      {
        "exception_object_id": 32260,
        "id": 44,
        "trace": "actionpack (6.0.2.2) lib/action_dispatch/journey/router.rb:32:in `serve'"
      },
      {
        "exception_object_id": 32260,
        "id": 45,
        "trace": "actionpack (6.0.2.2) lib/action_dispatch/routing/route_set.rb:837:in `call'"
      },
      {
        "exception_object_id": 32260,
        "id": 46,
        "trace": "railties (6.0.2.2) lib/rails/engine.rb:526:in `call'"
      },
      {
        "exception_object_id": 32260,
        "id": 47,
        "trace": "railties (6.0.2.2) lib/rails/railtie.rb:190:in `public_send'"
      },
      {
        "exception_object_id": 32260,
        "id": 48,
        "trace": "railties (6.0.2.2) lib/rails/railtie.rb:190:in `method_missing'"
      },
      {
        "exception_object_id": 32260,
        "id": 49,
        "trace": "actionpack (6.0.2.2) lib/action_dispatch/routing/mapper.rb:19:in `block in <class:Constraints>'"
      },
      {
        "exception_object_id": 32260,
        "id": 50,
        "trace": "actionpack (6.0.2.2) lib/action_dispatch/routing/mapper.rb:48:in `serve'"
      },
      {
        "exception_object_id": 32260,
        "id": 51,
        "trace": "actionpack (6.0.2.2) lib/action_dispatch/journey/router.rb:49:in `block in serve'"
      },
      {
        "exception_object_id": 32260,
        "id": 52,
        "trace": "actionpack (6.0.2.2) lib/action_dispatch/journey/router.rb:32:in `each'"
      },
      {
        "exception_object_id": 32260,
        "id": 53,
        "trace": "actionpack (6.0.2.2) lib/action_dispatch/journey/router.rb:32:in `serve'"
      },
      {
        "exception_object_id": 32260,
        "id": 54,
        "trace": "actionpack (6.0.2.2) lib/action_dispatch/routing/route_set.rb:837:in `call'"
      },
      {
        "exception_object_id": 32260,
        "id": 55,
        "trace": "rack (2.2.2) lib/rack/etag.rb:27:in `call'"
      },
      {
        "exception_object_id": 32260,
        "id": 56,
        "trace": "rack (2.2.2) lib/rack/conditional_get.rb:40:in `call'"
      },
      {
        "exception_object_id": 32260,
        "id": 57,
        "trace": "rack (2.2.2) lib/rack/head.rb:12:in `call'"
      },
      {
        "exception_object_id": 32260,
        "id": 58,
        "trace": "activerecord (6.0.2.2) lib/active_record/migration.rb:567:in `call'"
      },
      {
        "exception_object_id": 32260,
        "id": 59,
        "trace": "actionpack (6.0.2.2) lib/action_dispatch/middleware/callbacks.rb:27:in `block in call'"
      },
      {
        "exception_object_id": 32260,
        "id": 60,
        "trace": "activesupport (6.0.2.2) lib/active_support/callbacks.rb:101:in `run_callbacks'"
      },
      {
        "exception_object_id": 32260,
        "id": 61,
        "trace": "actionpack (6.0.2.2) lib/action_dispatch/middleware/callbacks.rb:26:in `call'"
      },
      {
        "exception_object_id": 32260,
        "id": 62,
        "trace": "actionpack (6.0.2.2) lib/action_dispatch/middleware/executor.rb:14:in `call'"
      },
      {
        "exception_object_id": 32260,
        "id": 63,
        "trace": "actionpack (6.0.2.2) lib/action_dispatch/middleware/actionable_exceptions.rb:17:in `call'"
      },
      {
        "exception_object_id": 32260,
        "id": 64,
        "trace": "actionpack (6.0.2.2) lib/action_dispatch/middleware/debug_exceptions.rb:32:in `call'"
      },
      {
        "exception_object_id": 32260,
        "id": 65,
        "trace": "actionpack (6.0.2.2) lib/action_dispatch/middleware/show_exceptions.rb:33:in `call'"
      },
      {
        "exception_object_id": 32260,
        "id": 66,
        "trace": "railties (6.0.2.2) lib/rails/rack/logger.rb:38:in `call_app'"
      },
      {
        "exception_object_id": 32260,
        "id": 67,
        "trace": "railties (6.0.2.2) lib/rails/rack/logger.rb:26:in `block in call'"
      },
      {
        "exception_object_id": 32260,
        "id": 68,
        "trace": "activesupport (6.0.2.2) lib/active_support/tagged_logging.rb:80:in `block in tagged'"
      },
      {
        "exception_object_id": 32260,
        "id": 69,
        "trace": "activesupport (6.0.2.2) lib/active_support/tagged_logging.rb:28:in `tagged'"
      },
      {
        "exception_object_id": 32260,
        "id": 70,
        "trace": "activesupport (6.0.2.2) lib/active_support/tagged_logging.rb:80:in `tagged'"
      },
      {
        "exception_object_id": 32260,
        "id": 71,
        "trace": "railties (6.0.2.2) lib/rails/rack/logger.rb:26:in `call'"
      },
      {
        "exception_object_id": 32260,
        "id": 72,
        "trace": "actionpack (6.0.2.2) lib/action_dispatch/middleware/remote_ip.rb:81:in `call'"
      },
      {
        "exception_object_id": 32260,
        "id": 73,
        "trace": "actionpack (6.0.2.2) lib/action_dispatch/middleware/request_id.rb:27:in `call'"
      },
      {
        "exception_object_id": 32260,
        "id": 74,
        "trace": "rack (2.2.2) lib/rack/runtime.rb:22:in `call'"
      },
      {
        "exception_object_id": 32260,
        "id": 75,
        "trace": "activesupport (6.0.2.2) lib/active_support/cache/strategy/local_cache_middleware.rb:29:in `call'"
      },
      {
        "exception_object_id": 32260,
        "id": 76,
        "trace": "actionpack (6.0.2.2) lib/action_dispatch/middleware/executor.rb:14:in `call'"
      },
      {
        "exception_object_id": 32260,
        "id": 77,
        "trace": "actionpack (6.0.2.2) lib/action_dispatch/middleware/static.rb:126:in `call'"
      },
      {
        "exception_object_id": 32260,
        "id": 78,
        "trace": "rack (2.2.2) lib/rack/sendfile.rb:110:in `call'"
      },
      {
        "exception_object_id": 32260,
        "id": 79,
        "trace": "actionpack (6.0.2.2) lib/action_dispatch/middleware/host_authorization.rb:83:in `call'"
      },
      {
        "exception_object_id": 32260,
        "id": 80,
        "trace": "railties (6.0.2.2) lib/rails/engine.rb:526:in `call'"
      },
      {
        "exception_object_id": 32260,
        "id": 81,
        "trace": "puma (4.3.3) lib/puma/configuration.rb:228:in `call'"
      },
      {
        "exception_object_id": 32260,
        "id": 82,
        "trace": "puma (4.3.3) lib/puma/server.rb:682:in `handle_request'"
      },
      {
        "exception_object_id": 32260,
        "id": 83,
        "trace": "puma (4.3.3) lib/puma/server.rb:472:in `process_client'"
      },
      {
        "exception_object_id": 32260,
        "id": 84,
        "trace": "puma (4.3.3) lib/puma/server.rb:328:in `block in run'"
      },
      {
        "exception_object_id": 32260,
        "id": 85,
        "trace": "puma (4.3.3) lib/puma/thread_pool.rb:134:in `block in spawn_thread'"
      }
    ],
    "Full Trace": [
      {
        "exception_object_id": 32260,
        "id": 0,
        "trace": "actionpack (6.0.2.2) lib/action_dispatch/routing/mapper.rb:687:in `block in define_generate_prefix'"
      },
      {
        "exception_object_id": 32260,
        "id": 1,
        "trace": "actionpack (6.0.2.2) lib/action_dispatch/routing/routes_proxy.rb:40:in `token_sign_in_url'"
      },
      {
        "exception_object_id": 32260,
        "id": 2,
        "trace": "passwordless (fd1fef1088da) app/mailers/passwordless/mailer.rb:14:in `magic_link'"
      },
      {
        "exception_object_id": 32260,
        "id": 3,
        "trace": "actionpack (6.0.2.2) lib/abstract_controller/base.rb:196:in `process_action'"
      },
      {
        "exception_object_id": 32260,
        "id": 4,
        "trace": "actionpack (6.0.2.2) lib/abstract_controller/callbacks.rb:42:in `block in process_action'"
      },
      {
        "exception_object_id": 32260,
        "id": 5,
        "trace": "activesupport (6.0.2.2) lib/active_support/callbacks.rb:101:in `run_callbacks'"
      },
      {
        "exception_object_id": 32260,
        "id": 6,
        "trace": "actionpack (6.0.2.2) lib/abstract_controller/callbacks.rb:41:in `process_action'"
      },
      {
        "exception_object_id": 32260,
        "id": 7,
        "trace": "actionpack (6.0.2.2) lib/abstract_controller/base.rb:136:in `process'"
      },
      {
        "exception_object_id": 32260,
        "id": 8,
        "trace": "actionmailer (6.0.2.2) lib/action_mailer/rescuable.rb:25:in `block in process'"
      },
      {
        "exception_object_id": 32260,
        "id": 9,
        "trace": "actionmailer (6.0.2.2) lib/action_mailer/rescuable.rb:17:in `handle_exceptions'"
      },
      {
        "exception_object_id": 32260,
        "id": 10,
        "trace": "actionmailer (6.0.2.2) lib/action_mailer/rescuable.rb:24:in `process'"
      },
      {
        "exception_object_id": 32260,
        "id": 11,
        "trace": "actionview (6.0.2.2) lib/action_view/rendering.rb:39:in `process'"
      },
      {
        "exception_object_id": 32260,
        "id": 12,
        "trace": "actionmailer (6.0.2.2) lib/action_mailer/base.rb:637:in `block in process'"
      },
      {
        "exception_object_id": 32260,
        "id": 13,
        "trace": "activesupport (6.0.2.2) lib/active_support/notifications.rb:180:in `block in instrument'"
      },
      {
        "exception_object_id": 32260,
        "id": 14,
        "trace": "activesupport (6.0.2.2) lib/active_support/notifications/instrumenter.rb:24:in `instrument'"
      },
      {
        "exception_object_id": 32260,
        "id": 15,
        "trace": "activesupport (6.0.2.2) lib/active_support/notifications.rb:180:in `instrument'"
      },
      {
        "exception_object_id": 32260,
        "id": 16,
        "trace": "actionmailer (6.0.2.2) lib/action_mailer/base.rb:636:in `process'"
      },
      {
        "exception_object_id": 32260,
        "id": 17,
        "trace": "actionmailer (6.0.2.2) lib/action_mailer/message_delivery.rb:123:in `block in processed_mailer'"
      },
      {
        "exception_object_id": 32260,
        "id": 18,
        "trace": "actionmailer (6.0.2.2) lib/action_mailer/message_delivery.rb:122:in `tap'"
      },
      {
        "exception_object_id": 32260,
        "id": 19,
        "trace": "actionmailer (6.0.2.2) lib/action_mailer/message_delivery.rb:122:in `processed_mailer'"
      },
      {
        "exception_object_id": 32260,
        "id": 20,
        "trace": "actionmailer (6.0.2.2) lib/action_mailer/message_delivery.rb:113:in `deliver_now'"
      },
      {
        "exception_object_id": 32260,
        "id": 21,
        "trace": "passwordless (fd1fef1088da) lib/passwordless.rb:24:in `block (2 levels) in <module:Passwordless>'"
      },
      {
        "exception_object_id": 32260,
        "id": 22,
        "trace": "passwordless (fd1fef1088da) app/controllers/passwordless/sessions_controller.rb:28:in `create'"
      },
      {
        "exception_object_id": 32260,
        "id": 23,
        "trace": "actionpack (6.0.2.2) lib/action_controller/metal/basic_implicit_render.rb:6:in `send_action'"
      },
      {
        "exception_object_id": 32260,
        "id": 24,
        "trace": "actionpack (6.0.2.2) lib/abstract_controller/base.rb:196:in `process_action'"
      },
      {
        "exception_object_id": 32260,
        "id": 25,
        "trace": "actionpack (6.0.2.2) lib/action_controller/metal/rendering.rb:30:in `process_action'"
      },
      {
        "exception_object_id": 32260,
        "id": 26,
        "trace": "actionpack (6.0.2.2) lib/abstract_controller/callbacks.rb:42:in `block in process_action'"
      },
      {
        "exception_object_id": 32260,
        "id": 27,
        "trace": "activesupport (6.0.2.2) lib/active_support/callbacks.rb:101:in `run_callbacks'"
      },
      {
        "exception_object_id": 32260,
        "id": 28,
        "trace": "actionpack (6.0.2.2) lib/abstract_controller/callbacks.rb:41:in `process_action'"
      },
      {
        "exception_object_id": 32260,
        "id": 29,
        "trace": "actionpack (6.0.2.2) lib/action_controller/metal/rescue.rb:22:in `process_action'"
      },
      {
        "exception_object_id": 32260,
        "id": 30,
        "trace": "actionpack (6.0.2.2) lib/action_controller/metal/instrumentation.rb:33:in `block in process_action'"
      },
      {
        "exception_object_id": 32260,
        "id": 31,
        "trace": "activesupport (6.0.2.2) lib/active_support/notifications.rb:180:in `block in instrument'"
      },
      {
        "exception_object_id": 32260,
        "id": 32,
        "trace": "activesupport (6.0.2.2) lib/active_support/notifications/instrumenter.rb:24:in `instrument'"
      },
      {
        "exception_object_id": 32260,
        "id": 33,
        "trace": "activesupport (6.0.2.2) lib/active_support/notifications.rb:180:in `instrument'"
      },
      {
        "exception_object_id": 32260,
        "id": 34,
        "trace": "actionpack (6.0.2.2) lib/action_controller/metal/instrumentation.rb:32:in `process_action'"
      },
      {
        "exception_object_id": 32260,
        "id": 35,
        "trace": "actionpack (6.0.2.2) lib/action_controller/metal/params_wrapper.rb:245:in `process_action'"
      },
      {
        "exception_object_id": 32260,
        "id": 36,
        "trace": "activerecord (6.0.2.2) lib/active_record/railties/controller_runtime.rb:27:in `process_action'"
      },
      {
        "exception_object_id": 32260,
        "id": 37,
        "trace": "actionpack (6.0.2.2) lib/abstract_controller/base.rb:136:in `process'"
      },
      {
        "exception_object_id": 32260,
        "id": 38,
        "trace": "actionpack (6.0.2.2) lib/action_controller/metal.rb:191:in `dispatch'"
      },
      {
        "exception_object_id": 32260,
        "id": 39,
        "trace": "actionpack (6.0.2.2) lib/action_controller/metal.rb:252:in `dispatch'"
      },
      {
        "exception_object_id": 32260,
        "id": 40,
        "trace": "actionpack (6.0.2.2) lib/action_dispatch/routing/route_set.rb:51:in `dispatch'"
      },
      {
        "exception_object_id": 32260,
        "id": 41,
        "trace": "actionpack (6.0.2.2) lib/action_dispatch/routing/route_set.rb:33:in `serve'"
      },
      {
        "exception_object_id": 32260,
        "id": 42,
        "trace": "actionpack (6.0.2.2) lib/action_dispatch/journey/router.rb:49:in `block in serve'"
      },
      {
        "exception_object_id": 32260,
        "id": 43,
        "trace": "actionpack (6.0.2.2) lib/action_dispatch/journey/router.rb:32:in `each'"
      },
      {
        "exception_object_id": 32260,
        "id": 44,
        "trace": "actionpack (6.0.2.2) lib/action_dispatch/journey/router.rb:32:in `serve'"
      },
      {
        "exception_object_id": 32260,
        "id": 45,
        "trace": "actionpack (6.0.2.2) lib/action_dispatch/routing/route_set.rb:837:in `call'"
      },
      {
        "exception_object_id": 32260,
        "id": 46,
        "trace": "railties (6.0.2.2) lib/rails/engine.rb:526:in `call'"
      },
      {
        "exception_object_id": 32260,
        "id": 47,
        "trace": "railties (6.0.2.2) lib/rails/railtie.rb:190:in `public_send'"
      },
      {
        "exception_object_id": 32260,
        "id": 48,
        "trace": "railties (6.0.2.2) lib/rails/railtie.rb:190:in `method_missing'"
      },
      {
        "exception_object_id": 32260,
        "id": 49,
        "trace": "actionpack (6.0.2.2) lib/action_dispatch/routing/mapper.rb:19:in `block in <class:Constraints>'"
      },
      {
        "exception_object_id": 32260,
        "id": 50,
        "trace": "actionpack (6.0.2.2) lib/action_dispatch/routing/mapper.rb:48:in `serve'"
      },
      {
        "exception_object_id": 32260,
        "id": 51,
        "trace": "actionpack (6.0.2.2) lib/action_dispatch/journey/router.rb:49:in `block in serve'"
      },
      {
        "exception_object_id": 32260,
        "id": 52,
        "trace": "actionpack (6.0.2.2) lib/action_dispatch/journey/router.rb:32:in `each'"
      },
      {
        "exception_object_id": 32260,
        "id": 53,
        "trace": "actionpack (6.0.2.2) lib/action_dispatch/journey/router.rb:32:in `serve'"
      },
      {
        "exception_object_id": 32260,
        "id": 54,
        "trace": "actionpack (6.0.2.2) lib/action_dispatch/routing/route_set.rb:837:in `call'"
      },
      {
        "exception_object_id": 32260,
        "id": 55,
        "trace": "rack (2.2.2) lib/rack/etag.rb:27:in `call'"
      },
      {
        "exception_object_id": 32260,
        "id": 56,
        "trace": "rack (2.2.2) lib/rack/conditional_get.rb:40:in `call'"
      },
      {
        "exception_object_id": 32260,
        "id": 57,
        "trace": "rack (2.2.2) lib/rack/head.rb:12:in `call'"
      },
      {
        "exception_object_id": 32260,
        "id": 58,
        "trace": "activerecord (6.0.2.2) lib/active_record/migration.rb:567:in `call'"
      },
      {
        "exception_object_id": 32260,
        "id": 59,
        "trace": "actionpack (6.0.2.2) lib/action_dispatch/middleware/callbacks.rb:27:in `block in call'"
      },
      {
        "exception_object_id": 32260,
        "id": 60,
        "trace": "activesupport (6.0.2.2) lib/active_support/callbacks.rb:101:in `run_callbacks'"
      },
      {
        "exception_object_id": 32260,
        "id": 61,
        "trace": "actionpack (6.0.2.2) lib/action_dispatch/middleware/callbacks.rb:26:in `call'"
      },
      {
        "exception_object_id": 32260,
        "id": 62,
        "trace": "actionpack (6.0.2.2) lib/action_dispatch/middleware/executor.rb:14:in `call'"
      },
      {
        "exception_object_id": 32260,
        "id": 63,
        "trace": "actionpack (6.0.2.2) lib/action_dispatch/middleware/actionable_exceptions.rb:17:in `call'"
      },
      {
        "exception_object_id": 32260,
        "id": 64,
        "trace": "actionpack (6.0.2.2) lib/action_dispatch/middleware/debug_exceptions.rb:32:in `call'"
      },
      {
        "exception_object_id": 32260,
        "id": 65,
        "trace": "actionpack (6.0.2.2) lib/action_dispatch/middleware/show_exceptions.rb:33:in `call'"
      },
      {
        "exception_object_id": 32260,
        "id": 66,
        "trace": "railties (6.0.2.2) lib/rails/rack/logger.rb:38:in `call_app'"
      },
      {
        "exception_object_id": 32260,
        "id": 67,
        "trace": "railties (6.0.2.2) lib/rails/rack/logger.rb:26:in `block in call'"
      },
      {
        "exception_object_id": 32260,
        "id": 68,
        "trace": "activesupport (6.0.2.2) lib/active_support/tagged_logging.rb:80:in `block in tagged'"
      },
      {
        "exception_object_id": 32260,
        "id": 69,
        "trace": "activesupport (6.0.2.2) lib/active_support/tagged_logging.rb:28:in `tagged'"
      },
      {
        "exception_object_id": 32260,
        "id": 70,
        "trace": "activesupport (6.0.2.2) lib/active_support/tagged_logging.rb:80:in `tagged'"
      },
      {
        "exception_object_id": 32260,
        "id": 71,
        "trace": "railties (6.0.2.2) lib/rails/rack/logger.rb:26:in `call'"
      },
      {
        "exception_object_id": 32260,
        "id": 72,
        "trace": "actionpack (6.0.2.2) lib/action_dispatch/middleware/remote_ip.rb:81:in `call'"
      },
      {
        "exception_object_id": 32260,
        "id": 73,
        "trace": "actionpack (6.0.2.2) lib/action_dispatch/middleware/request_id.rb:27:in `call'"
      },
      {
        "exception_object_id": 32260,
        "id": 74,
        "trace": "rack (2.2.2) lib/rack/runtime.rb:22:in `call'"
      },
      {
        "exception_object_id": 32260,
        "id": 75,
        "trace": "activesupport (6.0.2.2) lib/active_support/cache/strategy/local_cache_middleware.rb:29:in `call'"
      },
      {
        "exception_object_id": 32260,
        "id": 76,
        "trace": "actionpack (6.0.2.2) lib/action_dispatch/middleware/executor.rb:14:in `call'"
      },
      {
        "exception_object_id": 32260,
        "id": 77,
        "trace": "actionpack (6.0.2.2) lib/action_dispatch/middleware/static.rb:126:in `call'"
      },
      {
        "exception_object_id": 32260,
        "id": 78,
        "trace": "rack (2.2.2) lib/rack/sendfile.rb:110:in `call'"
      },
      {
        "exception_object_id": 32260,
        "id": 79,
        "trace": "actionpack (6.0.2.2) lib/action_dispatch/middleware/host_authorization.rb:83:in `call'"
      },
      {
        "exception_object_id": 32260,
        "id": 80,
        "trace": "railties (6.0.2.2) lib/rails/engine.rb:526:in `call'"
      },
      {
        "exception_object_id": 32260,
        "id": 81,
        "trace": "puma (4.3.3) lib/puma/configuration.rb:228:in `call'"
      },
      {
        "exception_object_id": 32260,
        "id": 82,
        "trace": "puma (4.3.3) lib/puma/server.rb:682:in `handle_request'"
      },
      {
        "exception_object_id": 32260,
        "id": 83,
        "trace": "puma (4.3.3) lib/puma/server.rb:472:in `process_client'"
      },
      {
        "exception_object_id": 32260,
        "id": 84,
        "trace": "puma (4.3.3) lib/puma/server.rb:328:in `block in run'"
      },
      {
        "exception_object_id": 32260,
        "id": 85,
        "trace": "puma (4.3.3) lib/puma/thread_pool.rb:134:in `block in spawn_thread'"
      }
    ]
  }
}

Response code: 500 (Internal Server Error); Time: 192ms; Content length: 21604 bytes

Integration with Devise

Hi! I love this gem and the codebase looks great.

I'd love to integrate it with my current app that uses Devise. I can write a PR if you prefer.

It seems like we could hook into this line:


Instead of using the passwordless #sign_in method, we could use the Devise #sign_in method which will accept the same authenticatable object.

Or the passwordless #sign_in could check if Devise is enabled, and then call out to that.

Thoughts?

Sessions expire despite setting Passwordless.expires_at

Hello! I'm having a really hard time debugging this one.

My intended behavior would be that once the user clicks the login link sent to their email, they stay logged into that browser forever or until they visit the logout link.

Currently, users are logged out in under 12 hours (timing not exact), even when the browser session stays open.

Here is some of my setup:

Initializer config/initializers/passwordless.rb

Passwordless.default_from_address = ENV['MAILER_SENDER']

Passwordless.expires_at = lambda { 16.years.from_now } # How long until a passwordless session expires.
Passwordless.timeout_at = lambda { 12.hours.from_now } # How long until a magic link expires.

# Default redirection paths
Passwordless.success_redirect_path = '/recipes' # When a user succeeds in logging in.
Passwordless.failure_redirect_path = '/' # When a a login is failed for any reason.
Passwordless.sign_out_redirect_path = '/' # When a user logs out.

Passwordless.after_session_save = lambda do |session, request|
  PasswordlessMailer.magic_link(session).deliver_later
end

The user ends up with these cookies:

image

There is a further configuration in config/application.rb:

module Mise
  class Application < Rails::Application
    # Initialize configuration defaults for originally generated Rails version.
    config.load_defaults 6.0

    config.session_store :cookie_store, {:expire_after => 1.year}
    config.always_write_cookie = true

I see the _session_id cookie which expires in 1 year, which I expect is coming from the Rails :cookie_store, and the _mise_session which I expect is from Passwordless, with an expiration of Session.

I'd like the Passwordless cookie to have near-permanent cookie persistence, and persist even after the browser tab has been closed. I don't understand why users are being logged out after ~12 hours even when the browser stays open, however.

JWT Support

Any plans to support JSON Web Tokens as well as sessions?

Ideas for compatibility with rails_admin?

Hi @mikker, thanks so much for building Passwordless! It does exactly what I need.

I am using this with rails_admin via the manual custom auth approach.

My user class looks like this:

#  id         :bigint           not null, primary key
#  email      :string
#  superuser  :boolean          default(FALSE)
#  created_at :datetime         not null
#  updated_at :datetime         not null
#
class User < ApplicationRecord
  passwordless_with :email
  # ...more domain stuff here...
end

I've sort of hacked something together that enables access if a user is signed in and has superuser: true. It looks like this:

RailsAdmin.config do |config|
  config.authorize_with do |controller|
    class RailsAdmin::MainController
      include Passwordless::ControllerHelpers
    end
    user = controller.authenticate_by_session(User)
    redirect_to main_app.root_path unless user&.superuser
  end
  # ...more config here...
end

However, this doesn't seem ideal. I don't like hacking the main admin controller open every request, but I can't seem to get at the authenticate_by_session method any other way.

Do you have any suggestions for what I could try? Happy to PR something to add support for Passwordless into that repo if I can get it working in an ergonomic way.

Best way to test in integration/system tests?

I've not seen this described anywhere in past issues. What's the best way to fake a sign in without triggering a email flow? I whipped this up and it seems to be working ok.

module Passwordless
  module SignInHelper
    def sign_in_as(authenticatable)
      session = Passwordless::Session.create! authenticatable_type: authenticatable.class,
                                              authenticatable_id: authenticatable.id,
                                              timeout_at: 1.day.from_now,
                                              expires_at: 1.day.from_now,
                                              user_agent: 'fake-user-agent',
                                              remote_addr: '127.0.0.1'

      visit Passwordless::Engine.routes.
                                 url_helpers.
                                 token_sign_in_path(session.token)
    end
  end
end

Any thoughts about adding this to the gem? Is there any other way to do it without hitting the database? Perhaps stubbing the private method find_session in Passwordless::SessionsController to return a fake session with the user attached?

Also, in test mode, we'd ideally not have the Bycrpt::Password line (https://github.com/mikker/passwordless/blob/master/app/controllers/passwordless/sessions_controller.rb#L45)

Any thoughts?

Non-passwordless route path helpers are prefixed with /users when rendering passwordless templates

Given this routes file:

# config/routes.rb
Rails.application.routes.draw do
  resources :stripe_charges, only: [:create]
  passwordless_for :users
end

And this application layout (app/views/layouts/application.html.erb):

<!DOCTYPE html>
<html>
  <head>
    <title>foo</title>
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>
    <%= stylesheet_link_tag    'application', media: 'all' %>
    <%= javascript_include_tag 'application' %>
  </head>

  <body>
    <nav>
      <form action="<%= stripe_charges_path %>" method="POST"></form>
    </nav>
    <%= yield %>
  </body>
</html>

When the application layout is rendered on normal pages, stripe_charges_path correctly returns /stripe_charges, but when rendered as part of a Passwordless template like app/views/passwordless/sessions/new.html.erb, it is returning /users/stripe_charges, which doesn't exist.

Depfu Error: Found both *.gemspec and Gemfile.lock

Hello,

We've tried to activate or update your repository on Depfu and found something odd:

Your repo contains both a *.gemspec file and a Gemfile.lock.

Depfu can't really handle that right now, unfortunately, and it's also not best practice:

If your repo contains a *.gemspec that usually means it is meant to be used as a Gem, or put differently, a library. Locking dependencies on a library (via Gemfile.lock) doesn't really make sense, especially since the Gemfile.lock can't and won't be honored when building and installing the gem.

Instead, you should declare your dependencies as specifically as needed (but as loose as possible) in the *.gemspec and add the Gemfile.lock to your .gitignore.

By checking in the Gemfile.lock, you will not only break Depfu (which we might fix at some point, maybe), but you will also keep your CI from testing against real life sets of dependencies.


This is an automated issue by Depfu. You're getting it because someone configured Depfu to automatically update dependencies on this project.

Dependabot can't resolve your Ruby dependency files

Dependabot can't resolve your Ruby dependency files.

As a result, Dependabot couldn't update your dependencies.

The error Dependabot encountered was:

Bundler::VersionConflict with message: Bundler could not find compatible versions for gem "url":
  In Gemfile:
    codecov was resolved to 0.1.14, which depends on
      url

Could not find gem 'url', which is required by gem 'codecov', in any of the sources.

If you think the above is an error on Dependabot's side please don't hesitate to get in touch - we'll do whatever we can to fix it.

You can mention @dependabot in the comments below to contact the Dependabot team.

LoadError: cannot load such file -- passwordless/test_helpers

I got this issue while running rspec

rails_helper.rb:

require 'spec_helper'

ENV['RAILS_ENV'] ||= 'test'

require File.expand_path('../config/environment', __dir__)

# Prevent database truncation if the environment is production
abort("The Rails environment is running in production mode!") if Rails.env.production?
require 'rspec/rails'
require "passwordless/test_helpers"

View generators

Custom views and controllers could be generated the same way they are generated in Rails or Devise via $ rails g devise:views. I would gladly submit a PR for that.

Instant sign up

First of all, thanks for the gem, great work!

Some apps do not require separate registration and combine sign in and sign up forms. If there's no user with such email, the app creates a user with that email and sends a confirmation link.

How can this flow be implemented with passwordless?

only allow users with emails matching a certain criteria?

I would like to restrict sending emails with sign-in-tokens to users with an email of a specific domain. How would I need to configure passwordless to return an error message if the email entered at sign_in does not match a specific criteria?

I assumed I would need to create a custom User#fetch_resource_for_passwordless method, but even when returning nil instead of a created user object I'm redirected to sign_in with the message "If we found you in the system, we've sent you an email." – is there a way to either redirect to another page or return a different flash message when creation of a user fails?

Thanks a lot!

(As additional information: the referenced app did not include any User model/controller, so there is no specific code other than mentioned in this README.md, this app basically only had a root page with a single controller, only protected with a simple http_basic_authenticate_with as it wasn't ready to be available to anyone except myself so far, but now I'd like to make it available to my colleagues at work)

Initializing parent_mailer

I simply copied your example and got an error. This line in initalizer:

Passwordless.parent_mailer = "ActionMailer::Base"

is producing this error:

 undefined method `parent_mailer=' for Passwordless:Module (NoMethodError)
Did you mean?  parent_name

Any idea what could be wrong there?

remote_addr or remote_ip?

I'm trying out the gem with an application that runs behind on cloudflare -> nginx -> puma and checking the remote_addr of all visits it's always 127.0.0.1, instead of the real ip (the app is live and requests were real / over the internet). From reading around it seems that when there is a proxy in between (like cloudflare, nginx, etc) remote_ip should be used instead.

Do you have any hints about this? Should it be changed in the gem itself or is it more "app based" (and thus override that setter)?

Routing error with new_user_path

Hello, my <%=link_to β€œNEW” new_user_path %> does not work.
If I use new_user_path, the link is /users/users/new not /users/new.
Please let me give some routing advise.

Here is my setting so far.
I can open the page and save email with direct url like β€œhttp://localhost:3000/users/new”

routes.rb

Rails.application.routes.draw do
  passwordless_for :users
  resources :users
end

rails routes

users        /users                 Passwordless::Engine {:authenticatable=>"user"}
GET    /users(.:format)       users#index
POST   /users(.:format)       users#create
new_user GET    /users/new(.:format)   users#new
edit_user GET    /users/:id/edit(.:format)   users#edit
user GET    /users/:id(.:format)   users#show
PATCH  /users/:id(.:format)   users#update
PUT    /users/:id(.:format)   users#update
DELETE /users/:id(.:format)   users#destroy

Thank you for your awesome gem.

allow for multi-tenancy

first of all, compliments to this nice and clean standalone implementation for passwordless authentication

in my use case we have a multi-tenant application where a user is not purely identified by it's email address but where there is another property that identifies the so called 'tennant'. this could be for example the hostname (in multi homed setups), an explicit tenant identifier in the path, etc

i've looked at the codebase and the current implementation is limiting to a single parameter name.

to make the code more general, instead of passing in only the email identifier to the fetch_resource_for_passwordless method, it's more generic to pass in all params. like that the users can user all params without restrictions

    def find_authenticatable
      authenticatable_class.fetch_resource_for_passwordless(params)
    end

in the readme i put some examples, the third shows how it could be used to allow for multi-tenancy based on an extra param.

  def self.fetch_resource_for_passwordless(params)
    find_by(email: params[:passwordless][:email])

    # # auto-create users
    # find_or_create_by(email: params[:passwordless][:email])

    # # multi tenant example
    # tenant = Tenant.find_by(tenant_uuid: params[:passwordless][:tenant_uuid])
    # find_or_create_by(email: params[:passwordless][:email], tenant: tenant)
  end

to be even more generic, the full request would need to be passed to be able to access the HOSTNAME for example in which case the host name is the discriminator of the tenant.

further i noticed also the following: the code assumes configuration of the name of the email field. However, one could simplify the code as follows. By default the code could assume the name of the field is 'email', and to cope for the case where the field name is different, one simply just needs to add an override for fetch_resource_for_passwordless. this simplification was also implemented in the fork. the sole impact is that the default template can not be rendered dynamically but that's almost a non issue because the user will override these anyhow.

 def self.fetch_resource_for_passwordless(params)
    find_by(email_address: params[:passwordless][:email])
end 

https://github.com/koenhandekyn/passwordless?organization=koenhandekyn&organization=koenhandekyn

note : does anyone know of an omni-auth adaptor? that seems to make sense to me as an easy way to bridge to devise also for example

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.