├── .gitignore ├── .rspec ├── .travis.yml ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── lib └── omniauth │ ├── hanami.rb │ ├── hanami │ └── version.rb │ └── strategies │ └── hanami.rb ├── omniauth-hanami.gemspec └── test ├── database_helper.rb ├── test.rb ├── test_app ├── config.ru └── test_app.rb └── test_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | test/test_app/database.sqlite 11 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: ruby 3 | rvm: 4 | - 2.3.1 5 | before_install: gem install bundler -v 1.13.7 6 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in omniauth-hanami.gemspec 4 | gemspec 5 | gem 'rack-test', require: 'rack/test' 6 | gem 'minitest' 7 | gem 'hanami-router' 8 | gem 'hanami-model', path: '~/dev/github/hanami/model' #, github: 'hanami/model' 9 | gem 'hanami-controller' 10 | gem 'sqlite3' 11 | gem 'scrypt' 12 | gem 'pry' 13 | gem 'warden' 14 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Paweł Świątkowski 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Omniauth::Hanami 2 | 3 | This is a provider for OmniAuth for (not only) Hanami applications. It's a very thin authentication library that does only authentication and nothing more. 4 | 5 | **Why would you want it?** 6 | 7 | * It makes almost no assumptions (apart from using Warden). 8 | * You can have any database schema you want. 9 | * You can choose our favourite hashing algorithm, be it BCrypt, SCrypt, Argon2 or even MD5 (or plaintext; equally secure). 10 | * `hanami/model` is not required. You can use what you want, for example load users from YAML file or even external API (although this is probably not the best idea). 11 | * It integrates seamlessly with other OmniAuth solution. You can start with Github authentication and add this later. Or if you want to add Facebook auth later, it's a no-brainer. 12 | 13 | **What it's not:** 14 | 15 | * Devise. It does not have methods to handle registration, confirmation, locking etc. for you. You have to write all of this by yourself. But on the other hand, this is usually a good thing, as you don't need to hack around what an "advanced library" does not let you do easily. 16 | 17 | ## Installation 18 | 19 | Add this line to your application's Gemfile: 20 | 21 | ```ruby 22 | gem 'omniauth-hanami' 23 | ``` 24 | 25 | And then execute: 26 | 27 | $ bundle 28 | 29 | Or install it yourself as: 30 | 31 | $ gem install omniauth-hanami 32 | 33 | ## Usage 34 | 35 | First of all, you need an interactor which takes auth key (e.g. email) + password and returns user object if it's sucessfully authenticated or fails otherwise. A common example would be: 36 | 37 | ```ruby 38 | require 'hanami/interactor' 39 | 40 | class FindUserForAuth 41 | include Hanami::Interactor 42 | 43 | expose :user 44 | 45 | def initialize(login, password) 46 | @login = login 47 | @password = password 48 | end 49 | 50 | def call 51 | user = UserRepository.new.find_by_email(@login) 52 | if user && SCrypt::Password.new(user.crypted_password) == @password 53 | @user = user 54 | else 55 | fail! 56 | end 57 | end 58 | end 59 | ``` 60 | 61 | Next, configure Warden and OmniAuth in your `apps/web/application.rb`: 62 | 63 | ```ruby 64 | middleware.use Warden::Manager do |manager| 65 | # manager.failure_app = Web::Controllers::Session::Failure.new 66 | end 67 | middleware.use OmniAuth::Builder do 68 | provider :hanami, interactor: FindUserForAuth 69 | end 70 | ``` 71 | 72 | And add this to `routes.rb` (this is ugly and will change when I find out how – or maybe you can help?): 73 | 74 | ```ruby 75 | post '/auth/:provider/callback', to: 'session#create' 76 | get '/auth/:provider/callback', to: 'session#create' 77 | ``` 78 | 79 | All that's left is a form that sends POST to `/auth/hanami/callback`. Like this: 80 | 81 | ```ruby 82 | form_for :user, '/auth/hanami/callback' do 83 | fieldset do 84 | div do 85 | label :email 86 | text_field :email, type: 'email' 87 | end 88 | div do 89 | label :password 90 | password_field :password 91 | end 92 | div do 93 | submit 'Sign in' 94 | end 95 | end 96 | end 97 | ``` 98 | 99 | To access your signed in user, you can use this code in the controller: 100 | 101 | ```ruby 102 | def current_user 103 | @current_user ||= warden.user 104 | end 105 | 106 | def warden 107 | request.env['warden'] 108 | end 109 | ``` 110 | 111 | ### Configuration options 112 | 113 | `interactor` option is mandatory for gem to work, but you can also provide others: 114 | 115 | | option | descriptions | default | 116 | |--------|--------------|---------| 117 | | `auth_key` | How to get auth key from params | `->(params) { params['user']['email'] }` | 118 | | `password_key` | How to get password from params | `->(params) { params['user']['password'] }` | 119 | 120 | Example: 121 | 122 | ```ruby 123 | middleware.use OmniAuth::Builder do 124 | provider :hanami, 125 | interactor: FindUserForAuth, 126 | auth_key: ->(params) { params['login_or_email'] }, 127 | password_key: ->(params) { params['password'] + '1234' } 128 | end 129 | ``` 130 | 131 | ### Sample application 132 | 133 | You can see it [here](https://gitlab.com/katafrakt/hanami_omniauth_example). 134 | 135 | ## Development 136 | 137 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 138 | 139 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). 140 | 141 | ## Contributing 142 | 143 | Bug reports and pull requests are welcome on GitHub at https://github.com/katafrakt/omniauth-hanami. 144 | 145 | 146 | ## License 147 | 148 | The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT). 149 | 150 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | require 'rake/testtask' 3 | 4 | Rake::TestTask.new(:test) do |t| 5 | t.test_files = FileList['test/**/*test.rb'] 6 | end 7 | desc 'Run tests' 8 | 9 | task default: :test 10 | -------------------------------------------------------------------------------- /lib/omniauth/hanami.rb: -------------------------------------------------------------------------------- 1 | require 'omniauth' 2 | require "omniauth/hanami/version" 3 | require 'omniauth/strategies/hanami' 4 | 5 | module Omniauth 6 | module Hanami 7 | # Your code goes here... 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/omniauth/hanami/version.rb: -------------------------------------------------------------------------------- 1 | module Omniauth 2 | module Hanami 3 | VERSION = "0.1.0" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /lib/omniauth/strategies/hanami.rb: -------------------------------------------------------------------------------- 1 | module OmniAuth 2 | module Strategies 3 | class Hanami 4 | include OmniAuth::Strategy 5 | 6 | option :auth_key, ->(params) { params.fetch('user', {})['email'] } 7 | option :password_key, ->(params) { params.fetch('user', {})['password'] } 8 | option :encryption, :bcrypt 9 | option :interactor 10 | 11 | def callback_phase 12 | return fail!(:invalid_credentials) unless identity 13 | super 14 | end 15 | 16 | private 17 | 18 | uid do 19 | identity 20 | end 21 | 22 | def identity 23 | return @identity if @identity 24 | 25 | login = options.fetch(:auth_key).(request.params) 26 | password = options.fetch(:password_key).(request.params) 27 | result = interactor.new(login, password).call 28 | 29 | if result.success? 30 | @identity = result.user 31 | else 32 | @identity = nil 33 | end 34 | end 35 | 36 | def interactor 37 | options.fetch(:interactor) 38 | end 39 | 40 | def model 41 | options.fetch(:model) 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /omniauth-hanami.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'omniauth/hanami/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = 'omniauth-hanami' 8 | spec.version = Omniauth::Hanami::VERSION 9 | spec.authors = ['Paweł Świątkowski'] 10 | spec.email = ['inquebrantable@gmail.com'] 11 | 12 | spec.summary = %q{A plugin for omniauth for Hanami applications} 13 | spec.license = 'MIT' 14 | 15 | spec.files = `git ls-files -z`.split("\x0").reject do |f| 16 | f.match(%r{^(test|spec|features)/}) 17 | end 18 | spec.bindir = 'exe' 19 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 20 | spec.require_paths = ['lib'] 21 | 22 | spec.add_runtime_dependency 'omniauth', '~> 1.0' 23 | 24 | spec.add_development_dependency 'bundler', '~> 1.13' 25 | spec.add_development_dependency 'rake', '~> 10.0' 26 | end 27 | -------------------------------------------------------------------------------- /test/database_helper.rb: -------------------------------------------------------------------------------- 1 | require 'scrypt' 2 | 3 | module Database 4 | def self.prepare(db) 5 | db.drop_table?(:users) 6 | db.create_table :users do 7 | primary_key :id 8 | 9 | column :name, String, null: false 10 | column :email, String, null: false 11 | column :crypted_password, String 12 | column :created_at, DateTime, null: false 13 | column :updated_at, DateTime, null: false 14 | end 15 | 16 | db.drop_table?(:credentials) 17 | db.create_table :credentials do 18 | primary_key :id 19 | foreign_key :user_id, :users, on_delete: :cascade, null: false 20 | 21 | column :provider, String, null: false 22 | column :crypted_password, String 23 | column :external_id, String 24 | column :created_at, DateTime, null: false 25 | column :updated_at, DateTime, null: false 26 | end 27 | 28 | password = SCrypt::Password.create('abc123xd') 29 | db[:users].insert( 30 | name: 'test user', 31 | email: 'test@user.com', 32 | crypted_password: password, 33 | created_at: Time.now, 34 | updated_at: Time.now 35 | ) 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /test/test.rb: -------------------------------------------------------------------------------- 1 | require_relative 'test_helper' 2 | 3 | class MyTest < MiniTest::Test 4 | include Rack::Test::Methods 5 | 6 | def app 7 | TEST_APP 8 | end 9 | 10 | def url 11 | '/auth/hanami/callback' 12 | end 13 | 14 | def incorrect_credentials 15 | { user: { email: 'aaa@wp.pl', password: 'xyz123' } } 16 | end 17 | 18 | def correct_credentials 19 | { user: { email: 'test@user.com', password: 'abc123xd' } } 20 | end 21 | 22 | def current_user 23 | last_request.env['warden'].user 24 | end 25 | 26 | def test_hello_world 27 | get '/' 28 | assert last_response.ok? 29 | assert_equal 'Hello, World!', last_response.body 30 | end 31 | 32 | def test_redirect_on_wrong_credentials 33 | post url, incorrect_credentials 34 | assert_equal 302, last_response.status 35 | redir_url = '/auth/failure?message=invalid_credentials&strategy=hanami' 36 | assert_equal redir_url, last_response.headers['Location'] 37 | refute last_response.ok? 38 | end 39 | 40 | def test_redirect_on_correct_credentials 41 | post url, correct_credentials 42 | assert_equal 302, last_response.status 43 | assert_equal '/', last_response.headers['Location'] 44 | end 45 | 46 | def test_user_in_session 47 | post url, correct_credentials 48 | assert current_user 49 | assert_equal 'test user', current_user.name 50 | end 51 | 52 | def test_nil_user_in_incorrect_session 53 | post url, incorrect_credentials 54 | refute current_user 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /test/test_app/config.ru: -------------------------------------------------------------------------------- 1 | require 'warden' 2 | require_relative 'test_app' 3 | 4 | use Rack::Session::Cookie, secret: 'xyz' 5 | use Warden::Manager 6 | use OmniAuth::Builder do 7 | provider :hanami, interactor: Interactors::FindUserForAuth 8 | end 9 | run HanamiTestApp 10 | -------------------------------------------------------------------------------- /test/test_app/test_app.rb: -------------------------------------------------------------------------------- 1 | require 'hanami/router' 2 | require 'omniauth/hanami' 3 | require 'hanami/controller' 4 | require 'hanami/interactor' 5 | 6 | class User < Hanami::Entity 7 | end 8 | 9 | class UserRepository < Hanami::Repository 10 | def find_by_email(email) 11 | root.where(email: email).map_to(User).one 12 | end 13 | end 14 | 15 | Hanami::Model.load! 16 | 17 | module Controllers 18 | module Session 19 | class Create 20 | include Hanami::Action 21 | 22 | def auth_hash 23 | request.env['omniauth.auth'] 24 | end 25 | 26 | def call(_params) 27 | if auth_hash.provider == 'hanami' 28 | user = auth_hash.uid 29 | else 30 | user = UserRepository.new.auth!(auth_hash) 31 | end 32 | 33 | warden.set_user user 34 | redirect_to '/' 35 | end 36 | 37 | def warden 38 | request.env['warden'] 39 | end 40 | end 41 | end 42 | end 43 | 44 | module Interactors 45 | class FindUserForAuth 46 | include Hanami::Interactor 47 | 48 | expose :user 49 | 50 | def initialize(login, password) 51 | @login = login 52 | @password = password 53 | end 54 | 55 | def call 56 | user = UserRepository.new.find_by_email(@login) 57 | if user && user.crypted_password && SCrypt::Password.new(user.crypted_password) == @password 58 | @user = user 59 | else 60 | fail! 61 | end 62 | end 63 | end 64 | end 65 | 66 | HanamiTestApp = Hanami::Router.new(namespace: Controllers) do 67 | get '/', to: ->(env) { [200, {}, ['Hello, World!']] } 68 | get '/auth/:provider/callback', to: 'session#create' 69 | post '/auth/:provider/callback', to: 'session#create' 70 | end 71 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | ENV['RACK_ENV'] = 'test' 2 | require 'minitest/autorun' 3 | require 'rack/test' 4 | require 'hanami/model' 5 | 6 | db_url = 'sqlite://test/test_app/database.sqlite' 7 | Hanami::Model.configure do 8 | adapter :sql, db_url 9 | end 10 | 11 | require_relative 'database_helper' 12 | 13 | DB = Sequel.connect(db_url) 14 | Database.prepare(DB) 15 | 16 | path = File.expand_path '../test_app/config.ru', __FILE__ 17 | TEST_APP = Rack::Builder.parse_file(path).first 18 | --------------------------------------------------------------------------------