rockoauth 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +7 -0
  2. data/History.txt +5 -0
  3. data/README.rdoc +422 -0
  4. data/example/README.rdoc +11 -0
  5. data/example/application.rb +158 -0
  6. data/example/config.ru +3 -0
  7. data/example/environment.rb +11 -0
  8. data/example/models/connection.rb +9 -0
  9. data/example/models/note.rb +4 -0
  10. data/example/models/user.rb +5 -0
  11. data/example/public/style.css +78 -0
  12. data/example/schema.rb +22 -0
  13. data/example/views/authorize.erb +28 -0
  14. data/example/views/create_user.erb +3 -0
  15. data/example/views/error.erb +6 -0
  16. data/example/views/home.erb +24 -0
  17. data/example/views/layout.erb +24 -0
  18. data/example/views/login.erb +20 -0
  19. data/example/views/new_client.erb +25 -0
  20. data/example/views/new_user.erb +22 -0
  21. data/example/views/show_client.erb +15 -0
  22. data/lib/rockoauth/model/authorization.rb +132 -0
  23. data/lib/rockoauth/model/client.rb +54 -0
  24. data/lib/rockoauth/model/client_owner.rb +13 -0
  25. data/lib/rockoauth/model/hashing.rb +26 -0
  26. data/lib/rockoauth/model/helpers.rb +14 -0
  27. data/lib/rockoauth/model/resource_owner.rb +22 -0
  28. data/lib/rockoauth/model.rb +38 -0
  29. data/lib/rockoauth/provider/access_token.rb +70 -0
  30. data/lib/rockoauth/provider/authorization.rb +185 -0
  31. data/lib/rockoauth/provider/error.rb +19 -0
  32. data/lib/rockoauth/provider/exchange.rb +225 -0
  33. data/lib/rockoauth/provider.rb +133 -0
  34. data/lib/rockoauth/router.rb +75 -0
  35. data/lib/rockoauth/schema/20120828112156_rockoauth_schema_original_schema.rb +35 -0
  36. data/lib/rockoauth/schema/20121024180930_rockoauth_schema_add_authorization_index.rb +13 -0
  37. data/lib/rockoauth/schema/20121025180447_rockoauth_schema_add_unique_indexes.rb +31 -0
  38. data/lib/rockoauth/schema.rb +25 -0
  39. data/lib/rockoauth.rb +1 -0
  40. data/spec/factories.rb +20 -0
  41. data/spec/request_helpers.rb +62 -0
  42. data/spec/rockoauth/model/authorization_spec.rb +237 -0
  43. data/spec/rockoauth/model/client_spec.rb +44 -0
  44. data/spec/rockoauth/model/helpers_spec.rb +25 -0
  45. data/spec/rockoauth/model/resource_owner_spec.rb +87 -0
  46. data/spec/rockoauth/provider/access_token_spec.rb +138 -0
  47. data/spec/rockoauth/provider/authorization_spec.rb +356 -0
  48. data/spec/rockoauth/provider/exchange_spec.rb +361 -0
  49. data/spec/rockoauth/provider_spec.rb +560 -0
  50. data/spec/spec_helper.rb +80 -0
  51. data/spec/test_app/helper.rb +36 -0
  52. data/spec/test_app/provider/application.rb +67 -0
  53. data/spec/test_app/provider/views/authorize.erb +19 -0
  54. metadata +238 -0
@@ -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
+
data/example/schema.rb ADDED
@@ -0,0 +1,22 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+
4
+ require 'rockoauth/provider'
5
+ require 'active_record'
6
+ require File.expand_path('../models/connection', __FILE__)
7
+
8
+ ActiveRecord::Schema.define do |version|
9
+ create_table :users, :force => true do |t|
10
+ t.timestamps
11
+ t.string :username
12
+ end
13
+
14
+ create_table :notes, :force => true do |t|
15
+ t.timestamps
16
+ t.belongs_to :user
17
+ t.string :title
18
+ t.text :body
19
+ end
20
+ end
21
+
22
+ RockOAuth::Model::Schema.up
@@ -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,24 @@
1
+ <p>Welcome to the <b>RockOAuth 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>
@@ -0,0 +1,24 @@
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, 2014 Rocketmade.com</p>
21
+ </div></div>
22
+
23
+ </body>
24
+ </html>
@@ -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,132 @@
1
+ module RockOAuth
2
+ module Model
3
+
4
+ class Authorization < ActiveRecord::Base
5
+ self.table_name = :oauth2_authorizations
6
+
7
+ belongs_to :oauth2_resource_owner, :polymorphic => true
8
+ alias :owner :oauth2_resource_owner
9
+ alias :owner= :oauth2_resource_owner=
10
+
11
+ belongs_to :client, :class_name => 'RockOAuth::Model::Client'
12
+
13
+ validates_presence_of :client, :owner
14
+
15
+ validates_uniqueness_of :code, :scope => :client_id, :allow_nil => true
16
+ validates_uniqueness_of :refresh_token_hash, :scope => :client_id, :allow_nil => true
17
+ validates_uniqueness_of :access_token_hash, :allow_nil => true
18
+
19
+ class << self
20
+ private :create, :new
21
+ end
22
+
23
+ extend Hashing
24
+ hashes_attributes :access_token, :refresh_token
25
+
26
+ def self.create_code(client)
27
+ RockOAuth.generate_id do |code|
28
+ Helpers.count(client.authorizations, :code => code).zero?
29
+ end
30
+ end
31
+
32
+ def self.create_access_token
33
+ RockOAuth.generate_id do |token|
34
+ hash = RockOAuth.hashify(token)
35
+ Helpers.count(self, :access_token_hash => hash).zero?
36
+ end
37
+ end
38
+
39
+ def self.create_refresh_token(client)
40
+ RockOAuth.generate_id do |refresh_token|
41
+ hash = RockOAuth.hashify(refresh_token)
42
+ Helpers.count(client.authorizations, :refresh_token_hash => hash).zero?
43
+ end
44
+ end
45
+
46
+ def self.for(owner, client, attributes = {})
47
+ return nil unless owner and client
48
+
49
+ unless client.is_a?(Client)
50
+ raise ArgumentError, "The argument should be a #{Client}, instead it was a #{client.class}"
51
+ end
52
+
53
+ instance = owner.oauth2_authorization_for(client) ||
54
+ new do |authorization|
55
+ authorization.owner = owner
56
+ authorization.client = client
57
+ end
58
+
59
+ case attributes[:response_type]
60
+ when CODE
61
+ instance.code ||= create_code(client)
62
+ when TOKEN
63
+ instance.access_token ||= create_access_token
64
+ instance.refresh_token ||= create_refresh_token(client)
65
+ when CODE_AND_TOKEN
66
+ instance.code = create_code(client)
67
+ instance.access_token ||= create_access_token
68
+ instance.refresh_token ||= create_refresh_token(client)
69
+ end
70
+
71
+ if attributes[:duration]
72
+ instance.expires_at = Time.now + attributes[:duration].to_i
73
+ else
74
+ instance.expires_at = nil
75
+ end
76
+
77
+ scopes = instance.scopes + (attributes[:scopes] || [])
78
+ scopes += attributes[:scope].split(/\s+/) if attributes[:scope]
79
+ instance.scope = scopes.empty? ? nil : scopes.entries.join(' ')
80
+
81
+ instance.save && instance
82
+
83
+ rescue Object => error
84
+ if Model.duplicate_record_error?(error)
85
+ retry
86
+ else
87
+ raise error
88
+ end
89
+ end
90
+
91
+ def exchange!
92
+ self.code = nil
93
+ self.access_token = self.class.create_access_token
94
+ self.refresh_token = nil
95
+ save!
96
+ end
97
+
98
+ def expired?
99
+ return false unless expires_at
100
+ expires_at < Time.now
101
+ end
102
+
103
+ def expires_in
104
+ expires_at && (expires_at - Time.now).ceil
105
+ end
106
+
107
+ def generate_code
108
+ self.code ||= self.class.create_code(client)
109
+ save && code
110
+ end
111
+
112
+ def generate_access_token
113
+ self.access_token ||= self.class.create_access_token
114
+ save && access_token
115
+ end
116
+
117
+ def grants_access?(user, *scopes)
118
+ not expired? and user == owner and in_scope?(scopes)
119
+ end
120
+
121
+ def in_scope?(request_scope)
122
+ [*request_scope].all?(&scopes.method(:include?))
123
+ end
124
+
125
+ def scopes
126
+ scopes = scope ? scope.split(/\s+/) : []
127
+ Set.new(scopes)
128
+ end
129
+ end
130
+
131
+ end
132
+ end
@@ -0,0 +1,54 @@
1
+ module RockOAuth
2
+ module Model
3
+
4
+ class Client < ActiveRecord::Base
5
+ self.table_name = :oauth2_clients
6
+
7
+ belongs_to :oauth2_client_owner, :polymorphic => true
8
+ alias :owner :oauth2_client_owner
9
+ alias :owner= :oauth2_client_owner=
10
+
11
+ has_many :authorizations, :class_name => 'RockOAuth::Model::Authorization', :dependent => :destroy
12
+
13
+ validates_uniqueness_of :client_id, :name
14
+ validates_presence_of :name, :redirect_uri
15
+ validate :check_format_of_redirect_uri
16
+
17
+ before_create :generate_credentials
18
+
19
+ def self.create_client_id
20
+ RockOAuth.generate_id do |client_id|
21
+ Helpers.count(self, :client_id => client_id).zero?
22
+ end
23
+ end
24
+
25
+ attr_reader :client_secret
26
+
27
+ def client_secret=(secret)
28
+ @client_secret = secret
29
+ hash = BCrypt::Password.create(secret)
30
+ hash.force_encoding('UTF-8') if hash.respond_to?(:force_encoding)
31
+ self.client_secret_hash = hash
32
+ end
33
+
34
+ def valid_client_secret?(secret)
35
+ BCrypt::Password.new(client_secret_hash) == secret
36
+ end
37
+
38
+ private
39
+
40
+ def check_format_of_redirect_uri
41
+ uri = URI.parse(redirect_uri)
42
+ errors.add(:redirect_uri, 'must be an absolute URI') unless uri.absolute?
43
+ rescue
44
+ errors.add(:redirect_uri, 'must be a URI')
45
+ end
46
+
47
+ def generate_credentials
48
+ self.client_id = self.class.create_client_id
49
+ self.client_secret = RockOAuth.random_string
50
+ end
51
+ end
52
+
53
+ end
54
+ end
@@ -0,0 +1,13 @@
1
+ module RockOAuth
2
+ module Model
3
+
4
+ module ClientOwner
5
+ def self.included(klass)
6
+ klass.has_many :oauth2_clients,
7
+ :class_name => 'RockOAuth::Model::Client',
8
+ :as => :oauth2_client_owner
9
+ end
10
+ end
11
+
12
+ end
13
+ end
@@ -0,0 +1,26 @@
1
+ module RockOAuth
2
+ module Model
3
+
4
+ module Hashing
5
+ def hashes_attributes(*attributes)
6
+ attributes.each do |attribute|
7
+ define_method("#{attribute}=") do |value|
8
+ instance_variable_set("@#{attribute}", value)
9
+ __send__("#{attribute}_hash=", value && RockOAuth.hashify(value))
10
+ end
11
+ attr_reader attribute
12
+ end
13
+
14
+ class_eval <<-RUBY
15
+ def reload(*args)
16
+ super
17
+ #{ attributes.inspect }.each do |attribute|
18
+ instance_variable_set('@' + attribute.to_s, nil)
19
+ end
20
+ end
21
+ RUBY
22
+ end
23
+ end
24
+
25
+ end
26
+ end
@@ -0,0 +1,14 @@
1
+ module RockOAuth
2
+ module Model
3
+
4
+ module Helpers
5
+ def self.count(model, conditions={})
6
+ if model.respond_to?(:where)
7
+ model.where(conditions).count
8
+ else
9
+ model.count(:conditions => conditions)
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,22 @@
1
+ module RockOAuth
2
+ module Model
3
+
4
+ module ResourceOwner
5
+ def self.included(klass)
6
+ klass.has_many :oauth2_authorizations,
7
+ :class_name => Authorization.name,
8
+ :as => :oauth2_resource_owner,
9
+ :dependent => :destroy
10
+ end
11
+
12
+ def grant_access!(client, options = {})
13
+ Authorization.for(self, client, options)
14
+ end
15
+
16
+ def oauth2_authorization_for(client)
17
+ oauth2_authorizations.find_by_client_id(client.id)
18
+ end
19
+ end
20
+
21
+ end
22
+ end
@@ -0,0 +1,38 @@
1
+ require 'active_record'
2
+
3
+ module RockOAuth
4
+ module Model
5
+ autoload :Helpers, ROOT + '/model/helpers'
6
+ autoload :ClientOwner, ROOT + '/model/client_owner'
7
+ autoload :ResourceOwner, ROOT + '/model/resource_owner'
8
+ autoload :Hashing, ROOT + '/model/hashing'
9
+ autoload :Authorization, ROOT + '/model/authorization'
10
+ autoload :Client, ROOT + '/model/client'
11
+
12
+ Schema = RockOAuth::Schema
13
+
14
+ DUPLICATE_RECORD_ERRORS = [
15
+ /^Mysql::Error:\s+Duplicate\s+entry\b/,
16
+ /^PG::Error:\s+ERROR:\s+duplicate\s+key\b/,
17
+ /\bConstraintException\b/
18
+ ]
19
+
20
+ # ActiveRecord::RecordNotUnique was introduced in Rails 3.0 so referring
21
+ # to it while running earlier versions will raise an error. The above
22
+ # error strings should match PostgreSQL, MySQL and SQLite errors on
23
+ # Rails 2. If you're running a different adapter, add a suitable regex to
24
+ # the list:
25
+ #
26
+ # RockOAuth::Model::DUPLICATE_RECORD_ERRORS << /DB2 found a dup/
27
+ #
28
+ def self.duplicate_record_error?(error)
29
+ error.class.name == 'ActiveRecord::RecordNotUnique' or
30
+ DUPLICATE_RECORD_ERRORS.any? { |re| re =~ error.message }
31
+ end
32
+
33
+ def self.find_access_token(access_token)
34
+ return nil if access_token.nil?
35
+ Authorization.find_by_access_token_hash(RockOAuth.hashify(access_token))
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,70 @@
1
+ module RockOAuth
2
+ class Provider
3
+
4
+ class AccessToken
5
+ attr_reader :authorization
6
+
7
+ def initialize(resource_owner = nil, scopes = [], access_token = nil, error = nil)
8
+ @resource_owner = resource_owner
9
+ @scopes = scopes
10
+ @access_token = access_token
11
+ @error = error && INVALID_REQUEST
12
+
13
+ authorize!(access_token, error)
14
+ validate!
15
+ end
16
+
17
+ def client
18
+ valid? ? @authorization.client : nil
19
+ end
20
+
21
+ def owner
22
+ valid? ? @authorization.owner : nil
23
+ end
24
+
25
+ def response_headers
26
+ return {} if valid?
27
+ error_message = "OAuth realm='#{ Provider.realm }'"
28
+ error_message << ", error='#{ @error }'" unless @error == ''
29
+ {'WWW-Authenticate' => error_message}
30
+ end
31
+
32
+ def response_status
33
+ case @error
34
+ when INVALID_REQUEST, INVALID_TOKEN, EXPIRED_TOKEN then 401
35
+ when INSUFFICIENT_SCOPE then 403
36
+ when '' then 401
37
+ else 200
38
+ end
39
+ end
40
+
41
+ def valid?
42
+ @error.nil?
43
+ end
44
+
45
+ private
46
+
47
+ def authorize!(access_token, error)
48
+ return unless @authorization = Model.find_access_token(access_token)
49
+ @authorization.update_attribute(:access_token, nil) if error
50
+ end
51
+
52
+ def validate!
53
+ return @error = '' unless @access_token
54
+ return @error = INVALID_TOKEN unless @authorization
55
+ return @error = EXPIRED_TOKEN if @authorization.expired?
56
+ return @error = INSUFFICIENT_SCOPE unless @authorization.in_scope?(@scopes)
57
+
58
+ case @resource_owner
59
+ when :implicit
60
+ # no error
61
+ when nil
62
+ @error = INVALID_TOKEN
63
+ else
64
+ @error = INSUFFICIENT_SCOPE if @authorization.owner != @resource_owner
65
+ end
66
+ end
67
+ end
68
+
69
+ end
70
+ end