tpitale-rack-oauth2-server 2.2.1

Sign up to get free protection for your applications and to get access to all the features.
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