songkick-oauth2-provider 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. data/README.rdoc +394 -0
  2. data/example/README.rdoc +11 -0
  3. data/example/application.rb +159 -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/error.erb +6 -0
  14. data/example/views/home.erb +25 -0
  15. data/example/views/layout.erb +25 -0
  16. data/example/views/login.erb +20 -0
  17. data/example/views/new_client.erb +25 -0
  18. data/example/views/new_user.erb +22 -0
  19. data/example/views/show_client.erb +15 -0
  20. data/lib/songkick/oauth2/model.rb +20 -0
  21. data/lib/songkick/oauth2/model/authorization.rb +126 -0
  22. data/lib/songkick/oauth2/model/client.rb +61 -0
  23. data/lib/songkick/oauth2/model/client_owner.rb +15 -0
  24. data/lib/songkick/oauth2/model/hashing.rb +29 -0
  25. data/lib/songkick/oauth2/model/resource_owner.rb +54 -0
  26. data/lib/songkick/oauth2/provider.rb +122 -0
  27. data/lib/songkick/oauth2/provider/access_token.rb +68 -0
  28. data/lib/songkick/oauth2/provider/authorization.rb +190 -0
  29. data/lib/songkick/oauth2/provider/error.rb +22 -0
  30. data/lib/songkick/oauth2/provider/exchange.rb +227 -0
  31. data/lib/songkick/oauth2/router.rb +79 -0
  32. data/lib/songkick/oauth2/schema.rb +17 -0
  33. data/lib/songkick/oauth2/schema/20120828112156_songkick_oauth2_schema_original_schema.rb +36 -0
  34. data/spec/factories.rb +27 -0
  35. data/spec/request_helpers.rb +52 -0
  36. data/spec/songkick/oauth2/model/authorization_spec.rb +216 -0
  37. data/spec/songkick/oauth2/model/client_spec.rb +55 -0
  38. data/spec/songkick/oauth2/model/resource_owner_spec.rb +88 -0
  39. data/spec/songkick/oauth2/provider/access_token_spec.rb +125 -0
  40. data/spec/songkick/oauth2/provider/authorization_spec.rb +346 -0
  41. data/spec/songkick/oauth2/provider/exchange_spec.rb +353 -0
  42. data/spec/songkick/oauth2/provider_spec.rb +545 -0
  43. data/spec/spec_helper.rb +62 -0
  44. data/spec/test_app/helper.rb +33 -0
  45. data/spec/test_app/provider/application.rb +68 -0
  46. data/spec/test_app/provider/views/authorize.erb +19 -0
  47. metadata +273 -0
@@ -0,0 +1,394 @@
1
+ = Songkick::OAuth2::Provider
2
+
3
+ {<img src="https://secure.travis-ci.org/songkick/oauth2-provider.png?branch=master" alt="Build Status" />}[http://travis-ci.org/songkick/oauth2-provider]
4
+ {<img src="https://codeclimate.com/badge.png" />}[https://codeclimate.com/github/songkick/oauth2-provider]
5
+
6
+ This gem provides a toolkit for adding OAuth2 provider capabilities to a Ruby
7
+ web app. It handles most of the protocol for you: it is designed to provide
8
+ a sufficient level of abstraction that it can implement updates to the protocol
9
+ without affecting your application code at all. All you have to deal with is
10
+ authenticating your users and letting them grant access to client apps.
11
+
12
+ It is also designed to be usable within any web frontend, at least those of
13
+ Rails and Sinatra. Its API uses Rack request-environment hashes rather than
14
+ framework-specific request objects, though you can pass those in and their
15
+ <tt>request.env</tt> property will be used internally.
16
+
17
+ It stores the clients and authorizations using ActiveRecord.
18
+
19
+
20
+ === A note on versioning
21
+
22
+ This library is based on draft-10[http://tools.ietf.org/html/draft-ietf-oauth-v2-10],
23
+ which was current when we began writing it. Having observed the development of
24
+ the OAuth 2.0 spec over time, we have decided not to upgrade to later drafts
25
+ until the spec is finalized. There is not enough meaningful change going on to
26
+ merit migrating to every draft version - it would simply create a lot of
27
+ turbulence and make it hard to reason about exactly what semantics the library
28
+ supports.
29
+
30
+ During draft state, the gem version will indicate which draft it implements
31
+ using the minor version, for example <tt>0.10.2</tt> means the second bug-fix
32
+ release for draft 10.
33
+
34
+
35
+ == Terminology
36
+
37
+ * <b>Client</b>: A third-party software system that integrates with the provider.
38
+ Twitter and Facebook call this an "app".
39
+ * <b>Client Owner</b>: The entity which owns a <b>client</b>, i.e. the
40
+ individual or company responsible for the client application.
41
+ * <b>Resource Owner</b>: This will almost certainly be a User. It's the entity
42
+ which has the data that the <b>client</b> is asking permission to see.
43
+ * <b>Authorization</b>: When a <b>resource owner</b> grants access to a
44
+ <b>client</b> (i.e., a user grants access to a company's app), an
45
+ authorization is created. This can typically be revoked by the user at any
46
+ time (which is the strength and flexibility of the OAuth architecture).
47
+ * <b>Access Token</b>: An opaque string representing an <b>authorization</b>.
48
+ A <b>client</b> is given an access token when a <b>resource owner</b> grants
49
+ it access to resources. The access token must be included in all requests for
50
+ protected resources.
51
+
52
+
53
+ == Usage
54
+
55
+ A basic example is in <tt>example/application.rb</tt>. To implement OAuth, you
56
+ need to provide four things:
57
+
58
+ * Some UI to register client applications
59
+ * The OAuth request endpoint
60
+ * A flow for logged-in users to grant access to clients
61
+ * Resources protected by access tokens
62
+
63
+
64
+ === Declare your app's name
65
+
66
+ Declare your app's name somewhere (for example in Rails, in <tt>application.rb</tt>
67
+ or an initializer):
68
+
69
+ require 'songkick/oauth2/provider'
70
+ Songkick::OAuth2::Provider.realm = 'My OAuth app'
71
+
72
+
73
+ === HTTPS
74
+
75
+ Your application should ensure that any endpoint that receives or returns OAuth
76
+ data is only accessible over a secure transport such as the <tt>https:</tt>
77
+ protocol. <tt>Songkick::OAuth2::Provider</tt> can enforce this to make it easier
78
+ to keep your users' data secure.
79
+
80
+ You can set <tt>Songkick::OAuth2::Provider.enforce_ssl = true</tt> in the same
81
+ place that you declared your app name above. This will result in the following
82
+ behavior:
83
+
84
+ * The <tt>Songkick::OAuth2::Provider.parse</tt> method will produce error
85
+ responses and will not process the incoming request unless the request was
86
+ made using the <tt>https:</tt> protocol.
87
+ * An access token constructed using <tt>Songkick::OAuth2::Provider.access_token</tt>
88
+ will return <tt>false</tt> for <tt>#valid?</tt> unless the request was made
89
+ using the <tt>https:</tt> protocol.
90
+ * Any access token received over an insecure connection is immediately destroyed
91
+ to prevent eavesdroppers getting access to the user's resources. A client
92
+ making an insecure request will have to send the user through the
93
+ authorization process again to get a new token.
94
+
95
+
96
+ === Schema
97
+
98
+ Add the <tt>Songkick::OAuth2::Provider</tt> tables to your app's schema. This is
99
+ done using <tt>Songkick::OAuth2::Model::Schema.up</tt>, which can be used inside
100
+ an <tt>ActiveRecord</tt> migration like so:
101
+
102
+ class CreateOauth2ProviderModels < ActiveRecord::Migration
103
+ def up
104
+ Songkick::OAuth2::Model::Schema.up
105
+ end
106
+ end
107
+
108
+
109
+ === Model Mixins
110
+
111
+ There are two mixins you need to put in your code,
112
+ <tt>Songkick::OAuth2::Model::ClientOwner</tt> for whichever model will own the
113
+ "apps", and <tt>Songkick::OAuth2::Model::ResourceOwner</tt> for whichever model
114
+ is the innocent, unassuming entity who will selectively share their data. It's
115
+ possible that this is the same model, such as User:
116
+
117
+ class User < ActiveRecord::Base
118
+ include Songkick::OAuth2::Model::ResourceOwner
119
+ include Songkick::OAuth2::Model::ClientOwner
120
+ has_many :interesting_pieces_of_data
121
+ end
122
+
123
+ Or they might go into two different models:
124
+
125
+ class User < ActiveRecord::Base
126
+ include Songkick::OAuth2::Model::ResourceOwner
127
+ has_many :interesting_pieces_of_data
128
+ end
129
+
130
+ class Company < ActiveRecord::Base
131
+ include Songkick::OAuth2::Model::ClientOwner
132
+ belongs_to :user
133
+ end
134
+
135
+ To see the methods and associations that these two mixins add to your models,
136
+ take a look at <b>lib/oauth2/model/client_owner.rb</b> and
137
+ <b>lib/oauth2/model/resource_owner.rb</b>.
138
+
139
+
140
+ === Registering client applications
141
+
142
+ Clients are modeled by the <tt>Songkick::OAuth2::Model::Client</tt> class, which
143
+ is an ActiveRecord model. You just need to implement a UI for creating them, for
144
+ example in a Sinatra app:
145
+
146
+ get '/oauth/apps/new' do
147
+ @client = Songkick::OAuth2::Model::Client.new
148
+ erb :new_client
149
+ end
150
+
151
+ post '/oauth/apps' do
152
+ @client = Songkick::OAuth2::Model::Client.new(params)
153
+ @client.save ? erb(:show_client) : erb(:new_client)
154
+ end
155
+
156
+ Client applications must have a <tt>name</tt> and a <tt>redirect_uri</tt>:
157
+ provide fields for editing these but do not allow the other fields to be edited,
158
+ since they are the client's access credentials. When you've created the client,
159
+ you should show its details to the user registering the client: its
160
+ <tt>name</tt>, <tt>redirect_uri</tt>, <tt>client_id</tt> and
161
+ <tt>client_secret</tt> (the last two are generated for you).
162
+ <tt>client_secret</tt> is not stored in plain text so you can only read it when
163
+ you initially create the client object.
164
+
165
+
166
+ === OAuth request endpoint
167
+
168
+ This is a path that your application exposes in order for clients to communicate
169
+ with your application. It is also the page that the client will send users to
170
+ so they can authenticate and grant access. Many requests to this endpoint will
171
+ be protocol-level requests that do not involve the user, and
172
+ <tt>Songkick::OAuth2::Provider</tt> gives you a generic way to handle all that.
173
+
174
+ You should use this to get the right response, status code and headers to send
175
+ to the client. In the event that <tt>Songkick::OAuth2::Provider</tt> does not
176
+ provide a response, you should render a page that lets the user begin to
177
+ authenticate and grant access. This can happen in two cases:
178
+
179
+ * The client makes a valid Authorization request. In this case you should
180
+ display a login flow to the user so they can authenticate and grant access to
181
+ the client.
182
+ * The client makes an invalid Authorization request and the provider cannot
183
+ redirect back to the client. In this case you should display an error page
184
+ to the user, possibly including the value of <tt>@oauth2.error_description</tt>.
185
+
186
+ This endpoint must be accessible via GET and POST. In this example we will
187
+ expose the OAuth service through the path <tt>/oauth/authorize</tt>. We check if
188
+ there is a logged-in resource owner and give this to <tt>OAuth::Provider</tt>,
189
+ since we may be able to immediately redirect if the user has already authorized
190
+ the client:
191
+
192
+ [:get, :post].each do |method|
193
+ __send__ method, '/oauth/authorize' do
194
+ @owner = User.find_by_id(session[:user_id])
195
+ @oauth2 = Songkick::OAuth2::Provider.parse(@owner, env)
196
+
197
+ if @oauth2.redirect?
198
+ redirect @oauth2.redirect_uri, @oauth2.response_status
199
+ end
200
+
201
+ headers @oauth2.response_headers
202
+ status @oauth2.response_status
203
+
204
+ if body = @oauth2.response_body
205
+ body
206
+ elsif @oauth2.valid?
207
+ erb :login
208
+ else
209
+ erb :error
210
+ end
211
+ end
212
+ end
213
+
214
+ There is a set of parameters that you will need to hold on to for when your app
215
+ needs to redirect back to the client. You could store them in the session, or
216
+ pass them through forms as the user completes the flow. For example to embed
217
+ them in the login form, do this:
218
+
219
+ <% @oauth2.params.each do |key, value| %>
220
+ <input type="hidden" name="<%= key %>" value="<%= value %>">
221
+ <% end %>
222
+
223
+ You may also want to use scopes to provide granular access to your domain using
224
+ <i>scopes</i>. The <tt>@oauth2</tt> object exposes the scopes the client has
225
+ asked for so you can display them to the user:
226
+
227
+ <p>The application <%= @oauth2.client.name %> wants the following permissions:</p>
228
+
229
+ <ul>
230
+ <% @oauth2.scopes.each do |scope| %>
231
+ <li><%= PERMISSION_UI_STRINGS[scope] %></li>
232
+ <% end %>
233
+ </ul>
234
+
235
+ You can also use the method <tt>@oauth2.unauthorized_scopes</tt> to get the list
236
+ of scopes the user has not already granted to the client, in the case where the
237
+ client already has some authorization. If no prior authorization exists between
238
+ the user and the client, <tt>@oauth2.unauthorized_scopes</tt> just returns all
239
+ the scopes the client has asked for.
240
+
241
+
242
+ === Granting access to clients
243
+
244
+ Once the user has authenticated you should show them a page to let them grant
245
+ or deny access to the client application. This is straightforward; let's say the
246
+ user checks a box before posting a form to indicate their intent:
247
+
248
+ post '/oauth/allow' do
249
+ @user = User.find_by_id(session[:user_id])
250
+ @auth = Songkick::OAuth2::Provider::Authorization.new(@user, params)
251
+
252
+ if params['allow'] == '1'
253
+ @auth.grant_access!
254
+ else
255
+ @auth.deny_access!
256
+ end
257
+ redirect @auth.redirect_uri, @auth.response_status
258
+ end
259
+
260
+ After granting or denying access, we just redirect back to the client using a
261
+ URI that <tt>Songkick::OAuth2::Provider</tt> will provide for you.
262
+
263
+
264
+ === Using password credentials
265
+
266
+ If you like, OAuth lets you use a user's login credentials to authenticate with
267
+ a provider. In this case the client application must request these credentials
268
+ directly from the user and then post them to the exchange endpoint. On the
269
+ provider side you can handle this using the <tt>handle_passwords</tt> and
270
+ <tt>grant_access!</tt> API methods, for example:
271
+
272
+ Songkick::OAuth2::Provider.handle_passwords do |client, username, password, scopes|
273
+ user = User.find_by_username(username)
274
+ if user.authenticate?(password)
275
+ user.grant_access!(client, :scopes => scopes, :duration => 1.day)
276
+ else
277
+ nil
278
+ end
279
+ end
280
+
281
+ The block receives the <tt>Client</tt> making the request, the username,
282
+ password and a <tt>Set</tt> of the requested scopes. It must return
283
+ <tt>user.grant_access!(client)</tt> if you want to allow access, otherwise it
284
+ should return <tt>nil</tt>.
285
+
286
+
287
+ === Using assertions
288
+
289
+ Assertions provide a way to access your OAuth services using user credentials
290
+ from another service. When using assertions, the user will not authenticate on
291
+ your web site; the OAuth client will authenticate the user using some other
292
+ framework and obtain a token, then exchange this token for an access token on
293
+ your domain.
294
+
295
+ For example, a client application may let a user authenticate using Facebook,
296
+ so the application obtains a Facebook access token from the user. The client
297
+ would then pass this token to your OAuth endpoint and exchange it for an access
298
+ token from your site. You will typically create an account in your database to
299
+ represent this, then have that new account grant access to the client.
300
+
301
+ To use assertions, you must tell <tt>Songkick::OAuth2::Provider</tt> how to
302
+ handle assertions based on their type. An assertion type must be a valid URI.
303
+ For the Facebook example we'd do the following. The block yields the
304
+ <tt>Client</tt> object making the exchange request, the value of the assertion,
305
+ which in this example will be a Facebook access token, and a <tt>Set</tt> of
306
+ requested scopes.
307
+
308
+ Songkick::OAuth2::Provider.handle_assertions 'https://graph.facebook.com/me' do |client, assertion, scopes|
309
+ facebook = URI.parse('https://graph.facebook.com/me?access_token=' + assertion)
310
+ response = Net::HTTP.get_response(facebook)
311
+
312
+ user_data = JSON.parse(response.body)
313
+ account = User.from_facebook_data(user_data)
314
+
315
+ account.grant_access!(client, :scopes => scopes)
316
+ end
317
+
318
+ This code should run when your app boots, not during a request handler - think
319
+ of it as configuration for <tt>Songkick::OAuth2::Provider</tt>. The framework
320
+ will invoke it when a client attempts to use assertions with your OAuth endpoint.
321
+
322
+ The final call in your handler should be to <tt>grant_access!</tt>; this returns
323
+ an <tt>Authorization</tt> object that the framework then uses to complete the
324
+ response to the client. If you want to deny the request for whatever reason, the
325
+ block must return <tt>nil</tt>. If a client tries to use an assertion type you
326
+ have no handler for, the client will get an error response.
327
+
328
+
329
+ === Protecting resources with access tokens
330
+
331
+ To protect the user's resources you need to check for access tokens. This is
332
+ simple, for example a call to get a user's notes:
333
+
334
+ get '/user/:username/notes' do
335
+ user = User.find_by_username(params[:username])
336
+ token = Songkick::OAuth2::Provider.access_token(user, ['read_notes'], env)
337
+
338
+ headers token.response_headers
339
+ status token.response_status
340
+
341
+ if token.valid?
342
+ JSON.unparse('notes' => user.notes)
343
+ else
344
+ JSON.unparse('error' => 'No notes for you!')
345
+ end
346
+ end
347
+
348
+ <tt>Songkick::OAuth2::Provider.access_token()</tt> takes a
349
+ <tt>ResourceOwner</tt>, a list of scopes required to access the resource, and a
350
+ request environment object. If the token was not granted for the required scopes,
351
+ has expired or is simply invalid, headers and a status code are set to indicate
352
+ this to the client. <tt>token.valid?</tt> is the call you should use to
353
+ determine whether to serve the request or not.
354
+
355
+ It is also common to provide a dynamic resource for getting some basic data
356
+ about a user by supplying their access token. This can be done by passing
357
+ <tt>nil</tt> as the resource owner:
358
+
359
+ get '/me' do
360
+ token = Songkick::OAuth2::Provider.access_token(nil, [], env)
361
+ if token.valid?
362
+ JSON.unparse('username' => token.owner.username)
363
+ else
364
+ JSON.unparse('error' => 'Keep out!')
365
+ end
366
+ end
367
+
368
+ <tt>token.owner</tt> returns the <tt>ResourceOwner</tt> that issued the token.
369
+ A token represents the fact that a single owner gave a single client a set of
370
+ permissions.
371
+
372
+
373
+ == License
374
+
375
+ Copyright (c) 2010-2012 Songkick.com
376
+
377
+ Permission is hereby granted, free of charge, to any person obtaining a copy
378
+ of this software and associated documentation files (the "Software"), to deal
379
+ in the Software without restriction, including without limitation the rights
380
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
381
+ copies of the Software, and to permit persons to whom the Software is
382
+ furnished to do so, subject to the following conditions:
383
+
384
+ The above copyright notice and this permission notice shall be included in
385
+ all copies or substantial portions of the Software.
386
+
387
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
388
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
389
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
390
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
391
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
392
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
393
+ THE SOFTWARE.
394
+
@@ -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,159 @@
1
+ dir = File.expand_path('..', __FILE__)
2
+ require dir + '/environment'
3
+
4
+ require 'sinatra'
5
+ require 'json'
6
+
7
+ set :static, true
8
+ set :root, dir
9
+ enable :sessions
10
+
11
+ PERMISSIONS = {
12
+ 'read_notes' => 'Read all your notes'
13
+ }
14
+
15
+ ERROR_RESPONSE = JSON.unparse('error' => 'No soup for you!')
16
+
17
+ get('/') { erb(:home) }
18
+
19
+
20
+ get '/users/new' do
21
+ @user = User.new
22
+ erb :new_user
23
+ end
24
+
25
+ post '/users/create' do
26
+ @user = User.create(params)
27
+ if @user.save
28
+ erb :create_user
29
+ else
30
+ erb :new_user
31
+ end
32
+ end
33
+
34
+ #================================================================
35
+ # Register applications
36
+
37
+ get '/oauth/apps/new' do
38
+ @client = Songkick::OAuth2::Model::Client.new
39
+ erb :new_client
40
+ end
41
+
42
+ post '/oauth/apps' do
43
+ @client = Songkick::OAuth2::Model::Client.new(params)
44
+ if @client.save
45
+ session[:client_secret] = @client.client_secret
46
+ redirect("/oauth/apps/#{@client.id}")
47
+ else
48
+ erb :new_client
49
+ end
50
+ end
51
+
52
+ get '/oauth/apps/:id' do
53
+ @client = Songkick::OAuth2::Model::Client.find_by_id(params[:id])
54
+ @client_secret = session[:client_secret]
55
+ erb :show_client
56
+ end
57
+
58
+
59
+ #================================================================
60
+ # OAuth 2.0 flow
61
+
62
+ # Initial request exmample:
63
+ # /oauth/authorize?response_type=token&client_id=7uljxxdgsksmecn5cycvug46v&redirect_uri=http%3A%2F%2Fexample.com%2Fcb&scope=read_notes
64
+ [:get, :post].each do |method|
65
+ __send__ method, '/oauth/authorize' do
66
+ @user = User.find_by_id(session[:user_id])
67
+ @oauth2 = Songkick::OAuth2::Provider.parse(@user, env)
68
+
69
+ if @oauth2.redirect?
70
+ redirect @oauth2.redirect_uri, @oauth2.response_status
71
+ end
72
+
73
+ headers @oauth2.response_headers
74
+ status @oauth2.response_status
75
+
76
+ if body = @oauth2.response_body
77
+ body
78
+ elsif @oauth2.valid?
79
+ erb :login
80
+ else
81
+ erb :error
82
+ end
83
+ end
84
+ end
85
+
86
+ post '/login' do
87
+ @user = User.find_by_username(params[:username])
88
+ @oauth2 = Songkick::OAuth2::Provider.parse(@user, env)
89
+ session[:user_id] = @user.id
90
+ erb(@user ? :authorize : :login)
91
+ end
92
+
93
+ post '/oauth/allow' do
94
+ @user = User.find_by_id(session[:user_id])
95
+ @auth = Songkick::OAuth2::Provider::Authorization.new(@user, params)
96
+ if params['allow'] == '1'
97
+ @auth.grant_access!
98
+ else
99
+ @auth.deny_access!
100
+ end
101
+ redirect @auth.redirect_uri, @auth.response_status
102
+ end
103
+
104
+ #================================================================
105
+ # Domain API
106
+
107
+ get '/me' do
108
+ authorization = Songkick::OAuth2::Provider.access_token(nil, [], env)
109
+ headers authorization.response_headers
110
+ status authorization.response_status
111
+
112
+ if authorization.valid?
113
+ user = authorization.owner
114
+ JSON.unparse('username' => user.username)
115
+ else
116
+ ERROR_RESPONSE
117
+ end
118
+ end
119
+
120
+ get '/users/:username/notes' do
121
+ verify_access :read_notes do |user|
122
+ notes = user.notes.map do |n|
123
+ {:note_id => n.id, :url => "#{host}/users/#{user.username}/notes/#{n.id}"}
124
+ end
125
+ JSON.unparse(:notes => notes)
126
+ end
127
+ end
128
+
129
+ get '/users/:username/notes/:note_id' do
130
+ verify_access :read_notes do |user|
131
+ note = user.notes.find_by_id(params[:note_id])
132
+ note ? note.to_json : JSON.unparse(:error => 'No such note')
133
+ end
134
+ end
135
+
136
+
137
+
138
+ helpers do
139
+ #================================================================
140
+ # Check for OAuth access before rendering a resource
141
+ def verify_access(scope)
142
+ user = User.find_by_username(params[:username])
143
+ token = Songkick::OAuth2::Provider.access_token(user, [scope.to_s], env)
144
+
145
+ headers token.response_headers
146
+ status token.response_status
147
+
148
+ return ERROR_RESPONSE unless token.valid?
149
+
150
+ yield user
151
+ end
152
+
153
+ #================================================================
154
+ # Return the full app domain
155
+ def host
156
+ request.scheme + '://' + request.host_with_port
157
+ end
158
+ end
159
+