https://github.com/rubymonolith/nopassword
Login via email, SMS, or whatever would use a temporary code
https://github.com/rubymonolith/nopassword
Last synced: 4 months ago
JSON representation
Login via email, SMS, or whatever would use a temporary code
- Host: GitHub
- URL: https://github.com/rubymonolith/nopassword
- Owner: rubymonolith
- Created: 2022-02-14T18:49:06.000Z (over 4 years ago)
- Default Branch: main
- Last Pushed: 2026-01-13T23:30:35.000Z (5 months ago)
- Last Synced: 2026-01-14T01:29:02.126Z (5 months ago)
- Language: Ruby
- Size: 170 KB
- Stars: 135
- Watchers: 3
- Forks: 5
- Open Issues: 5
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# NoPassword
[](https://github.com/rocketshipio/nopassword/actions/workflows/ruby.yml)
[](https://rubygems.org/gems/nopassword)
NoPassword 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.
## Installation
Add this line to your Rails application's Gemfile:
```bash
$ bundle add nopassword
```
Then install the controllers and views:
```bash
$ bundle exec rails generate nopassword:install
```
Add the route to your `config/routes.rb`:
```ruby
nopassword EmailAuthenticationsController
```
Restart the development server and head to `http://localhost:3000/email_authentications/new`.
## How It Works
NoPassword uses a session-bound token approach:
1. User enters their email in your app
2. A 128-bit random token is generated and stored in the user's session
3. A link containing the token is emailed to the user
4. User clicks the link — it only works in the same browser that requested it
### Why is this secure?
The token in the email is useless without the matching session. An attacker who intercepts the email would need BOTH:
- The link from the email
- The victim's session cookie
If they already have the session cookie, they already have access to the session anyway.
### How is this different from other magic link gems?
Most magic link gems put the entire secret in the email. Anyone with the link can authenticate from any browser.
NoPassword 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.
### Rate limiting
NoPassword does not rate limit email sending — that's your responsibility. Use Rails' built-in rate limiting:
```ruby
class EmailAuthenticationsController < NoPassword::EmailAuthenticationsController
rate_limit to: 5, within: 1.minute, only: :create, with: -> {
flash[:alert] = "Too many requests. Please wait a minute."
redirect_to url_for(action: :new)
}
end
```
## Usage
Customize the installed controller to integrate with your user system:
```ruby
class EmailAuthenticationsController < NoPassword::EmailAuthenticationsController
def verification_succeeded(email)
self.current_user = User.find_or_create_by!(email: email)
redirect_to dashboard_url
end
end
```
### Hook Methods
Override these methods to customize behavior:
```ruby
class EmailAuthenticationsController < NoPassword::EmailAuthenticationsController
# Called when the user successfully verifies their email
def verification_succeeded(email)
redirect_to root_url
end
# Called when the link has expired
def verification_expired(verification)
flash[:alert] = "Link has expired. Please try again."
redirect_to url_for(action: :new)
end
# Called when the token is invalid
def verification_failed(verification)
flash.now[:alert] = verification.errors.full_messages.to_sentence
render :show, status: :unprocessable_entity
end
# Called when the link is opened in a different browser
def verification_different_browser(verification)
flash.now[:alert] = "Please open this link in the browser where you requested it."
render :show, status: :unprocessable_entity
end
# Customize how the email is sent
def deliver_challenge(challenge)
EmailAuthenticationMailer
.with(email: challenge.email, url: show_url(challenge.token))
.authentication_email
.deliver_later
end
# Default URL to redirect to after authentication
def after_authentication_url
root_url
end
end
```
### Handling Different Browser
When 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:
```ruby
class EmailAuthenticationsController < NoPassword::EmailAuthenticationsController
def verification_different_browser(verification)
# Show a page explaining they need to copy the link to their original browser
render :different_browser
end
end
```
## Ejecting for Full Control
The generator gives you views you can customize. If you need full control over the controller too, include the concern directly:
```ruby
class SessionsController < ApplicationController
include NoPassword::EmailAuthentication
def verification_succeeded(email)
self.current_user = User.find_or_create_by!(email: email)
redirect_to dashboard_url
end
end
```
Then use `nopassword` with your controller — the routes come with the concern:
```ruby
# config/routes.rb
nopassword SessionsController # generates /sessions routes
```
The routes are derived from your controller name. To customize the path:
```ruby
# config/routes.rb
nopassword SessionsController, path: "login" # generates /login routes
```
Or skip the concern entirely and use the models directly with your own views and routes:
```ruby
class SessionsController < ApplicationController
def new
@authentication = NoPassword::Email::Authentication.new(session)
end
def create
@authentication = NoPassword::Email::Authentication.new(session)
@authentication.email = params[:email]
if @authentication.valid? && @authentication.challenge.save
@authentication.save
# Send your own email
SessionMailer.with(url: verify_url(@authentication.challenge.token)).deliver_later
redirect_to :check_email
else
render :new, status: :unprocessable_entity
end
end
def show
@authentication = NoPassword::Email::Authentication.new(session)
@verification = @authentication.verification(token: params[:id])
end
def update
@authentication = NoPassword::Email::Authentication.new(session)
@verification = @authentication.verification(token: params[:id])
if @verification.verify
self.current_user = User.find_or_create_by!(email: @authentication.email)
@authentication.delete
redirect_to dashboard_url
else
render :show, status: :unprocessable_entity
end
end
end
```
## Architecture
NoPassword is organized into composable modules:
```
NoPassword
├── Link # Token challenge/verification
│ ├── Base # Session storage mechanics
│ ├── Challenge # Generates token, stores identifier, TTL
│ └── Verification # Validates token, checks expiration
├── Session # Controller helpers for session management
│ └── Authentication # Stores return_url, wraps Link
├── Email # Email-specific implementation
│ ├── Authentication # Adds email validation
│ ├── Challenge # Aliases identifier as email
│ └── Mailer # ActionMailer for sending links
├── EmailAuthentication # Controller concern with all actions
├── EmailAuthenticationsController # Ready-to-use controller
└── OAuth
├── GoogleAuthorizationsController
└── AppleAuthorizationsController
```
### Extending for SMS or other channels
The `Link` module is channel-agnostic. To add SMS support:
```ruby
class SmsAuthentication < NoPassword::Session::Authentication
attribute :phone, :string
validates :phone, presence: true, format: { with: /\A\+?[1-9]\d{1,14}\z/ }
def identifier
phone
end
end
```
## OAuth Authorizations
NoPassword includes OAuth controllers for Google and Apple. Create a controller that inherits from the OAuth controller:
```ruby
# app/controllers/google_authorizations_controller.rb
class GoogleAuthorizationsController < NoPassword::OAuth::GoogleAuthorizationsController
def self.credentials = Rails.application.credentials.google
def self.client_id = credentials.client_id
def self.client_secret = credentials.client_secret
protected
def authorization_succeeded(sso)
user = User.find_or_create_by(email: sso.fetch("email"))
user.update!(name: sso.fetch("name"))
self.current_user = user
redirect_to root_url
end
def authorization_failed
redirect_to login_path, alert: "OAuth authorization failed"
end
end
```
Or with environment variables:
```ruby
class GoogleAuthorizationsController < NoPassword::OAuth::GoogleAuthorizationsController
def self.client_id = ENV["GOOGLE_CLIENT_ID"]
def self.client_secret = ENV["GOOGLE_CLIENT_SECRET"]
# ...
end
```
Add the route:
```ruby
# ./config/routes.rb
nopassword GoogleAuthorizationsController
```
Create a sign-in button:
```erb
<%= form_tag google_authorization_path, data: { turbo: false } do %>
<%= submit_tag "Sign in with Google" %>
<% end %>
```
## Why NoPassword?
Passwords are a pain:
1. **People choose weak passwords** - Complexity requirements make them hard to remember
2. **People forget passwords** - Password reset flows use email anyway
3. **Password fatigue** - Users appreciate not having to create yet another password
## Contributing
If you'd like to contribute, start a discussion at https://github.com/rocketshipio/nopassword/discussions/categories/ideas.
## License
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).