songkick-oauth2-provider 0.10.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 (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,3 @@
1
+ require File.expand_path('../application', __FILE__)
2
+ run Sinatra::Application
3
+
@@ -0,0 +1,11 @@
1
+ dir = File.expand_path('..', __FILE__)
2
+ $:.unshift(dir + '/../lib')
3
+ $:.unshift(dir)
4
+
5
+ require 'songkick/oauth2/provider'
6
+ Songkick::OAuth2::Provider.realm = 'Notes App'
7
+
8
+ require 'models/connection'
9
+ require 'models/user'
10
+ require 'models/note'
11
+
@@ -0,0 +1,9 @@
1
+ require 'rubygems'
2
+ require 'active_record'
3
+
4
+ dir = File.expand_path(File.dirname(__FILE__))
5
+
6
+ ActiveRecord::Base.establish_connection(
7
+ :adapter => 'sqlite3',
8
+ :database => dir + '/../db/notes.sqlite3')
9
+
@@ -0,0 +1,4 @@
1
+ class Note < ActiveRecord::Base
2
+ belongs_to :user
3
+ end
4
+
@@ -0,0 +1,6 @@
1
+ class User < ActiveRecord::Base
2
+ include Songkick::OAuth2::Model::ResourceOwner
3
+ include Songkick::OAuth2::Model::ClientOwner
4
+ has_many :notes
5
+ end
6
+
@@ -0,0 +1,78 @@
1
+ body {
2
+ font: 16px/1.4 FreeSans, Helvetica, Arial, sans-serif;
3
+ background: #353e4b;
4
+ }
5
+
6
+ .sub {
7
+ width: 640px;
8
+ margin: 0 auto;
9
+ padding: 1em 2em;
10
+ }
11
+
12
+ .header {
13
+ text-shadow: #23282e 0px -2px 0px;
14
+ }
15
+
16
+ .header h1 {
17
+ color: #e5dec7;
18
+ font-size: 4em;
19
+ letter-spacing: -0.06em;
20
+ margin: 0;
21
+ }
22
+
23
+ .header h2 {
24
+ color: #b3a784;
25
+ font-size: 1.5em;
26
+ font-weight: normal;
27
+ letter-spacing: -0.06em;
28
+ margin: 0 0 0.5em;
29
+ }
30
+
31
+ .content .sub {
32
+ background: #fff;
33
+ color: #333;
34
+ -webkit-border-radius: 16px;
35
+ -moz-border-radius: 16px;
36
+ border-radius: 16px;
37
+ }
38
+
39
+ h3 {
40
+ color: #888;
41
+ font-size: 1.5em;
42
+ font-weight: normal;
43
+ margin: 0 0 1em;
44
+ }
45
+
46
+ fieldset {
47
+ border: none;
48
+ border-top: 1px solid #ccc;
49
+ padding: 12px 0 0 0;
50
+ margin: 12px 0 0 0;
51
+ }
52
+
53
+ table {
54
+ border-collapse: collapse;
55
+ }
56
+
57
+ table th, table td {
58
+ border-top: 1px solid #eee;
59
+ padding: 8px 16px;
60
+ }
61
+
62
+ table th {
63
+ border-right: 2px solid #ccc;
64
+ text-align: left;
65
+ }
66
+
67
+ a {
68
+ color: #9ba749;
69
+ font-weight: bold;
70
+ text-decoration: none;
71
+ }
72
+
73
+ .footer {
74
+ font-size: 0.8em;
75
+ color: #999;
76
+ text-shadow: #23282e 0px -1px 0px;
77
+ }
78
+
@@ -0,0 +1,27 @@
1
+ dir = File.expand_path('..', __FILE__)
2
+ $:.unshift(dir + '/../lib')
3
+
4
+ require 'rubygems'
5
+ require 'songkick/oauth2/provider'
6
+ require 'fileutils'
7
+
8
+ require dir + '/models/connection'
9
+
10
+ FileUtils.mkdir_p(dir + '/db')
11
+
12
+ ActiveRecord::Schema.define do |version|
13
+ create_table :users, :force => true do |t|
14
+ t.timestamps
15
+ t.string :username
16
+ end
17
+
18
+ create_table :notes, :force => true do |t|
19
+ t.timestamps
20
+ t.belongs_to :user
21
+ t.string :title
22
+ t.text :body
23
+ end
24
+ end
25
+
26
+ Songkick::OAuth2::Model::Schema.up
27
+
@@ -0,0 +1,28 @@
1
+ <h3>Authorize OAuth client</h3>
2
+
3
+ <p>This application <b><%= @oauth2.client.name %></b> wants the following
4
+ permissions:</p>
5
+
6
+ <ul>
7
+ <% @oauth2.scopes.each do |scope| %>
8
+ <% next unless PERMISSIONS[scope] %>
9
+ <li><%= PERMISSIONS[scope] %></li>
10
+ <% end %>
11
+ </ul>
12
+
13
+ <form method="post" action="/oauth/allow">
14
+ <% @oauth2.params.each do |key, value| %>
15
+ <input type="hidden" name="<%= key %>" value="<%= value %>">
16
+ <% end %>
17
+ <input type="hidden" name="user_id" value="<%= @user.id %>">
18
+
19
+ <fieldset>
20
+ <input type="checkbox" name="allow" id="allow" value="1">
21
+ <label for="allow">Allow this application</label>
22
+ </fieldset>
23
+
24
+ <fieldset>
25
+ <input type="submit" value="Go!">
26
+ </fieldset>
27
+ </form>
28
+
@@ -0,0 +1,3 @@
1
+ <h3>New User Created</h3>
2
+
3
+ <p>Your username is: <%= @user.username %></p>
@@ -0,0 +1,6 @@
1
+ <h3>Oh noes, an error!</h3>
2
+
3
+ <p>The application made an invalid OAuth request.</p>
4
+
5
+ <pre><%= @oauth2.error_description %></pre>
6
+
@@ -0,0 +1,25 @@
1
+ <p>Welcome to the <b>Songkick OAuth 2.0 demo</b>. The endpoint you should direct
2
+ users to is:</p>
3
+
4
+ <pre> <%= host %>/oauth/authorize</pre>
5
+
6
+ <p>This handles both user authorization and token exchange requests. Before you
7
+ can use this though, you&rsquo;ll need to register your application.</p>
8
+
9
+ <ul>
10
+ <li><a href="/oauth/apps/new">Register your application</a></li>
11
+ </ul>
12
+
13
+ <p>This application is a note-taking app. It exposes a JSON API for reading a
14
+ user&rsquo;s notes, but you need an access token for this. Use the OAuth
15
+ protocol to get one, or see if you can hack in without permission!</p>
16
+
17
+ <p>The following resources are available. You&rsquo;ll need to ask the user for
18
+ the <b><code>read_notes</code></b> permission scope to get at them.</p>
19
+
20
+ <ul>
21
+ <li><code>/me</code> &mdash; returns the current user&rsquo;s data</li>
22
+ <li><code>/users/:username/notes</code></li>
23
+ <li><code>/users/:username/notes/:note_id</code></li>
24
+ </ul>
25
+
@@ -0,0 +1,25 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <meta http-equiv="Content-type" content="text/html; charset=utf-8">
5
+ <title>OAuth 2.0 demo</title>
6
+ <link rel="stylesheet" href="/style.css">
7
+ </head>
8
+ <body>
9
+
10
+ <div class="header"><div class="sub">
11
+ <h1>OAuth 2.0 demo</h1>
12
+ <h2>Steal my notes, why don&rsquo;t you</h2>
13
+ </div></div>
14
+
15
+ <div class="content"><div class="sub">
16
+ <%= yield %>
17
+ </div></div>
18
+
19
+ <div class="footer"><div class="sub">
20
+ <p>Copyright &copy; 2010 Songkick.com</p>
21
+ </div></div>
22
+
23
+ </body>
24
+ </html>
25
+
@@ -0,0 +1,20 @@
1
+ <h3>Sign in</h3>
2
+
3
+ <p>Who are you? We&rsquo;d ask for a password usually, but seeing as it&rsquo;s
4
+ <em>you</em>&hellip;</p>
5
+
6
+ <form method="post" action="/login">
7
+ <% @oauth2.params.each do |key, value| %>
8
+ <input type="hidden" name="<%= key %>" value="<%= value %>">
9
+ <% end %>
10
+
11
+ <fieldset>
12
+ <label for="username">Username</label>
13
+ <input type="text" name="username" id="username">
14
+ </fieldset>
15
+
16
+ <fieldset>
17
+ <input type="submit" value="Sign in">
18
+ </fieldset>
19
+ </form>
20
+
@@ -0,0 +1,25 @@
1
+ <h3>Register you application</h3>
2
+
3
+ <% if @client.errors.any? %>
4
+ <ul class="errors">
5
+ <% @client.errors.full_messages.each do |message| %>
6
+ <li><%= message %></li>
7
+ <% end %>
8
+ </ul>
9
+ <% end %>
10
+
11
+ <form method="post" action="/oauth/apps">
12
+ <fieldset>
13
+ <label for="name">Application name</label>
14
+ <input type="text" name="name" id="name">
15
+ </fieldset>
16
+ <fieldset>
17
+ <label for="redirect_uri">Callback URI</label>
18
+ <input type="text" name="redirect_uri" id="redirect_uri">
19
+ </fieldset>
20
+
21
+ <fieldset>
22
+ <input type="submit" value="Register">
23
+ </fieldset>
24
+ </form>
25
+
@@ -0,0 +1,22 @@
1
+ <h3>Register a User</h3>
2
+
3
+ <% if @user.errors.any? %>
4
+ <ul class="errors">
5
+ <% @user.errors.full_messages.each do |message| %>
6
+ <li><%= message %></li>
7
+ <% end %>
8
+ </ul>
9
+ <% end %>
10
+
11
+ <form method="post" action="/users/create">
12
+ <fieldset>
13
+ <label for="name">Username</label>
14
+ <input type="text" name="username" id="username">
15
+ </fieldset>
16
+
17
+ <fieldset>
18
+ <input type="submit" value="Register">
19
+ </fieldset>
20
+ </form>
21
+
22
+
@@ -0,0 +1,15 @@
1
+ <h3>Client app: <%= @client.name %></h3>
2
+
3
+ <table>
4
+ <tbody>
5
+ <tr>
6
+ <th scope="row">client_id</th>
7
+ <td><%= @client.client_id %></td>
8
+ </tr>
9
+ <tr>
10
+ <th scope="row">client_secret</th>
11
+ <td><%= @client_secret %></td>
12
+ </tr>
13
+ </tbody>
14
+ </table>
15
+
@@ -0,0 +1,20 @@
1
+ require 'active_record'
2
+
3
+ module Songkick
4
+ module OAuth2
5
+ module Model
6
+ autoload :ClientOwner, ROOT + '/oauth2/model/client_owner'
7
+ autoload :ResourceOwner, ROOT + '/oauth2/model/resource_owner'
8
+ autoload :Hashing, ROOT + '/oauth2/model/hashing'
9
+ autoload :Authorization, ROOT + '/oauth2/model/authorization'
10
+ autoload :Client, ROOT + '/oauth2/model/client'
11
+
12
+ Schema = Songkick::OAuth2::Schema
13
+
14
+ def self.find_access_token(access_token)
15
+ Authorization.find_by_access_token_hash(Songkick::OAuth2.hashify(access_token))
16
+ end
17
+ end
18
+ end
19
+ end
20
+
@@ -0,0 +1,126 @@
1
+ module Songkick
2
+ module OAuth2
3
+ module Model
4
+
5
+ class Authorization < ActiveRecord::Base
6
+ self.table_name = :oauth2_authorizations
7
+
8
+ belongs_to :oauth2_resource_owner, :polymorphic => true
9
+ alias :owner :oauth2_resource_owner
10
+ alias :owner= :oauth2_resource_owner=
11
+
12
+ belongs_to :client, :class_name => 'Songkick::OAuth2::Model::Client'
13
+
14
+ validates_presence_of :client, :owner
15
+
16
+ validates_uniqueness_of :code, :scope => :client_id, :allow_nil => true
17
+ validates_uniqueness_of :refresh_token_hash, :scope => :client_id, :allow_nil => true
18
+ validates_uniqueness_of :access_token_hash, :allow_nil => true
19
+
20
+ attr_accessible nil
21
+
22
+ extend Hashing
23
+ hashes_attributes :access_token, :refresh_token
24
+
25
+ def self.for(resource_owner, client)
26
+ return nil unless resource_owner and client
27
+ resource_owner.oauth2_authorizations.find_by_client_id(client.id)
28
+ end
29
+
30
+ def self.create_code(client)
31
+ Songkick::OAuth2.generate_id do |code|
32
+ client.authorizations.count(:conditions => {:code => code}).zero?
33
+ end
34
+ end
35
+
36
+ def self.create_access_token
37
+ Songkick::OAuth2.generate_id do |token|
38
+ hash = Songkick::OAuth2.hashify(token)
39
+ count(:conditions => {:access_token_hash => hash}).zero?
40
+ end
41
+ end
42
+
43
+ def self.create_refresh_token(client)
44
+ Songkick::OAuth2.generate_id do |refresh_token|
45
+ hash = Songkick::OAuth2.hashify(refresh_token)
46
+ client.authorizations.count(:conditions => {:refresh_token_hash => hash}).zero?
47
+ end
48
+ end
49
+
50
+ def self.for_response_type(response_type, attributes = {})
51
+ instance = self.for(attributes[:owner], attributes[:client]) ||
52
+ new do |authorization|
53
+ authorization.owner = attributes[:owner]
54
+ authorization.client = attributes[:client]
55
+ end
56
+
57
+ case response_type
58
+ when CODE
59
+ instance.code ||= create_code(attributes[:client])
60
+ when TOKEN
61
+ instance.access_token ||= create_access_token
62
+ instance.refresh_token ||= create_refresh_token(attributes[:client])
63
+ when CODE_AND_TOKEN
64
+ instance.code = create_code(attributes[:client])
65
+ instance.access_token ||= create_access_token
66
+ instance.refresh_token ||= create_refresh_token(attributes[:client])
67
+ end
68
+
69
+ if attributes[:duration]
70
+ instance.expires_at = Time.now + attributes[:duration].to_i
71
+ else
72
+ instance.expires_at = nil
73
+ end
74
+
75
+ if attributes[:scope]
76
+ scopes = instance.scopes + attributes[:scope].split(/\s+/)
77
+ instance.scope = scopes.entries.join(' ')
78
+ end
79
+
80
+ instance.save && instance
81
+ end
82
+
83
+ def exchange!
84
+ self.code = nil
85
+ self.access_token = self.class.create_access_token
86
+ self.refresh_token = nil
87
+ save!
88
+ end
89
+
90
+ def expired?
91
+ return false unless expires_at
92
+ expires_at < Time.now
93
+ end
94
+
95
+ def expires_in
96
+ expires_at && (expires_at - Time.now).ceil
97
+ end
98
+
99
+ def generate_code
100
+ self.code ||= self.class.create_code(client)
101
+ save && code
102
+ end
103
+
104
+ def generate_access_token
105
+ self.access_token ||= self.class.create_access_token
106
+ save && access_token
107
+ end
108
+
109
+ def grants_access?(user, *scopes)
110
+ not expired? and user == owner and in_scope?(scopes)
111
+ end
112
+
113
+ def in_scope?(request_scope)
114
+ [*request_scope].all?(&scopes.method(:include?))
115
+ end
116
+
117
+ def scopes
118
+ scopes = scope ? scope.split(/\s+/) : []
119
+ Set.new(scopes)
120
+ end
121
+ end
122
+
123
+ end
124
+ end
125
+ end
126
+