rack-oauth2-server 1.2.2 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. data/CHANGELOG +14 -0
  2. data/Gemfile +3 -0
  3. data/README.rdoc +26 -7
  4. data/Rakefile +1 -1
  5. data/VERSION +1 -0
  6. data/lib/rack/oauth2/admin/css/screen.css +233 -0
  7. data/lib/rack/oauth2/admin/images/loading.gif +0 -0
  8. data/lib/rack/oauth2/admin/js/application.js +154 -0
  9. data/lib/rack/oauth2/admin/js/jquery.js +166 -0
  10. data/lib/rack/oauth2/admin/js/jquery.tmpl.js +414 -0
  11. data/lib/rack/oauth2/admin/js/sammy.js +5 -0
  12. data/lib/rack/oauth2/admin/js/sammy.json.js +5 -0
  13. data/lib/rack/oauth2/admin/js/sammy.storage.js +5 -0
  14. data/lib/rack/oauth2/admin/js/sammy.title.js +5 -0
  15. data/lib/rack/oauth2/admin/js/sammy.tmpl.js +5 -0
  16. data/lib/rack/oauth2/admin/js/underscore.js +722 -0
  17. data/lib/rack/oauth2/admin/views/client.tmpl +48 -0
  18. data/lib/rack/oauth2/admin/views/clients.tmpl +36 -0
  19. data/lib/rack/oauth2/admin/views/edit.tmpl +57 -0
  20. data/lib/rack/oauth2/admin/views/index.html +26 -0
  21. data/lib/rack/oauth2/models/access_grant.rb +6 -4
  22. data/lib/rack/oauth2/models/access_token.rb +36 -4
  23. data/lib/rack/oauth2/models/auth_request.rb +4 -3
  24. data/lib/rack/oauth2/models/client.rb +15 -2
  25. data/lib/rack/oauth2/server.rb +71 -58
  26. data/lib/rack/oauth2/server/admin.rb +216 -0
  27. data/lib/rack/oauth2/server/helper.rb +4 -4
  28. data/lib/rack/oauth2/sinatra.rb +2 -2
  29. data/rack-oauth2-server.gemspec +2 -3
  30. data/test/admin/api_test.rb +196 -0
  31. data/test/admin_test_.rb +49 -0
  32. data/test/{access_grant_test.rb → oauth/access_grant_test.rb} +1 -1
  33. data/test/{access_token_test.rb → oauth/access_token_test.rb} +83 -12
  34. data/test/{authorization_test.rb → oauth/authorization_test.rb} +1 -1
  35. data/test/rails/config/environment.rb +2 -0
  36. data/test/rails/log/test.log +72938 -0
  37. data/test/setup.rb +17 -1
  38. data/test/sinatra/my_app.rb +1 -1
  39. metadata +27 -9
  40. data/lib/rack/oauth2/server/version.rb +0 -9
data/CHANGELOG CHANGED
@@ -1,3 +1,17 @@
1
+ 2010-11-07 version 1.3.0
2
+
3
+ Added OAuth authorization console.
4
+
5
+ Added param_authentication option: turn this on if you need to support
6
+ oauth_token query parameter or form field. Disabled by default.
7
+
8
+ Added host option: only check requests sent to that host (e.g. only check
9
+ requests to api.example.com).
10
+
11
+ Added path option: only check requests under this path (e.g. only check
12
+ requests for /api/...).
13
+
14
+
1
15
  2010-11-03 version 1.2.2
2
16
 
3
17
  Store ObjectId references in database.
data/Gemfile CHANGED
@@ -2,6 +2,8 @@ source :rubygems
2
2
  gemspec
3
3
 
4
4
  group :development do
5
+ gem "sinatra"
6
+ gem "thin"
5
7
  gem "yard"
6
8
  end
7
9
 
@@ -13,5 +15,6 @@ group :test do
13
15
  gem "rails", "~>2.3"
14
16
  gem "shoulda"
15
17
  gem "sinatra"
18
+ gem "therubyracer", "~>0.8.0.pre"
16
19
  gem "timecop"
17
20
  end
data/README.rdoc CHANGED
@@ -72,6 +72,10 @@ The configuration options are:
72
72
  - +:authorize_path+ -- Path for requesting end-user authorization. By
73
73
  convention defaults to +/oauth/authorize+.
74
74
  - +:database+ -- +Mongo::DB+ instance.
75
+ - +:host+ -- Only check requests sent to this host.
76
+ - +:path+ -- Only check requests for resources under this path.
77
+ - +:param_authentication+ -- If true, supports authentication using query/form
78
+ parameters.
75
79
  - +:realm+ -- Authorization realm that will show up in 401 responses. Defaults
76
80
  to use the request host name.
77
81
  - +:scopes+ -- Array listing all supported scopes, e.g. ["read", "write"].
@@ -113,6 +117,11 @@ In Rails, the entire flow would look something like this:
113
117
 
114
118
  class OauthController < ApplicationController
115
119
  def authorize
120
+ if current_user
121
+ render :action=>"authorize"
122
+ else
123
+ redirect_to :action=>"login", :authorization=>oauth.authorization
124
+ end
116
125
  end
117
126
 
118
127
  def grant
@@ -132,7 +141,11 @@ method.
132
141
  In Sinatra/Padrino, it would look something like this:
133
142
 
134
143
  get "/oauth/authorize" do
135
- render "oauth/authorize"
144
+ if current_user
145
+ render "oauth/authorize"
146
+ else
147
+ redirect "/oauth/login?authorization=#{oauth.authorization}"
148
+ end
136
149
  end
137
150
 
138
151
  post "/oauth/grant" do
@@ -159,13 +172,19 @@ The view would look something like this:
159
172
 
160
173
  == Step 4: Protect Your Path
161
174
 
162
- Rack::OAuth2::Server intercepts all incoming requests and looks for either
163
- OAuth authentication header, or +oauth_token+ parameter. If it finds either
164
- one, and the access token is still valid, it sets the request header
165
- +oauth.identity+ to the value you supplied during authorization (step 3).
175
+ Rack::OAuth2::Server intercepts all incoming requests and looks for an
176
+ Authorization header that uses OAuth authentication scheme, like so:
177
+
178
+ Authorization: OAuth e57807eb99f8c29f60a27a75a80fec6e
179
+
180
+ It can also support the +oauth_token+ query parameter or form field, if you set
181
+ +param_authentication+ to true. This option is off by default to prevent
182
+ conflict with OAuth 1.0 callback.
166
183
 
167
- You can use +oauth.identity+ to resolve the access token back to user, account
168
- or whatever you put there.
184
+ If Rack::OAuth2::Server finder a valid access token in the request, it sets the
185
+ request header +oauth.identity+ to the value you supplied during authorization
186
+ (step 3). You can use +oauth.identity+ to resolve the access token back to
187
+ user, account or whatever you put there.
169
188
 
170
189
  If the access token is invalid or revoked, it returns 401 (Unauthorized) to the
171
190
  client. However, if there's no access token, the request goes through. You
data/Rakefile CHANGED
@@ -26,7 +26,7 @@ end
26
26
 
27
27
  desc "Run all tests"
28
28
  Rake::TestTask.new do |task|
29
- task.test_files = FileList['test/*_test.rb']
29
+ task.test_files = FileList['test/**/*_test.rb']
30
30
  if Rake.application.options.trace
31
31
  #task.warning = true
32
32
  task.verbose = true
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.3.0
@@ -0,0 +1,233 @@
1
+ body {
2
+ margin: 0;
3
+ width: 100%;
4
+ font: 12pt "Helvetica", "Lucida Sans", "Verdana";
5
+ }
6
+
7
+ a { text-decoration: none; color: #00c; }
8
+ a:hover, a:focus { text-decoration: underline; color: #00c; }
9
+ h1, h2 {
10
+ text-shadow: rgba(255,255,255,.2) 0 1px 1px;
11
+ color: rgb(76, 86, 108);
12
+ }
13
+ h1 { font-size: 18pt; margin: 0.6em 0 }
14
+ h2 { font-size: 16pt; margin: 0.3em 0 }
15
+
16
+ label {
17
+ display: block;
18
+ color: #000;
19
+ font-weight: 600;
20
+ font-size: 0.9em;
21
+ margin: 0.9em 0;
22
+ }
23
+ label input, label textarea, label select { display: block }
24
+ label input, label textarea {
25
+ font-size: 12pt;
26
+ line-height: 1.3em;
27
+ }
28
+ label .hint {
29
+ font-weight: normal;
30
+ color: #666;
31
+ margin: 0;
32
+ }
33
+ button {
34
+ font-size: 11pt;
35
+ text-shadow: 0 -1px 1px rgba(0,0,0,0.25);
36
+ border: 1px solid #dddddd;
37
+ background: #f6f6f6 50% 50% repeat-x;
38
+ font-weight: bold;
39
+ color: #0073ea;
40
+ outline: none;
41
+ line-height: 1.3em;
42
+ vertical-align: bottom;
43
+ padding: 2px 8px;
44
+ background: -webkit-gradient(linear, 0% 0%, 0% 100%, from(rgba(226,226,226,0.0)), to(rgba(226,226,226,1.0)));
45
+ -webkit-border-radius: 4px; -moz-border-radius: 4px;
46
+ -moz-box-shadow: 0 0 4px rgba(0,0,0,0.0);
47
+ -webkit-box-shadow: 0 0 4px rgba(0,0,0,0.0);
48
+ }
49
+ button:hover, button:focus {
50
+ text-shadow: 0 -1px 1px rgba(255,255,255,0.25);
51
+ border: 1px solid #0073ea;
52
+ background: #0073ea 50% 50% repeat-x;
53
+ background: -webkit-gradient(linear, 0% 0%, 0% 100%, from(rgba(0, 115, 234, 0.5)), to(rgba(0,115,234, 1.0)));
54
+ color: #fff;
55
+ text-decoration: none;
56
+ cursor: pointer;
57
+ -moz-box-shadow: 0 2px 4px rgba(0,0,0,0.5);
58
+ -webkit-box-shadow: 0 1px 3px rgba(0,0,0,0.5);
59
+ }
60
+ button:active { background: -webkit-gradient(linear, 0% 0%, 0% 100%, from(rgba(0, 115, 234, 1.0)), to(rgba(0,115,234, 0.5))); position: relative; top: 1px }
61
+
62
+
63
+ /* Message dropping down from the top */
64
+ #notice {
65
+ position: absolute;
66
+ top: 0;
67
+ left: 0;
68
+ right: 0;
69
+ line-height: 1.6em;
70
+ background: #ffd;
71
+ color: #000;
72
+ border-bottom: 1px solid #ddd;
73
+ text-align: center;
74
+ z-index: 99;
75
+ }
76
+
77
+ #header {
78
+ margin: 0 2em;
79
+ }
80
+ #header .title {
81
+ font-size: 1.4em;
82
+ font-weight: bold;
83
+ color: #000;
84
+ text-decoration: none;
85
+ margin: 0.6em 0;
86
+ display: block;
87
+ text-align: right;
88
+ }
89
+
90
+ #main {
91
+ margin: 0 2em;
92
+ }
93
+
94
+ table {
95
+ width: 100%;
96
+ table-layout: auto;
97
+ empty-cells: show;
98
+ border-collapse: separate;
99
+ border-spacing: 0px;
100
+ }
101
+ table th {
102
+ text-align: left;
103
+ border-bottom: 1px solid #ccc;
104
+ margin-right: 48px;
105
+ }
106
+ table td {
107
+ text-align: left;
108
+ vertical-align: top;
109
+ border-bottom: 1px solid #ddf;
110
+ line-height: 32px;
111
+ margin: 0;
112
+ padding: 0;
113
+ }
114
+ table tr:hover td {
115
+ background: #ddf;
116
+ }
117
+ table td.created, table td.revoke {
118
+ width: 6em;
119
+ }
120
+ table tr.revoked td, table tr.revoked a {
121
+ color: #888;
122
+ }
123
+ table button {
124
+ margin-top: -2px;
125
+ font-size: 10pt;
126
+ }
127
+
128
+ table.clients td.name {
129
+ padding-left: 32px;
130
+ }
131
+ table.clients td.name img {
132
+ width: 24px;
133
+ height: 24px;
134
+ border: none;
135
+ margin: 4px 4px -4px -32px;
136
+ }
137
+ table.clients td.secrets {
138
+ width: 28em;
139
+ }
140
+ table.clients td.secrets dl {
141
+ display: none;
142
+ width: 40em;
143
+ margin: 0 -12em 0.6em 0;
144
+ line-height: 1.3em;
145
+ }
146
+ table.clients td.secrets dt {
147
+ width: 4em;
148
+ float: left;
149
+ color: #888;
150
+ margin-bottom: 0.3em;
151
+ }
152
+ table.clients td.secrets dd:after {
153
+ content: ".";
154
+ display: block;
155
+ clear: both;
156
+ visibility: hidden;
157
+ line-height: 0;
158
+ height: 0;
159
+ }
160
+
161
+ table.tokens td.token {
162
+ width: 32em;
163
+ }
164
+ table.tokens td.scope {
165
+ }
166
+
167
+ .pagination {
168
+ width: 100%;
169
+ margin-top: 2em;
170
+ }
171
+ .pagination a[rel=next] {
172
+ float: right;
173
+ }
174
+ .pagination a[rel=previous] {
175
+ float: left;
176
+ }
177
+
178
+ .badges {
179
+ list-style: none;
180
+ margin: 1.1em 0;
181
+ padding: 0;
182
+ text-align: right;
183
+ width: 100%;
184
+ }
185
+ .badges li {
186
+ display: inline-block;
187
+ margin-left: 8px;
188
+ min-width: 8em;
189
+ }
190
+ .badges big {
191
+ font-size: 22pt;
192
+ display: block;
193
+ text-align: center;
194
+ }
195
+ .badges small {
196
+ font-size: 11pt;
197
+ display: block;
198
+ text-align: center;
199
+ }
200
+
201
+ .client .details {
202
+ float: left;
203
+ }
204
+ .client .details .name {
205
+ font-size: 15pt;
206
+ font-weight: bold;
207
+ }
208
+ .client .details img {
209
+ border: none;
210
+ width: 24px;
211
+ height: 24px;
212
+ vertical-align: bottom;
213
+ }
214
+ .client .details a[rel=edit] {
215
+ margin: 0 0.3em 0 1em;
216
+ }
217
+ .client .details .meta {
218
+ color: #888;
219
+ font-size: 10pt;
220
+ }
221
+ .client.new>#image, .client.edit>#image {
222
+ float: left;
223
+ margin: 0 12px 0 0;
224
+ width: 48px;
225
+ height: 48px;
226
+ }
227
+ .client.new>*, .client.edit>* {
228
+ margin-left: 60px;
229
+ }
230
+
231
+ .loading {
232
+ background: url("../images/loading.gif") no-repeat 50% 50%;
233
+ }
@@ -0,0 +1,154 @@
1
+ Sammy("#main", function(app) {
2
+ this.use(Sammy.Tmpl);
3
+ this.use(Sammy.Session);
4
+ this.use(Sammy.Title);
5
+ this.setTitle("OAuth Console - ");
6
+
7
+ // Use OAuth access token in all API requests.
8
+ $(document).ajaxSend(function(e, xhr) {
9
+ xhr.setRequestHeader("Authorization", "OAuth " + app.session("oauth.token"));
10
+ });
11
+ // For all request (except callback), if we don't have an OAuth access token,
12
+ // ask for one by requesting authorization.
13
+ this.before({ except: { path: /^#(access_token=|[^\\].*&access_token=)/ } }, function(context) {
14
+ if (!app.session("oauth.token"))
15
+ context.redirect(document.location.pathname + "/authorize?state=" + escape(context.path));
16
+ })
17
+ // We recognize the OAuth authorization callback based on one of its
18
+ // parameters. Crude but works here.
19
+ this.get(/^#(access_token=|[^\\].*&access_token=)/, function(context) {
20
+ // Instead of a hash we get query parameters, so turn those into an object.
21
+ var params = context.path.substring(1).split("&"), args = {};
22
+ for (var i in params) {
23
+ var splat = params[i].split("=");
24
+ args[splat[0]] = splat[1];
25
+ }
26
+ app.session("oauth.token", args.access_token);
27
+ // When the filter redirected the original request, it passed the original
28
+ // request's URL in the state parameter, which we get back after
29
+ // authorization.
30
+ context.redirect(args.state.length == 0 ? "#/" : unescape(args.state));
31
+ });
32
+
33
+
34
+ var api = document.location.pathname + "/api";
35
+ // View all clients
36
+ this.get("#/", function(context) {
37
+ context.title("All Clients");
38
+ $.getJSON(api + "/clients", function(json) {
39
+ context.partial("admin/views/clients.tmpl", { clients: json.list, tokens: json.tokens });
40
+ });
41
+ });
42
+ // Edit client
43
+ this.get("#/client/:id/edit", function(context) {
44
+ $.getJSON(api + "/client/" + context.params.id, function(client) {
45
+ context.title(client.displayName);
46
+ context.partial("admin/views/edit.tmpl", client)
47
+ })
48
+ });
49
+ this.put("#/client/:id", function(context) {
50
+ $.ajax({ type: "put", url: api + "/client/" + context.params.id,
51
+ data: {
52
+ displayName: context.params.displayName,
53
+ link: context.params.link,
54
+ redirectUri: context.params.redirectUri,
55
+ imageUrl: context.params.imageUrl
56
+ },
57
+ success: function(client) {
58
+ context.redirect("#/client/" + context.params.id);
59
+ app.trigger("notice", "Saved your changes");
60
+ },
61
+ error: function(xhr) {
62
+ context.partial("admin/views/edit.tmpl", context.params);
63
+ app.trigger("notice", xhr.responseText);
64
+ }
65
+ })
66
+ });
67
+ // View single client
68
+ this.get("#/client/:id", function(context) {
69
+ $.getJSON(api + "/client/" + context.params.id, function(client) {
70
+ context.title(client.displayName);
71
+ context.partial("admin/views/client.tmpl", client)
72
+ });
73
+ });
74
+ this.get("#/client/:id/:page", function(context) {
75
+ $.getJSON(api + "/client/" + context.params.id + "?page=" + context.params.page, function(client) {
76
+ context.title(client.displayName);
77
+ context.partial("admin/views/client.tmpl", client)
78
+ });
79
+ });
80
+ // Create new client
81
+ this.get("#/new", function(context) {
82
+ context.title("Add New Client");
83
+ context.partial("admin/views/edit.tmpl", context.params);
84
+ });
85
+ this.post("#/clients", function(context) {
86
+ context.title("Add New Client");
87
+ $.ajax({ type: "post", url: api + "/clients",
88
+ data: {
89
+ displayName: context.params.displayName,
90
+ link: context.params.link,
91
+ redirectUri: context.params.redirectUri,
92
+ imageUrl: context.params.imageUrl
93
+ },
94
+ success: function(client) {
95
+ app.trigger("notice", "Added new client application " + client.displayName);
96
+ context.redirect("#/");
97
+ },
98
+ error: function(xhr) {
99
+ app.trigger("notice", xhr.responseText);
100
+ context.partial("admin/views/edit.tmpl", context.params);
101
+ }
102
+ });
103
+ });
104
+
105
+ // Client/token revoke buttons do this.
106
+ $("a[data-method=post]").live("click", function(evt) {
107
+ evt.preventDefault();
108
+ var link = $(this);
109
+ if (link.attr("data-confirm") && !confirm(link.attr("data-confirm")))
110
+ return;
111
+ $.post(link.attr("href"), function(success) {
112
+ app.trigger("notice", "Revoked!");
113
+ app.refresh();
114
+ });
115
+ });
116
+ // Link to reveal/hide client ID/secret
117
+ $("td.secrets a[rel=toggle]").live("click", function(evt) {
118
+ evt.preventDefault();
119
+ var dl = $(this).next("dl");
120
+ if (dl.is(":visible")) {
121
+ $(this).html("Reveal");
122
+ dl.hide();
123
+ } else {
124
+ $(this).html("Hide");
125
+ dl.show();
126
+ }
127
+ });
128
+ // Error/notice at top of screen
129
+ var noticeTimeout;
130
+ app.bind("notice", function(evt, message) {
131
+ $("#notice").text(message).fadeIn("fast");
132
+ if (noticeTimeout) {
133
+ cancelTimeout(noticeTimeout);
134
+ noticeTimeout = null;
135
+ }
136
+ noticeTimeout = setTimeout(function() {
137
+ noticeTimeout = null;
138
+ $("#notice").fadeOut("slow");
139
+ }, 5000);
140
+ });
141
+ $("#notice").live("click", function() { $(this).fadeOut("slow") });
142
+ });
143
+
144
+ // Adds thousands separator to integer or float (can also pass formatted string
145
+ // if you care about precision).
146
+ $.thousands = function(integer) {
147
+ return integer.toString().replace(/^(\d+?)((\d{3})+)$/g, function(x,a,b) { return a + b.replace(/(\d{3})/g, ",$1") })
148
+ .replace(/\.((\d{3})+)(\d+)$/g, function(x,a,b,c) { return "." + a.replace(/(\d{3})/g, "$1,") + c })
149
+ }
150
+
151
+ $.shortdate = function(integer) {
152
+ var date = new Date(integer * 1000);
153
+ return "<span title='" + date.toLocaleString() + "'>" + date.toISOString().substring(0,10) + "</span>";
154
+ }