├── .gitignore ├── Gemfile ├── Gemfile.lock ├── MIT-LICENSE ├── README.md ├── Rakefile ├── app ├── controllers │ └── nopassword │ │ ├── application_controller.rb │ │ ├── check_session.rb │ │ └── nopassword_controller.rb ├── helpers │ └── nopassword │ │ └── application_helper.rb ├── mailers │ └── nopassword │ │ └── no_password_emails.rb ├── models │ └── nopassword │ │ └── login_session.rb └── views │ └── nopassword │ └── no_password_emails │ └── no_password_email.html.erb ├── config ├── locales │ └── nopassword.en.yml └── routes.rb ├── db └── migrate │ ├── 20120412041547_create_login_codes.rb │ ├── 20120810233403_add_requesting_geo_to_login_sessions.rb │ └── 20120810233418_add_activating_geo_to_login_sessions.rb ├── lib ├── nopassword.rb ├── nopassword │ ├── engine.rb │ └── version.rb └── tasks │ └── nopassword_tasks.rake ├── nopassword.gemspec ├── script └── rails └── test ├── dummy ├── Gemfile ├── Gemfile.lock ├── README.md ├── Rakefile ├── app │ ├── assets │ │ ├── images │ │ │ └── rails.png │ │ ├── javascripts │ │ │ └── application.js │ │ └── stylesheets │ │ │ └── application.css │ ├── controllers │ │ └── application_controller.rb │ ├── helpers │ │ └── application_helper.rb │ ├── mailers │ │ └── .gitkeep │ ├── models │ │ └── .gitkeep │ └── views │ │ ├── application │ │ └── index.html.erb │ │ └── layouts │ │ └── application.html.erb ├── config.ru ├── config │ ├── application.rb │ ├── boot.rb │ ├── database.yml │ ├── environment.rb │ ├── environments │ │ ├── development.rb │ │ ├── production.rb │ │ └── test.rb │ ├── initializers │ │ ├── aws.rb │ │ ├── backtrace_silencers.rb │ │ ├── inflections.rb │ │ ├── mime_types.rb │ │ ├── secret_token.rb │ │ ├── session_store.rb │ │ └── wrap_parameters.rb │ ├── locales │ │ └── en.yml │ └── routes.rb ├── db │ ├── migrate │ │ ├── 20120903014052_create_login_codes.nopassword.rb │ │ ├── 20120903014053_add_requesting_geo_to_login_sessions.nopassword.rb │ │ └── 20120903014054_add_activating_geo_to_login_sessions.nopassword.rb │ ├── schema.rb │ └── test.sqlite3 ├── lib │ └── assets │ │ └── .gitkeep ├── log │ └── .gitkeep ├── public │ ├── 404.html │ ├── 422.html │ ├── 500.html │ ├── assets │ │ └── rails.png │ └── favicon.ico └── script │ └── rails ├── functional └── nopassword │ └── nopassword_controller_test.rb ├── integration └── navigation_test.rb ├── login_session_test.rb ├── nopassword_test.rb └── test_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .bundle 3 | test/dummy/db/*.sqlite3 4 | test/dummy/log/* 5 | test/dummy/tmp/**/* 6 | test/dummy/config/passwords/* 7 | *.swp 8 | .env 9 | GeoLiteCity.db 10 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gem 'rails' 3 | group :development, :test do 4 | gem 'sqlite3' 5 | end 6 | group :production do 7 | gem 'pg' 8 | end 9 | 10 | # Gems used only for assets and not required 11 | # in production environments by default. 12 | group :assets do 13 | gem 'sass-rails' 14 | gem 'coffee-rails' 15 | gem 'therubyracer' 16 | gem 'uglifier' 17 | end 18 | 19 | gem 'bcrypt' 20 | gem 'browser' 21 | gem 'geoip' 22 | gem 'aws-sdk-rails' 23 | gem 'dotenv' 24 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | actioncable (5.2.0) 5 | actionpack (= 5.2.0) 6 | nio4r (~> 2.0) 7 | websocket-driver (>= 0.6.1) 8 | actionmailer (5.2.0) 9 | actionpack (= 5.2.0) 10 | actionview (= 5.2.0) 11 | activejob (= 5.2.0) 12 | mail (~> 2.5, >= 2.5.4) 13 | rails-dom-testing (~> 2.0) 14 | actionpack (5.2.0) 15 | actionview (= 5.2.0) 16 | activesupport (= 5.2.0) 17 | rack (~> 2.0) 18 | rack-test (>= 0.6.3) 19 | rails-dom-testing (~> 2.0) 20 | rails-html-sanitizer (~> 1.0, >= 1.0.2) 21 | actionview (5.2.0) 22 | activesupport (= 5.2.0) 23 | builder (~> 3.1) 24 | erubi (~> 1.4) 25 | rails-dom-testing (~> 2.0) 26 | rails-html-sanitizer (~> 1.0, >= 1.0.3) 27 | activejob (5.2.0) 28 | activesupport (= 5.2.0) 29 | globalid (>= 0.3.6) 30 | activemodel (5.2.0) 31 | activesupport (= 5.2.0) 32 | activerecord (5.2.0) 33 | activemodel (= 5.2.0) 34 | activesupport (= 5.2.0) 35 | arel (>= 9.0) 36 | activestorage (5.2.0) 37 | actionpack (= 5.2.0) 38 | activerecord (= 5.2.0) 39 | marcel (~> 0.3.1) 40 | activesupport (5.2.0) 41 | concurrent-ruby (~> 1.0, >= 1.0.2) 42 | i18n (>= 0.7, < 2) 43 | minitest (~> 5.1) 44 | tzinfo (~> 1.1) 45 | arel (9.0.0) 46 | aws-partitions (1.83.0) 47 | aws-sdk-core (3.20.2) 48 | aws-partitions (~> 1.0) 49 | aws-sigv4 (~> 1.0) 50 | jmespath (~> 1.0) 51 | aws-sdk-rails (2.0.1) 52 | aws-sdk-ses (~> 1) 53 | railties (>= 3) 54 | aws-sdk-ses (1.6.0) 55 | aws-sdk-core (~> 3) 56 | aws-sigv4 (~> 1.0) 57 | aws-sigv4 (1.0.2) 58 | bcrypt (3.1.11) 59 | browser (2.5.3) 60 | builder (3.2.3) 61 | coffee-rails (4.2.2) 62 | coffee-script (>= 2.2.0) 63 | railties (>= 4.0.0) 64 | coffee-script (2.4.1) 65 | coffee-script-source 66 | execjs 67 | coffee-script-source (1.12.2) 68 | concurrent-ruby (1.1.5) 69 | crass (1.0.5) 70 | dotenv (2.4.0) 71 | erubi (1.7.1) 72 | execjs (2.7.0) 73 | ffi (1.11.1) 74 | geoip (1.6.4) 75 | globalid (0.4.1) 76 | activesupport (>= 4.2.0) 77 | i18n (1.0.1) 78 | concurrent-ruby (~> 1.0) 79 | jmespath (1.4.0) 80 | libv8 (3.16.14.19) 81 | loofah (2.3.1) 82 | crass (~> 1.0.2) 83 | nokogiri (>= 1.5.9) 84 | mail (2.7.0) 85 | mini_mime (>= 0.1.1) 86 | marcel (0.3.2) 87 | mimemagic (~> 0.3.2) 88 | method_source (0.9.0) 89 | mimemagic (0.3.2) 90 | mini_mime (1.0.0) 91 | mini_portile2 (2.4.0) 92 | minitest (5.11.3) 93 | nio4r (2.3.1) 94 | nokogiri (1.10.5) 95 | mini_portile2 (~> 2.4.0) 96 | pg (1.0.0) 97 | rack (2.0.8) 98 | rack-test (1.0.0) 99 | rack (>= 1.0, < 3) 100 | rails (5.2.0) 101 | actioncable (= 5.2.0) 102 | actionmailer (= 5.2.0) 103 | actionpack (= 5.2.0) 104 | actionview (= 5.2.0) 105 | activejob (= 5.2.0) 106 | activemodel (= 5.2.0) 107 | activerecord (= 5.2.0) 108 | activestorage (= 5.2.0) 109 | activesupport (= 5.2.0) 110 | bundler (>= 1.3.0) 111 | railties (= 5.2.0) 112 | sprockets-rails (>= 2.0.0) 113 | rails-dom-testing (2.0.3) 114 | activesupport (>= 4.2.0) 115 | nokogiri (>= 1.6) 116 | rails-html-sanitizer (1.0.4) 117 | loofah (~> 2.2, >= 2.2.2) 118 | railties (5.2.0) 119 | actionpack (= 5.2.0) 120 | activesupport (= 5.2.0) 121 | method_source 122 | rake (>= 0.8.7) 123 | thor (>= 0.18.1, < 2.0) 124 | rake (12.3.1) 125 | rb-fsevent (0.10.3) 126 | rb-inotify (0.9.10) 127 | ffi (>= 0.5.0, < 2) 128 | ref (2.0.0) 129 | sass (3.5.6) 130 | sass-listen (~> 4.0.0) 131 | sass-listen (4.0.0) 132 | rb-fsevent (~> 0.9, >= 0.9.4) 133 | rb-inotify (~> 0.9, >= 0.9.7) 134 | sass-rails (5.0.7) 135 | railties (>= 4.0.0, < 6) 136 | sass (~> 3.1) 137 | sprockets (>= 2.8, < 4.0) 138 | sprockets-rails (>= 2.0, < 4.0) 139 | tilt (>= 1.1, < 3) 140 | sprockets (3.7.2) 141 | concurrent-ruby (~> 1.0) 142 | rack (> 1, < 3) 143 | sprockets-rails (3.2.1) 144 | actionpack (>= 4.0) 145 | activesupport (>= 4.0) 146 | sprockets (>= 3.0.0) 147 | sqlite3 (1.3.13) 148 | therubyracer (0.12.3) 149 | libv8 (~> 3.16.14.15) 150 | ref 151 | thor (0.20.0) 152 | thread_safe (0.3.6) 153 | tilt (2.0.8) 154 | tzinfo (1.2.5) 155 | thread_safe (~> 0.1) 156 | uglifier (4.1.10) 157 | execjs (>= 0.3.0, < 3) 158 | websocket-driver (0.7.0) 159 | websocket-extensions (>= 0.1.0) 160 | websocket-extensions (0.1.3) 161 | 162 | PLATFORMS 163 | ruby 164 | 165 | DEPENDENCIES 166 | aws-sdk-rails 167 | bcrypt 168 | browser 169 | coffee-rails 170 | dotenv 171 | geoip 172 | pg 173 | rails 174 | sass-rails 175 | sqlite3 176 | therubyracer 177 | uglifier 178 | 179 | BUNDLED WITH 180 | 1.16.0 181 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2012 YOURNAME 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | NoPassword is a simple authentication and session engine that removes 2 | the need for passwords. Instead, it uses tokens sent to an email 3 | address, similar to most forgot password functionality. These tokens 4 | created long-lived sessions that can be tracked and revoked easily. 5 | 6 | [Ben Brown](http://ilovebenbrown.com/) wrote a great article about [password-less logins](http://notes.xoxco.com/post/27999787765/is-it-time-for-password-less-login), the same concept implemented by NoPassword. 7 | 8 | NoPassword is structured as a Rails Engine, which you can mount in your 9 | routes file: 10 | 11 | mount Nopassword::Engine, :at => "/nopassword" 12 | 13 | You'll need to install the migrations: 14 | 15 | rake nopassword:install:migrations 16 | rake db:migrate 17 | 18 | You can set up a signin form with the `send_login_email` route and a 19 | request parameter named `email`. 20 | 21 | You'll need to get Rails' [ActionMailer](http://guides.rubyonrails.org/action_mailer_basics.html) configured correctly for sending NoPassword emails. 22 | 23 | Finally, you need to download the latest GeoIP database: 24 | 25 | cd db 26 | wget http://geolite.maxmind.com/download/geoip/database/GeoLiteCity.dat.gz && gzip -d GeoLiteCity.dat.gz 27 | 28 | NoPassword uses the MIT-LICENSE. 29 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | begin 3 | require 'bundler/setup' 4 | rescue LoadError 5 | puts 'You must `gem install bundler` and `bundle install` to run rake tasks' 6 | end 7 | begin 8 | require 'rdoc/task' 9 | rescue LoadError 10 | require 'rdoc/rdoc' 11 | require 'rake/rdoctask' 12 | RDoc::Task = Rake::RDocTask 13 | end 14 | 15 | RDoc::Task.new(:rdoc) do |rdoc| 16 | rdoc.rdoc_dir = 'rdoc' 17 | rdoc.title = 'Nopassword' 18 | rdoc.options << '--line-numbers' 19 | rdoc.rdoc_files.include('README.rdoc') 20 | rdoc.rdoc_files.include('lib/**/*.rb') 21 | end 22 | 23 | APP_RAKEFILE = File.expand_path("../test/dummy/Rakefile", __FILE__) 24 | load 'rails/tasks/engine.rake' 25 | 26 | 27 | 28 | Bundler::GemHelper.install_tasks 29 | 30 | require 'rake/testtask' 31 | 32 | Rake::TestTask.new(:test) do |t| 33 | t.libs << 'lib' 34 | t.libs << 'test' 35 | t.pattern = 'test/**/*_test.rb' 36 | t.verbose = false 37 | end 38 | 39 | 40 | task :default => :test 41 | -------------------------------------------------------------------------------- /app/controllers/nopassword/application_controller.rb: -------------------------------------------------------------------------------- 1 | module Nopassword 2 | class ApplicationController < ActionController::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/controllers/nopassword/check_session.rb: -------------------------------------------------------------------------------- 1 | module Nopassword 2 | module CheckSession 3 | def check_valid_session 4 | if session[:login_session] 5 | @current_session = Nopassword::LoginSession.find_by_id(session[:login_session]) 6 | if !@current_session.active? 7 | session[:login_session] = nil 8 | redirect_to main_app.root_url 9 | end 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/controllers/nopassword/nopassword_controller.rb: -------------------------------------------------------------------------------- 1 | module Nopassword 2 | class NopasswordController < ApplicationController 3 | include CheckSession 4 | 5 | EMAIL_REGEX = /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/ 6 | 7 | before_action :check_valid_session 8 | 9 | def send_login_email 10 | redirect_to main_app.root_url if !request.post? 11 | email = request[:email] 12 | remote_ip = request.remote_ip 13 | user_agent = request.env["HTTP_USER_AGENT"] 14 | host = ENV["NO_PASSWORD_HOST"] || request.host 15 | protocol = request.protocol 16 | if email =~ EMAIL_REGEX 17 | LoginSession.create_session(email, remote_ip, user_agent, host, protocol) 18 | flash[:notice] = t('nopassword.sent_login_email.mail_sent') % { :email => email } 19 | else 20 | flash[:notice] = t('nopassword.sent_login_email.invalid_mail') 21 | end 22 | redirect_to main_app.root_url 23 | end 24 | 25 | def login 26 | id = request[:id] 27 | code = request[:code] 28 | remote_ip = request.remote_ip 29 | user_agent = request.env["HTTP_USER_AGENT"] 30 | login_session = LoginSession.find_by_id(id) 31 | if !login_session 32 | flash[:notice] = t('nopassword.login.invalid_link') 33 | elsif login_session.activated? || login_session.terminated? 34 | flash[:notice] = t('nopassword.login.already_used') 35 | elsif login_session.expired? 36 | flash[:notice] = t('nopassword.login.expired') 37 | elsif !login_session.activate_session(code, remote_ip, user_agent) 38 | flash[:notice] = t('nopassword.login.could_not_be_used') 39 | else 40 | session[:login_session] = login_session.id 41 | end 42 | redirect_to main_app.root_url 43 | end 44 | 45 | def logout 46 | @current_session.logout 47 | session[:login_session] = nil 48 | redirect_to main_app.root_url 49 | end 50 | 51 | def revoke 52 | id = request[:id] 53 | ls = LoginSession.find_by_id(id) 54 | render :json => { :success => :false } unless ls 55 | result = @current_session.revoke(ls) 56 | render :json => { :success => !!result } 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /app/helpers/nopassword/application_helper.rb: -------------------------------------------------------------------------------- 1 | module Nopassword 2 | module ApplicationHelper 3 | def friendly_time(time) 4 | time_ago_in_words(time) + " ago" 5 | end 6 | 7 | def browser_name(ua) 8 | b = Browser.new(ua) 9 | b.name.capitalize + " (" + b.platform.name + ")" 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /app/mailers/nopassword/no_password_emails.rb: -------------------------------------------------------------------------------- 1 | module Nopassword 2 | class NoPasswordEmails < ActionMailer::Base 3 | include Nopassword::ApplicationHelper 4 | 5 | def no_password_email(email, id, time, remote_ip, user_agent, geo, code, host, protocol) 6 | @id = id 7 | @time = time.strftime("%e %b %Y %H:%m") 8 | @remote_ip = remote_ip 9 | @user_agent = browser_name(user_agent) 10 | @geo = geo 11 | @code = code 12 | @email = email 13 | @host = host 14 | @protocol = protocol 15 | mail(:to => email, :from => ENV["FROM_EMAIL"], 16 | :subject => "Login request from #{host}") 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /app/models/nopassword/login_session.rb: -------------------------------------------------------------------------------- 1 | require 'bcrypt' 2 | require 'geoip' 3 | 4 | module Nopassword 5 | class LoginSession < ActiveRecord::Base 6 | EXPIRY = 60 * 60 # 1 hour 7 | 8 | def self.create_session(email, requesting_ip, requesting_user_agent, host, protocol) 9 | requesting_geo = geoip(requesting_ip) 10 | session = LoginSession.new(:email => email, :requesting_ip => requesting_ip, :requesting_user_agent => requesting_user_agent, :requesting_geo => requesting_geo) 11 | code = session.generate_code 12 | session.save 13 | NoPasswordEmails.no_password_email(email, session.id, session.created_at, requesting_ip, requesting_user_agent, requesting_geo, code, host, protocol).deliver 14 | return session, code 15 | end 16 | 17 | def activate_session(code, activating_ip, activating_user_agent) 18 | return nil if self.activated || self.terminated || self.expired? 19 | return nil if BCrypt::Password.new(self.hashed_code) != code 20 | self.activated_at = DateTime.now 21 | self.activating_ip = activating_ip 22 | self.activating_user_agent = activating_user_agent 23 | self.activating_geo = LoginSession.geoip(activating_ip) 24 | self.activated = true 25 | save 26 | end 27 | 28 | def active_sessions 29 | LoginSession.where("email = :email AND activated = 't' AND terminated = 'f'", { :email => self.email }).order("activated_at DESC") 30 | end 31 | 32 | def terminated_sessions 33 | LoginSession.find_all_by_email_and_terminated(self.email, true) 34 | end 35 | 36 | def active? 37 | self.activated && !self.terminated 38 | end 39 | 40 | def logout 41 | self.revoke(self) 42 | end 43 | 44 | def revoke(login_session) 45 | return nil if login_session.email != self.email 46 | login_session.terminated_at = DateTime.now 47 | login_session.terminated = true 48 | login_session.save 49 | end 50 | 51 | def expired? 52 | DateTime.current.to_i - self.created_at.to_i > EXPIRY 53 | end 54 | 55 | def generate_code 56 | code = SecureRandom.hex(32).to_s 57 | self.hashed_code = BCrypt::Password.create(code) 58 | code 59 | end 60 | 61 | def self.geoip(ip) 62 | return 'localhost' if ip == '127.0.0.1' 63 | c = GeoIP.new('db/GeoLiteCity.dat').city(ip) 64 | return 'Unknown' if c.nil? 65 | "#{c.city_name}, #{c.country_code3}" 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /app/views/nopassword/no_password_emails/no_password_email.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Login request from <%= @host %> 7 | 8 | 9 |

We received a request for <%= @email %> to log in to <%= @host %>.

10 | 11 | 12 | 16 | 17 | 18 | 22 | 23 | 24 | 28 | 29 |
13 | Location: 14 | <%= @geo %> 15 |
19 | User agent: 20 | <%= @user_agent %> 21 |
25 | Time: 26 | <%= @time %> 27 |
30 |

31 | To log in, click here: <%= main_app.url_for(:host => @host, :controller => "nopassword/nopassword", :action => "login", :protocol => @protocol, :id => @id, :code => @code) %> 32 |

33 |

34 | If not, please ignore this email. 35 |

36 | 37 | 38 | -------------------------------------------------------------------------------- /config/locales/nopassword.en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | nopassword: 3 | sent_login_email: 4 | mail_sent: "We sent an email to %{email}." 5 | invalid_mail: "That doesn't look like a valid email address." 6 | login: 7 | invalid_link: "That's not a valid login link." 8 | already_used: "That code is already used." 9 | expired: "That code is expired." 10 | could_not_be_used: "That code could not be used. Please request another." 11 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | post 'send_login_email' => 'nopassword/nopassword#send_login_email' 3 | get 'login/:id/:code' => 'nopassword/nopassword#login' 4 | delete 'logout' => 'nopassword/nopassword#logout' 5 | delete 'revoke/:id' => 'nopassword/nopassword#revoke' 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20120412041547_create_login_codes.rb: -------------------------------------------------------------------------------- 1 | class CreateLoginCodes < ActiveRecord::Migration 2 | def change 3 | create_table :nopassword_login_sessions do |t| 4 | t.string :email 5 | t.string :hashed_code 6 | t.string :requesting_ip 7 | t.string :requesting_user_agent 8 | t.string :activating_ip 9 | t.string :activating_user_agent 10 | t.boolean :activated, :default => false 11 | t.timestamp :activated_at 12 | t.boolean :terminated, :default => false 13 | t.timestamp :terminated_at 14 | 15 | t.timestamps 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /db/migrate/20120810233403_add_requesting_geo_to_login_sessions.rb: -------------------------------------------------------------------------------- 1 | class AddRequestingGeoToLoginSessions < ActiveRecord::Migration 2 | def change 3 | add_column :nopassword_login_sessions, :requesting_geo, :string 4 | 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20120810233418_add_activating_geo_to_login_sessions.rb: -------------------------------------------------------------------------------- 1 | class AddActivatingGeoToLoginSessions < ActiveRecord::Migration 2 | def change 3 | add_column :nopassword_login_sessions, :activating_geo, :string 4 | 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /lib/nopassword.rb: -------------------------------------------------------------------------------- 1 | require "nopassword/engine" 2 | 3 | module Nopassword 4 | end 5 | -------------------------------------------------------------------------------- /lib/nopassword/engine.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bcrypt' 3 | require 'browser' 4 | require 'geoip' 5 | 6 | module Nopassword 7 | class Engine < ::Rails::Engine 8 | isolate_namespace Nopassword 9 | 10 | initializer "load_helpers" do 11 | ActionController::Base.send :include, CheckSession 12 | ActionView::Base.send :include, ApplicationHelper 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/nopassword/version.rb: -------------------------------------------------------------------------------- 1 | module Nopassword 2 | VERSION = "0.0.1" 3 | end 4 | -------------------------------------------------------------------------------- /lib/tasks/nopassword_tasks.rake: -------------------------------------------------------------------------------- 1 | # desc "Explaining what the task does" 2 | # task :nopassword do 3 | # # Task goes here 4 | # end 5 | -------------------------------------------------------------------------------- /nopassword.gemspec: -------------------------------------------------------------------------------- 1 | $:.push File.expand_path("../lib", __FILE__) 2 | 3 | # Maintain your gem's version: 4 | require "nopassword/version" 5 | 6 | # Describe your gem and declare its dependencies: 7 | Gem::Specification.new do |s| 8 | s.name = "nopassword" 9 | s.version = Nopassword::VERSION 10 | s.authors = ["Alex Smolen"] 11 | s.email = ["me@alexsmolen.com"] 12 | s.homepage = "https://github.com/alsmola/nopassword" 13 | s.summary = "NoPassword is a simple authentication and session engine that doesn't use passwords." 14 | s.description = "NoPassword uses tokens sent to an email address, similar to most forgot password functionality. These tokens created long-lived sessions that can be tracked and revoked easily." 15 | 16 | s.files = Dir["{app,config,db,lib}/**/*"] + ["MIT-LICENSE", "Rakefile", "README.md"] 17 | s.test_files = Dir["test/**/*"] 18 | 19 | s.add_dependency "rails", '~> 4.2.7.1' 20 | s.add_dependency "browser", '~> 2.5.2' 21 | s.add_dependency "geoip", '~> 1.6.3' 22 | s.add_dependency "bcrypt", '~> 3.1.11' 23 | s.add_dependency "pg", '~> 0.21.0' 24 | 25 | s.add_development_dependency "sqlite3", '~> 1.3.13' 26 | end 27 | -------------------------------------------------------------------------------- /script/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application. 3 | 4 | ENGINE_ROOT = File.expand_path('../..', __FILE__) 5 | ENGINE_PATH = File.expand_path('../../lib/nopassword/engine', __FILE__) 6 | 7 | require 'rails/all' 8 | require 'rails/engine/commands' 9 | -------------------------------------------------------------------------------- /test/dummy/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gem 'rails' 3 | group :development, :test do 4 | gem 'sqlite3' 5 | end 6 | group :production do 7 | gem 'pg' 8 | end 9 | 10 | # Gems used only for assets and not required 11 | # in production environments by default. 12 | group :assets do 13 | gem 'sass-rails' 14 | gem 'coffee-rails' 15 | gem 'therubyracer' 16 | gem 'uglifier' 17 | end 18 | 19 | gem 'bcrypt' 20 | gem 'browser' 21 | gem 'geoip' 22 | gem 'aws-sdk-rails' 23 | gem 'dotenv' 24 | -------------------------------------------------------------------------------- /test/dummy/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | actioncable (5.1.4) 5 | actionpack (= 5.1.4) 6 | nio4r (~> 2.0) 7 | websocket-driver (~> 0.6.1) 8 | actionmailer (5.1.4) 9 | actionpack (= 5.1.4) 10 | actionview (= 5.1.4) 11 | activejob (= 5.1.4) 12 | mail (~> 2.5, >= 2.5.4) 13 | rails-dom-testing (~> 2.0) 14 | actionpack (5.1.4) 15 | actionview (= 5.1.4) 16 | activesupport (= 5.1.4) 17 | rack (~> 2.0) 18 | rack-test (>= 0.6.3) 19 | rails-dom-testing (~> 2.0) 20 | rails-html-sanitizer (~> 1.0, >= 1.0.2) 21 | actionview (5.1.4) 22 | activesupport (= 5.1.4) 23 | builder (~> 3.1) 24 | erubi (~> 1.4) 25 | rails-dom-testing (~> 2.0) 26 | rails-html-sanitizer (~> 1.0, >= 1.0.3) 27 | activejob (5.1.4) 28 | activesupport (= 5.1.4) 29 | globalid (>= 0.3.6) 30 | activemodel (5.1.4) 31 | activesupport (= 5.1.4) 32 | activerecord (5.1.4) 33 | activemodel (= 5.1.4) 34 | activesupport (= 5.1.4) 35 | arel (~> 8.0) 36 | activesupport (5.1.4) 37 | concurrent-ruby (~> 1.0, >= 1.0.2) 38 | i18n (~> 0.7) 39 | minitest (~> 5.1) 40 | tzinfo (~> 1.1) 41 | arel (8.0.0) 42 | aws-partitions (1.34.0) 43 | aws-sdk-core (3.7.0) 44 | aws-partitions (~> 1.0) 45 | aws-sigv4 (~> 1.0) 46 | jmespath (~> 1.0) 47 | aws-sdk-rails (2.0.1) 48 | aws-sdk-ses (~> 1) 49 | railties (>= 3) 50 | aws-sdk-ses (1.3.0) 51 | aws-sdk-core (~> 3) 52 | aws-sigv4 (~> 1.0) 53 | aws-sigv4 (1.0.2) 54 | bcrypt (3.1.11) 55 | browser (2.5.2) 56 | builder (3.2.3) 57 | coffee-rails (4.2.2) 58 | coffee-script (>= 2.2.0) 59 | railties (>= 4.0.0) 60 | coffee-script (2.4.1) 61 | coffee-script-source 62 | execjs 63 | coffee-script-source (1.12.2) 64 | concurrent-ruby (1.1.5) 65 | crass (1.0.5) 66 | dotenv (2.2.1) 67 | erubi (1.7.0) 68 | execjs (2.7.0) 69 | ffi (1.11.1) 70 | geoip (1.6.3) 71 | globalid (0.4.1) 72 | activesupport (>= 4.2.0) 73 | i18n (0.9.1) 74 | concurrent-ruby (~> 1.0) 75 | jmespath (1.3.1) 76 | libv8 (3.16.14.19) 77 | loofah (2.3.0) 78 | crass (~> 1.0.2) 79 | nokogiri (>= 1.5.9) 80 | mail (2.7.0) 81 | mini_mime (>= 0.1.1) 82 | method_source (0.9.0) 83 | mini_mime (1.0.0) 84 | mini_portile2 (2.4.0) 85 | minitest (5.10.3) 86 | nio4r (2.1.0) 87 | nokogiri (1.10.4) 88 | mini_portile2 (~> 2.4.0) 89 | pg (0.21.0) 90 | rack (2.0.8) 91 | rack-test (0.7.0) 92 | rack (>= 1.0, < 3) 93 | rails (5.1.4) 94 | actioncable (= 5.1.4) 95 | actionmailer (= 5.1.4) 96 | actionpack (= 5.1.4) 97 | actionview (= 5.1.4) 98 | activejob (= 5.1.4) 99 | activemodel (= 5.1.4) 100 | activerecord (= 5.1.4) 101 | activesupport (= 5.1.4) 102 | bundler (>= 1.3.0) 103 | railties (= 5.1.4) 104 | sprockets-rails (>= 2.0.0) 105 | rails-dom-testing (2.0.3) 106 | activesupport (>= 4.2.0) 107 | nokogiri (>= 1.6) 108 | rails-html-sanitizer (1.3.0) 109 | loofah (~> 2.3) 110 | railties (5.1.4) 111 | actionpack (= 5.1.4) 112 | activesupport (= 5.1.4) 113 | method_source 114 | rake (>= 0.8.7) 115 | thor (>= 0.18.1, < 2.0) 116 | rake (12.2.1) 117 | rb-fsevent (0.10.2) 118 | rb-inotify (0.9.10) 119 | ffi (>= 0.5.0, < 2) 120 | ref (2.0.0) 121 | sass (3.5.3) 122 | sass-listen (~> 4.0.0) 123 | sass-listen (4.0.0) 124 | rb-fsevent (~> 0.9, >= 0.9.4) 125 | rb-inotify (~> 0.9, >= 0.9.7) 126 | sass-rails (5.0.6) 127 | railties (>= 4.0.0, < 6) 128 | sass (~> 3.1) 129 | sprockets (>= 2.8, < 4.0) 130 | sprockets-rails (>= 2.0, < 4.0) 131 | tilt (>= 1.1, < 3) 132 | sprockets (3.7.2) 133 | concurrent-ruby (~> 1.0) 134 | rack (> 1, < 3) 135 | sprockets-rails (3.2.1) 136 | actionpack (>= 4.0) 137 | activesupport (>= 4.0) 138 | sprockets (>= 3.0.0) 139 | sqlite3 (1.3.13) 140 | therubyracer (0.12.3) 141 | libv8 (~> 3.16.14.15) 142 | ref 143 | thor (0.20.0) 144 | thread_safe (0.3.6) 145 | tilt (2.0.8) 146 | tzinfo (1.2.4) 147 | thread_safe (~> 0.1) 148 | uglifier (3.2.0) 149 | execjs (>= 0.3.0, < 3) 150 | websocket-driver (0.6.5) 151 | websocket-extensions (>= 0.1.0) 152 | websocket-extensions (0.1.3) 153 | 154 | PLATFORMS 155 | ruby 156 | 157 | DEPENDENCIES 158 | aws-sdk-rails 159 | bcrypt 160 | browser 161 | coffee-rails 162 | dotenv 163 | geoip 164 | pg 165 | rails 166 | sass-rails 167 | sqlite3 168 | therubyracer 169 | uglifier 170 | 171 | BUNDLED WITH 172 | 1.16.0 173 | -------------------------------------------------------------------------------- /test/dummy/README.md: -------------------------------------------------------------------------------- 1 | This is the code of the site that is running at https://nopassword.alexsmolen.com. 2 | 3 | Tp run this, you'll need to put a .env file in the base directory with the following secrets/config in it: 4 | 5 | WS_ACCESS_KEY_ID= 6 | AWS_SECRET_ACCESS_KEY= 7 | FROM_EMAIL= 8 | SECRET_TOKEN= 9 | -------------------------------------------------------------------------------- /test/dummy/Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | # Add your own tasks in files placed in lib/tasks ending in .rake, 3 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 4 | 5 | require File.expand_path('../config/application', __FILE__) 6 | 7 | Dummy::Application.load_tasks 8 | -------------------------------------------------------------------------------- /test/dummy/app/assets/images/rails.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alsmola/nopassword/bfc7a352fa694b9a5074424aa17dc7abe974cb23/test/dummy/app/assets/images/rails.png -------------------------------------------------------------------------------- /test/dummy/app/assets/javascripts/application.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function(){ 2 | $.ajaxSetup({ 3 | headers: { 'X-CSRF-Token': $('meta[name="csrf-token"]').attr('content') } 4 | }); 5 | 6 | $('#logout').click(function(e) { 7 | logoutForm = $(this).parents("form"); 8 | logoutForm.submit(); 9 | }); 10 | 11 | $('.revoke').click(function(e) { 12 | id = $(this).siblings("input").val(); 13 | that = this; 14 | $.post('revoke/' + id, { _method: 'delete' }, function(data) { 15 | $(that).parents("tr").fadeOut(); 16 | }); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /test/dummy/app/assets/stylesheets/application.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: 60px; 3 | padding-left: 20px; 4 | } 5 | 6 | #send-login-email form { 7 | padding: 20px; 8 | background-color: gray; 9 | margin-top: 40px; 10 | margin-bottom: 40px; 11 | border-radius: 10px; 12 | border: 5px solid white; 13 | -moz-box-shadow: 0 0 2px 2px #888; 14 | -webkit-box-shadow: 0 2px 2px #888; 15 | box-shadow: 0 0 2px 2px #888; 16 | } 17 | 18 | #send-login-email input { 19 | font-size: 200%; 20 | line-height: 1em; 21 | height: 1.5em; 22 | width: 12em; 23 | margin-right: .5em; 24 | } 25 | 26 | .big { 27 | font-size: 18px; 28 | line-height: 24px; 29 | } 30 | 31 | .highlight td { 32 | background-color: #049cdb !important; 33 | color: white; 34 | font-weight: bold; 35 | } 36 | 37 | .table-sessions { 38 | width: auto; 39 | margin: auto; 40 | } 41 | 42 | .table-sessions button { 43 | min-width: 6em; 44 | } 45 | 46 | .logout { 47 | margin: 0; 48 | } 49 | -------------------------------------------------------------------------------- /test/dummy/app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | protect_from_forgery 3 | before_action :check_valid_session 4 | end 5 | -------------------------------------------------------------------------------- /test/dummy/app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | end 3 | -------------------------------------------------------------------------------- /test/dummy/app/mailers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alsmola/nopassword/bfc7a352fa694b9a5074424aa17dc7abe974cb23/test/dummy/app/mailers/.gitkeep -------------------------------------------------------------------------------- /test/dummy/app/models/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alsmola/nopassword/bfc7a352fa694b9a5074424aa17dc7abe974cb23/test/dummy/app/models/.gitkeep -------------------------------------------------------------------------------- /test/dummy/app/views/application/index.html.erb: -------------------------------------------------------------------------------- 1 | <% if @current_session %> 2 | 3 | 4 | 5 | 6 | 7 | 8 | <% @current_session.active_sessions.each do |as| %> 9 | > 10 | 13 | 16 | 19 | 29 | 30 | <% end %> 31 | 32 |

Active sessions

Activated atLocationUser agent
11 | <%= h friendly_time(as.activated_at) %> 12 | 14 | <%= h as.activating_geo %> 15 | 17 | <%= h browser_name(as.activating_user_agent) %> 18 | 20 | <% if (as.id == session[:login_session]) %> 21 | <%= form_tag("logout", {:method => :delete, :class => 'logout'}) do %> 22 | 23 | <% end %> 24 | <% else %> 25 | 26 | 27 | <% end %> 28 |
33 | <% else %> 34 | 39 |
40 |
41 | 47 |
48 |
49 |
50 |

51 | Most web sites ask for a password when you register. 52 | After logging in, you can access the site until your session expires. 53 | When you forget your password, you can request an email with a link to a password change form. 54 |

55 |

56 | NoPassword factors out the password from this process. You register with an email address and receive a link that gives you a session on that browser until you log out. 57 | If you ever need to log in from somewhere else, you can request another email with a link that will log you in wherever you are. 58 |

59 |

60 | Ben Brown wrote a great article about password-less logins, the same concept implemented by NoPassword. 61 |

62 |

63 | Check out the code on Github. Built by @alsmola. 64 |

65 |
66 | 67 | <% end %> 68 | -------------------------------------------------------------------------------- /test/dummy/app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | NoPassword 5 | <%= stylesheet_link_tag "//netdna.bootstrapcdn.com/twitter-bootstrap/2.1.0/css/bootstrap-combined.min.css" %> 6 | <%= stylesheet_link_tag "application", :media => "all" %> 7 | <%= javascript_include_tag "//ajax.googleapis.com/ajax/libs/jquery/1.10.1/jquery.min.js" %> 8 | <%= javascript_include_tag "//netdna.bootstrapcdn.com/twitter-bootstrap/2.1.0/js/bootstrap.min.js" %> 9 | <%= javascript_include_tag "application" %> 10 | <%= csrf_meta_tags %> 11 | 12 | 13 | 37 |
38 | <% if flash[:notice] %> 39 |

<%= flash[:notice] %>

40 | <% end %> 41 | <%= yield %> 42 |
43 | 44 | 45 | -------------------------------------------------------------------------------- /test/dummy/config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require ::File.expand_path('../config/environment', __FILE__) 4 | run Dummy::Application 5 | -------------------------------------------------------------------------------- /test/dummy/config/application.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../boot', __FILE__) 2 | 3 | require 'rails/all' 4 | 5 | Bundler.require 6 | require 'nopassword' 7 | require 'dotenv' 8 | 9 | module Dummy 10 | class Application < Rails::Application 11 | Dotenv.load 12 | # Settings in config/environments/* take precedence over those specified here. 13 | # Application configuration should go into files in config/initializers 14 | # -- all .rb files in that directory are automatically loaded. 15 | 16 | # Custom directories with classes and modules you want to be autoloadable. 17 | # config.autoload_paths += %W(#{config.root}/extras) 18 | 19 | # Only load the plugins named here, in the order given (default is alphabetical). 20 | # :all can be used as a placeholder for all plugins not explicitly named. 21 | # config.plugins = [ :exception_notification, :ssl_requirement, :all ] 22 | 23 | # Activate observers that should always be running. 24 | # config.active_record.observers = :cacher, :garbage_collector, :forum_observer 25 | 26 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. 27 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. 28 | # config.time_zone = 'Central Time (US & Canada)' 29 | 30 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. 31 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] 32 | # config.i18n.default_locale = :de 33 | 34 | # Configure the default encoding used in templates for Ruby 1.9. 35 | config.encoding = "utf-8" 36 | 37 | # Configure sensitive parameters which will be filtered from the log file. 38 | config.filter_parameters += [:password] 39 | 40 | # Use SQL instead of Active Record's schema dumper when creating the database. 41 | # This is necessary if your schema can't be completely dumped by the schema dumper, 42 | # like if you have constraints or database-specific column types 43 | # config.active_record.schema_format = :sql 44 | 45 | # Enforce whitelist mode for mass assignment. 46 | # This will create an empty whitelist of attributes available for mass-assignment for all models 47 | # in your app. As such, your models will need to explicitly whitelist or blacklist accessible 48 | # parameters by using an attr_accessible or attr_protected declaration. 49 | # config.active_record.whitelist_attributes = true 50 | 51 | # Enable the asset pipeline 52 | config.assets.enabled = true 53 | 54 | # Version of your assets, change this if you want to expire all your assets 55 | config.assets.version = '1.0' 56 | end 57 | end 58 | 59 | -------------------------------------------------------------------------------- /test/dummy/config/boot.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | gemfile = File.expand_path('../../../../Gemfile', __FILE__) 3 | 4 | if File.exist?(gemfile) 5 | ENV['BUNDLE_GEMFILE'] = gemfile 6 | require 'bundler' 7 | Bundler.setup 8 | end 9 | 10 | $:.unshift File.expand_path('../../../../lib', __FILE__) -------------------------------------------------------------------------------- /test/dummy/config/database.yml: -------------------------------------------------------------------------------- 1 | # SQLite version 3.x 2 | # gem install sqlite3 3 | # 4 | # Ensure the SQLite 3 gem is defined in your Gemfile 5 | # gem 'sqlite3' 6 | development: 7 | adapter: sqlite3 8 | database: db/development.sqlite3 9 | pool: 5 10 | timeout: 5000 11 | 12 | # Warning: The database defined as "test" will be erased and 13 | # re-generated from your development database when you run "rake". 14 | # Do not set this db to the same as development or production. 15 | test: 16 | adapter: sqlite3 17 | database: db/test.sqlite3 18 | pool: 5 19 | timeout: 5000 20 | 21 | production: 22 | adapter: sqlite3 23 | database: db/production.sqlite3 24 | pool: 5 25 | timeout: 5000 26 | -------------------------------------------------------------------------------- /test/dummy/config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the rails application 2 | require File.expand_path('../application', __FILE__) 3 | 4 | # Initialize the rails application 5 | Dummy::Application.initialize! 6 | -------------------------------------------------------------------------------- /test/dummy/config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Dummy::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb 3 | 4 | # In the development environment your application's code is reloaded on 5 | # every request. This slows down response time but is perfect for development 6 | # since you don't have to restart the web server when you make code changes. 7 | config.cache_classes = false 8 | 9 | # Log error messages when you accidentally call methods on nil. 10 | config.whiny_nils = true 11 | 12 | # Show full error reports and disable caching 13 | config.consider_all_requests_local = true 14 | config.action_controller.perform_caching = false 15 | 16 | # Don't care if the mailer can't send 17 | config.action_mailer.raise_delivery_errors = false 18 | 19 | # Print deprecation notices to the Rails logger 20 | config.active_support.deprecation = :log 21 | 22 | # Only use best-standards-support built into browsers 23 | config.action_dispatch.best_standards_support = :builtin 24 | 25 | # Do not compress assets 26 | config.assets.compress = false 27 | 28 | # Expands the lines which load the assets 29 | config.assets.debug = true 30 | 31 | config.action_mailer.delivery_method = :aws_sdk 32 | config.action_mailer.raise_delivery_errors = true 33 | config.action_mailer.perform_deliveries = true 34 | config.eager_load = false 35 | end 36 | -------------------------------------------------------------------------------- /test/dummy/config/environments/production.rb: -------------------------------------------------------------------------------- 1 | Dummy::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb 3 | 4 | # Code is not reloaded between requests 5 | config.cache_classes = true 6 | 7 | # Full error reports are disabled and caching is turned on 8 | config.consider_all_requests_local = false 9 | config.action_controller.perform_caching = true 10 | 11 | # Disable Rails's static asset server (Apache or nginx will already do this) 12 | config.serve_static_assets = false 13 | 14 | # Compress JavaScripts and CSS 15 | config.assets.compress = true 16 | 17 | # Don't fallback to assets pipeline if a precompiled asset is missed 18 | config.assets.compile = false 19 | 20 | # Generate digests for assets URLs 21 | config.assets.digest = true 22 | 23 | # Defaults to Rails.root.join("public/assets") 24 | # config.assets.manifest = YOUR_PATH 25 | 26 | # Specifies the header that your server uses for sending files 27 | # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache 28 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx 29 | 30 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 31 | # config.force_ssl = true 32 | 33 | # See everything in the log (default is :info) 34 | # config.log_level = :debug 35 | 36 | # Prepend all log lines with the following tags 37 | # config.log_tags = [ :subdomain, :uuid ] 38 | 39 | # Use a different logger for distributed setups 40 | # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) 41 | 42 | # Use a different cache store in production 43 | # config.cache_store = :mem_cache_store 44 | 45 | # Enable serving of images, stylesheets, and JavaScripts from an asset server 46 | # config.action_controller.asset_host = "http://assets.example.com" 47 | 48 | # Precompile additional assets (application.js, application.css, and all non-JS/CSS are already added) 49 | # config.assets.precompile += %w( search.js ) 50 | 51 | # Disable delivery errors, bad email addresses will be ignored 52 | # config.action_mailer.raise_delivery_errors = false 53 | 54 | # Enable threaded mode 55 | # config.threadsafe! 56 | 57 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 58 | # the I18n.default_locale when a translation can not be found) 59 | config.i18n.fallbacks = true 60 | 61 | # Send deprecation notices to registered listeners 62 | config.active_support.deprecation = :notify 63 | 64 | config.action_mailer.delivery_method = :ses 65 | config.assets.compile = true 66 | config.assets.precompile = ['*.js', '*.css', '*.css.erb'] 67 | config.eager_load = false 68 | end 69 | -------------------------------------------------------------------------------- /test/dummy/config/environments/test.rb: -------------------------------------------------------------------------------- 1 | Dummy::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb 3 | 4 | # The test environment is used exclusively to run your application's 5 | # test suite. You never need to work with it otherwise. Remember that 6 | # your test database is "scratch space" for the test suite and is wiped 7 | # and recreated between test runs. Don't rely on the data there! 8 | config.cache_classes = true 9 | 10 | # Configure static asset server for tests with Cache-Control for performance 11 | config.serve_static_assets = true 12 | config.static_cache_control = "public, max-age=3600" 13 | 14 | # Log error messages when you accidentally call methods on nil 15 | config.whiny_nils = true 16 | 17 | # Show full error reports and disable caching 18 | config.consider_all_requests_local = true 19 | config.action_controller.perform_caching = false 20 | 21 | # Raise exceptions instead of rendering exception templates 22 | config.action_dispatch.show_exceptions = false 23 | 24 | # Disable request forgery protection in test environment 25 | config.action_controller.allow_forgery_protection = false 26 | 27 | # Tell Action Mailer not to deliver emails to the real world. 28 | # The :test delivery method accumulates sent emails in the 29 | # ActionMailer::Base.deliveries array. 30 | config.action_mailer.delivery_method = :test 31 | 32 | # Print deprecation notices to the stderr 33 | config.active_support.deprecation = :stderr 34 | config.eager_load = false 35 | end 36 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/aws.rb: -------------------------------------------------------------------------------- 1 | ActionMailer::Base.default :from => ENV['FROM_EMAIL'] 2 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format 4 | # (all these examples are active by default): 5 | # ActiveSupport::Inflector.inflections do |inflect| 6 | # inflect.plural /^(ox)$/i, '\1en' 7 | # inflect.singular /^(ox)en/i, '\1' 8 | # inflect.irregular 'person', 'people' 9 | # inflect.uncountable %w( fish sheep ) 10 | # end 11 | # 12 | # These inflection rules are supported but not enabled by default: 13 | # ActiveSupport::Inflector.inflections do |inflect| 14 | # inflect.acronym 'RESTful' 15 | # end 16 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | # Mime::Type.register_alias "text/html", :iphone 6 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/secret_token.rb: -------------------------------------------------------------------------------- 1 | Dummy::Application.config.secret_token = ENV['secret_token'] || '3eb6db5a9026c547c72708438d496d942e976b252138db7e4e0ee5edd7539457d3ed0fa02ee5e7179420ce5290462018591adaf5f42adcf855da04877827def3' 2 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Dummy::Application.config.session_store :cookie_store, key: '_dummy_session' 4 | 5 | # Use the database for sessions instead of the cookie-based default, 6 | # which shouldn't be used to store highly confidential information 7 | # (create the session table with "rails generate session_migration") 8 | # Dummy::Application.config.session_store :active_record_store 9 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | # 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] 9 | end 10 | 11 | # Disable root element in JSON by default. 12 | ActiveSupport.on_load(:active_record) do 13 | self.include_root_in_json = false 14 | end 15 | -------------------------------------------------------------------------------- /test/dummy/config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Sample localization file for English. Add more files in this directory for other locales. 2 | # See https://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points. 3 | 4 | en: 5 | hello: "Hello world" 6 | -------------------------------------------------------------------------------- /test/dummy/config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | root :to => "application#index" 3 | mount Nopassword::Engine => "/nopassword" 4 | end 5 | -------------------------------------------------------------------------------- /test/dummy/db/migrate/20120903014052_create_login_codes.nopassword.rb: -------------------------------------------------------------------------------- 1 | # This migration comes from nopassword (originally 20120412041547) 2 | class CreateLoginCodes < ActiveRecord::Migration[4.2] 3 | def change 4 | create_table :nopassword_login_sessions do |t| 5 | t.string :email 6 | t.string :hashed_code 7 | t.string :requesting_ip 8 | t.string :requesting_user_agent 9 | t.string :activating_ip 10 | t.string :activating_user_agent 11 | t.boolean :activated, :default => false 12 | t.timestamp :activated_at 13 | t.boolean :terminated, :default => false 14 | t.timestamp :terminated_at 15 | 16 | t.timestamps 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /test/dummy/db/migrate/20120903014053_add_requesting_geo_to_login_sessions.nopassword.rb: -------------------------------------------------------------------------------- 1 | # This migration comes from nopassword (originally 20120810233403) 2 | class AddRequestingGeoToLoginSessions < ActiveRecord::Migration[4.2] 3 | def change 4 | add_column :nopassword_login_sessions, :requesting_geo, :string 5 | 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /test/dummy/db/migrate/20120903014054_add_activating_geo_to_login_sessions.nopassword.rb: -------------------------------------------------------------------------------- 1 | # This migration comes from nopassword (originally 20120810233418) 2 | class AddActivatingGeoToLoginSessions < ActiveRecord::Migration[4.2] 3 | def change 4 | add_column :nopassword_login_sessions, :activating_geo, :string 5 | 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /test/dummy/db/schema.rb: -------------------------------------------------------------------------------- 1 | # This file is auto-generated from the current state of the database. Instead 2 | # of editing this file, please use the migrations feature of Active Record to 3 | # incrementally modify your database, and then regenerate this schema definition. 4 | # 5 | # Note that this schema.rb definition is the authoritative source for your 6 | # database schema. If you need to create the application database on another 7 | # system, you should be using db:schema:load, not running all the migrations 8 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations 9 | # you'll amass, the slower it'll run and the greater likelihood for issues). 10 | # 11 | # It's strongly recommended that you check this file into your version control system. 12 | 13 | ActiveRecord::Schema.define(version: 20120903014054) do 14 | 15 | create_table "nopassword_login_sessions", force: :cascade do |t| 16 | t.string "email" 17 | t.string "hashed_code" 18 | t.string "requesting_ip" 19 | t.string "requesting_user_agent" 20 | t.string "activating_ip" 21 | t.string "activating_user_agent" 22 | t.boolean "activated", default: false 23 | t.datetime "activated_at" 24 | t.boolean "terminated", default: false 25 | t.datetime "terminated_at" 26 | t.datetime "created_at" 27 | t.datetime "updated_at" 28 | t.string "requesting_geo" 29 | t.string "activating_geo" 30 | end 31 | 32 | end 33 | -------------------------------------------------------------------------------- /test/dummy/db/test.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alsmola/nopassword/bfc7a352fa694b9a5074424aa17dc7abe974cb23/test/dummy/db/test.sqlite3 -------------------------------------------------------------------------------- /test/dummy/lib/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alsmola/nopassword/bfc7a352fa694b9a5074424aa17dc7abe974cb23/test/dummy/lib/assets/.gitkeep -------------------------------------------------------------------------------- /test/dummy/log/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alsmola/nopassword/bfc7a352fa694b9a5074424aa17dc7abe974cb23/test/dummy/log/.gitkeep -------------------------------------------------------------------------------- /test/dummy/public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 17 | 18 | 19 | 20 | 21 |
22 |

The page you were looking for doesn't exist.

23 |

You may have mistyped the address or the page may have moved.

24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /test/dummy/public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 17 | 18 | 19 | 20 | 21 |
22 |

The change you wanted was rejected.

23 |

Maybe you tried to change something you didn't have access to.

24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /test/dummy/public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 17 | 18 | 19 | 20 | 21 |
22 |

We're sorry, but something went wrong.

23 |
24 | 25 | 26 | -------------------------------------------------------------------------------- /test/dummy/public/assets/rails.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alsmola/nopassword/bfc7a352fa694b9a5074424aa17dc7abe974cb23/test/dummy/public/assets/rails.png -------------------------------------------------------------------------------- /test/dummy/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alsmola/nopassword/bfc7a352fa694b9a5074424aa17dc7abe974cb23/test/dummy/public/favicon.ico -------------------------------------------------------------------------------- /test/dummy/script/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application. 3 | 4 | APP_PATH = File.expand_path('../../config/application', __FILE__) 5 | require File.expand_path('../../config/boot', __FILE__) 6 | require 'rails/commands' 7 | -------------------------------------------------------------------------------- /test/functional/nopassword/nopassword_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Nopassword::NopasswordControllerTest < ActionController::TestCase 4 | test "send_login_email" do 5 | post :send_login_email, :use_route => :Nopassword, :email => 'me@example.com' 6 | assert_redirected_to "/" 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /test/integration/navigation_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class NavigationTest < ActionDispatch::IntegrationTest 4 | fixtures :all 5 | 6 | # test "the truth" do 7 | # assert true 8 | # end 9 | end 10 | 11 | -------------------------------------------------------------------------------- /test/login_session_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Nopassword::LoginSessionTest < ActiveSupport::TestCase 4 | def setup 5 | @ls, @code = Nopassword::LoginSession.create_session("test@test.com", "127.0.0.1", "some_user_agent", "example.com") 6 | end 7 | 8 | test "create_session sends an email" do 9 | assert_difference("ActionMailer::Base.deliveries.size", 1) do 10 | Nopassword::LoginSession.create_session("test@test.com", "127.0.0.1", "some_user_agent", "example.com") 11 | end 12 | end 13 | 14 | test "create_session creates a session with a code" do 15 | assert @ls != nil 16 | assert @code != nil 17 | assert @ls.hashed_code != nil 18 | end 19 | 20 | test "create_session correctly stores IP and user agent" do 21 | assert @ls.requesting_ip == "127.0.0.1" 22 | assert @ls.requesting_user_agent == "some_user_agent" 23 | end 24 | 25 | test "create_session generates geo" do 26 | assert @ls.requesting_geo == "localhost" 27 | end 28 | 29 | test "activate_session sets the activated flag to true" do 30 | @ls.activate_session(@code, "127.0.0.1", "some_user_agent") 31 | assert @ls.activated == true 32 | end 33 | 34 | test "activate_session stores IP and user agent" do 35 | @ls.activate_session(@code, "127.0.0.1", "some_user_agent") 36 | assert @ls.activating_ip == "127.0.0.1" 37 | assert @ls.activating_user_agent == "some_user_agent" 38 | end 39 | 40 | test "activate_session generates geo" do 41 | @ls.activate_session(@code, "127.0.0.1", "some_user_agent") 42 | assert @ls.activating_geo == "localhost" 43 | end 44 | 45 | test "active_sessions only contains session that are active" do 46 | @ls2, @code2 = Nopassword::LoginSession.create_session("test@test.com", "127.0.0.2", "some_other_user_agent", "example.com") 47 | assert @ls.active_sessions.count == 0 48 | @ls.activate_session(@code, '127.0.0.1', "some_user_agent") 49 | assert @ls.active_sessions.count == 1 50 | @ls2.activate_session(@code2, "127.0.0.2", "some_other_user_agent") 51 | assert @ls.active_sessions.count == 2 52 | end 53 | 54 | test "terminated_sessions only contains session that are active" do 55 | @ls2, @code2 = Nopassword::LoginSession.create_session("test@test.com", "127.0.0.2", "some_other_user_agent", "example.com") 56 | assert @ls.terminated_sessions.count == 0 57 | @ls.activate_session(@code, '127.0.0.1', "some_user_agent") 58 | @ls.revoke(@ls) 59 | assert @ls.terminated_sessions.count == 1 60 | @ls2.activate_session(@code2, "127.0.0.2", "some_other_user_agent") 61 | @ls2.revoke(@ls2) 62 | assert @ls.terminated_sessions.count == 2 63 | end 64 | 65 | test "logout sets terminated to true" do 66 | @ls.activate_session(@code, '127.0.0.1', "some_user_agent") 67 | @ls.logout 68 | assert @ls.terminated 69 | end 70 | 71 | test "logout sets terminated_at time" do 72 | @ls.activate_session(@code, '127.0.0.1', "some_user_agent") 73 | @ls.logout 74 | assert @ls.terminated_at != nil 75 | end 76 | 77 | test "expired is true when created_at is past expiry" do 78 | @ls.created_at = 1.week.ago 79 | @ls.save 80 | assert @ls.expired? 81 | end 82 | 83 | test "generate code returns a 32 character hex string" do 84 | assert @ls.generate_code =~ /^[a-f0-9]{64}$/ 85 | end 86 | 87 | test "geoip returns Mountain View for 8.8.8.8" do 88 | assert Nopassword::LoginSession.geoip("8.8.8.8") == "Mountain View, USA" 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /test/nopassword_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class NopasswordTest < ActiveSupport::TestCase 4 | test "truth" do 5 | assert_kind_of Module, Nopassword 6 | end 7 | 8 | end 9 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | # Configure Rails Environment 2 | ENV["RAILS_ENV"] = "test" 3 | 4 | require File.expand_path("../dummy/config/environment.rb", __FILE__) 5 | require "rails/test_help" 6 | 7 | Rails.backtrace_cleaner.remove_silencers! 8 | 9 | # Load support files 10 | Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f } 11 | --------------------------------------------------------------------------------