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,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
+