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 +7 -0
- data/.env.sample +5 -0
- data/.gitignore +15 -0
- data/.rspec +2 -0
- data/.rubocop.yml +48 -0
- data/.ruby-version +1 -0
- data/.travis.yml +5 -0
- data/Gemfile +4 -0
- data/Guardfile +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +228 -0
- data/Rakefile +8 -0
- data/bin/console +14 -0
- data/bin/guard +17 -0
- data/bin/rake +17 -0
- data/bin/rspec +17 -0
- data/bin/setup +8 -0
- data/config.ru +13 -0
- data/config/boot.rb +10 -0
- data/config/puma.rb +34 -0
- data/db/migrations/20170602120030_create_users.rb +32 -0
- data/db/schema.rb +33 -0
- data/lib/user_auth.rb +21 -0
- data/lib/user_auth/api.rb +120 -0
- data/lib/user_auth/configuration.rb +15 -0
- data/lib/user_auth/models/refresh_token.rb +13 -0
- data/lib/user_auth/models/user.rb +42 -0
- data/lib/user_auth/password_hasher.rb +9 -0
- data/lib/user_auth/password_verifier.rb +16 -0
- data/lib/user_auth/rake_tasks.rb +1 -0
- data/lib/user_auth/tasks/import_migrations.rake +11 -0
- data/lib/user_auth/version.rb +3 -0
- data/lib/user_auth/web/helpers.rb +33 -0
- data/user-auth.gemspec +37 -0
- metadata +246 -0
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
data/.gitignore
ADDED
data/.rspec
ADDED
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
data/Gemfile
ADDED
data/Guardfile
ADDED
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
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
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,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,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,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: []
|