tpitale-rack-oauth2-server 2.2.1

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 (64) hide show
  1. data/CHANGELOG +202 -0
  2. data/Gemfile +16 -0
  3. data/MIT-LICENSE +21 -0
  4. data/README.rdoc +604 -0
  5. data/Rakefile +90 -0
  6. data/VERSION +1 -0
  7. data/bin/oauth2-server +206 -0
  8. data/lib/rack-oauth2-server.rb +4 -0
  9. data/lib/rack/oauth2/admin/css/screen.css +347 -0
  10. data/lib/rack/oauth2/admin/images/loading.gif +0 -0
  11. data/lib/rack/oauth2/admin/images/oauth-2.png +0 -0
  12. data/lib/rack/oauth2/admin/js/application.coffee +220 -0
  13. data/lib/rack/oauth2/admin/js/jquery.js +166 -0
  14. data/lib/rack/oauth2/admin/js/jquery.tmpl.js +414 -0
  15. data/lib/rack/oauth2/admin/js/protovis-r3.2.js +277 -0
  16. data/lib/rack/oauth2/admin/js/sammy.js +5 -0
  17. data/lib/rack/oauth2/admin/js/sammy.json.js +5 -0
  18. data/lib/rack/oauth2/admin/js/sammy.oauth2.js +142 -0
  19. data/lib/rack/oauth2/admin/js/sammy.storage.js +5 -0
  20. data/lib/rack/oauth2/admin/js/sammy.title.js +5 -0
  21. data/lib/rack/oauth2/admin/js/sammy.tmpl.js +5 -0
  22. data/lib/rack/oauth2/admin/js/underscore.js +722 -0
  23. data/lib/rack/oauth2/admin/views/client.tmpl +58 -0
  24. data/lib/rack/oauth2/admin/views/clients.tmpl +52 -0
  25. data/lib/rack/oauth2/admin/views/edit.tmpl +80 -0
  26. data/lib/rack/oauth2/admin/views/index.html +39 -0
  27. data/lib/rack/oauth2/admin/views/no_access.tmpl +4 -0
  28. data/lib/rack/oauth2/models.rb +27 -0
  29. data/lib/rack/oauth2/models/access_grant.rb +54 -0
  30. data/lib/rack/oauth2/models/access_token.rb +129 -0
  31. data/lib/rack/oauth2/models/auth_request.rb +61 -0
  32. data/lib/rack/oauth2/models/client.rb +93 -0
  33. data/lib/rack/oauth2/rails.rb +105 -0
  34. data/lib/rack/oauth2/server.rb +458 -0
  35. data/lib/rack/oauth2/server/admin.rb +250 -0
  36. data/lib/rack/oauth2/server/errors.rb +104 -0
  37. data/lib/rack/oauth2/server/helper.rb +147 -0
  38. data/lib/rack/oauth2/server/practice.rb +79 -0
  39. data/lib/rack/oauth2/server/railtie.rb +24 -0
  40. data/lib/rack/oauth2/server/utils.rb +30 -0
  41. data/lib/rack/oauth2/sinatra.rb +71 -0
  42. data/rack-oauth2-server.gemspec +24 -0
  43. data/rails/init.rb +11 -0
  44. data/test/admin/api_test.rb +228 -0
  45. data/test/admin/ui_test.rb +38 -0
  46. data/test/oauth/access_grant_test.rb +276 -0
  47. data/test/oauth/access_token_test.rb +311 -0
  48. data/test/oauth/authorization_test.rb +298 -0
  49. data/test/oauth/server_methods_test.rb +292 -0
  50. data/test/rails2/app/controllers/api_controller.rb +40 -0
  51. data/test/rails2/app/controllers/application_controller.rb +2 -0
  52. data/test/rails2/app/controllers/oauth_controller.rb +17 -0
  53. data/test/rails2/config/environment.rb +19 -0
  54. data/test/rails2/config/environments/test.rb +0 -0
  55. data/test/rails2/config/routes.rb +13 -0
  56. data/test/rails3/app/controllers/api_controller.rb +40 -0
  57. data/test/rails3/app/controllers/application_controller.rb +2 -0
  58. data/test/rails3/app/controllers/oauth_controller.rb +17 -0
  59. data/test/rails3/config/application.rb +19 -0
  60. data/test/rails3/config/environment.rb +2 -0
  61. data/test/rails3/config/routes.rb +12 -0
  62. data/test/setup.rb +120 -0
  63. data/test/sinatra/my_app.rb +69 -0
  64. metadata +145 -0
@@ -0,0 +1,58 @@
1
+ <div class="client">
2
+ <div class="details">
3
+ <h2 class="name">{{if imageUrl}}<img src="${imageUrl}">{{/if}} ${displayName}</h2>
4
+ <div class="actions">
5
+ <a href="#/client/${id}/edit" rel="edit">Edit</a>
6
+ {{if !revoked}}
7
+ <a href="#/client/${id}/revoke" data-method="post" data-confirm="There is no undo. Are you really really sure?" rel="revoke">Revoke</a>
8
+ {{/if}}
9
+ <a href="#/client/${id}" data-method="delete" data-confirm="There is no undo. Are you really really sure?" rel="delete">Delete</a>
10
+ </div>
11
+ <div class="meta">Site: <a href="${link}">${link}</a></div>
12
+ <div class="meta">
13
+ Created {{html $.shortdate(revoked)}}
14
+ {{if revoked}}Revoked {{html $.shortdate(revoked)}}{{/if}}
15
+ </div>
16
+ {{each notes}}<p class="notes">${this}</p>{{/each}}
17
+ </div>
18
+ <div class="metrics">
19
+ <div id="fig"></div>
20
+ <ul class="badges">
21
+ <li title="Access tokens granted, lifetime total"><big>${$.thousands(tokens.total)}</big><small>Granted</small></li>
22
+ <li title="Access tokens granted, last 7 days"><big>${$.thousands(tokens.week)}</big><small>This Week</small></li>
23
+ <li title="Access tokens revoked, last 7 days"><big>${$.thousands(tokens.revoked)}</big><small>Revoked (Week)</small></li>
24
+ </ul>
25
+ </div>
26
+ <table class="tokens">
27
+ <thead>
28
+ <th>Token</th>
29
+ <th>Identity</th>
30
+ <th>Scope</th>
31
+ <th>Created</th>
32
+ <th>Accessed</th>
33
+ <th>Revoked</th>
34
+ </thead>
35
+ <tbody>
36
+ {{each tokens.list}}
37
+ <tr>
38
+ <td class="token">${token}</td>
39
+ <td class="identity">{{if link}}<a href="${link}">${identity}</a>{{else}}${identity}{{/if}}</td>
40
+ <td class="scope">${scope}</td>
41
+ <td class="created">{{html $.shortdate(created)}}</td>
42
+ <td class="accessed">{{if last_access}}{{html $.shortdate(last_access)}}{{/if}}</td>
43
+ <td class="revoke">
44
+ {{if revoked}}
45
+ {{html $.shortdate(revoked)}}
46
+ {{else}}
47
+ <a href="#/token/${token}/revoke" data-method="post" data-confirm="Are you sure?" rel="revoke">Revoke</a>
48
+ {{/if}}
49
+ </td>
50
+ </tr>
51
+ {{/each}}
52
+ </tbody>
53
+ </table>
54
+ <div class="pagination">
55
+ {{if tokens.previous}}<a href="#/client/${id}/page/${tokens.page - 1}" rel="previous">Previous</a>{{/if}}
56
+ {{if tokens.next}}<a href="#/client/${id}/page/${tokens.page + 1}" rel="next">Next</a>{{/if}}
57
+ </div>
58
+ </div>
@@ -0,0 +1,52 @@
1
+ <div class="client">
2
+ <div class="metrics">
3
+ <div id="fig"></div>
4
+ <ul class="badges">
5
+ <li title="Access tokens granted, lifetime total"><big>${$.thousands(tokens.total)}</big><small>Granted</small></li>
6
+ <li title="Access tokens granted, last 7 days"><big>${$.thousands(tokens.week)}</big><small>This Week</small></li>
7
+ <li title="Access tokens revoked, last 7 days"><big>${$.thousands(tokens.revoked)}</big><small>Revoked (Week)</small></li>
8
+ </ul>
9
+ </div>
10
+ <a href="#/new" style="float:left">Add New Client</a>
11
+ <table class="clients">
12
+ <thead>
13
+ <th>Application</th>
14
+ <th>ID/Secret</th>
15
+ <th>Created</th>
16
+ <th>Revoked</th>
17
+ </thead>
18
+ {{each clients}}
19
+ <tr class="${revoked ? "revoked" : "active"}">
20
+ <td class="name">
21
+ <a href="#/client/${id}">
22
+ {{if imageUrl}}<img src="${imageUrl}">{{/if}}
23
+ ${displayName.trim() == "" ? "untitled" : displayName}
24
+ </a>
25
+ </td>
26
+ <td class="secrets">
27
+ <a href="" rel="toggle">Reveal</a>
28
+ <dl>
29
+ <dt>ID</dt><dd>${id}</dd>
30
+ <dt>Secret</dt><dd>${secret}</dd>
31
+ <dt>Redirect</dt><dd>${redirectUri}</dd>
32
+ </dl>
33
+ </td>
34
+ <td class="created">{{html $.shortdate(created)}}</td>
35
+ <td class="revoke">{{if revoked}}{{html $.shortdate(revoked)}}{{/if}}</td>
36
+ </tr>
37
+ {{/each}}
38
+ </table>
39
+ </div>
40
+ <script type="text/javascript">
41
+ $("td.secrets a[rel=toggle]").click(function(evt) {
42
+ evt.preventDefault();
43
+ var dl = $(this).next("dl");
44
+ if (dl.is(":visible")) {
45
+ $(this).html("Reveal");
46
+ dl.hide();
47
+ } else {
48
+ $(this).html("Hide");
49
+ dl.show();
50
+ }
51
+ });
52
+ </script>
@@ -0,0 +1,80 @@
1
+ {{if id}}<form action="#/client/${id}" method="put" class="client edit">
2
+ {{else}}<form action="#/clients" method="post" class="client new">{{/if}}
3
+ <div class="fields">
4
+ <h2>Identification</h2>
5
+ <img id="image">
6
+ <label>Display Name
7
+ <input type="text" name="displayName" value="${displayName}" size="70" required autofocus>
8
+ <p class="hint">This is the application name that users see when asked to authorize.</p>
9
+ </label>
10
+ <label>Site URL
11
+ <input type="url" name="link" value="${link}" size="70" required>
12
+ <p class="hint">This is a link to the application's site.</p>
13
+ </label>
14
+ <label>Image URL
15
+ <input type="url" name="imageUrl" value="${imageUrl}" size="70">
16
+ <p class="hint">This is a link to the application's icon (48x48).</p>
17
+ </label>
18
+ <label>Redirect URI
19
+ <input type="url" name="redirectUri" value="${redirectUri}" size="70" required>
20
+ <p class="hint">Users redirected back to this URL on successful authorization.</p>
21
+ </label>
22
+ <label>Notes
23
+ <textarea name="notes" cols="50" rows="5">${notes}</textarea>
24
+ <p class="hint">For internal use.</p>
25
+ </label>
26
+ {{if id}}<button>Save Changes</button>
27
+ {{else}}<button>Create Client</button>{{/if}}
28
+ </div>
29
+ <div class="scope">
30
+ <h2>Scope</h2>
31
+ {{each common}}
32
+ <label class="check"><input type="checkbox" name="scope" value="${this}" ${scope.indexOf(this.toString()) < 0 ? null : "checked"}>${this}</label>
33
+ {{/each}}
34
+ {{each scope}}
35
+ {{if common.indexOf(this.toString()) < 0}}
36
+ <label class="check uncommon"><input type="checkbox" name="scope" value="${this}" checked>${this}</label>
37
+ {{/if}}
38
+ {{/each}}
39
+ <label>Uncommon
40
+ <input type="text" name="scope" value="" size="35">
41
+ <p class="hint">Space separated list of uncommon scope.</p>
42
+ </label>
43
+ </div>
44
+ <hr>
45
+ </form>
46
+ <script type="text/javascript">
47
+ $(function() {
48
+ var image = $("#image");
49
+ image.load(function() {
50
+ image.show().removeClass("loading");
51
+ }).error(function() {
52
+ if (image.attr("src"))
53
+ image.removeClass("loading");
54
+ });
55
+
56
+ var imageUrl = $("input[name=imageUrl]");
57
+ imageUrl.change(function() {
58
+ var url = $(this).val().trim();
59
+ if (url == "") {
60
+ image.hide();
61
+ } else {
62
+ image.attr("src", "admin/images/loading.gif").show().addClass("loading");
63
+ setTimeout(function() { image.attr("src", url); }, 10);
64
+ }
65
+ }).trigger("change");
66
+
67
+ $("input[name=link]").change(function() {
68
+ if (imageUrl.val().trim() == "") {
69
+ $("#image").show().addClass("loading").attr("src", null);
70
+ var image = new Image();
71
+ image.src = $(this).val().trim().replace(/^(https?:\/\/)(.+?)(\/.*|$)/, "$1$2/favicon.ico");
72
+ image.onload = function() {
73
+ if (imageUrl.val().trim() == "") {
74
+ imageUrl.val(image.src).trigger("change");
75
+ }
76
+ }
77
+ }
78
+ });
79
+ })
80
+ </script>
@@ -0,0 +1,39 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>OAuth Console</title>
5
+ <link href="admin/css/screen.css" media="screen, projection" rel="stylesheet" type="text/css">
6
+ <script src="admin/js/jquery.js" type="text/javascript"></script>
7
+ <script src="admin/js/jquery.tmpl.js" type="text/javascript"></script>
8
+ <script src="admin/js/sammy.js" type="text/javascript"></script>
9
+ <script src="admin/js/sammy.tmpl.js" type="text/javascript"></script>
10
+ <script src="admin/js/sammy.json.js" type="text/javascript"></script>
11
+ <script src="admin/js/sammy.storage.js" type="text/javascript"></script>
12
+ <script src="admin/js/sammy.title.js" type="text/javascript"></script>
13
+ <script src="admin/js/sammy.oauth2.js" type="text/javascript"></script>
14
+ <script src="admin/js/underscore.js" type="text/javascript"></script>
15
+ <script src="admin/js/protovis-r3.2.js" type="text/javascript"></script>
16
+ <script src="admin/js/application.js" type="text/javascript"></script>
17
+ <link rel="shortcut icon" href="admin/images/oauth-2.png">
18
+ </head>
19
+ <body>
20
+ <div id="notice" style="display:none"></div>
21
+ <div id="header">
22
+ <div class="title">
23
+ <a href="#/"><img src="admin/images/oauth-2.png"> OAuth Console</a>
24
+ </div>
25
+ <a href="admin/authorize" class="signin">Sign in</a>
26
+ <a href="#/signout" class="signout">Sign out</a>
27
+ </div>
28
+ <div id="throbber" class="loading"></div>
29
+ <div id="main"></div>
30
+ <div id="footer">
31
+ <p>Powered by <a href="http://github.com/flowtown/rack-oauth2-server">Rack::OAuth2::Server</a></p>
32
+ </div>
33
+ <script>
34
+ var loading = new Image();
35
+ loading.src = "admin/images/loading.gif";
36
+ $(function() { Sammy("#main").run("#/"); });
37
+ </script>
38
+ </body>
39
+ </html>
@@ -0,0 +1,4 @@
1
+ <div class="no-access">
2
+ <h1>${error}</h1>
3
+ <p>You can try to <a href="#/">authenticate again</a></p>
4
+ </div>
@@ -0,0 +1,27 @@
1
+ # require "mongo"
2
+ require "openssl"
3
+ require "rack/oauth2/server/errors"
4
+ require "rack/oauth2/server/utils"
5
+
6
+ module Rack
7
+ module OAuth2
8
+ class Server
9
+ # class << self
10
+ # # unused!
11
+ # attr_accessor :database
12
+ # end
13
+
14
+ # Long, random and hexy.
15
+ def self.secure_random
16
+ OpenSSL::Random.random_bytes(32).unpack("H*")[0]
17
+ end
18
+ end
19
+ end
20
+ end
21
+
22
+
23
+ require "rack/oauth2/models/client"
24
+ require "rack/oauth2/models/auth_request"
25
+ require "rack/oauth2/models/access_grant"
26
+ require "rack/oauth2/models/access_token"
27
+
@@ -0,0 +1,54 @@
1
+ module Rack
2
+ module OAuth2
3
+ class Server
4
+
5
+ # The access grant is a nonce, new grant created each time we need it and
6
+ # good for redeeming one access token.
7
+ class AccessGrant < ActiveRecord::Base
8
+ belongs_to :client, :class_name => 'Rack::OAuth2::Server::Client'
9
+
10
+ # Find AccessGrant from authentication code.
11
+ def self.from_code(code)
12
+ first(:conditions => {:code => code, :revoked => nil})
13
+ end
14
+
15
+ # Create a new access grant.
16
+ def self.create(identity, client, scope, redirect_uri = nil, expires = nil)
17
+ raise ArgumentError, "Identity must be String or Integer" unless String === identity || Integer === identity
18
+ scope = Utils.normalize_scope(scope) & Utils.normalize_scope(client.scope) # Only allowed scope
19
+ expires_at = Time.now.to_i + (expires || 300)
20
+
21
+ attributes = {
22
+ :code => Server.secure_random,
23
+ :identity=>identity,
24
+ :scope=>scope,
25
+ :client_id=>client.id,
26
+ :redirect_uri=>client.redirect_uri || redirect_uri,
27
+ :created_at=>Time.now.to_i,
28
+ :expires_at=>expires_at
29
+ }
30
+
31
+ super(attributes)
32
+ end
33
+
34
+ # Authorize access and return new access token.
35
+ #
36
+ # Access grant can only be redeemed once, but client can make multiple
37
+ # requests to obtain it, so we need to make sure only first request is
38
+ # successful in returning access token, futher requests raise
39
+ # InvalidGrantError.
40
+ def authorize!
41
+ raise InvalidGrantError, "You can't use the same access grant twice" if self.access_token || self.revoked
42
+ access_token = AccessToken.get_token_for(identity, client, scope)
43
+ update_attributes(:access_token => access_token.token, :granted_at => Time.now)
44
+ access_token
45
+ end
46
+
47
+ def revoke!
48
+ update_attributes(:revoked => Time.now)
49
+ end
50
+ end
51
+
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,129 @@
1
+ module Rack
2
+ module OAuth2
3
+ class Server
4
+
5
+ # Access token. This is what clients use to access resources.
6
+ #
7
+ # An access token is a unique code, associated with a client, an identity
8
+ # and scope. It may be revoked, or expire after a certain period.
9
+ class AccessToken < ActiveRecord::Base
10
+ belongs_to :client, :class_name => 'Rack::OAuth2::Server::Client' # counter_cache?
11
+
12
+ # Creates a new AccessToken for the given client and scope.
13
+ def self.create_token_for(client, scope)
14
+ scope = Utils.normalize_scope(scope) & Utils.normalize_scope(client.scope) # Only allowed scope
15
+
16
+ attributes = {
17
+ :code => Server.secure_random,
18
+ :scope => scope,
19
+ :client => client
20
+ }
21
+
22
+ create(attributes)
23
+
24
+ # Client.collection.update({ :_id=>client.id }, { :$inc=>{ :tokens_granted=>1 } })
25
+ # Server.new_instance self, token
26
+ end
27
+
28
+ # Find AccessToken from token. Does not return revoked tokens.
29
+ def self.from_token(token) # token == code??
30
+ first(:conditions => {:code => token, :revoked => nil})
31
+ end
32
+
33
+ # Get an access token (create new one if necessary).
34
+ def self.get_token_for(identity, client, scope)
35
+ raise ArgumentError, "Identity must be String or Integer" unless String === identity || Integer === identity
36
+ scope = Utils.normalize_scope(scope) & Utils.normalize_scope(client.scope) # Only allowed scope
37
+
38
+ token = first(:conditions => {:identity=>identity, :scope=>scope, :client_id=>client.id, :revoked=>nil})
39
+
40
+ token ||= begin
41
+ attributes = {
42
+ :code => Server.secure_random,
43
+ :identity => identity,
44
+ :scope => scope,
45
+ :client_id => client.id
46
+ }
47
+
48
+ create(attributes)
49
+ # Client.collection.update({ :_id=>client.id }, { :$inc=>{ :tokens_granted=>1 } })
50
+ end
51
+
52
+ token
53
+ end
54
+
55
+ alias_attribute :token, :code
56
+
57
+ # Find all AccessTokens for an identity.
58
+ def self.from_identity(identity)
59
+ all(:condition => {:identity => identity})
60
+ end
61
+
62
+ # Returns all access tokens for a given client, Use limit and offset
63
+ # to return a subset of tokens, sorted by creation date.
64
+ def self.for_client(client_id, offset = 0, limit = 100)
65
+ all(:conditions => {:client_id => client.id}, :offset => offset, :limit => limit, :order => :created_at)
66
+ end
67
+
68
+ # Returns count of access tokens.
69
+ #
70
+ # @param [Hash] filter Count only a subset of access tokens
71
+ # @option filter [Integer] days Only count that many days (since now)
72
+ # @option filter [Boolean] revoked Only count revoked (true) or non-revoked (false) tokens; count all tokens if nil
73
+ # @option filter [String, ObjectId] client_id Only tokens grant to this client
74
+ def self.count(filter = {})
75
+ conditions = []
76
+ if filter[:days]
77
+ now = Time.now
78
+ start_time = now - (filter[:days] * 86400)
79
+
80
+ key = filter[:revoked] ? 'revoked' : 'created_at'
81
+ conditions = ["#{key} > ? AND #{key} <= ?", start_time, now]
82
+ elsif filter.has_key?(:revoked)
83
+ conditions = ["revoked " + (filter[:revoked] ? "IS NOT NULL" : "IS NULL")]
84
+ end
85
+
86
+ if filter.has_key?(:client_id)
87
+ conditions.first = conditions.empty? ? "client_id = ?" : " AND client_id = ?"
88
+ conditions << filter[:client_id]
89
+ end
90
+
91
+ super(:conditions => conditions)
92
+ end
93
+
94
+ # def self.historical(filter = {})
95
+ # # days = filter[:days] || 60
96
+ # # select = { :$gt=> { :created_at=>Time.now - 86400 * days } }
97
+ # # select = {}
98
+ #
99
+ # if filter.has_key?(:client_id)
100
+ # conditions << "client_id = ?" << filter[:client_id]
101
+ # end
102
+ #
103
+ # raw = Server::AccessToken.collection.group("function (token) { return { ts: Math.floor(token.created_at / 86400) } }",
104
+ # select, { :granted=>0 }, "function (token, state) { state.granted++ }")
105
+ # raw.sort { |a, b| a["ts"] - b["ts"] }
106
+ # end
107
+
108
+ # Updates the last access timestamp.
109
+ def access!
110
+ today = (Time.now.to_i / 3600) * 3600
111
+ if last_access.nil? || last_access < today
112
+ AccessToken.update_all({:last_access=>today, :prev_access=>last_access}, {:code => code})
113
+ reload
114
+ end
115
+ end
116
+
117
+ # Revokes this access token.
118
+ def revoke!
119
+ revoked = Time.now
120
+ AccessToken.update_all({:revoked=>revoked}, {:id => id})
121
+ reload
122
+
123
+ # Client.collection.update({ :_id=>client_id }, { :$inc=>{ :tokens_revoked=>1 } })
124
+ end
125
+ end
126
+
127
+ end
128
+ end
129
+ end