{"id":13631707,"url":"https://github.com/mikker/passwordless","last_synced_at":"2025-05-13T19:06:14.152Z","repository":{"id":37611947,"uuid":"109621263","full_name":"mikker/passwordless","owner":"mikker","description":"🗝 Authentication for your Rails app without the icky-ness of passwords","archived":false,"fork":false,"pushed_at":"2025-04-03T08:27:52.000Z","size":391,"stargazers_count":1302,"open_issues_count":14,"forks_count":92,"subscribers_count":11,"default_branch":"master","last_synced_at":"2025-04-28T00:54:32.554Z","etag":null,"topics":["authentication","engine","passwordless","rails","ruby"],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mikker.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":".github/FUNDING.yml","license":"MIT-LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null},"funding":{"github":"mikker"}},"created_at":"2017-11-05T22:15:51.000Z","updated_at":"2025-04-27T00:12:55.000Z","dependencies_parsed_at":"2023-11-09T12:56:40.828Z","dependency_job_id":"4262c671-3b9e-4c7e-8be2-f09ade82321e","html_url":"https://github.com/mikker/passwordless","commit_stats":{"total_commits":235,"total_committers":47,"mean_commits":5.0,"dds":0.2978723404255319,"last_synced_commit":"435f3df152f60e98c97bbcf95cb77d80d98641a2"},"previous_names":[],"tags_count":36,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mikker%2Fpasswordless","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mikker%2Fpasswordless/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mikker%2Fpasswordless/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mikker%2Fpasswordless/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mikker","download_url":"https://codeload.github.com/mikker/passwordless/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254010803,"owners_count":21998993,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["authentication","engine","passwordless","rails","ruby"],"created_at":"2024-08-01T22:02:35.249Z","updated_at":"2025-05-13T19:06:14.085Z","avatar_url":"https://github.com/mikker.png","language":"Ruby","funding_links":["https://github.com/sponsors/mikker"],"categories":["Ruby"],"sub_categories":[],"readme":"\u003cp align='center'\u003e\n  \u003cimg src='https://s3.brnbw.com/Passwordless-title-gaIVkX0sPg.svg' alt='Passwordless' /\u003e\n  \u003cbr /\u003e\n  \u003cbr /\u003e\n\u003c/p\u003e\n\n[![CI](https://github.com/mikker/passwordless/actions/workflows/ci.yml/badge.svg)](https://github.com/mikker/passwordless/actions/workflows/ci.yml) [![Rubygems](https://img.shields.io/gem/v/passwordless.svg)](https://rubygems.org/gems/passwordless) [![codecov](https://codecov.io/gh/mikker/passwordless/branch/master/graph/badge.svg)](https://codecov.io/gh/mikker/passwordless)\n\nAdd authentication to your Rails app without all the icky-ness of passwords. _Magic link_ authentication, if you will. We call it _passwordless_.\n\n- [Installation](#installation)\n  - [Upgrading](#upgrading)\n- [Usage](#usage)\n  - [Getting the current user, restricting access, the usual](#getting-the-current-user-restricting-access-the-usual)\n  - [Providing your own templates](#providing-your-own-templates)\n  - [Registering new users](#registering-new-users)\n  - [URLs and links](#urls-and-links)\n  - [Route constraints](#route-constraints)\n- [Configuration](#configuration)\n  - [Delivery method](#delivery-method)\n  - [Token generation](#token-generation)\n  - [Timeout and Expiry](#timeout-and-expiry)\n  - [Redirection after sign-in](#redirection-after-sign-in)\n  - [Looking up the user](#looking-up-the-user)\n- [Test helpers](#test-helpers)\n- [Security considerations](#security-considerations)\n- [Alternatives](#alternatives)\n- [License](#license)\n\n## Installation\n\nAdd to your bundle and copy over the migrations:\n\n```sh\n$ bundle add passwordless\n$ bin/rails passwordless_engine:install:migrations\n```\n\n### Upgrading\n\nSee [Upgrading to Passwordless 1.0](docs/upgrading_to_1_0.md) for more details.\n\n## Usage\n\nPasswordless 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](https://github.com/mikker/passwordless/wiki/Creating-the-user-model).\n\nEnable Passwordless on your user model by pointing it to the email field:\n\n```ruby\nclass User \u003c ApplicationRecord\n  # your other code..\n\n  passwordless_with :email # \u003c-- here! this needs to be a column in `users` table\n\n  # more of your code..\nend\n```\n\nThen mount the engine in your routes:\n\n```ruby\nRails.application.routes.draw do\n  passwordless_for :users\n\n  # other routes\nend\n```\n\n### Getting the current user, restricting access, the usual\n\nPasswordless doesn't give you `current_user` automatically. Here's how you could add it:\n\n```ruby\nclass ApplicationController \u003c ActionController::Base\n  include Passwordless::ControllerHelpers # \u003c-- This!\n\n  # ...\n\n  helper_method :current_user\n\n  private\n\n  def current_user\n    @current_user ||= authenticate_by_session(User)\n  end\n\n  def require_user!\n    return if current_user\n    save_passwordless_redirect_location!(User) # \u003c-- optional, see below\n    redirect_to root_path, alert: \"You are not worthy!\"\n  end\nend\n```\n\nEt voilà:\n\n```ruby\nclass VerySecretThingsController \u003c ApplicationController\n  before_action :require_user!\n\n  def index\n    @things = current_user.very_secret_things\n  end\nend\n```\n\n### Providing your own templates\n\nTo 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`.\n\nPasswordless has 2 action views and 1 mailer view:\n\n```sh\n# the form where the user inputs their email address\napp/views/passwordless/sessions/new.html.erb\n# the form where the user inputs their just received token\napp/views/passwordless/sessions/show.html.erb\n# the email with the token and magic link\napp/views/passwordless/mailer/sign_in.text.erb\n```\n\nSee [the bundled views](https://github.com/mikker/passwordless/tree/master/app/views/passwordless).\n\n### Registering new users\n\nBecause 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:\n\n```ruby\nclass UsersController \u003c ApplicationController\n  include Passwordless::ControllerHelpers # \u003c-- This!\n  # (unless you already have it in your ApplicationController)\n\n  def create\n    @user = User.new(user_params)\n\n    if @user.save\n      sign_in(create_passwordless_session(@user)) # \u003c-- This!\n      redirect_to(@user, flash: { notice: 'Welcome!' })\n    else\n      render(:new)\n    end\n  end\n\n  # ...\nend\n```\n\n### URLs and links\n\nBy default, Passwordless uses the resource name given to `passwordless_for` to generate its routes and helpers.\n\n```ruby\npasswordless_for :users\n  # \u003c%= users_sign_in_path %\u003e # =\u003e /users/sign_in\n\npasswordless_for :users, at: '/', as: :auth\n  # \u003c%= auth_sign_in_path %\u003e # =\u003e /sign_in\n```\n\nAlso be sure to\n[specify ActionMailer's `default_url_options.host`](http://guides.rubyonrails.org/action_mailer_basics.html#generating-urls-in-action-mailer-views) and tell the routes as well:\n\n```ruby\n# config/application.rb for example:\nconfig.action_mailer.default_url_options = {host: \"www.example.com\"}\nroutes.default_url_options[:host] ||= \"www.example.com\"\n```\n\nNote as well that `passwordless_for` accepts a custom controller. One possible application of this\nis to add a `before_action` that redirects authenticated users from the sign-in routes, as in this example:\n\n\n```ruby\n# config/routes.rb\npasswordless_for :users, controller: \"sessions\"\n```\n\n```ruby\n# app/controllers/sessions_controller.rb\n\nclass SessionsController \u003c Passwordless::SessionsController\n  before_action :require_unauth!, only: %i[new show]\n\n  private\n\n  def require_unauth!\n    return unless current_user\n    redirect_to(\"/\", notice: \"You are already signed in.\")\n  end\nend\n```\n\n### Route constraints\n\nWith [constraints](https://guides.rubyonrails.org/routing.html#request-based-constraints) you can restrict access to certain routes.\nPasswordless provides `Passwordless::Constraint` and it's negative counterpart `Passwordless::ConstraintNot` for this purpose.\n\nTo limit a route to only authenticated `User`s:\n\n```ruby\nconstraints Passwordless::Constraint.new(User) do\n  # ...\nend\n```\n\nThe constraint takes a second `if:` argument, that expects a block and is passed the `authenticatable` record, (ie. `User`):\n\n```ruby\nconstraints Passwordless::Constraint.new(User, if: -\u003e (user) { user.email.include?(\"john\") }) do\n  # ...\nend\n```\n\nThe negated version has the same API but with the opposite result, ie. ensuring authenticated user **don't** have access:\n\n```ruby\nconstraints Passwordless::ConstraintNot.new(User) do\n  get(\"/no-users-allowed\", to: \"secrets#index\")\nend\n```\n\n## Configuration\n\nTo customize Passwordless, create a file `config/initializers/passwordless.rb`.\n\nThe default values are shown below. It's recommended to only include the ones that you specifically want to modify.\n\n```ruby\nPasswordless.configure do |config|\n  config.default_from_address = \"CHANGE_ME@example.com\"\n  config.parent_controller = \"ApplicationController\"\n  config.parent_mailer = \"ActionMailer::Base\"\n  config.restrict_token_reuse = true # Can a token/link be used multiple times?\n  config.token_generator = Passwordless::ShortTokenGenerator.new # Used to generate magic link tokens.\n\n  config.expires_at = lambda { 1.year.from_now } # How long until a signed in session expires.\n  config.timeout_at = lambda { 10.minutes.from_now } # How long until a token/magic link times out.\n\n  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.\n  config.redirect_to_response_options = {} # Additional options for redirects.\n  config.success_redirect_path = '/' # After a user successfully signs in\n  config.failure_redirect_path = '/' # After a sign in fails\n  config.sign_out_redirect_path = '/' # After a user signs out\n\n  config.paranoid = false # Display email sent notice even when the resource is not found.\n\n  config.after_session_confirm = -\u003e(request, session) {} # Called after a session is confirmed.\nend\n```\n\n### Delivery method\n\nBy default, Passwordless sends emails. See [Providing your own templates](#providing-your-own-templates). If you need to customize this further, you can do so in the `after_session_save` callback.\n\nIn `config/initializers/passwordless.rb`:\n\n```ruby\nPasswordless.configure do |config|\n  config.after_session_save = lambda do |session, request|\n    # Default behavior is\n    # Passwordless::Mailer.sign_in(session).deliver_now\n\n    # You can change behavior to do something with session model. For example,\n    # SmsApi.send_sms(session.authenticatable.phone_number, session.token)\n  end\nend\n```\n\n## After Session Confirm Hook\n\nAn `after_session_confirm` hook is called after a successful session confirmation – in other words: after a user signs in successfully.\n\n```ruby\nPasswordless.configure do |config|\n  config.after_session_confirm = -\u003e(session, request) {\n    user = session.authenticatable\n    user.update!(\n      email_verified: true,\n      last_login_ip: request.remote_ip\n    )\n  }\nend\n```\n\n### Token generation\n\nBy 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.:\n\n```ruby\nPasswordless.configure do |config|\n  config.token_generator = lambda do |session|\n    \"probably-stupid-token-#{session.user_agent}-#{Time.current}\"\n  end\nend\n```\n\nPasswordless 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.\n\n### Timeout and Expiry\n\nThe _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.\n\nThe _expiry_ is the expiration time of the session of a logged in user. Once this is expired, the user is signed out.\n\n**Note:** Passwordless' session relies on Rails' own session and so will never live longer than that.\n\nTo configure your Rails session, in `config/initializers/session_store.rb`:\n\n```ruby\nRails.application.config.session_store :cookie_store,\n  expire_after: 1.year,\n  # ...\n```\n\n### Redirection after sign-in\n\nBy 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:\n\n```ruby\nclass ApplicationController \u003c ActionController::Base\n  include Passwordless::ControllerHelpers # \u003c-- Probably already have this!\n\n  # ...\n\n  def require_user!\n    return if current_user\n    save_passwordless_redirect_location!(User) # \u003c-- this one!\n    redirect_to root_path, alert: \"You are not worthy!\"\n  end\nend\n```\n\nThis can also be turned off with `Passwordless.config.redirect_back_after_sign_in = false`.\n\n### Looking up the user\n\nBy default Passwordless uses the `passwordless_with` column to _case insensitively_ fetch the user resource.\n\nYou 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.\n\n```ruby\nclass User \u003c ApplicationRecord\n  def self.fetch_resource_for_passwordless(email)\n    find_or_create_by(email: email)\n  end\nend\n```\n\n## Test helpers\n\nTo help with testing, a set of test helpers are provided.\n\nIf you are using RSpec, add the following line to your `spec/rails_helper.rb`:\n\n```ruby\nrequire \"passwordless/test_helpers\"\n```\n\nIf you are using TestUnit, add this line to your `test/test_helper.rb`:\n\n```ruby\nrequire \"passwordless/test_helpers\"\n```\n\nThen in your controller, request, and system tests/specs, you can utilize the following methods:\n\n```ruby\npasswordless_sign_in(user) # signs you in as a user\npasswordless_sign_out # signs out user\n```\n\n## Security considerations\n\nThere'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.\n\nBut 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](https://thenextweb.com/hardfork/2018/01/05/reddit-bitcoin-cash-stolen-hack/) using this method.\n\nIdeally 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.\n\n## Alternatives\n\n- [OTP JWT](https://github.com/stas/otp-jwt) -- Passwordless JSON Web Tokens\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmikker%2Fpasswordless","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmikker%2Fpasswordless","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmikker%2Fpasswordless/lists"}