da-user-auth 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 7762d15f81b195fd9c44ee29faa5c7424afc5a04
4
+ data.tar.gz: 66e857a42ec38aaf63a7dd4e41664e9f5acc80ac
5
+ SHA512:
6
+ metadata.gz: 278897d0e62ebf600b2298da68b57fbed94839215946b4d4f56a5997d9264335c98f868d0a21527585f460560e845c60591c059663e8dd5c6231eaac3f742497
7
+ data.tar.gz: 8e0425ed9b628cf878a8b711f4d718f86be9e907d68084f979c1abdea908dd52c5c50cf9623103af81742b4881ba504258e4870878fded3b857e3cf3d23ad2d0
data/.env.sample ADDED
@@ -0,0 +1,5 @@
1
+ PORT=3030
2
+ APP_ENV=development
3
+ DATABASE_URL=postgres://127.0.0.1/services_user_auth_dev
4
+ TEST_DATABASE_URL=postgres://127.0.0.1/services_user_auth_test
5
+ JWT_SECRET_KEY=XXX
data/.gitignore ADDED
@@ -0,0 +1,15 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+
11
+ # rspec failure tracking
12
+ .rspec_status
13
+
14
+ # Environment variables
15
+ .env
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.rubocop.yml ADDED
@@ -0,0 +1,48 @@
1
+ AllCops:
2
+ DisplayCopNames: true
3
+ TargetRubyVersion: 2.4
4
+ Include:
5
+ - 'app/**/*.rb'
6
+ - 'lib/**/*.rb'
7
+ - 'lib/**/*.rake'
8
+ Exclude:
9
+ - 'Gemfile'
10
+ - 'Guardfile'
11
+ - 'Rakefile'
12
+ - 'bin/*'
13
+ - 'config/**/*'
14
+ - 'db/**/*'
15
+ - 'vendor/**/*'
16
+ - 'node_modules/**/*'
17
+ - '*.gemspec'
18
+ Metrics/LineLength:
19
+ Max: 120
20
+ Exclude:
21
+ - spec/**/*
22
+ Style/Documentation:
23
+ Enabled: false
24
+ Style/StringLiterals:
25
+ EnforcedStyle: double_quotes
26
+ Style/FrozenStringLiteralComment:
27
+ Enabled: false
28
+ Style/IndentArray:
29
+ EnforcedStyle: consistent
30
+ Style/GlobalVars:
31
+ Enabled: false
32
+ Style/MultilineMethodCallIndentation:
33
+ EnforcedStyle: aligned
34
+ Style/Alias:
35
+ EnforcedStyle: prefer_alias_method
36
+ Style/Lambda:
37
+ EnforcedStyle: lambda
38
+ Style/IfUnlessModifier:
39
+ Enabled: false
40
+ Style/GuardClause:
41
+ MinBodyLength: 3
42
+ Style/RaiseArgs:
43
+ EnforcedStyle: compact
44
+ Style/SpecialGlobalVars:
45
+ EnforcedStyle: use_perl_names
46
+ Metrics/BlockLength:
47
+ Exclude:
48
+ - spec/**/*
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.4.1
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.4.0
5
+ before_install: gem install bundler -v 1.15.0
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in user-auth.gemspec
4
+ gemspec
data/Guardfile ADDED
@@ -0,0 +1,5 @@
1
+ guard :rspec, cmd: "bin/rspec" do
2
+ watch(%r{^spec/.+_spec\.rb$})
3
+ watch(%r{^lib/user_auth/api.rb$}) { |m| "spec/api/" }
4
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
5
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2017 Pete Hawkins
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.
data/README.md ADDED
@@ -0,0 +1,228 @@
1
+ ## User auth service
2
+
3
+ Rack compatible user authentication microservice. Can be run standalone or mounted into another rack app.
4
+
5
+ #### Dependencies
6
+
7
+ Ensure you have Sequel (> v.4.44.0) setup and connected in your application before attempting to mount UserAuth.
8
+
9
+ - Sequel (model plugins: :timestamps, :validation_helpers, :defaults_setter)
10
+ - Postgres (pg gem)
11
+ - Rack 2.0
12
+
13
+ ## Usage
14
+
15
+ Add this line to your application's Gemfile:
16
+
17
+ ```ruby
18
+ gem 'user-auth', git: 'https://github.com/dawsonandrews/services-user-auth'
19
+ ```
20
+
21
+ And then execute:
22
+
23
+ ```sh
24
+ $ bundle
25
+ ```
26
+
27
+ ### Import and run migrations
28
+
29
+ ```ruby
30
+ # Add this to your Rakefile
31
+ require "bundler/setup"
32
+ require "user_auth/rake_tasks"
33
+ ```
34
+
35
+ Copy migrations and migrate:
36
+
37
+ ```sh
38
+ $ bin/rake user_auth:import_migrations
39
+ $ bin/rake db:migrate
40
+ ```
41
+
42
+ ### Map to a URL in config.ru
43
+
44
+ ```ruby
45
+ require_relative "./config/boot"
46
+ require "user_auth/api"
47
+
48
+ # Somewhere after your middleware
49
+
50
+ map("/auth") { run UserAuth::Api }
51
+ ```
52
+
53
+ ### Configuration
54
+
55
+ ```ruby
56
+ # config/initializers/user_auth.rb
57
+ require "user_auth"
58
+
59
+ UserAuth.configure do |config|
60
+ config.jwt_exp = 3600 # Expire JWT tokens in 1 hour
61
+ config.require_account_confirmations = false
62
+ config.allow_signups = true
63
+
64
+ # Lambda that gets called each time an email should be delivered
65
+ # configure this with however your application sends email.
66
+ config.deliver_mail = lambda do |params|
67
+ # example params =>
68
+ # {
69
+ # template: "user_signup",
70
+ # to: "email@email.com",
71
+ # user: {
72
+ # user_id: 123,
73
+ # email: "email@email.com",
74
+ # name: "Jane"
75
+ # }
76
+ # }
77
+ EmailDeliveryJob.new(params)
78
+ end
79
+ end
80
+ ```
81
+
82
+ That’s you all setup, see endpoints below for documentation on how to get a token etc.
83
+
84
+
85
+ ## Endpoints
86
+
87
+ ### `POST /signup`
88
+
89
+ **Params**
90
+
91
+ - **email** - Users email address
92
+ - **password** - Users password
93
+ - **info** - Basic key value json style object to store additional data about the user
94
+
95
+ ```ruby
96
+ resp = HTTP.post("/signup", email: "test@example.org", password: "hunter2", info: { name: "Test" })
97
+
98
+ resp.parsed # =>
99
+
100
+ {
101
+ token_type: "Bearer",
102
+ token: "jwt-stateless-token-includes-user-data",
103
+ refresh_token: "refresh-token"
104
+ }
105
+ ```
106
+
107
+ ### `POST /token`
108
+
109
+ **Params**
110
+
111
+ - **grant_type** - If 'password' provide username and password, if 'refresh_token' provide refresh_token param.
112
+ - **username** - Users email address
113
+ - **password** - Users password
114
+ - **refresh_token** - Users refresh token
115
+
116
+ ```ruby
117
+ resp = HTTP.post("/token", grant_type: "password", username: "test@example.org", password: "hunter2") # or...
118
+ resp = HTTP.post("/token", grant_type: "refresh_token", refresh_token: "some-refresh-token")
119
+
120
+ resp.parsed # =>
121
+
122
+ {
123
+ token_type: "Bearer",
124
+ token: "jwt-stateless-token-includes-user-data",
125
+ refresh_token: "refresh-token"
126
+ }
127
+ ```
128
+
129
+ ### `PUT /user`
130
+
131
+ **Params**
132
+
133
+ - **email** - Users email address
134
+ - **info** - Basic key value json style object to store additional data about the user
135
+
136
+ ```ruby
137
+ resp = HTTP.put("/user", email: "newemail@example.org", info: { foo: "Bar" })
138
+
139
+ resp.parsed # =>
140
+
141
+ {
142
+ token_type: "Bearer",
143
+ token: "jwt-stateless-token-includes-user-data"
144
+ }
145
+ ```
146
+
147
+ ### `POST /logout`
148
+
149
+ Destroys all active refresh tokens (current JWT is still active for the standard timeout of 1 hour)
150
+
151
+ ```ruby
152
+ resp = HTTP.post("/logout")
153
+
154
+ resp.parsed # =>
155
+
156
+ {}
157
+ ```
158
+
159
+ ### `POST /recover`
160
+
161
+ Delivers password reset emails
162
+
163
+ **Params**
164
+
165
+ - **email** - Users email address
166
+
167
+ ```ruby
168
+ resp = HTTP.post("/recover", email: "test@example.org")
169
+
170
+ resp.parsed # =>
171
+
172
+ {}
173
+ ```
174
+
175
+ ### `PUT /user/attributes/password`
176
+
177
+ Update users password either by authenticating with a valid JWT or providing a password reset token (sent in the email from POST /recover)
178
+
179
+ **Params**
180
+
181
+ - **password** - Users new password
182
+
183
+ ```ruby
184
+ resp = HTTP.put("/user/attributes/password", password: "new-password", token: "password-reset-token")
185
+
186
+ resp.parsed # =>
187
+
188
+ {
189
+ token_type: "Bearer",
190
+ token: "jwt-stateless-token-includes-user-data",
191
+ refresh_token: "refresh-token"
192
+ }
193
+ ```
194
+
195
+ ### TODO: `POST /verify`
196
+
197
+ Verify account, only required if confirmations are configured
198
+
199
+ ```ruby
200
+ resp = HTTP.post("/verify", token: "confirmation-token")
201
+
202
+ resp.parsed # =>
203
+
204
+ {
205
+ token_type: "Bearer",
206
+ token: "jwt-stateless-token-includes-user-data",
207
+ refresh_token: "refresh-token"
208
+ }
209
+ ```
210
+
211
+ ### TODO: Social auth
212
+
213
+ Using [omniauth](https://github.com/omniauth/omniauth) to allow signin with third party services such as facebook.
214
+
215
+
216
+ ## Development
217
+
218
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
219
+
220
+ 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).
221
+
222
+ ## Contributing
223
+
224
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/user-auth.
225
+
226
+ ## License
227
+
228
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require "bundler/gem_tasks"
2
+ require "da/rake_tasks"
3
+
4
+ # Add current path and lib to the load path
5
+ $: << File.expand_path('../', __FILE__)
6
+ $: << File.expand_path('../lib', __FILE__)
7
+
8
+ task default: ["ci:all"]
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "user/auth"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/guard ADDED
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+ #
4
+ # This file was generated by Bundler.
5
+ #
6
+ # The application 'guard' is installed as part of a gem, and
7
+ # this file is here to facilitate running it.
8
+ #
9
+
10
+ require "pathname"
11
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
12
+ Pathname.new(__FILE__).realpath)
13
+
14
+ require "rubygems"
15
+ require "bundler/setup"
16
+
17
+ load Gem.bin_path("guard", "guard")
data/bin/rake ADDED
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+ #
4
+ # This file was generated by Bundler.
5
+ #
6
+ # The application 'rake' is installed as part of a gem, and
7
+ # this file is here to facilitate running it.
8
+ #
9
+
10
+ require "pathname"
11
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
12
+ Pathname.new(__FILE__).realpath)
13
+
14
+ require "rubygems"
15
+ require "bundler/setup"
16
+
17
+ load Gem.bin_path("rake", "rake")
data/bin/rspec ADDED
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+ #
4
+ # This file was generated by Bundler.
5
+ #
6
+ # The application 'rspec' is installed as part of a gem, and
7
+ # this file is here to facilitate running it.
8
+ #
9
+
10
+ require "pathname"
11
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
12
+ Pathname.new(__FILE__).realpath)
13
+
14
+ require "rubygems"
15
+ require "bundler/setup"
16
+
17
+ load Gem.bin_path("rspec-core", "rspec")
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/config.ru ADDED
@@ -0,0 +1,13 @@
1
+ require_relative "./config/boot"
2
+ require_relative "./lib/user_auth/api"
3
+ require "rack/cors"
4
+
5
+ use Rack::Deflater
6
+ use Rack::Cors do
7
+ allow do
8
+ origins ENV.fetch("ALLOWED_CORS_ORIGINS", "*")
9
+ resource "*", headers: :any, methods: :any, max_age: 2_592_000 # 30 days
10
+ end
11
+ end
12
+
13
+ map("/") { run UserAuth::Api }
data/config/boot.rb ADDED
@@ -0,0 +1,10 @@
1
+ # Add current path and lib to the load path
2
+ $: << File.expand_path("../../", __FILE__)
3
+ $: << File.expand_path("../../lib", __FILE__)
4
+
5
+ # Default ENV to dev if not present
6
+ ENV["APP_ENV"] ||= "development"
7
+
8
+ require "da/core/environment"
9
+ require "da/db"
10
+ require "da/web"
data/config/puma.rb ADDED
@@ -0,0 +1,34 @@
1
+ # Puma can serve each request in a thread from an internal thread pool.
2
+ # The `threads` method setting takes two numbers a minimum and maximum.
3
+ # Any libraries that use thread pools should be configured to match
4
+ # the maximum value specified for Puma. Default is set to 5 threads for minimum
5
+ # and maximum, this matches the default thread size of Active Record.
6
+ #
7
+ threads_count = ENV.fetch("MAX_THREADS") { 5 }.to_i
8
+ threads threads_count, threads_count
9
+
10
+ # Specifies the `port` that Puma will listen on to receive requests, default is 3000.
11
+ #
12
+ port ENV.fetch("PORT") { 3000 }
13
+
14
+ # Specifies the `environment` that Puma will run in.
15
+ #
16
+ environment ENV.fetch("APP_ENV") { "development" }
17
+
18
+ # Specifies the number of `workers` to boot in clustered mode.
19
+ # Workers are forked webserver processes. If using threads and workers together
20
+ # the concurrency of the application would be max `threads` * `workers`.
21
+ # Workers do not work on JRuby or Windows (both of which do not support
22
+ # processes).
23
+ #
24
+ workers ENV.fetch("WEB_CONCURRENCY") { 1 }
25
+
26
+ # The code in the `on_worker_boot` will be called if you are using
27
+ # clustered mode by specifying a number of `workers`. After each worker
28
+ # process is booted this block will be run, if you are using `preload_app!`
29
+ # option you will want to use this block to reconnect to any threads
30
+ # or connections that may have been created at application boot, Ruby
31
+ # cannot share connections between processes.
32
+ #
33
+ # on_worker_boot do
34
+ # end
@@ -0,0 +1,32 @@
1
+ Sequel.migration do
2
+ up do
3
+ create_table :users do
4
+ primary_key :id
5
+ String :username, size: 255
6
+ String :email, null: false, size: 255
7
+ String :password_digest, null: false, size: 255
8
+ jsonb :info, null: false, default: Sequel.pg_jsonb({})
9
+ DateTime :created_at, null: false
10
+ DateTime :updated_at, null: false
11
+
12
+ index :email, unique: true
13
+ index :username, unique: true
14
+ end
15
+
16
+ create_table :refresh_tokens do
17
+ primary_key :id
18
+ foreign_key :user_id, :users, null: false
19
+ String :token, null: false, size: 64
20
+ DateTime :revoked_at
21
+ DateTime :created_at, null: false
22
+ DateTime :updated_at, null: false
23
+
24
+ index :token, unique: true
25
+ end
26
+ end
27
+
28
+ down do
29
+ drop_table :refresh_tokens
30
+ drop_table :users
31
+ end
32
+ end
data/db/schema.rb ADDED
@@ -0,0 +1,33 @@
1
+ Sequel.migration do
2
+ change do
3
+ create_table(:schema_migrations) do
4
+ String :filename, :text=>true, :null=>false
5
+
6
+ primary_key [:filename]
7
+ end
8
+
9
+ create_table(:users, :ignore_index_errors=>true) do
10
+ primary_key :id
11
+ String :username, :size=>255
12
+ String :email, :size=>255, :null=>false
13
+ String :password_digest, :size=>255, :null=>false
14
+ String :info, :null=>false
15
+ DateTime :created_at, :null=>false
16
+ DateTime :updated_at, :null=>false
17
+
18
+ index [:email], :unique=>true
19
+ index [:username], :unique=>true
20
+ end
21
+
22
+ create_table(:refresh_tokens, :ignore_index_errors=>true) do
23
+ primary_key :id
24
+ foreign_key :user_id, :users, :null=>false, :key=>[:id]
25
+ String :token, :size=>64, :null=>false
26
+ DateTime :revoked_at
27
+ DateTime :created_at, :null=>false
28
+ DateTime :updated_at, :null=>false
29
+
30
+ index [:token], :unique=>true
31
+ end
32
+ end
33
+ end
data/lib/user_auth.rb ADDED
@@ -0,0 +1,21 @@
1
+ require "user_auth/version"
2
+ require "user_auth/configuration"
3
+ require "user_auth/api"
4
+
5
+ module UserAuth
6
+ class << self
7
+ attr_accessor :configuration
8
+ end
9
+
10
+ def self.configuration
11
+ @configuration ||= Configuration.new
12
+ end
13
+
14
+ def self.reset
15
+ @configuration = Configuration.new
16
+ end
17
+
18
+ def self.configure
19
+ yield(configuration)
20
+ end
21
+ end
@@ -0,0 +1,120 @@
1
+ require "da/web"
2
+ require_relative "./models/refresh_token"
3
+ require_relative "./models/user"
4
+ require_relative "./web/helpers"
5
+ require_relative "./password_verifier"
6
+
7
+ module UserAuth
8
+ class Api < DA::Web::BaseRoute
9
+ include UserAuth::Models
10
+
11
+ helpers Web::Helpers
12
+
13
+ get "/" do
14
+ json(service: "user-auth")
15
+ end
16
+
17
+ post "/signup" do
18
+ user = User.create(
19
+ email: params[:email],
20
+ password: params[:password],
21
+ info: params.fetch(:info, {})
22
+ )
23
+ deliver_email(
24
+ to: user.email,
25
+ user: user.to_json,
26
+ template: "user_signup"
27
+ )
28
+
29
+ status 201
30
+
31
+ json_user_token(user)
32
+ end
33
+
34
+ post "/token" do
35
+ case params[:grant_type]
36
+ when "password"
37
+ user = User.first(email: params[:username])
38
+ verifier = PasswordVerifier.new(user&.password_digest)
39
+
40
+ if user && verifier.verify(params[:password])
41
+ json_user_token(user)
42
+ else
43
+ halt 404, json(error_code: "not_found", message: "Your email / password is incorrect")
44
+ end
45
+ when "refresh_token"
46
+ refresh_token = RefreshToken.first(token: params[:refresh_token])
47
+
48
+ if refresh_token
49
+ json_user_token(refresh_token.user)
50
+ else
51
+ halt 400, json(error_code: "bad_request", message: "Invalid refresh_token")
52
+ end
53
+ else
54
+ halt 400, json(error_code: "bad_request", message: "grant_type must be one of password, refresh_token")
55
+ end
56
+ end
57
+
58
+ put "/user" do
59
+ warden.authenticate!
60
+
61
+ update_params = {
62
+ info: params.fetch(:info, {})
63
+ }
64
+
65
+ update_params[:email] = params[:email] if params[:email]
66
+
67
+ user = current_user.update(update_params)
68
+
69
+ json_user_token(user)
70
+ end
71
+
72
+ post "/logout" do
73
+ warden.authenticate!
74
+ current_user.clear_refresh_tokens!
75
+ json({})
76
+ end
77
+
78
+ post "/recover" do
79
+ user = User.first(email: params[:email])
80
+
81
+ if user
82
+ deliver_email(
83
+ to: user.email,
84
+ user: user.to_json,
85
+ template: "password_reset",
86
+ reset_token: build_jwt(user.to_json)
87
+ )
88
+ end
89
+
90
+ json({})
91
+ end
92
+
93
+ put "/user/attributes/password" do
94
+ warden.authenticate!
95
+
96
+ current_user.password_changing = true
97
+ current_user.update(password: params[:password])
98
+
99
+ deliver_email(
100
+ to: current_user.email,
101
+ user: current_user.to_json,
102
+ template: "password_updated"
103
+ )
104
+
105
+ json_user_token(current_user)
106
+ end
107
+
108
+ error Sequel::ValidationFailed do |record|
109
+ halt 422, json(
110
+ errors: record.errors,
111
+ error_code: "validation_failed",
112
+ message: "Validation failed"
113
+ )
114
+ end
115
+
116
+ error Sinatra::NotFound, Sequel::NoMatchingRow do
117
+ halt 404, json(error_code: "not_found", message: "Endpoint '#{request.path}' not found")
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,15 @@
1
+ module UserAuth
2
+ class Configuration
3
+ attr_accessor :deliver_mail, :require_account_confirmations, :allow_signups,
4
+ :jwt_exp
5
+
6
+ def initialize
7
+ @deliver_mail = lambda do |options|
8
+ $logger.info("TODO: Deliver mail #{options.inspect}")
9
+ end
10
+ @require_account_confirmations = false
11
+ @allow_signups = true
12
+ @jwt_exp = 3600
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,13 @@
1
+ require "securerandom"
2
+
3
+ module UserAuth
4
+ module Models
5
+ class RefreshToken < Sequel::Model
6
+ many_to_one :user
7
+
8
+ def before_save
9
+ self.token ||= SecureRandom.hex(32)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,42 @@
1
+ require_relative "../password_hasher"
2
+ require "active_support/core_ext/hash/conversions"
3
+
4
+ module UserAuth
5
+ module Models
6
+ class User < Sequel::Model
7
+ attr_reader :password
8
+ attr_accessor :password_changing
9
+ one_to_many :refresh_tokens
10
+
11
+ def validate
12
+ super
13
+ validates_presence :email
14
+ validates_unique :email
15
+ validates_format(/\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i, :email, message: "is not a valid email address")
16
+ validates_presence :password if new? || password_changing
17
+ validates_min_length 8, :password if new? || password_changing
18
+ end
19
+
20
+ def password=(plaintext)
21
+ @password = plaintext
22
+ self.password_digest = PasswordHasher.new.hash_plaintext(plaintext)
23
+ end
24
+
25
+ def email=(email)
26
+ super(email.try(:downcase))
27
+ end
28
+
29
+ def to_json
30
+ info.merge(email: email, user_id: id).symbolize_keys
31
+ end
32
+
33
+ def refresh_token!
34
+ RefreshToken.find_or_create(user: self).token
35
+ end
36
+
37
+ def clear_refresh_tokens!
38
+ refresh_tokens_dataset.destroy
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,9 @@
1
+ require "bcrypt"
2
+
3
+ module UserAuth
4
+ class PasswordHasher
5
+ def hash_plaintext(plaintext)
6
+ BCrypt::Password.create(plaintext)
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,16 @@
1
+ require "bcrypt"
2
+
3
+ module UserAuth
4
+ class PasswordVerifier
5
+ def initialize(digest)
6
+ @bcrypt = BCrypt::Password.new(digest)
7
+ rescue BCrypt::Errors::InvalidHash
8
+ @bcrypt = nil
9
+ end
10
+
11
+ def verify(plaintext)
12
+ return false unless @bcrypt
13
+ @bcrypt == plaintext
14
+ end
15
+ end
16
+ end
@@ -0,0 +1 @@
1
+ import File.expand_path(File.join(__dir__, "tasks", "import_migrations.rake"))
@@ -0,0 +1,11 @@
1
+ require "fileutils"
2
+
3
+ namespace :user_auth do
4
+ desc "Import migrations for user-auth service"
5
+ task :import_migrations do |t|
6
+ import_to = File.join(t.application.original_dir, "db")
7
+ import_form = File.expand_path(File.join(__dir__, "..", "..", "..", "db", "migrations"))
8
+
9
+ FileUtils.cp_r(import_form, import_to, preserve: true)
10
+ end
11
+ end
@@ -0,0 +1,3 @@
1
+ module UserAuth
2
+ VERSION = "0.1.0".freeze
3
+ end
@@ -0,0 +1,33 @@
1
+ require "da/core/auth_token"
2
+
3
+ module UserAuth
4
+ module Web
5
+ module Helpers
6
+ def current_user
7
+ @current_user ||= UserAuth::Models::User.with_pk!(warden.user.user_id)
8
+ end
9
+
10
+ def deliver_email(options)
11
+ UserAuth.configuration.deliver_mail.call(options)
12
+ end
13
+
14
+ def json(data)
15
+ content_type(:json)
16
+ JSON.dump(data)
17
+ end
18
+
19
+ def json_user_token(user)
20
+ json(
21
+ token_type: "Bearer",
22
+ token: build_jwt(user.to_json),
23
+ refresh_token: user.refresh_token!
24
+ )
25
+ end
26
+
27
+ def build_jwt(data)
28
+ exp = Time.now.to_i + UserAuth.configuration.jwt_exp
29
+ AuthToken.new.create(data.merge(exp: exp))
30
+ end
31
+ end
32
+ end
33
+ end
data/user-auth.gemspec ADDED
@@ -0,0 +1,37 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "user_auth/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "da-user-auth"
8
+ spec.version = UserAuth::VERSION
9
+ spec.authors = ["Pete Hawkins"]
10
+ spec.email = ["pete@phawk.co.uk"]
11
+
12
+ spec.summary = %q{Rack compatible user authentication microservice}
13
+ spec.description = %q{Rack compatible user authentication microservice. Can be run standalone or mounted into another rack app.}
14
+ spec.homepage = "https://dawsonandrews.com/"
15
+ spec.license = "MIT"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
18
+ f.match(%r{^(test|spec|features)/})
19
+ end
20
+ spec.bindir = "exe"
21
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
22
+ spec.require_paths = ["lib"]
23
+
24
+ spec.add_runtime_dependency "da-core", "~> 0.1.1"
25
+ spec.add_runtime_dependency "pg", "~> 0.20"
26
+ spec.add_runtime_dependency "sequel", "~> 4.44.0"
27
+ spec.add_runtime_dependency "bcrypt"
28
+
29
+ spec.add_development_dependency "bundler", ">= 1.14"
30
+ spec.add_development_dependency "rake", "~> 10.0"
31
+ spec.add_development_dependency "rspec", "~> 3.0"
32
+ spec.add_development_dependency "rack-test"
33
+ spec.add_development_dependency "rubocop"
34
+ spec.add_development_dependency "bundler-audit"
35
+ spec.add_development_dependency "guard"
36
+ spec.add_development_dependency "guard-rspec"
37
+ end
metadata ADDED
@@ -0,0 +1,246 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: da-user-auth
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Pete Hawkins
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2017-06-12 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: da-core
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 0.1.1
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 0.1.1
27
+ - !ruby/object:Gem::Dependency
28
+ name: pg
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0.20'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0.20'
41
+ - !ruby/object:Gem::Dependency
42
+ name: sequel
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 4.44.0
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 4.44.0
55
+ - !ruby/object:Gem::Dependency
56
+ name: bcrypt
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: bundler
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '1.14'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '1.14'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rake
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '10.0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '10.0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rspec
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '3.0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '3.0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rack-test
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: rubocop
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: bundler-audit
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ - !ruby/object:Gem::Dependency
154
+ name: guard
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - ">="
158
+ - !ruby/object:Gem::Version
159
+ version: '0'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - ">="
165
+ - !ruby/object:Gem::Version
166
+ version: '0'
167
+ - !ruby/object:Gem::Dependency
168
+ name: guard-rspec
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - ">="
172
+ - !ruby/object:Gem::Version
173
+ version: '0'
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - ">="
179
+ - !ruby/object:Gem::Version
180
+ version: '0'
181
+ description: Rack compatible user authentication microservice. Can be run standalone
182
+ or mounted into another rack app.
183
+ email:
184
+ - pete@phawk.co.uk
185
+ executables: []
186
+ extensions: []
187
+ extra_rdoc_files: []
188
+ files:
189
+ - ".env.sample"
190
+ - ".gitignore"
191
+ - ".rspec"
192
+ - ".rubocop.yml"
193
+ - ".ruby-version"
194
+ - ".travis.yml"
195
+ - Gemfile
196
+ - Guardfile
197
+ - LICENSE.txt
198
+ - README.md
199
+ - Rakefile
200
+ - bin/console
201
+ - bin/guard
202
+ - bin/rake
203
+ - bin/rspec
204
+ - bin/setup
205
+ - config.ru
206
+ - config/boot.rb
207
+ - config/puma.rb
208
+ - db/migrations/20170602120030_create_users.rb
209
+ - db/schema.rb
210
+ - lib/user_auth.rb
211
+ - lib/user_auth/api.rb
212
+ - lib/user_auth/configuration.rb
213
+ - lib/user_auth/models/refresh_token.rb
214
+ - lib/user_auth/models/user.rb
215
+ - lib/user_auth/password_hasher.rb
216
+ - lib/user_auth/password_verifier.rb
217
+ - lib/user_auth/rake_tasks.rb
218
+ - lib/user_auth/tasks/import_migrations.rake
219
+ - lib/user_auth/version.rb
220
+ - lib/user_auth/web/helpers.rb
221
+ - user-auth.gemspec
222
+ homepage: https://dawsonandrews.com/
223
+ licenses:
224
+ - MIT
225
+ metadata: {}
226
+ post_install_message:
227
+ rdoc_options: []
228
+ require_paths:
229
+ - lib
230
+ required_ruby_version: !ruby/object:Gem::Requirement
231
+ requirements:
232
+ - - ">="
233
+ - !ruby/object:Gem::Version
234
+ version: '0'
235
+ required_rubygems_version: !ruby/object:Gem::Requirement
236
+ requirements:
237
+ - - ">="
238
+ - !ruby/object:Gem::Version
239
+ version: '0'
240
+ requirements: []
241
+ rubyforge_project:
242
+ rubygems_version: 2.6.11
243
+ signing_key:
244
+ specification_version: 4
245
+ summary: Rack compatible user authentication microservice
246
+ test_files: []