devise_token_auth 0.1.13 → 0.1.14
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 +4 -4
- data/LICENSE +13 -0
- data/README.md +244 -0
- data/app/controllers/devise_token_auth/confirmations_controller.rb +3 -2
- data/app/controllers/devise_token_auth/registrations_controller.rb +19 -10
- data/app/controllers/devise_token_auth/sessions_controller.rb +20 -12
- data/app/models/user.rb +1 -0
- data/lib/devise_token_auth/version.rb +1 -1
- data/test/controllers/devise_token_auth/confirmations_controller_test.rb +53 -0
- data/test/controllers/devise_token_auth/registrations_controller_test.rb +135 -0
- data/test/controllers/devise_token_auth/sessions_controller_test.rb +96 -0
- data/test/dummy/bin/rails +4 -0
- data/test/dummy/bin/rake +4 -0
- data/test/dummy/bin/spring +18 -0
- data/test/dummy/config/environments/test.rb +1 -0
- data/test/dummy/db/development.sqlite3 +0 -0
- data/test/dummy/db/test.sqlite3 +0 -0
- data/test/dummy/log/development.log +332 -0
- data/test/dummy/log/test.log +25673 -0
- data/test/fixtures/users.yml +25 -0
- data/test/test_helper.rb +29 -9
- metadata +17 -5
- data/test/devise_token_auth_test.rb +0 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 85dbd55f7d4269a7061c016208d1fbb7c106f190
|
4
|
+
data.tar.gz: 3eefd3ae437e88ca1a7960556e4a9708c29b3cad
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8c8f4f54d6701084ffcbcd7217de162b7cb2b999e2d18fbddc65a24cca83f956ccf63900c052b3b253da1ee367f60339315745e4e7e306e9ba66851d3b15e527
|
7
|
+
data.tar.gz: 0e5002e2584cef474734197b538266643dfabfa9c28e84ca6a60b9b8770e47b22886f7224e816534204c04ec150bc14a72e85be76a567b78faded45ed1359d07
|
data/LICENSE
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
2
|
+
Version 2, December 2004
|
3
|
+
|
4
|
+
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
|
5
|
+
|
6
|
+
Everyone is permitted to copy and distribute verbatim or modified
|
7
|
+
copies of this license document, and changing it is allowed as long
|
8
|
+
as the name is changed.
|
9
|
+
|
10
|
+
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
11
|
+
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
12
|
+
|
13
|
+
0. You just DO WHAT THE FUCK YOU WANT TO.
|
data/README.md
ADDED
@@ -0,0 +1,244 @@
|
|
1
|
+
# Devise Token Auth
|
2
|
+
|
3
|
+

|
4
|
+
|
5
|
+
This gem provides simple, secure token based authentication.
|
6
|
+
|
7
|
+
This gem was designed to work with the venerable [ng-token-auth](https://github.com/lynndylanhurley/ng-token-auth) module for [angular.js](https://github.com/angular/angular.js).
|
8
|
+
|
9
|
+
# Demo
|
10
|
+
|
11
|
+
[Here is a demo](http://ng-token-auth-demo.herokuapp.com/) of this app running with the [ng-token-auth](https://github.com/lynndylanhurley/ng-token-auth) module.
|
12
|
+
|
13
|
+
The fully configured api used in the demo can be found [here](https://github.com/lynndylanhurley/devise_token_auth_demo).
|
14
|
+
|
15
|
+
# Dependencies
|
16
|
+
This project leverages the following gems:
|
17
|
+
|
18
|
+
* [Devise](https://github.com/plataformatec/devise)
|
19
|
+
* [Omniauth](https://github.com/intridea/omniauth)
|
20
|
+
|
21
|
+
# Installation
|
22
|
+
Add the following to your `Gemfile`:
|
23
|
+
|
24
|
+
~~~ruby
|
25
|
+
gem devise_token_auth
|
26
|
+
~~~
|
27
|
+
|
28
|
+
Then install the gem using bundle:
|
29
|
+
|
30
|
+
~~~bash
|
31
|
+
bundle install
|
32
|
+
~~~
|
33
|
+
|
34
|
+
## Migrations
|
35
|
+
You will need to create a user model. Run the following to generate and run the `User` model migration:
|
36
|
+
|
37
|
+
~~~bash
|
38
|
+
rake devise_token_auth:install:migrations
|
39
|
+
~~~
|
40
|
+
|
41
|
+
Then run the migration:
|
42
|
+
|
43
|
+
~~~bash
|
44
|
+
rake db:migrate
|
45
|
+
~~~
|
46
|
+
|
47
|
+
## Omniauth authentication
|
48
|
+
|
49
|
+
If you wish to use omniauth authentication, add all of your desired authentication provider gems as well.
|
50
|
+
|
51
|
+
##### Omniauth example using github, facebook, and google:
|
52
|
+
~~~ruby
|
53
|
+
gem 'omniauth-github', :git => 'git://github.com/intridea/omniauth-github.git'
|
54
|
+
gem 'omniauth-facebook', :git => 'git://github.com/mkdynamic/omniauth-facebook.git'
|
55
|
+
gem 'omniauth-google-oauth2', :git => 'git://github.com/zquestz/omniauth-google-oauth2.git'
|
56
|
+
~~~
|
57
|
+
|
58
|
+
Then run `bundle install`.
|
59
|
+
|
60
|
+
[List of oauth2 providers](https://github.com/intridea/omniauth/wiki/List-of-Strategies)
|
61
|
+
|
62
|
+
#### Provider settings
|
63
|
+
In `config/initializers/omniauth.rb`, add the settings for each of your providers.
|
64
|
+
|
65
|
+
These settings must be obtained from the providers themselves.
|
66
|
+
|
67
|
+
##### Example using github, facebook, and google:
|
68
|
+
~~~ruby
|
69
|
+
# config/initializers/omniauth.rb
|
70
|
+
Rails.application.config.middleware.use OmniAuth::Builder do
|
71
|
+
provider :github, ENV['GITHUB_KEY'], ENV['GITHUB_SECRET'], scope: 'email,profile'
|
72
|
+
provider :facebook, ENV['FACEBOOK_KEY'], ENV['FACEBOOK_SECRET']
|
73
|
+
provider :google_oauth2, ENV['GOOGLE_KEY'], ENV['GOOGLE_SECRET']
|
74
|
+
end
|
75
|
+
~~~
|
76
|
+
|
77
|
+
The above example assumes that your provider keys and secrets are stored in environmental variables. Use the [figaro](https://github.com/laserlemon/figaro) gem (or [dotenv](https://github.com/bkeepers/dotenv) or [secrets.yml](https://github.com/rails/rails/blob/v4.1.0/railties/lib/rails/generators/rails/app/templates/config/secrets.yml) or equivalent) to accomplish this.
|
78
|
+
|
79
|
+
|
80
|
+
**Note for [pow](http://pow.cx/) and [xip.io](http://xip.io) users**: if you receive `redirect-uri-mismatch` errors from your provider when using pow or xip.io urls, set the following in your development config:
|
81
|
+
|
82
|
+
~~~ruby
|
83
|
+
# config/environments/development.rb
|
84
|
+
|
85
|
+
# when using pow
|
86
|
+
OmniAuth.config.full_host = "http://app-name.dev"
|
87
|
+
|
88
|
+
# when using xip.io
|
89
|
+
OmniAuth.config.full_host = "http://xxx.xxx.xxx.app-name.xip.io"
|
90
|
+
~~~
|
91
|
+
|
92
|
+
There may be a better way to accomplish this. Please post an issue if you have any suggestions.
|
93
|
+
|
94
|
+
## Email authentication
|
95
|
+
If you wish to use email authentication, you must configure your Rails application to send email. [Read here](http://guides.rubyonrails.org/action_mailer_basics.html) for more information.
|
96
|
+
|
97
|
+
I recommend using [mailcatcher](http://mailcatcher.me/) for development.
|
98
|
+
|
99
|
+
##### mailcatcher development example configuration:
|
100
|
+
~~~ruby
|
101
|
+
# config/environments/development.rb
|
102
|
+
Rails.application.configure do
|
103
|
+
config.action_mailer.default_url_options = { :host => 'your-dev-host.dev' }
|
104
|
+
config.action_mailer.delivery_method = :smtp
|
105
|
+
config.action_mailer.smtp_settings = { :address => 'your-dev-host.dev', :port => 1025 }
|
106
|
+
end
|
107
|
+
~~~
|
108
|
+
|
109
|
+
## Routes
|
110
|
+
|
111
|
+
The authentication routes must be mounted to your project.
|
112
|
+
|
113
|
+
In `config/routes.rb`, add the following line:
|
114
|
+
|
115
|
+
~~~ruby
|
116
|
+
# config/routes.rb
|
117
|
+
mount DeviseTokenAuth::Engine => "/auth"
|
118
|
+
~~~
|
119
|
+
|
120
|
+
Note that you can mount this engine to any route that you like. `/auth` is used to conform to the defaults of the [ng-token-auth](https://github.com/lynndylanhurley/ng-token-auth) module.
|
121
|
+
|
122
|
+
## CORS
|
123
|
+
|
124
|
+
If your API and client live on different domains, you will need to configure your Rails API to allow cross origin requests. The [rack-cors](https://github.com/cyu/rack-cors) gem can be used to accomplish this.
|
125
|
+
|
126
|
+
The following example will allow cross domain requests from any domain.
|
127
|
+
|
128
|
+
##### Example rack-cors configuration:
|
129
|
+
~~~ruby
|
130
|
+
# gemfile
|
131
|
+
gem 'rack-cors', :require => 'rack/cors'
|
132
|
+
|
133
|
+
# config/application.rb
|
134
|
+
module YourApp
|
135
|
+
class Application < Rails::Application
|
136
|
+
config.middleware.use Rack::Cors do
|
137
|
+
allow do
|
138
|
+
origins '*'
|
139
|
+
resource '*',
|
140
|
+
:headers => :any,
|
141
|
+
:expose => ['Authorization'], # <-- important!
|
142
|
+
:methods => [:get, :post, :options, :delete, :put]
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
~~~
|
148
|
+
|
149
|
+
Make extra sure that the `Access-Control-Expose-Headers` includes `Authorization` (as is set in the example above by the`:expose` param). If your client experiences erroneous 401 responses, this is likely the cause.
|
150
|
+
|
151
|
+
CORS may not be possible with older browsers (IE8, IE9). I usually set up a proxy for those browsers. See the [ng-token-auth readme](https://github.com/lynndylanhurley/ng-token-auth) for more information.
|
152
|
+
|
153
|
+
# Usage
|
154
|
+
|
155
|
+
The following routes are available for use by your client. These routes live relative to the path at which this engine is mounted (`/auth` in the example above).
|
156
|
+
|
157
|
+
| path | method | purpose |
|
158
|
+
|:-----|:-------|:--------|
|
159
|
+
| / | POST | email registration. accepts **email**, **password**, and **password_confirmation** params. |
|
160
|
+
| /sign_in | POST | email authentication. accepts **email** and **password** as params. |
|
161
|
+
| /sign_out | DELETE | invalidate tokens (end session) |
|
162
|
+
| /:provider | GET | set this route as the destination for client authentication. ideally this will happen in an external window or popup. |
|
163
|
+
| /:provider/callback | GET/POST | destination for the oauth2 provider's callback uri. `postMessage` events containing the authenticated user's data will be sent back to the main client window from this page. |
|
164
|
+
| /validate_token | POST | use this route to validate tokens on return visits to the client. accepts **uid** and **auth_token** as params. these values should correspond to the columns in your `User` table of the same names. |
|
165
|
+
|
166
|
+
If you're using [ng-token-auth](https://github.com/lynndylanhurley/ng-token-auth) for angular.js, then your client is ready to go.
|
167
|
+
|
168
|
+
## Identifying users in controllers
|
169
|
+
|
170
|
+
The authentication information should be included by the client in the `Authorization` header of each request. The header should follow this format:
|
171
|
+
|
172
|
+
~~~
|
173
|
+
token=xxxxx client=yyyyy uid=zzzzz
|
174
|
+
~~~
|
175
|
+
|
176
|
+
Replace `xxxxx` with the user's `auth_token` and `zzzzz` with the user's `uid`. The `client` field exists to allow for multiple simultaneous sessions per user. The client field defaults to `default` if omitted.
|
177
|
+
|
178
|
+
This all happens effortlessly and invisibly when using [ng-token-auth](https://github.com/lynndylanhurley/ng-token-auth).
|
179
|
+
|
180
|
+
### DeviseTokenAuth::Concerns::SetUserByToken
|
181
|
+
|
182
|
+
This gem includes a [Rails concern](http://api.rubyonrails.org/classes/ActiveSupport/Concern.html) that can be used to identify users by the `Authorization` header.
|
183
|
+
|
184
|
+
This concern runs a [before_action](http://guides.rubyonrails.org/action_controller_overview.html#filters), setting the `@user` variable for use in your controllers. The user will be signed in via devise for the duration of the request.
|
185
|
+
|
186
|
+
The concern also runs an [after_action](http://guides.rubyonrails.org/action_controller_overview.html#filters) that changes the auth token after each request.
|
187
|
+
|
188
|
+
It is recommended to include the concern in your base `ApplicationController` so that all children of that controller include the concern as well.
|
189
|
+
|
190
|
+
~~~ruby
|
191
|
+
# app/controllers/application_controller.rb
|
192
|
+
class ApplicationController < ActionController::Base
|
193
|
+
include DeviseTokenAuth::Concerns::SetUserByToken
|
194
|
+
end
|
195
|
+
|
196
|
+
# app/controllers/test_controller.rb
|
197
|
+
class TestController < ApplicationController
|
198
|
+
def members_only
|
199
|
+
if @user
|
200
|
+
render json: {
|
201
|
+
data: {
|
202
|
+
message: "Welcome #{@user.name}",
|
203
|
+
user: @user
|
204
|
+
}
|
205
|
+
}, status: 200
|
206
|
+
else
|
207
|
+
render json: {
|
208
|
+
errors: ["Authorized users only."]
|
209
|
+
}, status: 401
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
213
|
+
~~~
|
214
|
+
|
215
|
+
# Security
|
216
|
+
|
217
|
+
This gem takes the following steps to ensure security.
|
218
|
+
|
219
|
+
This gem uses auth tokens that are:
|
220
|
+
* changed after every request,
|
221
|
+
* [of cryptographic strength](http://ruby-doc.org/stdlib-2.1.0/libdoc/securerandom/rdoc/SecureRandom.html),
|
222
|
+
* hashed using [BCrypt](https://github.com/codahale/bcrypt-ruby) (not stored in plain-text),
|
223
|
+
* securely compared (to protect against timing attacks),
|
224
|
+
* invalidated after 2 weeks
|
225
|
+
|
226
|
+
These measures were inspired by [this stackoverflow post](http://stackoverflow.com/questions/18605294/is-devises-token-authenticatable-secure).
|
227
|
+
|
228
|
+
This gem further mitigates timing attacks by using [this technique](https://gist.github.com/josevalim/fb706b1e933ef01e4fb6).
|
229
|
+
|
230
|
+
But the most important step is to use HTTPS. You are on the hook for that.
|
231
|
+
|
232
|
+
# TODO
|
233
|
+
|
234
|
+
* Write tests
|
235
|
+
* `User` model is currently baked into this gem. Allow for dynamic definition using concerns (or other means).
|
236
|
+
* Find a way to expose devise + omniauth configs, maybe using generators.
|
237
|
+
|
238
|
+
# Contributing
|
239
|
+
Just send a pull request. I will grant you commit access if you send quality pull requests.
|
240
|
+
|
241
|
+
Guidelines will be posted if the need arises.
|
242
|
+
|
243
|
+
# License
|
244
|
+
This project uses the WTFPL
|
@@ -4,16 +4,17 @@ module DeviseTokenAuth
|
|
4
4
|
|
5
5
|
def show
|
6
6
|
@user = User.confirm_by_token(params[:confirmation_token])
|
7
|
-
|
7
|
+
|
8
|
+
if @user.id
|
8
9
|
# create client id
|
9
10
|
client_id = SecureRandom.urlsafe_base64(nil, false)
|
10
|
-
|
11
11
|
token = SecureRandom.urlsafe_base64(nil, false)
|
12
12
|
token_hash = BCrypt::Password.create(token)
|
13
13
|
@user.tokens[client_id] = {
|
14
14
|
token: token_hash,
|
15
15
|
expiry: Time.now + 2.weeks
|
16
16
|
}
|
17
|
+
|
17
18
|
@user.save!
|
18
19
|
|
19
20
|
redirect_to generate_url(@user.confirm_success_url, {
|
@@ -11,18 +11,27 @@ module DeviseTokenAuth
|
|
11
11
|
@resource.uid = resource_params[:email]
|
12
12
|
@resource.provider = "email"
|
13
13
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
14
|
+
begin
|
15
|
+
if @resource.save
|
16
|
+
render json: {
|
17
|
+
status: 'success',
|
18
|
+
data: @resource.as_json
|
19
|
+
}
|
20
|
+
else
|
21
|
+
clean_up_passwords @resource
|
22
|
+
render json: {
|
23
|
+
status: 'error',
|
24
|
+
data: @resource,
|
25
|
+
errors: ["An account already exists for #{@resource.email}"]
|
26
|
+
}, status: 403
|
27
|
+
end
|
28
|
+
rescue ActiveRecord::RecordNotUnique
|
20
29
|
clean_up_passwords @resource
|
21
|
-
render
|
30
|
+
render json: {
|
22
31
|
status: 'error',
|
23
|
-
data: @resource
|
24
|
-
errors: @resource.
|
25
|
-
}
|
32
|
+
data: @resource,
|
33
|
+
errors: ["An account already exists for #{@resource.email}"]
|
34
|
+
}, status: 403
|
26
35
|
end
|
27
36
|
end
|
28
37
|
|
@@ -8,17 +8,9 @@ module DeviseTokenAuth
|
|
8
8
|
respond_to :json
|
9
9
|
|
10
10
|
def create
|
11
|
-
|
12
|
-
|
13
|
-
unless resource and valid_params? and resource.valid_password?(params[:password])
|
14
|
-
render json: {
|
15
|
-
success: false,
|
16
|
-
errors: ["Invalid login credentials. Please try again."]
|
17
|
-
}, status: 401
|
18
|
-
|
19
|
-
else
|
20
|
-
@user = resource
|
11
|
+
@user = User.find_by_email(resource_params[:email])
|
21
12
|
|
13
|
+
if @user and valid_params? and @user.valid_password?(resource_params[:password]) and @user.confirmed?
|
22
14
|
# create client id
|
23
15
|
@client_id = SecureRandom.urlsafe_base64(nil, false)
|
24
16
|
@token = SecureRandom.urlsafe_base64(nil, false)
|
@@ -31,8 +23,24 @@ module DeviseTokenAuth
|
|
31
23
|
|
32
24
|
render json: {
|
33
25
|
success: true,
|
34
|
-
data:
|
26
|
+
data: @user.as_json
|
35
27
|
}
|
28
|
+
|
29
|
+
elsif @user and not @user.confirmed?
|
30
|
+
render json: {
|
31
|
+
success: false,
|
32
|
+
errors: [
|
33
|
+
"A confirmation email was sent to your account at #{@user.email}. "+
|
34
|
+
"You must follow the instructions in the email before your account "+
|
35
|
+
"can be activated"
|
36
|
+
]
|
37
|
+
}, status: 401
|
38
|
+
|
39
|
+
else
|
40
|
+
render json: {
|
41
|
+
success: false,
|
42
|
+
errors: ["Invalid login credentials. Please try again."]
|
43
|
+
}, status: 401
|
36
44
|
end
|
37
45
|
end
|
38
46
|
|
@@ -45,7 +53,7 @@ module DeviseTokenAuth
|
|
45
53
|
end
|
46
54
|
|
47
55
|
def valid_params?
|
48
|
-
|
56
|
+
resource_params[:password] && resource_params[:email]
|
49
57
|
end
|
50
58
|
|
51
59
|
def resource_params
|
data/app/models/user.rb
CHANGED
@@ -9,6 +9,7 @@ class User < ActiveRecord::Base
|
|
9
9
|
|
10
10
|
# only validate unique emails among email registration users
|
11
11
|
validates_uniqueness_of :email, conditions: -> { where(provider: 'email') }
|
12
|
+
validates_presence_of :confirm_success_url
|
12
13
|
|
13
14
|
def valid_token?(client_id, token)
|
14
15
|
return false unless self.tokens[client_id]['expiry'] > 2.weeks.ago
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
# was the web request successful?
|
4
|
+
# was the user redirected to the right page?
|
5
|
+
# was the user successfully authenticated?
|
6
|
+
# was the correct object stored in the response?
|
7
|
+
# was the appropriate message delivered in the json payload?
|
8
|
+
|
9
|
+
class DeviseTokenAuth::ConfirmationsControllerTest < ActionController::TestCase
|
10
|
+
describe DeviseTokenAuth::ConfirmationsController, "Confirmation" do
|
11
|
+
fixtures :users
|
12
|
+
|
13
|
+
before do
|
14
|
+
@new_user = users(:unconfirmed_email_user)
|
15
|
+
@new_user.send_confirmation_instructions
|
16
|
+
@mail = ActionMailer::Base.deliveries.last
|
17
|
+
@token = @mail.body.match(/confirmation_token=(.*)\"/)[1]
|
18
|
+
end
|
19
|
+
|
20
|
+
test 'should generate raw token' do
|
21
|
+
assert @token
|
22
|
+
end
|
23
|
+
|
24
|
+
test "should store token hash in user" do
|
25
|
+
assert @new_user.confirmation_token
|
26
|
+
end
|
27
|
+
|
28
|
+
describe "success" do
|
29
|
+
before do
|
30
|
+
xhr :get, :show, {confirmation_token: @token}
|
31
|
+
@user = assigns(:user)
|
32
|
+
end
|
33
|
+
|
34
|
+
test "user should now be confirmed" do
|
35
|
+
assert @user.confirmed?
|
36
|
+
end
|
37
|
+
|
38
|
+
test "should redirect to success url" do
|
39
|
+
assert_redirected_to(/^#{@user.confirm_success_url}/)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
describe "failure" do
|
44
|
+
test "user should not be confirmed" do
|
45
|
+
assert_raises(ActionController::RoutingError) {
|
46
|
+
xhr :get, :show, {confirmation_token: "bogus"}
|
47
|
+
}
|
48
|
+
@user = assigns(:user)
|
49
|
+
refute @user.confirmed?
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|