devise_token_auth 0.1.13 → 0.1.14
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
![build](https://travis-ci.org/lynndylanhurley/devise_token_auth.svg)
|
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
|