This repository was archived by the owner on May 8, 2026. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 103
[WIP] User authentication tutorial - ROM, Rails, Warden #177
Open
janjiss
wants to merge
7
commits into
main
Choose a base branch
from
auth-with-rails
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
30bc9ae
Added auth with Rails guide
janjiss 3af957b
Added more comperhansive description of the code parts
janjiss fad674b
Fixed spelling mistakes and added file locations
janjiss 8644af9
Fixed missing file names in the begginning of the file
janjiss 3e22d71
Added empty lines before the code
janjiss 64148b6
Added more whitespaces
janjiss df42020
Procfile => Rakefile
janjiss File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,272 @@ | ||
| --- | ||
| chapter: Getting Started | ||
| title: Simple authentication with ROM and Rails | ||
| --- | ||
|
|
||
| ## Initial setup | ||
|
|
||
| This guide describes how to add a simplest form of user authentication using ROM, Warden and Rails. | ||
|
|
||
| > Please note that this guide is only for illustrative purposes and does not include validations | ||
|
|
||
| First of all, let's add following gems to the Gemfile: | ||
|
|
||
| ```ruby | ||
| # Gemfile | ||
| gem 'rom-rails' | ||
| gem 'rom-sql' | ||
| gem 'rom-repository' | ||
| gem 'rom' | ||
| gem 'warden' | ||
| gem 'bcrypt' | ||
| gem 'sqlite3' | ||
| ``` | ||
|
|
||
| Now we will need to add configuration for ROM to work. In this example we are going to use SQLite3 to store our data: | ||
|
|
||
| ```ruby | ||
| # config/initializers/rom.rb | ||
| ROM::Rails::Railtie.configure do |config| | ||
| config.gateways[:default] = [:sql, "sqlite://db/dev.db"] | ||
| end | ||
| ``` | ||
|
|
||
| After this, we will need to add ROM rake tasks in our Rakefile | ||
|
|
||
| ```ruby | ||
| # Rakefile | ||
| require 'rom/sql/rake_task' | ||
| ``` | ||
|
|
||
| When this is done, let's generate a migration that adds users table to our database: | ||
|
|
||
| ```bash | ||
| rake db:create_migration[create_users] | ||
| # <= migration file created db/migrate/20161109173831_create_users.rb | ||
| ``` | ||
|
|
||
| Let's open up our migration file and add the following lines: | ||
|
|
||
| ```ruby | ||
| # db/migrate/20161109173831_create_users.rb | ||
| ROM::SQL.migration do | ||
| change do | ||
| create_table :users do | ||
| primary_key :id | ||
| column :email, String, null: false, unique: true | ||
| column :encrypted_password, String, null: false | ||
| end | ||
| end | ||
| end | ||
| ``` | ||
|
|
||
| We want emails to be unique, and both password and email fields to not be NULL. | ||
|
|
||
| Let's run our migration task: | ||
|
|
||
| ```bash | ||
| rake db:migrate | ||
| # <= db:migrate executed | ||
| ``` | ||
|
|
||
| Once that is done, we can start working on our repositories and rom components. Let's add schema file first: | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. here as well, and in all other places, basically there must always be an empty line between a line of text and code block syntax
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, missed this one. Should be fixed |
||
|
|
||
| ```ruby | ||
| # app/relations/users.rb | ||
| class Users < ROM::Relation[:sql] | ||
| end | ||
| ``` | ||
|
|
||
| After that, let's add repository that will do all the heavy lifting for us: | ||
|
|
||
| ```ruby | ||
| # app/repos/users_repo.rb | ||
| include BCrypt | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you replace this with |
||
|
|
||
| class UsersRepo < ROM::Repository[:users] | ||
| def find_by_id(id) | ||
| users.where(id: id).one | ||
| end | ||
|
|
||
| def authenticate(email, password) | ||
| user = users.where(email: email).one | ||
| if user && Password.new(user.encrypted_password) == password | ||
| user | ||
| else | ||
| nil | ||
| end | ||
| end | ||
|
|
||
| def create(email, password) | ||
| users.insert(email: email, encrypted_password: Password.create(password)) | ||
| end | ||
| end | ||
| ``` | ||
|
|
||
| In this repo we will have three methods. `find_by_id` finds a user by id, which will be used in warden later on. Another is `authenticate`, that will take email and password and will return user if it is present and password is correct. The third one, `create` is for creating user with encrypted password. | ||
|
|
||
| Now that this is done, we can implement warden password strategy. In the initializers you can put the following code: | ||
|
|
||
| ```ruby | ||
| # config/initializers/warden.rb | ||
| Rails.application.config.middleware.use Warden::Manager do |manager| | ||
| manager.default_strategies :password | ||
| end | ||
|
|
||
| Warden::Manager.serialize_into_session do |user| | ||
| user.id | ||
| end | ||
|
|
||
| Warden::Manager.serialize_from_session do |id| | ||
| UsersRepo.new(ROM.env).find_by_id(id) | ||
| end | ||
|
|
||
| Warden::Strategies.add(:password) do | ||
| def authenticate! | ||
| user = UsersRepo.new(ROM.env).authenticate(params["session"]["email"], params["session"]["password"]) | ||
| if user | ||
| success! user | ||
| else | ||
| fail! "Invalid email or password" | ||
| end | ||
| end | ||
| end | ||
| ``` | ||
|
|
||
| In first block we define that we want to use password strategy for authentication. | ||
| Following are the lines we are identifying how we want to serialize and deserialize user from and to session. | ||
| At last part we define what the authentication for the user will look like and call appropriate warden methods. | ||
|
|
||
| Let's add a controller for handling sessions with a new session form, create session action and destroy session action. | ||
| We use wardens `authenticate` method to get required parameters and authenticate user. If warden returns a user object | ||
| that means we authenticated successfully and can proceed to logging in. In a case where there is no user matching credentials, | ||
| we redirect user with an error message from warden. | ||
|
|
||
| ```ruby | ||
| # app/controllers/user_sessions_controller.rb | ||
| class UserSessionsController < ApplicationController | ||
| def new | ||
| end | ||
|
|
||
| def create | ||
| user = env['warden'].authenticate | ||
| if user | ||
| redirect_to new_user_session_path, notice: "Logged in!" | ||
| else | ||
| flash[:alert] = env['warden'].message | ||
| render "new" | ||
| end | ||
| end | ||
|
|
||
| def destroy | ||
| env['warden'].logout | ||
| redirect_to new_user_session_path, notice: "Logged out!" | ||
| end | ||
| end | ||
| ``` | ||
|
|
||
| The next step in line is to add user creation. Please take a note that user creation has no validations in place, so don't put this code in production. | ||
| We use `create` method on the UsersRepo that we created to achieve that: | ||
|
|
||
| ```ruby | ||
| # app/controllers/users_controller.rb | ||
| class UsersController < ApplicationController | ||
| def new | ||
| end | ||
|
|
||
| def create | ||
| UsersRepo.new(ROM.env).create(params[:user][:email], params[:user][:password]) | ||
| redirect_to new_user_session_path, notice: "User created. Now you can log in!" | ||
| end | ||
| end | ||
| ``` | ||
|
|
||
| There is one additional step that we need to take and define a `current_user` helper method so that it can be used in views. | ||
| We achieve that by getting user object from warden middleware: | ||
|
|
||
| ```ruby | ||
| # app/controllers/application_controller.rb | ||
| class ApplicationController < ActionController::Base | ||
| protect_from_forgery with: :exception | ||
| helper_method :current_user | ||
|
|
||
| def current_user | ||
| env["warden"].user | ||
| end | ||
| end | ||
| ``` | ||
|
|
||
| After we are done with controllers, we need to map them to routes: | ||
|
|
||
| ```ruby | ||
| # config/routes.rb | ||
| Rails.application.routes.draw do | ||
| resources :users, only: [:new, :create] | ||
| resources :user_sessions, only: [:new, :create] | ||
| match 'user_sessions/destroy' => "user_sessions#destroy", via: :delete, as: :destroy_user_sessions | ||
| end | ||
| ``` | ||
|
|
||
| Let's create a header partial that can be included in all our view files and that has links to Log Out and Sign Up in `app/views/layouts/_header.html.erb`: | ||
|
|
||
| ```ruby | ||
| <% if current_user %> | ||
| Hello <%= current_user.email %>! | ||
| <%= link_to "Log out", destroy_user_sessions_path, method: :delete %> | ||
| <% else %> | ||
| Hello guest! | ||
| <%= link_to "Sign up", new_user_path %> | ||
| <% end %> | ||
| <% flash.each do |key, value| %> | ||
| <div class="alert alert-<%= key %>"><%= value %></div> | ||
| <% end %> | ||
| ``` | ||
|
|
||
| The next thing that we should do is to add login form in `app/views/user_sessions/new.html.erb`: | ||
|
|
||
| ```ruby | ||
| <%= render partial: "layouts/header" %> | ||
| <h1>Log in</h1> | ||
|
|
||
| <div class="row"> | ||
| <div class="col-md-6 col-md-offset-3"> | ||
| <%= form_for(:session, url: user_sessions_path) do |f| %> | ||
|
|
||
| <%= f.label :email %> | ||
| <%= f.email_field :email, class: 'form-control' %> | ||
|
|
||
| <%= f.label :password %> | ||
| <%= f.password_field :password, class: 'form-control' %> | ||
|
|
||
| <%= f.submit "Log in", class: "btn btn-primary" %> | ||
| <% end %> | ||
|
|
||
| </div> | ||
| </div> | ||
| ``` | ||
|
|
||
| Now let's create login form for users to have ability to sign up in `app/views/users/new.html.erb`: | ||
|
|
||
| ```ruby | ||
| <%= render partial: "layouts/header" %> | ||
|
|
||
| <h1>Sign up</h1> | ||
|
|
||
| <div class="row"> | ||
| <div class="col-md-6 col-md-offset-3"> | ||
| <%= form_for(:user, url: users_path) do |f| %> | ||
|
|
||
| <%= f.label :email %> | ||
| <%= f.email_field :email, class: 'form-control' %> | ||
|
|
||
| <%= f.label :password %> | ||
| <%= f.password_field :password, class: 'form-control' %> | ||
|
|
||
| <%= f.submit "Log in", class: "btn btn-primary" %> | ||
| <% end %> | ||
|
|
||
| </div> | ||
| </div> | ||
| ``` | ||
|
|
||
| And we are done! Check out `localhost:3000/user_sessions/new` to have ability to log in or to navigate to the sign up form. | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you gotta add an empty line here, for some reason middleman doesn't render html correctly without it