oauth2-provider-jonrowe 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. data/README.rdoc +314 -0
  2. data/example/README.rdoc +11 -0
  3. data/example/application.rb +151 -0
  4. data/example/config.ru +3 -0
  5. data/example/environment.rb +11 -0
  6. data/example/models/connection.rb +9 -0
  7. data/example/models/note.rb +4 -0
  8. data/example/models/user.rb +6 -0
  9. data/example/public/style.css +78 -0
  10. data/example/schema.rb +27 -0
  11. data/example/views/authorize.erb +28 -0
  12. data/example/views/create_user.erb +3 -0
  13. data/example/views/home.erb +25 -0
  14. data/example/views/layout.erb +25 -0
  15. data/example/views/login.erb +20 -0
  16. data/example/views/new_client.erb +25 -0
  17. data/example/views/new_user.erb +22 -0
  18. data/example/views/show_client.erb +15 -0
  19. data/lib/oauth2/model.rb +17 -0
  20. data/lib/oauth2/model/authorization.rb +113 -0
  21. data/lib/oauth2/model/client.rb +55 -0
  22. data/lib/oauth2/model/client_owner.rb +13 -0
  23. data/lib/oauth2/model/hashing.rb +27 -0
  24. data/lib/oauth2/model/resource_owner.rb +26 -0
  25. data/lib/oauth2/model/schema.rb +42 -0
  26. data/lib/oauth2/provider.rb +117 -0
  27. data/lib/oauth2/provider/access_token.rb +66 -0
  28. data/lib/oauth2/provider/authorization.rb +168 -0
  29. data/lib/oauth2/provider/error.rb +29 -0
  30. data/lib/oauth2/provider/exchange.rb +212 -0
  31. data/lib/oauth2/router.rb +60 -0
  32. data/spec/factories.rb +27 -0
  33. data/spec/oauth2/model/authorization_spec.rb +216 -0
  34. data/spec/oauth2/model/client_spec.rb +55 -0
  35. data/spec/oauth2/model/resource_owner_spec.rb +55 -0
  36. data/spec/oauth2/provider/access_token_spec.rb +125 -0
  37. data/spec/oauth2/provider/authorization_spec.rb +323 -0
  38. data/spec/oauth2/provider/exchange_spec.rb +330 -0
  39. data/spec/oauth2/provider_spec.rb +531 -0
  40. data/spec/request_helpers.rb +46 -0
  41. data/spec/spec_helper.rb +44 -0
  42. data/spec/test_app/helper.rb +33 -0
  43. data/spec/test_app/provider/application.rb +61 -0
  44. data/spec/test_app/provider/views/authorize.erb +19 -0
  45. metadata +220 -0
@@ -0,0 +1,314 @@
1
+ = OAuth2::Provider
2
+
3
+ This gem provides a toolkit for adding OAuth2 provider capabilities to a Ruby
4
+ web app. It handles most of the protocol for you: it is designed to provide
5
+ a sufficient level of abstraction that it can implement updates to the protocol
6
+ without affecting your application code at all. All you have to deal with is
7
+ authenticating your users and letting them grant access to client apps.
8
+
9
+ It is also designed to be usable within any web frontend, at least those of
10
+ Rails and Sinatra. It assumes very little about the request objects in your
11
+ environment, namely they:
12
+
13
+ * respond to <tt>#params</tt> with a <tt>Hash</tt> of request parameters
14
+ * respond to <tt>#get?</tt> and <tt>#post?</tt>
15
+ * respond to <tt>#url</tt> with the full URL string of the request
16
+ * respond to <tt>#env</tt>, returning the HTTP environment <tt>Hash</tt>
17
+
18
+ It stores the clients and authorizations using ActiveRecord; the schema is in
19
+ <tt>lib/oauth2/model/schema.rb</tt>. Run it to update your database to
20
+ store OAuth data:
21
+
22
+ OAuth2::Model::Schema.up
23
+
24
+ The current imeplementation is based on draft-10[http://tools.ietf.org/html/draft-ietf-oauth-v2-10].
25
+
26
+
27
+ == Usage
28
+
29
+ A basic example is in <tt>example/application.rb</tt>. To implement OAuth, you
30
+ need to provide four things:
31
+
32
+ * Some UI to register client applications
33
+ * The OAuth request endpoint
34
+ * A flow for logged-in users to grant access to clients
35
+ * Resources protected by access tokens
36
+
37
+
38
+ === Configuration
39
+
40
+ <tt>OAuth2::Provider</tt> requires very little configuration. The only thing it
41
+ needs to know about your app is its name, which is used in the headers for some
42
+ authentication errors. To load the library, just do this:
43
+
44
+ require 'oauth2/provider'
45
+ OAuth2::Provider.realm = 'My OAuth app'
46
+
47
+ You may also need to configure assertion handlers if your application supports
48
+ third-party access credentials. See 'Using Assertions' below.
49
+
50
+
51
+ === Registering client applications
52
+
53
+ Clients are modelled by the <tt>OAuth2::Model::Client</tt> class, which is an
54
+ ActiveRecord model. You just need to implement a UI for creating them, for
55
+ example in a Sinatra app:
56
+
57
+ get '/oauth/apps/new' do
58
+ @client = OAuth2::Model::Client.new
59
+ erb :new_client
60
+ end
61
+
62
+ post '/oauth/apps' do
63
+ @client = OAuth2::Model::Client.new(params)
64
+ @client.save ? erb(:show_client) : erb(:new_client)
65
+ end
66
+
67
+ Client applications must have a <tt>name</tt> and a <tt>redirect_uri</tt>:
68
+ provide fields for editing these but do not allow the other fields to be edited,
69
+ since they are the client's access credentials. When you've created the client,
70
+ you should show its details to the user registering the client: its <tt>name</tt>,
71
+ <tt>redirect_uri</tt>, <tt>client_id</tt> and <tt>client_secret</tt> (the last
72
+ two are generated for you). <tt>client_secret</tt> is not stored in plain text
73
+ so you can only read it when you initially create the client object.
74
+
75
+
76
+ === OAuth request endpoint
77
+
78
+ This is a path that your application exposes in order for clients to communicate
79
+ with your application. It is also the page that the client will send users to
80
+ so they can authenticate and grant access. Many requests to this endpoint will
81
+ be protocol-level requests that do not involve the user, and <tt>OAuth2::Provider</tt>
82
+ gives you a generic way to handle all that.
83
+
84
+ You should use this to get the right response, status code and headers to send to
85
+ the client. In the event that <tt>OAuth2::Provider</tt> does not provide a response,
86
+ you should render a page that lets the user begin to authenticate and grant access.
87
+
88
+ This endpoint must be accessible via GET and POST. In this example we will expose
89
+ the OAuth service through the path <tt>/oauth/authorize</tt>. We check if there is
90
+ a logged-in resource owner and give this to <tt>OAuth::Provider</tt>, since we
91
+ may be able to immediately redirect if the user has already authorized the client:
92
+
93
+ [:get, :post].each do |method|
94
+ __send__ method, '/oauth/authorize' do
95
+ @owner = User.find_by_id(session[:user_id])
96
+ @oauth2 = OAuth2::Provider.parse(@owner, request)
97
+
98
+ redirect @oauth2.redirect_uri if @oauth2.redirect?
99
+
100
+ headers @oauth2.response_headers
101
+ status @oauth2.response_status
102
+
103
+ @oauth2.response_body || erb(:login)
104
+ end
105
+ end
106
+
107
+ There is a set of parameters that you will need to hold on to for when your app
108
+ needs to redirect back to the client. You could store them in the session, or
109
+ pass them through forms as the user completes the flow. For example to embed
110
+ them in the login form, do this:
111
+
112
+ <% @oauth2.params.each do |key, value| %>
113
+ <input type="hidden" name="<%= key %>" value="<%= value %>">
114
+ <% end %>
115
+
116
+ You may also want to use scopes to provide granular access to your domain using
117
+ <i>scopes</i>. The <tt>@oauth2</tt> object exposes the scopes the client has asked
118
+ for so you can display them to the user:
119
+
120
+ <p>The application <%= @oauth2.client.name %> wants the following permissions:</p>
121
+
122
+ <ul>
123
+ <% @oauth2.scopes.each do |scope| %>
124
+ <li><%= PERMISSION_UI_STRINGS[scope] %></li>
125
+ <% end %>
126
+ </ul>
127
+
128
+ You can also use the method <tt>@oauth2.unauthorized_scopes</tt> to get the list
129
+ of scopes the user has not already granted to the client, in the case where the
130
+ client already has some authorization. If no prior authorization exists between
131
+ the user and the client, <tt>@oauth2.unauthorized_scopes</tt> just returns all
132
+ the scopes the client has asked for.
133
+
134
+
135
+ === Granting access to clients
136
+
137
+ Your application will probably have some concept of a user, or a <i>resource
138
+ owner</i> in OAuth lingo. Add this mixin to the model that represents your
139
+ users:
140
+
141
+ class User < ActiveRecord::Base
142
+ include OAuth2::Model::ResourceOwner
143
+ end
144
+
145
+ This just adds a couple of relations and methods to the model to let it interact
146
+ with the <tt>OAuth2</tt> models.
147
+
148
+ Once the user has authenticated you should show them a page to let them grant
149
+ or deny access to the client application. This is straightforward; let's say
150
+ the user checks a box before posting a form to indicate their intent:
151
+
152
+ post '/oauth/allow' do
153
+ @user = User.find_by_id(session[:user_id])
154
+ @auth = OAuth2::Provider::Authorization.new(@user, params)
155
+
156
+ if params['allow'] == '1'
157
+ @auth.grant_access!
158
+ else
159
+ @auth.deny_access!
160
+ end
161
+ redirect @auth.redirect_uri
162
+ end
163
+
164
+ After granting or denying access, we just redirect back to the client using a
165
+ URI that <tt>OAuth2::Provider</tt> will provide for you.
166
+
167
+
168
+ === Using password credentials
169
+
170
+ If you like, OAuth lets you use a user's login credentials to authenticate with
171
+ a provider. In this case the client application must request these credentials
172
+ directly from the user and then post them to the exchange endpoint. On the
173
+ provider side you can handle this using the <tt>handle_passwords</tt> and
174
+ <tt>grant_access!</tt> API methods, for example:
175
+
176
+ OAuth2::Provider.handle_passwords do |client, username, password|
177
+ user = User.find_by_username(username)
178
+ if user.authenticate?(password)
179
+ user.grant_access!(client)
180
+ else
181
+ nil
182
+ end
183
+ end
184
+
185
+ The block must return <tt>user.grant_access!(client)</tt> if you want to allow
186
+ access, otherwise it should return <tt>nil</tt>.
187
+
188
+
189
+ === Using assertions
190
+
191
+ Assertions provide a way to access your OAuth services using user credentials
192
+ from another service. When using assertions, the user will not authenticate on
193
+ your web site; the OAuth client will authenticate the user using some other
194
+ framework and obtain a token, then exchange this token for an access token on
195
+ your domain.
196
+
197
+ For example, a client application may let a user authenticate using Facebook,
198
+ so the application obtains a Facebook access token from the user. The client
199
+ would then pass this token to your OAuth endpoint and exchange it for an
200
+ access token from your site. You will typically create an account in your
201
+ database to represent this, then have that new account grant access to the
202
+ client.
203
+
204
+ To use assertions, you must tell <tt>OAuth2::Provider</tt> how to handle
205
+ assertions based on their type. An assertion type must be a valid URI. For
206
+ the Facebook example we'd do the following. The block yields the <tt>Client</tt>
207
+ object making the exchange request, and the value of the assertion, which in
208
+ this example will be a Facebook access token.
209
+
210
+ OAuth2::Provider.handle_assertions 'https://graph.facebook.com/me' do |client, assertion|
211
+ facebook = URI.parse('https://graph.facebook.com/me?access_token=' + assertion)
212
+ response = Net::HTTP.get_response(facebook)
213
+
214
+ user_data = JSON.parse(response.body)
215
+ account = User.from_facebook_data(user_data)
216
+
217
+ account.grant_access!(client)
218
+ end
219
+
220
+ This code should run when your app boots, not during a request handler - think
221
+ of it as configuration for <tt>OAuth2::Provider</tt>. The framework will invoke
222
+ it when a client attempts to use assertions with your OAuth endpoint.
223
+
224
+ The final call in your handler should be to <tt>grant_access!</tt>; this returns
225
+ an <tt>Authorization</tt> object that the framework then uses to complete the
226
+ response to the client. If you want to deny the request for whatever reason, the
227
+ block must return <tt>nil</tt>. If a client tries to use an assertion type you
228
+ have no handler for, the client will get an error response.
229
+
230
+
231
+ === Protecting resources with access tokens
232
+
233
+ To protect the user's resources you need to check for access tokens. This is
234
+ simple, for example a call to get a user's notes:
235
+
236
+ get '/user/:username/notes' do
237
+ user = User.find_by_username(params[:username])
238
+ token = OAuth2::Provider.access_token(user, ['read_notes'], request)
239
+
240
+ headers token.response_headers
241
+ status token.response_status
242
+
243
+ if token.valid?
244
+ JSON.unparse('notes' => user.notes)
245
+ else
246
+ JSON.unparse('error' => 'No notes for you!')
247
+ end
248
+ end
249
+
250
+ <tt>OAuth2::Provider.access_token()</tt> takes a <tt>ResourceOwner</tt>, a list
251
+ of scopes required to access the resource, and a request object. If the token
252
+ was not granted for the required scopes, has expired or is simply invalid,
253
+ headers and a status code are set to indicate this to the client. <tt>token.valid?</tt>
254
+ is the call you should use to determine whether to server the request or not.
255
+
256
+ It is also common to provide a dynamic resource for getting some basic data
257
+ about a user by supplying their access token. This can be done by passing
258
+ <tt>nil</tt> as the resource owner:
259
+
260
+ get '/me' do
261
+ token = OAuth2::Provider.access_token(nil, [], request)
262
+ if token.valid?
263
+ JSON.unparse('username' => token.owner.username)
264
+ else
265
+ JSON.unparse('error' => 'Keep out!')
266
+ end
267
+ end
268
+
269
+ <tt>token.owner</tt> returns the <tt>ResourceOwner</tt> that issued the token.
270
+ A token represents the fact that a single owner gave a single client a set of
271
+ permissions.
272
+
273
+
274
+ === Transport security
275
+
276
+ Your application should ensure that any endpoint that receives or returns OAuth
277
+ data is only accessible over a secure transport such as the <tt>https:</tt>
278
+ protocol. <tt>OAuth2::Provider</tt> can enforces this to make it easier to keep
279
+ your users' data secure. If you want to enable these behaviours, set
280
+ <tt>OAuth2::Provider.enforce_ssl = true</tt>.
281
+
282
+ * The <tt>OAuth2::Provider.parse</tt> method will produce error responses and
283
+ will not process the incoming request unless the request was made using the
284
+ <tt>https:</tt> protocol.
285
+ * An access token constructed using <tt>OAuth2::Provider.access_token</tt> will
286
+ return <tt>false</tt> for <tt>#valid?</tt> unless the request was made using the
287
+ <tt>https:</tt> protocol.
288
+ * Any access token received over an insecure connection is immediately destroyed
289
+ to prevent eavesdroppers getting access to the user's resources. A client
290
+ making an insecure request will have to send the user through the authorization
291
+ process again to get a new token.
292
+
293
+
294
+ == License
295
+
296
+ Copyright (c) 2010-2011 Songkick.com
297
+
298
+ Permission is hereby granted, free of charge, to any person obtaining a copy
299
+ of this software and associated documentation files (the "Software"), to deal
300
+ in the Software without restriction, including without limitation the rights
301
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
302
+ copies of the Software, and to permit persons to whom the Software is
303
+ furnished to do so, subject to the following conditions:
304
+
305
+ The above copyright notice and this permission notice shall be included in
306
+ all copies or substantial portions of the Software.
307
+
308
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
309
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
310
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
311
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
312
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
313
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
314
+ THE SOFTWARE.
@@ -0,0 +1,11 @@
1
+ = OAuth2::Provider example app
2
+
3
+ To get up and running:
4
+
5
+ # in parent directory
6
+ bundle install
7
+
8
+ cd example/
9
+ ruby schema.rb
10
+ rackup config.ru
11
+
@@ -0,0 +1,151 @@
1
+ dir = File.expand_path(File.dirname(__FILE__))
2
+ require dir + '/environment'
3
+
4
+ require 'sinatra'
5
+ require 'json'
6
+
7
+ set :static, true
8
+ set :public, dir + '/public'
9
+ set :views, dir + '/views'
10
+ enable :sessions
11
+
12
+ PERMISSIONS = {
13
+ 'read_notes' => 'Read all your notes'
14
+ }
15
+
16
+ ERROR_RESPONSE = JSON.unparse('error' => 'No soup for you!')
17
+
18
+ get('/') { erb(:home) }
19
+
20
+
21
+ get '/users/new' do
22
+ @user = User.new
23
+ erb :new_user
24
+ end
25
+
26
+ post '/users/create' do
27
+ @user = User.create(params)
28
+ if @user.save
29
+ erb :create_user
30
+ else
31
+ erb :new_user
32
+ end
33
+ end
34
+
35
+ #================================================================
36
+ # Register applications
37
+
38
+ get '/oauth/apps/new' do
39
+ @client = OAuth2::Model::Client.new
40
+ erb :new_client
41
+ end
42
+
43
+ post '/oauth/apps' do
44
+ @client = OAuth2::Model::Client.new(params)
45
+ if @client.save
46
+ session[:client_secret] = @client.client_secret
47
+ redirect("/oauth/apps/#{@client.id}")
48
+ else
49
+ erb :new_client
50
+ end
51
+ end
52
+
53
+ get '/oauth/apps/:id' do
54
+ @client = OAuth2::Model::Client.find_by_id(params[:id])
55
+ @client_secret = session[:client_secret]
56
+ erb :show_client
57
+ end
58
+
59
+
60
+ #================================================================
61
+ # OAuth 2.0 flow
62
+
63
+ # Initial request exmample:
64
+ # /oauth/authorize?response_type=token&client_id=7uljxxdgsksmecn5cycvug46v&redirect_uri=http%3A%2F%2Fexample.com%2Fcb&scope=read_notes
65
+ [:get, :post].each do |method|
66
+ __send__ method, '/oauth/authorize' do
67
+ @user = User.find_by_id(session[:user_id])
68
+ @oauth2 = OAuth2::Provider.parse(@user, request)
69
+ redirect @oauth2.redirect_uri if @oauth2.redirect?
70
+
71
+ headers @oauth2.response_headers
72
+ status @oauth2.response_status
73
+
74
+ @oauth2.response_body || erb(:login)
75
+ end
76
+ end
77
+
78
+ post '/login' do
79
+ @user = User.find_by_username(params[:username])
80
+ @oauth2 = OAuth2::Provider.parse(@user, request)
81
+ session[:user_id] = @user.id
82
+ erb(@user ? :authorize : :login)
83
+ end
84
+
85
+ post '/oauth/allow' do
86
+ @user = User.find_by_id(session[:user_id])
87
+ @auth = OAuth2::Provider::Authorization.new(@user, params)
88
+ if params['allow'] == '1'
89
+ @auth.grant_access!
90
+ else
91
+ @auth.deny_access!
92
+ end
93
+ redirect @auth.redirect_uri
94
+ end
95
+
96
+ #================================================================
97
+ # Domain API
98
+
99
+ get '/me' do
100
+ authorization = OAuth2::Provider.access_token(nil, [], request)
101
+ headers authorization.response_headers
102
+ status authorization.response_status
103
+
104
+ if authorization.valid?
105
+ user = authorization.owner
106
+ JSON.unparse('username' => user.username)
107
+ else
108
+ ERROR_RESPONSE
109
+ end
110
+ end
111
+
112
+ get '/users/:username/notes' do
113
+ verify_access :read_notes do |user|
114
+ notes = user.notes.map do |n|
115
+ {:note_id => n.id, :url => "#{host}/users/#{user.username}/notes/#{n.id}"}
116
+ end
117
+ JSON.unparse(:notes => notes)
118
+ end
119
+ end
120
+
121
+ get '/users/:username/notes/:note_id' do
122
+ verify_access :read_notes do |user|
123
+ note = user.notes.find_by_id(params[:note_id])
124
+ note ? note.to_json : JSON.unparse(:error => 'No such note')
125
+ end
126
+ end
127
+
128
+
129
+
130
+ helpers do
131
+ #================================================================
132
+ # Check for OAuth access before rendering a resource
133
+ def verify_access(scope)
134
+ user = User.find_by_username(params[:username])
135
+ token = OAuth2::Provider.access_token(user, [scope.to_s], request)
136
+
137
+ headers token.response_headers
138
+ status token.response_status
139
+
140
+ return ERROR_RESPONSE unless token.valid?
141
+
142
+ yield user
143
+ end
144
+
145
+ #================================================================
146
+ # Return the full app domain
147
+ def host
148
+ request.scheme + '://' + request.host_with_port
149
+ end
150
+ end
151
+