{"id":13878131,"url":"https://github.com/rubymonolith/nopassword","last_synced_at":"2026-01-29T22:41:15.203Z","repository":{"id":45389482,"uuid":"459285632","full_name":"rubymonolith/nopassword","owner":"rubymonolith","description":"Login via email, SMS, or whatever would use a temporary code","archived":false,"fork":false,"pushed_at":"2026-01-13T23:30:35.000Z","size":174,"stargazers_count":135,"open_issues_count":5,"forks_count":5,"subscribers_count":3,"default_branch":"main","last_synced_at":"2026-01-14T01:29:02.126Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/rubymonolith.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"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}},"created_at":"2022-02-14T18:49:06.000Z","updated_at":"2026-01-13T23:30:38.000Z","dependencies_parsed_at":"2024-08-27T07:28:06.848Z","dependency_job_id":"f9d8c6f8-3cb7-4874-be86-846d7df49cd4","html_url":"https://github.com/rubymonolith/nopassword","commit_stats":null,"previous_names":["rocketshipio/nopassword"],"tags_count":5,"template":false,"template_full_name":null,"purl":"pkg:github/rubymonolith/nopassword","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rubymonolith%2Fnopassword","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rubymonolith%2Fnopassword/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rubymonolith%2Fnopassword/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rubymonolith%2Fnopassword/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/rubymonolith","download_url":"https://codeload.github.com/rubymonolith/nopassword/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rubymonolith%2Fnopassword/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28888429,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-29T21:06:44.224Z","status":"ssl_error","status_checked_at":"2026-01-29T21:06:42.160Z","response_time":59,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"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":[],"created_at":"2024-08-06T08:01:40.685Z","updated_at":"2026-01-29T22:41:15.197Z","avatar_url":"https://github.com/rubymonolith.png","language":"Ruby","funding_links":[],"categories":["Ruby"],"sub_categories":[],"readme":"# NoPassword\n\n[![Ruby](https://github.com/rocketshipio/nopassword/actions/workflows/ruby.yml/badge.svg)](https://github.com/rocketshipio/nopassword/actions/workflows/ruby.yml)\n[![Gem Version](https://badge.fury.io/rb/nopassword.svg)](https://rubygems.org/gems/nopassword)\n\nNoPassword is a toolkit that makes it easy to implement secure, passwordless authentication via email, SMS, or any other side-channel. It also includes OAuth controllers for Google and Apple sign-in.\n\n## Installation\n\nAdd this line to your Rails application's Gemfile:\n\n```bash\n$ bundle add nopassword\n```\n\nThen install the controllers and views:\n\n```bash\n$ bundle exec rails generate nopassword:install\n```\n\nAdd the route to your `config/routes.rb`:\n\n```ruby\nnopassword EmailAuthenticationsController\n```\n\nRestart the development server and head to `http://localhost:3000/email_authentications/new`.\n\n## How It Works\n\nNoPassword uses a session-bound token approach:\n\n1. User enters their email in your app\n2. A 128-bit random token is generated and stored in the user's session\n3. A link containing the token is emailed to the user\n4. User clicks the link — it only works in the same browser that requested it\n\n### Why is this secure?\n\nThe token in the email is useless without the matching session. An attacker who intercepts the email would need BOTH:\n- The link from the email\n- The victim's session cookie\n\nIf they already have the session cookie, they already have access to the session anyway.\n\n### How is this different from other magic link gems?\n\nMost magic link gems put the entire secret in the email. Anyone with the link can authenticate from any browser.\n\nNoPassword binds the link to the user's session — the link only works in the browser that requested it. This adds a second factor: possession of the session cookie.\n\n### Rate limiting\n\nNoPassword does not rate limit email sending — that's your responsibility. Use Rails' built-in rate limiting:\n\n```ruby\nclass EmailAuthenticationsController \u003c NoPassword::EmailAuthenticationsController\n  rate_limit to: 5, within: 1.minute, only: :create, with: -\u003e {\n    flash[:alert] = \"Too many requests. Please wait a minute.\"\n    redirect_to url_for(action: :new)\n  }\nend\n```\n\n## Usage\n\nCustomize the installed controller to integrate with your user system:\n\n```ruby\nclass EmailAuthenticationsController \u003c NoPassword::EmailAuthenticationsController\n  def verification_succeeded(email)\n    self.current_user = User.find_or_create_by!(email: email)\n    redirect_to dashboard_url\n  end\nend\n```\n\n### Hook Methods\n\nOverride these methods to customize behavior:\n\n```ruby\nclass EmailAuthenticationsController \u003c NoPassword::EmailAuthenticationsController\n  # Called when the user successfully verifies their email\n  def verification_succeeded(email)\n    redirect_to root_url\n  end\n\n  # Called when the link has expired\n  def verification_expired(verification)\n    flash[:alert] = \"Link has expired. Please try again.\"\n    redirect_to url_for(action: :new)\n  end\n\n  # Called when the token is invalid\n  def verification_failed(verification)\n    flash.now[:alert] = verification.errors.full_messages.to_sentence\n    render :show, status: :unprocessable_entity\n  end\n\n  # Called when the link is opened in a different browser\n  def verification_different_browser(verification)\n    flash.now[:alert] = \"Please open this link in the browser where you requested it.\"\n    render :show, status: :unprocessable_entity\n  end\n\n  # Customize how the email is sent\n  def deliver_challenge(challenge)\n    EmailAuthenticationMailer\n      .with(email: challenge.email, url: show_url(challenge.token))\n      .authentication_email\n      .deliver_later\n  end\n\n  # Default URL to redirect to after authentication\n  def after_authentication_url\n    root_url\n  end\nend\n```\n\n### Handling Different Browser\n\nWhen a user opens the link in a different browser (e.g., email app's webview), the verification will fail because there's no matching session. Override the `verification_different_browser` hook to customize this behavior:\n\n```ruby\nclass EmailAuthenticationsController \u003c NoPassword::EmailAuthenticationsController\n  def verification_different_browser(verification)\n    # Show a page explaining they need to copy the link to their original browser\n    render :different_browser\n  end\nend\n```\n\n## Ejecting for Full Control\n\nThe generator gives you views you can customize. If you need full control over the controller too, include the concern directly:\n\n```ruby\nclass SessionsController \u003c ApplicationController\n  include NoPassword::EmailAuthentication\n\n  def verification_succeeded(email)\n    self.current_user = User.find_or_create_by!(email: email)\n    redirect_to dashboard_url\n  end\nend\n```\n\nThen use `nopassword` with your controller — the routes come with the concern:\n\n```ruby\n# config/routes.rb\nnopassword SessionsController  # generates /sessions routes\n```\n\nThe routes are derived from your controller name. To customize the path:\n\n```ruby\n# config/routes.rb\nnopassword SessionsController, path: \"login\"  # generates /login routes\n```\n\nOr skip the concern entirely and use the models directly with your own views and routes:\n\n```ruby\nclass SessionsController \u003c ApplicationController\n  def new\n    @authentication = NoPassword::Email::Authentication.new(session)\n  end\n\n  def create\n    @authentication = NoPassword::Email::Authentication.new(session)\n    @authentication.email = params[:email]\n\n    if @authentication.valid? \u0026\u0026 @authentication.challenge.save\n      @authentication.save\n      # Send your own email\n      SessionMailer.with(url: verify_url(@authentication.challenge.token)).deliver_later\n      redirect_to :check_email\n    else\n      render :new, status: :unprocessable_entity\n    end\n  end\n\n  def show\n    @authentication = NoPassword::Email::Authentication.new(session)\n    @verification = @authentication.verification(token: params[:id])\n  end\n\n  def update\n    @authentication = NoPassword::Email::Authentication.new(session)\n    @verification = @authentication.verification(token: params[:id])\n\n    if @verification.verify\n      self.current_user = User.find_or_create_by!(email: @authentication.email)\n      @authentication.delete\n      redirect_to dashboard_url\n    else\n      render :show, status: :unprocessable_entity\n    end\n  end\nend\n```\n\n## Architecture\n\nNoPassword is organized into composable modules:\n\n```\nNoPassword\n├── Link                            # Token challenge/verification\n│   ├── Base                        # Session storage mechanics\n│   ├── Challenge                   # Generates token, stores identifier, TTL\n│   └── Verification                # Validates token, checks expiration\n├── Session                         # Controller helpers for session management\n│   └── Authentication              # Stores return_url, wraps Link\n├── Email                           # Email-specific implementation\n│   ├── Authentication              # Adds email validation\n│   ├── Challenge                   # Aliases identifier as email\n│   └── Mailer                      # ActionMailer for sending links\n├── EmailAuthentication             # Controller concern with all actions\n├── EmailAuthenticationsController  # Ready-to-use controller\n└── OAuth\n    ├── GoogleAuthorizationsController\n    └── AppleAuthorizationsController\n```\n\n### Extending for SMS or other channels\n\nThe `Link` module is channel-agnostic. To add SMS support:\n\n```ruby\nclass SmsAuthentication \u003c NoPassword::Session::Authentication\n  attribute :phone, :string\n  validates :phone, presence: true, format: { with: /\\A\\+?[1-9]\\d{1,14}\\z/ }\n\n  def identifier\n    phone\n  end\nend\n```\n\n## OAuth Authorizations\n\nNoPassword includes OAuth controllers for Google and Apple. Create a controller that inherits from the OAuth controller:\n\n```ruby\n# app/controllers/google_authorizations_controller.rb\nclass GoogleAuthorizationsController \u003c NoPassword::OAuth::GoogleAuthorizationsController\n  def self.credentials = Rails.application.credentials.google\n  def self.client_id = credentials.client_id\n  def self.client_secret = credentials.client_secret\n\n  protected\n    def authorization_succeeded(sso)\n      user = User.find_or_create_by(email: sso.fetch(\"email\"))\n      user.update!(name: sso.fetch(\"name\"))\n\n      self.current_user = user\n      redirect_to root_url\n    end\n\n    def authorization_failed\n      redirect_to login_path, alert: \"OAuth authorization failed\"\n    end\nend\n```\n\nOr with environment variables:\n\n```ruby\nclass GoogleAuthorizationsController \u003c NoPassword::OAuth::GoogleAuthorizationsController\n  def self.client_id = ENV[\"GOOGLE_CLIENT_ID\"]\n  def self.client_secret = ENV[\"GOOGLE_CLIENT_SECRET\"]\n\n  # ...\nend\n```\n\nAdd the route:\n\n```ruby\n# ./config/routes.rb\nnopassword GoogleAuthorizationsController\n```\n\nCreate a sign-in button:\n\n```erb\n\u003c%= form_tag google_authorization_path, data: { turbo: false } do %\u003e\n  \u003c%= submit_tag \"Sign in with Google\" %\u003e\n\u003c% end %\u003e\n```\n\n## Why NoPassword?\n\nPasswords are a pain:\n\n1. **People choose weak passwords** - Complexity requirements make them hard to remember\n2. **People forget passwords** - Password reset flows use email anyway\n3. **Password fatigue** - Users appreciate not having to create yet another password\n\n## Contributing\n\nIf you'd like to contribute, start a discussion at https://github.com/rocketshipio/nopassword/discussions/categories/ideas.\n\n## License\n\nThe gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frubymonolith%2Fnopassword","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frubymonolith%2Fnopassword","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frubymonolith%2Fnopassword/lists"}