oauth2-provider-jonrowe 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.
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
+