passwordless icon indicating copy to clipboard operation
passwordless copied to clipboard

Create auth token links for email

Open stuyam opened this issue 4 years ago • 6 comments

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! 😃

stuyam avatar Dec 17 '19 03:12 stuyam

It seems like from the implementation that I have, there are two methods that could be helpful to have build in.

  1. Session creation with the 'generated_on_users_behalf' part made into a helper method for generating a new session for a user without requiring request data.

  2. If the authenticate_by_token method was built in it could make that easier too.

It is easy enough that it doesn't need to be built in, but just an idea.

stuyam avatar Dec 17 '19 04:12 stuyam

I added this method to the user model to generate tokens easily:

def create_auth_token!
  Passwordless::Session.create(
    remote_addr: 'generated_by_rails',
    user_agent: 'generated_by_rails',
    authenticatable: self
  ).token
end

stuyam avatar Dec 17 '19 05:12 stuyam

I like this idea and have wanted to do something similar, however I think we should take the other way around and make the urls like /users/sign_in/:token?destination_path=/where/you/want/them/to/end/up. Work on this is already underway in https://github.com/mikker/passwordless/pull/69

mikker avatar Dec 17 '19 10:12 mikker

Yeah I think that is smart actually, then current_user and everything doesn't get muddied up. Plus I do need the ability to set a custom redirect url after sign in which is covered in that pr. Do you have any suggestions as of what to do with the remote_addr and user_agent, I guess some unique key like generated_by_rails seemed fine to me but I wasn't sure.

It would be nice if the passwordless_with :email class macro added a passwordless_token! and passwordless_url! which accepted an optional url that adds the destination_path if provided. I'll probably just do this in my project but might be interesting for people that want to solve a similar problem.

def passwordless_token!
  @passwordless_token ||= Passwordless::Session.create(
    remote_addr: 'generated_by_rails',
    user_agent: 'generated_by_rails',
    authenticatable: self
  ).token
end

def passwordless_url!(destination_path: nil)
  url_helper = Passwordless::Mailer.new.send(Passwordless.mounted_as)
  url = url_helper.token_sign_in_url(passwordless_token!)
  return url unless destination_path

  "#{url}?destination_path=#{destination_path}"
end

stuyam avatar Dec 17 '19 14:12 stuyam

I usually just put "N/A" when manually creating them. Don't have any better ideas than something like you already do.

Having a few helpers like you suggest would be nice. I'm not sure about the API. Will let it stew a bit in my mind and see what I can do 😊

mikker avatar Dec 17 '19 15:12 mikker

I need this!

The code presented here no longer works with the latest version, but I managed to do something similar in my controller:

 def passwordless_url_to(authenticatable, destination_path = '')
      session = create_passwordless_session!(authenticatable)
      link = Passwordless.context.url_for(
        session,
        action: "confirm",
        id: session.to_param,
        token: session.token
      )
      return "#{link}?destination_path=#{destination_path}"
 end

It would be nice to have something like this in Passwordless::ControllerHelpers.

I don't understand the purpose of action: "confirm" though.

andreaskundig avatar Jan 19 '24 17:01 andreaskundig