├── .gitignore ├── .travis.yml ├── CHANGELOG ├── Gemfile ├── MIT-LICENSE ├── README.md ├── Rakefile ├── VERSION ├── bin └── oauth2-server ├── config.ru ├── gemfiles ├── Rails2 ├── Rails3 ├── Sinatra1.1 ├── Sinatra1.2 └── Sinatra1.3 ├── lib ├── rack-oauth2-server.rb └── rack │ └── oauth2 │ ├── admin │ ├── css │ │ └── screen.css │ ├── images │ │ ├── loading.gif │ │ └── oauth-2.png │ ├── js │ │ ├── application.coffee │ │ ├── jquery.js │ │ ├── jquery.tmpl.js │ │ ├── protovis-r3.2.js │ │ ├── sammy.js │ │ ├── sammy.json.js │ │ ├── sammy.oauth2.js │ │ ├── sammy.storage.js │ │ ├── sammy.title.js │ │ ├── sammy.tmpl.js │ │ └── underscore.js │ └── views │ │ ├── client.tmpl │ │ ├── clients.tmpl │ │ ├── edit.tmpl │ │ ├── index.html │ │ └── no_access.tmpl │ ├── models.rb │ ├── models │ ├── access_grant.rb │ ├── access_token.rb │ ├── auth_request.rb │ ├── client.rb │ └── issuer.rb │ ├── rails.rb │ ├── server.rb │ ├── server │ ├── admin.rb │ ├── errors.rb │ ├── helper.rb │ ├── practice.rb │ ├── railtie.rb │ └── utils.rb │ └── sinatra.rb ├── rack-oauth2-server.gemspec ├── rails └── init.rb ├── spec └── admin-spec.coffee └── test ├── admin ├── api_test.rb └── ui_test.rb ├── oauth ├── access_grant_test.rb ├── access_token_test.rb ├── authorization_test.rb ├── server_methods_test.rb └── server_test.rb ├── rails2 ├── app │ └── controllers │ │ ├── api_controller.rb │ │ ├── application_controller.rb │ │ └── oauth_controller.rb └── config │ ├── environment.rb │ ├── environments │ └── test.rb │ └── routes.rb ├── rails3 ├── app │ └── controllers │ │ ├── api_controller.rb │ │ ├── application_controller.rb │ │ └── oauth_controller.rb └── config │ ├── application.rb │ ├── environment.rb │ └── routes.rb ├── setup.rb └── sinatra └── my_app.rb /.gitignore: -------------------------------------------------------------------------------- 1 | .bundle 2 | .gems 3 | doc 4 | .yardoc 5 | *.gem 6 | test/rails/log 7 | *.lock 8 | test/*/log/test.log 9 | test.log 10 | lib/rack/oauth2/admin/js/application.js 11 | .project -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | rvm: 2 | - 1.8.7 3 | - 1.9.2 4 | - 1.9.3 5 | - ree 6 | gemfile: 7 | - gemfiles/Rails2 8 | - gemfiles/Rails3 9 | - gemfiles/Sinatra1.1 10 | - gemfiles/Sinatra1.2 11 | - gemfiles/Sinatra1.3 12 | matrix: 13 | exclude: 14 | - rvm: 1.9.3 15 | gemfile: gemfiles/Rails2 16 | notifications: 17 | email: 18 | - assaf@labnotes.org 19 | - bploetz@gmail.com 20 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | 2013-09-30 Version 2.8.1 2 | 3 | Includes a content-type for unauthorized requests (johnallen3d) 4 | 5 | Prevents stack level too deep error (johnallen3d) 6 | 7 | Allow token use in MongoDB cappedCollections (cstuss) 8 | 9 | 10 | 2012-07-16 Version 2.8.0 11 | 12 | Support for assertion type callbacks (Paul Covell) 13 | 14 | 15 | 2012-07-10 Version 2.7.0 16 | 17 | Support for expiring tokens (Fabrizio Regini & Daniel Vartanov) 18 | 19 | 20 | 2012-06-18 Version 2.6.1 21 | 22 | Added missing require "json" statement (Gaku Ueda) 23 | 24 | 25 | 2012-03-27 Version 2.6.0 26 | 27 | Adding support for JWT tokens in the "assertion" grant type (Brian Ploetz) 28 | 29 | 30 | 2012-03-26 Version 2.5.1 31 | 32 | Issue #13: Fix bug in oauth2-server introduced by pull request #9 (Rafael Chacon) 33 | 34 | 35 | 2012-03-23 Version 2.5.0 36 | 37 | Issue #12: Changed two-legged OAuth flow back to always returning a new access token 38 | for a given client_id/client_secret pair (Brian Ploetz) 39 | 40 | 41 | 2012-03-15 Version 2.4.2 42 | 43 | Fixed Server::Admin.mount ignoring path (ABrukish). 44 | 45 | Fixed a few issues with documentation (Jesse Miller). 46 | 47 | Fixed issue with Rack::Lint in development mode using rackup (Jared Allen) 48 | 49 | 50 | 2011-08-01 Version 2.4.1 51 | 52 | Fixes error in oauth2-server command line (Michael Saffitz) 53 | 54 | 55 | 2011-07-28 Version 2.4.0 56 | 57 | Added fourth argument to Server.token_for that allows setting token expiration, 58 | and Server option expires_in that does that same thing for all tokens. 59 | 60 | Set to number of seconds token should be accepted. If nil or zero, access token 61 | never expires. For example: 62 | 63 | config.oauth.expires_in = 1.day 64 | 65 | 66 | 2011-07-18 Version 2.3.0 67 | 68 | Setting oauth.database = in configuration block now works as you would 69 | expect it to. As a side note, this is now a global setting (i.e. shared by all 70 | handlers). 71 | 72 | 73 | 2011-07-13 Version 2.2.2 74 | 75 | Fix for unknown [] for NilClass when database not setup (epinault-ttc) 76 | 77 | Warn people when they forgot to set Server.database or set it to 78 | Mongo::Connection instead of Mongo::DB. 79 | 80 | Fixes the strict url scheme issue (Martin Wawrusch). 81 | 82 | 83 | 2011-04-11 version 2.2.1 84 | 85 | Content type header on redirects (Marc Schwieterman) 86 | 87 | 88 | 2011-02-02 version 2.2.0 89 | 90 | Don't require client_secret when requesting authorization (George Ogata). 91 | 92 | Don't check the redirect_uri if the client does not have one set (George Ogata). 93 | 94 | Look for post params if request is a POST (George Ogata). 95 | 96 | 97 | 2010-12-22 version 2.1.0 98 | 99 | Added support for two-legged OAuth flow (Brian Ploetz) 100 | 101 | Fixed query parameter authorization and allowed access_token to be defined 102 | (Ari) 103 | 104 | 105 | 2010-11-30 version 2.0.1 106 | 107 | Change: username/password authentication with no scope results in access token 108 | with default scope. Makes like easier for everyone. 109 | 110 | 111 | 2010-11-23 version 2.0.0 112 | 113 | MAJOR CHANGE: 114 | Keeping with OAuth 2.0 spec terminology, we'll call it scope all around. Some 115 | places in the API that previously used "scopes" have been changed to "scope". 116 | 117 | OTOH, the scope is not consistently stored and returned as array of names, 118 | previous was stored as comma-separated string, and often returned as such. 119 | Whatever you have stored with pre 2.0 will probably not work with 2.0 and 120 | forward. 121 | 122 | Clients now store their scope, and only those names are allowed in access 123 | tokens. The global setting oauth.scope is no longer in use. Forget about it. 124 | 125 | To migrate from 1.4.x to 2.0: 126 | 127 | oauth2-server migrate --db 128 | 129 | Application client registrations will change from having no scope to having an 130 | empty scope, so you would want to update their registration, either using the 131 | Web console, or from your code: 132 | 133 | Client.all.each { |client| client.update(:scope=>%w{read write}) } 134 | 135 | 136 | Use Rack::OAuth2::Server token_for and access_grant to generate access tokens 137 | and access grants, respectively. These are mighty useful if you're using the 138 | OAuth 2.0 infrastructure, but have different ways for authorizing, e.g. using 139 | access tokens instead of cookies. 140 | 141 | Rack::OAuth2::Server method register to register new client applications and 142 | update existing records. This method is idempotent, so you can use it in rake 143 | db:seed, deploy scripts, migrations, etc. 144 | 145 | If your authenticator accepts four arguments, it will receive, in addition to 146 | username and password, also the client identifier and requested scopes. 147 | 148 | Web console now allows you to set/unset individual scopes for each client 149 | application, and store a note on each client. 150 | 151 | Added Sammy.js OAuth 2.0 plugin. 152 | 153 | 154 | 2010-11-12 version 1.4.6 155 | 156 | Added Railtie support for Rails 3.x and now running tests against both Rails 157 | 2.x and 3.x. 158 | 159 | 160 | 2010-11-11 version 1.4.5 161 | 162 | Cosmetic changes to UI. Added throbber and error messages when AJAX requests go 163 | foul. Header on the left, sign-out on the right, as most people expect it. 164 | Client name is no longer a link to the site, site link shown separately. 165 | 166 | 167 | 2010-11-10 version 1.4.4 168 | 169 | Added a practice server. You can use it to test your OAuth 2.0 client library. 170 | To fire up the practice server: oauth2-server practice 171 | 172 | Bumped up dependencies on Rack 1.1 or later, Sinatra 1.1 or later. 173 | 174 | 175 | 2010-11-09 version 1.4.3 176 | 177 | Renamed Rack::OAuth2::Server::Admin to just Rack::OAuth2::Admin. 178 | 179 | Checked in config.ru, I use this for testing the Web console. 180 | 181 | 182 | 2010-11-09 version 1.4.2 183 | 184 | Fix to commend line tool to properly do authentication. 185 | 186 | Added Sinatra as dependency. 187 | 188 | 189 | 2010-11-09 version 1.4.1 190 | 191 | Fix to command line tool when accessing MongoDB with username/password. 192 | 193 | 194 | 2010-11-09 version 1.4.0 195 | 196 | If authorization handle is passed as request parameter (the recommended way), 197 | then you can call oauth.grant! with a single argument and oauth.deny! with no 198 | arguments. 199 | 200 | You can now call oauth.deny! at any point during the authorization flow, e.g. 201 | automatically deny all requests based on scope and client. 202 | 203 | To deny access, return status code 403 (was, incorrectly 401). Or just use 204 | oauth.deny!. 205 | 206 | Web console gets template_url setting you can use to map access token identity 207 | into a URL in your application. The substitution variable is "{id}". 208 | 209 | Added error page when authorization attempt fails (instead of endless 210 | redirect). 211 | 212 | Fixed mounting of Web console on Rails. If it failed you before, try again. 213 | 214 | Fixed documentation for configuration under Rails, clarify that all the 215 | interesting stuff happens in after_initialize. 216 | 217 | Fixed error responses for response_type=token to use fragment identifier. 218 | 219 | 220 | 2010-11-08 version 1.3.1 221 | 222 | Added command line tool, helps you get started and setup: 223 | $ oauth2-server setup --db my_db 224 | 225 | Added a touch of color to the UI and ability to delete a client. 226 | 227 | You can not sign out of the Web console. 228 | 229 | 230 | 2010-11-07 version 1.3.0 231 | 232 | Added OAuth authorization console. 233 | 234 | Added param_authentication option: turn this on if you need to support 235 | oauth_token query parameter or form field. Disabled by default. 236 | 237 | Added host option: only check requests sent to that host (e.g. only check 238 | requests to api.example.com). 239 | 240 | Added path option: only check requests under this path (e.g. only check 241 | requests for /api/...). 242 | 243 | 244 | 2010-11-03 version 1.2.2 245 | 246 | Store ObjectId references in database. 247 | 248 | 249 | 2010-11-03 version 1.2.1 250 | 251 | Make sure order of scope no longer important for access token lookup. 252 | 253 | 254 | 2010-11-02 version 1.2.0 255 | 256 | You can now redirect to /oauth/authorize with authorization query parameter and 257 | it will do the right thing. 258 | 259 | 260 | 2010-11-02 version 1.1.1 261 | 262 | Fixed missing rails/init.rb. 263 | 264 | 265 | 2010-11-02 version 1.1.0 266 | 267 | Renamed oauth.resource as oauth.identity to remove confusion, besides, it's 268 | more often identity than anything else. 269 | 270 | Added automagic loading under Rails, no need to require special path. 271 | 272 | Added Rack::OAuth2::Server::Options class, easier to user than Hash. 273 | 274 | Added indexes for speedier queries. 275 | 276 | 277 | 2010-11-02 version 1.0.0 278 | 279 | World premiere. 280 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gemspec 3 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010 Flowtown, Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "rake/testtask" 2 | 3 | spec = Gem::Specification.load(Dir["*.gemspec"].first) 4 | 5 | GEMFILE_MAP = {"gemfiles/Rails2" => "Rails 2.3", "gemfiles/Rails3" => "Rails 3.x", "gemfiles/Sinatra1.1" => "Sinatra 1.1", "gemfiles/Sinatra1.2" => "Sinatra 1.2", "gemfiles/Sinatra1.3" => "Sinatra 1.3"} 6 | 7 | desc "Install dependencies" 8 | task :setup do 9 | GEMFILE_MAP.each do |gemfile, name| 10 | puts "Installing gems for testing with #{name} ..." 11 | sh "env BUNDLE_GEMFILE=#{File.dirname(__FILE__) + '/' + gemfile} bundle install" 12 | end 13 | end 14 | 15 | desc "Run all tests" 16 | Rake::TestTask.new do |task| 17 | task.test_files = FileList['test/**/*_test.rb'] 18 | if Rake.application.options.trace 19 | #task.warning = true 20 | task.verbose = true 21 | elsif Rake.application.options.silent 22 | task.ruby_opts << "-W0" 23 | else 24 | task.verbose = true 25 | end 26 | task.ruby_opts << "-I." 27 | end 28 | 29 | namespace :test do 30 | GEMFILE_MAP.each do |gemfile, name| 31 | desc "Run all tests against #{name}" 32 | task gemfile.downcase.gsub(/\./, "_") do 33 | sh "env BUNDLE_GEMFILE=#{gemfile} bundle exec rake" 34 | end 35 | end 36 | task :all=>GEMFILE_MAP.map {|gemfile, name| "test:#{gemfile.downcase.gsub(/\./, "_")}"} 37 | end 38 | 39 | desc "Run this in development mode when updating the CoffeeScript file" 40 | task :coffee do 41 | sh "coffee -w -o lib/rack/oauth2/admin/js/ lib/rack/oauth2/admin/js/application.coffee" 42 | end 43 | 44 | task :compile do 45 | sh "coffee -c -l -o lib/rack/oauth2/admin/js/ lib/rack/oauth2/admin/js/application.coffee" 46 | end 47 | 48 | desc "Build the Gem" 49 | task :build=>:compile do 50 | sh "gem build #{spec.name}.gemspec" 51 | end 52 | 53 | desc "Install #{spec.name} locally" 54 | task :install=>:build do 55 | sudo = "sudo" unless File.writable?( Gem::ConfigMap[:bindir]) 56 | sh "#{sudo} gem install #{spec.name}-#{spec.version}.gem" 57 | end 58 | 59 | desc "Push new release to gemcutter and git tag" 60 | task :push=>["test:all", "build"] do 61 | puts "Tagging version #{spec.version} .." 62 | sh "git push" 63 | sh "git tag -a v#{spec.version} -m \"Tagging v#{spec.version}\"" 64 | sh "git push --tags" 65 | puts "Publishing gem.." 66 | sh "gem push #{spec.name}-#{spec.version}.gem" 67 | end 68 | 69 | task :default do 70 | ENV["FRAMEWORK"] = "rails" 71 | begin 72 | require "rails" # check for Rails3 73 | rescue LoadError 74 | begin 75 | require "initializer" # check for Rails2 76 | rescue LoadError 77 | ENV["FRAMEWORK"] = "sinatra" 78 | end 79 | end 80 | task("test").invoke 81 | end 82 | 83 | 84 | begin 85 | require "yard" 86 | YARD::Rake::YardocTask.new do |doc| 87 | doc.files = FileList["lib/**/*.rb"] 88 | end 89 | rescue LoadError 90 | end 91 | 92 | task :clean do 93 | rm_rf %w{doc .yardoc *.gem} 94 | end 95 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 2.8.1 2 | -------------------------------------------------------------------------------- /bin/oauth2-server: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/../lib') 3 | require "rack/oauth2/server" 4 | require "uri" 5 | include Rack::OAuth2 6 | 7 | 8 | if (i = ARGV.index("--db")) && ARGV[i+1] 9 | url = ARGV[i + 1] 10 | uri = URI.parse(url) 11 | uri = URI.parse("mongo://#{url}") if uri.opaque 12 | db = Mongo::Connection.new(uri.host, uri.port)[uri.path.sub(/^\//, "")] 13 | db.authenticate uri.user, uri.password if uri.user 14 | Server.options.database = db 15 | ARGV[i,2] = [] 16 | end 17 | 18 | if (i = ARGV.index("--port") || ARGV.index("-p")) && ARGV[i+1] 19 | port = ARGV[i + 1].to_i 20 | ARGV[i,2] = [] 21 | end 22 | 23 | 24 | if (i = ARGV.index("--collection-prefix") || ARGV.index("-c")) && ARGV[i+1] 25 | prefix = ARGV[i + 1] 26 | Server.options.collection_prefix = prefix 27 | ARGV[i,2] = [] 28 | else 29 | Server.options.collection_prefix = 'oauth2' 30 | end 31 | 32 | 33 | 34 | case ARGV[0] 35 | when "list" 36 | 37 | fail "No database. Use the --db option to tell us which database to use" unless Server.options.database 38 | Server::Client.all.each do |client| 39 | next if client.revoked 40 | print "%-30s\t%s\n" % [client.display_name, client.link] 41 | print " ID %s\tSecret %s\n" % [client.id, client.secret] 42 | print "\n" 43 | end 44 | 45 | when "register" 46 | 47 | fail "No database. Use the --db option to tell us which database to use" unless Server.options.database 48 | begin 49 | print "Application name:\t" 50 | display_name = $stdin.gets 51 | print "Application URL:\t" 52 | link = $stdin.gets 53 | print "Redirect URI:\t\t" 54 | redirect_uri = $stdin.gets 55 | print "Scope (space separated names):\t\t" 56 | scope = $stdin.gets 57 | client = Server.register(:display_name=>display_name, :link=>link, :redirect_uri=>redirect_uri, :scope=>scope) 58 | rescue 59 | puts "\nFailed to register client: #{$!}" 60 | exit -1 61 | end 62 | puts "Registered #{client.display_name}" 63 | puts "ID\t#{client.id}" 64 | puts "Secret\t#{client.secret}" 65 | 66 | when "register_issuer" 67 | fail "No database. Use the --db option to tell us which database to use" unless Server.options.database 68 | begin 69 | print "Identifier (typically a URL):\t" 70 | identifier = $stdin.gets 71 | print "HMAC secret:\t" 72 | hmac_secret = $stdin.gets 73 | print "RSA public key:\t\t" 74 | public_key = $stdin.gets 75 | issuer = Server.register_issuer(:identifier => identifier, :hmac_secret => hmac_secret, :public_key => public_key) 76 | rescue 77 | puts "\nFailed to register issuer: #{$!}" 78 | exit -1 79 | end 80 | puts "Registered Issuer #{issuer.identifier}" 81 | puts "HMAC secret\t#{issuer.hmac_secret}" 82 | puts "RSA public key\t#{issuer.public_key}" 83 | 84 | when "setup" 85 | 86 | fail "No database. Use the --db option to tell us which database to use" unless Server.options.database 87 | puts "Where would you mount the Web console? This is a URL that must end with /admin," 88 | puts "for example, http://example.com/oauth/admin" 89 | print ": " 90 | uri = URI.parse($stdin.gets) 91 | begin 92 | uri.normalize! 93 | fail "No an HTTP/S URL" unless uri.absolute? && %{http https}.include?(uri.scheme) 94 | fail "Path must end with /admin" unless uri.path[/\/admin$/] 95 | client = Server.register(:display_name=>"OAuth Console", :link=>uri.to_s, :image_url=>"#{uri.to_s}/images/oauth-2.png", 96 | :redirect_uri=>uri.to_s, :scope=>"oauth-admin") 97 | rescue 98 | puts "\nFailed to register client: #{$!}" 99 | exit -1 100 | end 101 | print <<-TEXT 102 | 103 | Next Steps 104 | ========== 105 | 106 | Make sure you ONLY authorize administrators to use the oauth-admin scope. 107 | For example: 108 | 109 | before_filter do 110 | # Only admins allowed to authorize the scope oauth-admin 111 | head oauth.deny! if oauth.scope.include?("oauth-admin") && !current_user.admin? 112 | end 113 | 114 | Rails 2.x, add the following to config/environment.rb: 115 | 116 | config.after_initialize do 117 | config.middleware.use Rack::OAuth2::Server::Admin.mount "#{uri.path}" 118 | Rack::OAuth2::Server::Admin.set :client_id, "#{client.id}" 119 | Rack::OAuth2::Server::Admin.set :client_secret, "#{client.secret}" 120 | end 121 | 122 | Rails 3.x, add the following to config/application.rb: 123 | 124 | config.after_initialize do 125 | Rack::OAuth2::Server::Admin.set :client_id, "#{client.id}" 126 | Rack::OAuth2::Server::Admin.set :client_secret, "#{client.secret}" 127 | end 128 | 129 | And add the follownig to config/routes.rb: 130 | 131 | mount Rack::OAuth2::Server::Admin=>"/oauth/admin" 132 | 133 | Sinatra, Padrino and other Rack applications, mount the console: 134 | 135 | Rack::Builder.new do 136 | map("#{uri.path}") { run Rack::OAuth2::Server::Admin } 137 | map("/") { run MyApp } 138 | end 139 | Rack::OAuth2::Server::Admin.set :client_id, "#{client.id}" 140 | Rack::OAuth2::Server::Admin.set :client_secret, "#{client.secret}" 141 | 142 | The console will authorize access by redirecting to 143 | https://#{uri.host}/oauth/authorize 144 | 145 | If this is not your OAuth 2.0 authorization endpoint, you can change it by 146 | setting the :authorize option. 147 | TEXT 148 | 149 | when "practice" 150 | 151 | require "logger" 152 | begin 153 | require "thin" 154 | rescue LoadError 155 | puts "Needs the Thin Web server. Please gem install thin and run again" 156 | exit -1 157 | end 158 | require "rack/oauth2/server/practice" 159 | 160 | fail "No database. Use the --db option to tell us which database to use" unless Server.options.database 161 | port ||= 8080 162 | admin_url = "http://localhost:#{port}/oauth/admin" 163 | unless client = Server::Client.lookup(admin_url) 164 | client = Server.register(:display_name=>"Practice OAuth Console", :image_url=>"#{admin_url}/images/oauth-2.png", 165 | :link=>admin_url, :redirect_uri=>admin_url, :scope=>"oauth-admin") 166 | end 167 | Server::Admin.configure do |config| 168 | logger = Logger.new(STDOUT) 169 | logger.level = Logger::DEBUG 170 | config.set :client_id, client.id 171 | config.set :client_secret, client.secret 172 | config.set :scope, "nobody sudo" 173 | config.set :logger, logger 174 | config.set :logging, true 175 | config.set :dump_errors, true 176 | config.oauth.logger = logger 177 | end 178 | 179 | Server::Practice.configure do |config| 180 | logger = Logger.new(STDOUT) 181 | logger.level = Logger::DEBUG 182 | config.set :logger, logger 183 | config.set :logging, true 184 | config.set :dump_errors, true 185 | config.oauth.logger = logger 186 | end 187 | 188 | print "\nFiring up the practice server.\nFor instructions, go to http://localhost:#{port}/\n\n\n" 189 | Thin::Server.new "127.0.0.1", port do 190 | map("/") { run Server::Practice.new } 191 | map("/oauth/admin") { run Server::Admin.new } 192 | end.start 193 | 194 | when "migrate" 195 | 196 | fail "No database. Use the --db option to tell us which database to use" unless Server.options.database 197 | puts "Set all clients to this scope (can change later by calling Client.register):" 198 | print ": " 199 | scope = $stdin.gets.strip.split 200 | puts "Updating Client scope to #{scope.join(", ")}" 201 | Server::Client.collection.find({ :scope=>{ :$exists=>false } }, :fields=>[]).each do |client| 202 | update = { :scope=>scope, 203 | :tokens_granted=>Server::AccessToken.count(:client_id=>client["_id"]), 204 | :tokens_revoked=>Server::AccessToken.count(:client_id=>client["_id"], :revoked=>true) } 205 | Server::Client.collection.update({ :_id=>client["_id"] }, { :$set=>update }) 206 | end 207 | [Server::AccessToken, Server::AccessGrant, Server::AuthRequest].each do |mod| 208 | puts "Updating #{mod.name} scope from string to array" 209 | mod.collection.find({ :scope=>{ :$type=>2 } }, :fields=>[]).each do |token| 210 | scope = token["scope"].split 211 | mod.collection.update({ :_id=>token["_id"] }, { :$set=>{ :scope=>scope } }) 212 | end 213 | end 214 | else 215 | 216 | print <<-TEXT 217 | Usage: oauth2-server [options] COMMAND [args] 218 | Version #{Server::VERSION} 219 | 220 | Commands: 221 | list Lists all active clients 222 | migrate Run this when migrating from 1.x to 2.x 223 | practice Runs a dummy OAuth 2.0 server, use this to test your OAuth 2.0 client 224 | register Register a new client application 225 | register_issuer Register a new assertion issuer 226 | setup Create new admin account and help you setup the OAuth Web console 227 | 228 | Options: 229 | --db database Database name or connection URL 230 | --port number Port to run admin server, detault is 8080 231 | --collection-prefix Prefix to use for MongoDB collections created by rack-oauth2-server, defaults to "oauth2". 232 | TEXT 233 | exit -1 234 | 235 | end 236 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | $: << File.dirname(__FILE__) + "/lib" 2 | require "rack/oauth2/server" 3 | Rack::OAuth2::Server.database = Mongo::Connection.new["test"] 4 | 5 | class Authorize < Sinatra::Base 6 | register Rack::OAuth2::Sinatra 7 | get "/oauth/authorize" do 8 | content_type "text/html" 9 | <<-HTML 10 |

#{oauth.client.display_name} wants to access your account.

11 |
12 | 13 |
14 | HTML 15 | end 16 | 17 | post "/oauth/grant" do 18 | oauth.grant! params[:auth], "Superman" 19 | end 20 | end 21 | # NOTE: This client must exist in your database. To get started, run: 22 | # oauth-server setup --db test 23 | # And enter the URL 24 | # http://localhost:3000/oauth/admin 25 | # Then plug the client ID/secret you get instead of these values, and run: 26 | # thin start 27 | # open http://localhost:3000/oauth/admin 28 | Rack::OAuth2::Server::Admin.set :client_id, "4cd9cbc03321e8367d000001" 29 | Rack::OAuth2::Server::Admin.set :client_secret, "c531191fb208aa34d6b44d6f69e61e97e56abceadb336ebb0f2f5757411a0a19" 30 | Rack::OAuth2::Server::Admin.set :template_url, "http://localhost:3000/accounts/{id}" 31 | app = Rack::Builder.new do 32 | map "/" do 33 | run Authorize.new 34 | end 35 | map "/oauth/admin" do 36 | run Rack::OAuth2::Server::Admin.new 37 | end 38 | end 39 | run app.to_app 40 | -------------------------------------------------------------------------------- /gemfiles/Rails2: -------------------------------------------------------------------------------- 1 | source :rubygems 2 | gemspec :path=>"../" 3 | 4 | gem "rails", "~>2.3" 5 | 6 | group :development do 7 | gem "rake" 8 | gem "thin" 9 | gem "yard" 10 | end 11 | 12 | group :test do 13 | gem "awesome_print" 14 | gem "rack-test" 15 | gem "shoulda" 16 | gem "timecop" 17 | end 18 | -------------------------------------------------------------------------------- /gemfiles/Rails3: -------------------------------------------------------------------------------- 1 | source :rubygems 2 | gemspec :path=>"../" 3 | 4 | gem "rails", "~>3.0" 5 | 6 | group :development do 7 | gem "rake" 8 | gem "thin" 9 | gem "yard" 10 | end 11 | 12 | group :test do 13 | gem "awesome_print" 14 | gem "rack-test" 15 | gem "shoulda" 16 | gem "timecop" 17 | end 18 | -------------------------------------------------------------------------------- /gemfiles/Sinatra1.1: -------------------------------------------------------------------------------- 1 | source :rubygems 2 | gemspec :path=>"../" 3 | 4 | gem "sinatra", "~>1.1.0" 5 | 6 | group :development do 7 | gem "rake" 8 | gem "thin" 9 | gem "yard" 10 | end 11 | 12 | group :test do 13 | gem "awesome_print" 14 | gem "rack-test" 15 | gem "shoulda" 16 | gem "timecop" 17 | end 18 | -------------------------------------------------------------------------------- /gemfiles/Sinatra1.2: -------------------------------------------------------------------------------- 1 | source :rubygems 2 | gemspec :path=>"../" 3 | 4 | gem "sinatra", "~>1.2.0" 5 | 6 | group :development do 7 | gem "rake" 8 | gem "thin" 9 | gem "yard" 10 | end 11 | 12 | group :test do 13 | gem "awesome_print" 14 | gem "rack-test" 15 | gem "shoulda" 16 | gem "timecop" 17 | end 18 | -------------------------------------------------------------------------------- /gemfiles/Sinatra1.3: -------------------------------------------------------------------------------- 1 | source :rubygems 2 | gemspec :path=>"../" 3 | 4 | gem "sinatra", "~>1.3.0" 5 | 6 | group :development do 7 | gem "rake" 8 | gem "thin" 9 | gem "yard" 10 | end 11 | 12 | group :test do 13 | gem "awesome_print" 14 | gem "rack-test" 15 | gem "shoulda" 16 | gem "timecop" 17 | end 18 | -------------------------------------------------------------------------------- /lib/rack-oauth2-server.rb: -------------------------------------------------------------------------------- 1 | require "rack/oauth2/server" 2 | require "rack/oauth2/sinatra" if defined?(Sinatra) 3 | require "rack/oauth2/rails" if defined?(Rails) 4 | require "rack/oauth2/server/railtie" if defined?(Rails::Railtie) 5 | -------------------------------------------------------------------------------- /lib/rack/oauth2/admin/css/screen.css: -------------------------------------------------------------------------------- 1 | html { 2 | background: #eee; 3 | } 4 | body { 5 | margin: 0; 6 | padding: 0; 7 | width: 100%; 8 | font: 12pt "Helvetica", "Lucida Sans", "Verdana"; 9 | } 10 | 11 | a { text-decoration: none; color: #00c; } 12 | a:hover, a:focus { text-decoration: underline; color: #00c; } 13 | h1, h2 { 14 | text-shadow: rgba(255,255,255,.2) 0 1px 1px; 15 | color: rgb(76, 86, 108); 16 | } 17 | h1 { font-size: 18pt; margin: 0.6em 0 } 18 | h2 { font-size: 16pt; margin: 0.3em 0 } 19 | 20 | label { 21 | display: block; 22 | color: #000; 23 | font-weight: 600; 24 | font-size: 0.9em; 25 | margin: 0.9em 0; 26 | } 27 | label input, label textarea, label select { 28 | display: block; 29 | } 30 | label.check { 31 | font-weight: normal; 32 | } 33 | label.check input { 34 | display: inline; 35 | } 36 | label input, label textarea { 37 | font-size: 12pt; 38 | line-height: 1.3em; 39 | } 40 | label .hint { 41 | font-weight: normal; 42 | color: #666; 43 | margin: 0; 44 | } 45 | button { 46 | font-size: 11pt; 47 | text-shadow: 0 -1px 1px rgba(0,0,0,0.25); 48 | border: 1px solid #dddddd; 49 | background: #f6f6f6 50% 50% repeat-x; 50 | font-weight: bold; 51 | color: #0073ea; 52 | outline: none; 53 | line-height: 1.3em; 54 | vertical-align: bottom; 55 | padding: 2px 8px; 56 | background: -webkit-gradient(linear, 0% 0%, 0% 100%, from(rgba(226,226,226,0.0)), to(rgba(226,226,226,1.0))); 57 | -webkit-border-radius: 4px; -moz-border-radius: 4px; 58 | -moz-box-shadow: 0 0 4px rgba(0,0,0,0.0); 59 | -webkit-box-shadow: 0 0 4px rgba(0,0,0,0.0); 60 | } 61 | button:hover, button:focus { 62 | text-shadow: 0 -1px 1px rgba(255,255,255,0.25); 63 | border: 1px solid #0073ea; 64 | background: #0073ea 50% 50% repeat-x; 65 | background: -webkit-gradient(linear, 0% 0%, 0% 100%, from(rgba(0, 115, 234, 0.5)), to(rgba(0,115,234, 1.0))); 66 | color: #fff; 67 | text-decoration: none; 68 | cursor: pointer; 69 | -moz-box-shadow: 0 2px 4px rgba(0,0,0,0.5); 70 | -webkit-box-shadow: 0 1px 3px rgba(0,0,0,0.5); 71 | } 72 | 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 } 73 | .error { 74 | color: #f44; 75 | padding-left: 1em; 76 | } 77 | 78 | 79 | /* Message dropping down from the top */ 80 | #notice { 81 | position: absolute; 82 | top: 0; 83 | left: 0; 84 | right: 0; 85 | line-height: 1.6em; 86 | background: #FFFE36; 87 | color: #000; 88 | border-bottom: 1px solid #ddd; 89 | text-align: center; 90 | z-index: 99; 91 | } 92 | 93 | #header { 94 | margin: 0; 95 | padding: 1em 2em 3em 2em; 96 | border-bottom: 2px solid #CCC; 97 | background: #6595A4; 98 | color: #fff; 99 | } 100 | #header .title { 101 | font-size: 18pt; 102 | font-weight: bold; 103 | display: block; 104 | line-height: 32px; 105 | float: left; 106 | } 107 | #header .title a { 108 | color: #fff; 109 | text-shadow: white 0px 0px 1px; 110 | text-decoration: none; 111 | } 112 | #header .title img { 113 | width: 32px; 114 | height: 32px; 115 | vertical-align: bottom; 116 | } 117 | #header .signout, #header .signin { 118 | color: #C8E9F3; 119 | float: right; 120 | font-size: 11pt; 121 | line-height: 32px; 122 | display: none; 123 | } 124 | 125 | #main { 126 | margin: 0; 127 | padding: 2em 2em 4em 2em; 128 | background: #fff; 129 | border: 1px solid #fff; 130 | min-width: 960px; 131 | } 132 | #footer { 133 | background: #eee; 134 | color: #666; 135 | border-top: 1px solid #ccc; 136 | font-size: 90%; 137 | padding: 0 2em 2em 2em; 138 | } 139 | 140 | table { 141 | width: 100%; 142 | table-layout: auto; 143 | empty-cells: show; 144 | border-collapse: separate; 145 | border-spacing: 0px; 146 | margin-top: 2em; 147 | } 148 | table th { 149 | text-align: left; 150 | border-bottom: 1px solid #ccc; 151 | margin-right: 48px; 152 | } 153 | table td { 154 | text-align: left; 155 | vertical-align: top; 156 | border-bottom: 1px solid #ddf; 157 | line-height: 32px; 158 | margin: 0; 159 | padding: 0; 160 | } 161 | table tr:hover td { 162 | background: #ddf; 163 | } 164 | table td.created, table td.revoke, table td.accessed { 165 | width: 6em; 166 | } 167 | table tr.revoked td, table tr.revoked a { 168 | color: #888; 169 | } 170 | table button { 171 | margin-top: -2px; 172 | font-size: 10pt; 173 | } 174 | 175 | table.clients td.name { 176 | padding-left: 32px; 177 | } 178 | table.clients td.name img { 179 | width: 24px; 180 | height: 24px; 181 | border: none; 182 | margin: 4px 4px -4px -32px; 183 | } 184 | table.clients td.secrets { 185 | width: 28em; 186 | } 187 | table.clients td.secrets dl { 188 | display: none; 189 | width: 40em; 190 | margin: 0 -12em 0.6em 0; 191 | line-height: 1.3em; 192 | } 193 | table.clients td.secrets dt { 194 | width: 4em; 195 | float: left; 196 | color: #888; 197 | margin-bottom: 0.3em; 198 | } 199 | table.clients td.secrets dd:after { 200 | content: "."; 201 | display: block; 202 | clear: both; 203 | visibility: hidden; 204 | line-height: 0; 205 | height: 0; 206 | } 207 | 208 | table.tokens td.token { 209 | width: 36em; 210 | } 211 | table.tokens td.scope { 212 | float: none; 213 | } 214 | 215 | .pagination { 216 | width: 100%; 217 | margin-top: 2em; 218 | } 219 | .pagination a[rel=next] { 220 | float: right; 221 | } 222 | .pagination a[rel=previous] { 223 | float: left; 224 | } 225 | 226 | .metrics { 227 | height: 100px; 228 | } 229 | .metrics #fig { 230 | float: left; 231 | width: 500px; 232 | height: 60px; 233 | } 234 | .metrics:after { 235 | content: "."; 236 | display: block; 237 | clear: both; 238 | visibility: hidden; 239 | line-height: 0; 240 | height: 0; 241 | } 242 | .badges { 243 | list-style: none; 244 | margin: 0; 245 | padding: 0; 246 | text-align: right; 247 | width: 100%; 248 | } 249 | .badges li { 250 | display: inline-block; 251 | margin-left: 8px; 252 | min-width: 8em; 253 | } 254 | .badges big { 255 | font-size: 22pt; 256 | display: block; 257 | text-align: center; 258 | } 259 | .badges small { 260 | font-size: 11pt; 261 | display: block; 262 | text-align: center; 263 | } 264 | 265 | .client .details { 266 | margin: 0 0 2em 0; 267 | } 268 | .client .details .name { 269 | margin: 0; 270 | font-size: 16pt; 271 | font-weight: bold; 272 | float: left; 273 | } 274 | .client .details img { 275 | border: none; 276 | width: 24px; 277 | height: 24px; 278 | vertical-align: bottom; 279 | } 280 | .client .details .actions { 281 | float: left; 282 | line-height: 20pt; 283 | margin-left: 1em; 284 | } 285 | .client .details .actions a { 286 | margin: 0 0 0 0.3em; 287 | } 288 | .client .details .meta { 289 | clear: both; 290 | display: block; 291 | color: #888; 292 | font-size: 10pt; 293 | } 294 | .client .details .notes { 295 | font-size: 11pt; 296 | margin: 0.2em 0; 297 | } 298 | 299 | .client.new, .client.edit { 300 | margin: 0; 301 | padding: 1em; 302 | border: 1px solid #eee; 303 | } 304 | .client .fields>#image { 305 | float: left; 306 | margin: 0.5em 12px 0 0; 307 | width: 48px; 308 | height: 48px; 309 | } 310 | .client .fields>* { 311 | margin-left: 60px; 312 | } 313 | .client .fields { 314 | float: left; 315 | } 316 | .client .scope { 317 | float: right; 318 | } 319 | .client .scope .uncommon { 320 | color: red; 321 | } 322 | .client hr { 323 | clear: both; 324 | border: none; 325 | margin: 1em; 326 | } 327 | 328 | .no-access { 329 | margin: 0; 330 | padding: 0; 331 | } 332 | .no-access h1 { 333 | color: red; 334 | } 335 | 336 | #throbber { 337 | display: none; 338 | position: absolute; 339 | top: 6em; 340 | right: 2em; 341 | width: 48px; 342 | height: 48px; 343 | content: ""; 344 | } 345 | .loading { 346 | background: url("../images/loading.gif") no-repeat 50% 50%; 347 | } 348 | -------------------------------------------------------------------------------- /lib/rack/oauth2/admin/images/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/assaf/rack-oauth2-server/91c0dd8a57754b37bbb779b95233b502129def75/lib/rack/oauth2/admin/images/loading.gif -------------------------------------------------------------------------------- /lib/rack/oauth2/admin/images/oauth-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/assaf/rack-oauth2-server/91c0dd8a57754b37bbb779b95233b502129def75/lib/rack/oauth2/admin/images/oauth-2.png -------------------------------------------------------------------------------- /lib/rack/oauth2/admin/js/application.coffee: -------------------------------------------------------------------------------- 1 | Sammy "#main", (app) -> 2 | @.use Sammy.Tmpl 3 | @.use Sammy.Session 4 | @.use Sammy.Title 5 | @.setTitle "OAuth Admin - " 6 | @.use Sammy.OAuth2 7 | @.authorize = document.location.pathname + "/authorize" 8 | 9 | # All XHR errors we don't catch explicitly handled here 10 | $(document).ajaxError (evt, xhr)-> 11 | if xhr.status == 401 12 | app.loseAccessToken() 13 | app.trigger("notice", xhr.responseText) 14 | # Show something when XHR request in progress 15 | $(document).ajaxStart (evt)-> $("#throbber").show() 16 | $(document).ajaxStop (evt)-> $("#throbber").hide() 17 | 18 | @.requireOAuth() 19 | # Show error message if access denied 20 | @bind "oauth.denied", (evt, error)-> 21 | app.partial("admin/views/no_access.tmpl", { error: error.message }) 22 | # Show signout link if authenticated, hide if not 23 | @.bind "oauth.connected", ()-> 24 | $("#header .signin").hide() 25 | $("#header .signout").show() 26 | @.bind "oauth.disconnected", ()-> 27 | $("#header .signin").show() 28 | $("#header .signout").hide() 29 | 30 | api = "#{document.location.pathname}/api" 31 | # Takes array of string with scope names (typically request parameters) and 32 | # normalizes them into an array of scope names. 33 | mergeScope = (scope) -> 34 | if $.isArray(scope) 35 | scope = scope.join(" ") 36 | scope = (scope || "").trim().split(/\s+/) 37 | if scope.length == 1 && scope[0] == "" then [] else _.uniq(scope).sort() 38 | commonScope = null 39 | # Run callback with list of common scopes. First time, make an API call to 40 | # retrieve that list and cache is in memory, since it rarely changes. 41 | withCommonScope = (cb) -> 42 | if commonScope 43 | cb commonScope 44 | else 45 | $.getJSON "#{api}/clients", (json)-> cb(commonScope = json.scope) 46 | 47 | # View all clients 48 | @.get "#/", (context)-> 49 | context.title "All Clients" 50 | $.getJSON "#{api}/clients", (clients)-> 51 | commonScope = clients.scope 52 | context.partial("admin/views/clients.tmpl", { clients: clients.list, tokens: clients.tokens }). 53 | load(clients.history). 54 | then( (json)-> $("#fig").chart(json.data, "granted") ) 55 | 56 | # View single client 57 | @.get "#/client/:id", (context)-> 58 | $.getJSON "#{api}/client/#{context.params.id}", (client)-> 59 | context.title client.displayName 60 | client.notes = (client.notes || "").split(/\n\n/) 61 | context.partial("admin/views/client.tmpl", client). 62 | load(client.history).then((json)-> $("#fig").chart(json.data, "granted")) 63 | # With pagination 64 | @.get "#/client/:id/page/:page", (context)-> 65 | $.getJSON "#{api}/client/#{context.params.id}?page=#{context.params.page}", (client)-> 66 | context.title client.displayName 67 | client.notes = client.notes.split(/\n\n/) 68 | context.partial("admin/views/client.tmpl", client). 69 | load(client.history).then((json)-> $("#fig").chart(json.data, "granted")) 70 | 71 | # Revoke token 72 | @.post "#/token/:id/revoke", (context)-> 73 | $.post "#{api}/token/#{context.params.id}/revoke", ()-> 74 | context.redirect "#/" 75 | 76 | # Edit client 77 | @.get "#/client/:id/edit", (context)-> 78 | $.getJSON "#{api}/client/#{context.params.id}", (client)-> 79 | context.title client.displayName 80 | withCommonScope (scope)-> 81 | client.common = scope 82 | context.partial "admin/views/edit.tmpl", client 83 | @.put "#/client/:id", (context)-> 84 | context.params.scope = mergeScope(context.params.scope) 85 | $.ajax 86 | type: "put" 87 | url: "#{api}/client/#{context.params.id}" 88 | data: 89 | displayName: context.params.displayName 90 | link: context.params.link 91 | imageUrl: context.params.imageUrl 92 | redirectUri: context.params.redirectUri 93 | notes: context.params.notes 94 | scope: context.params.scope 95 | success: (client)-> 96 | context.redirect "#/client/#{context.params.id}" 97 | app.trigger "notice", "Saved your changes" 98 | error: (xhr)-> 99 | withCommonScope (scope)-> 100 | context.params.common = scope 101 | context.partial "admin/views/edit.tmpl", context.params 102 | 103 | # Delete client 104 | @.del "#/client/:id", (context)-> 105 | $.ajax 106 | type: "post" 107 | url: "#{api}/client/#{context.params.id}" 108 | data: { _method: "delete" } 109 | success: ()-> context.redirect("#/") 110 | 111 | # Revoke client 112 | @.post "#/client/:id/revoke", (context)-> 113 | $.post "#{api}/client/#{context.params.id}/revoke", ()-> 114 | context.redirect "#/" 115 | 116 | # Create new client 117 | @.get "#/new", (context)-> 118 | context.title "Add New Client" 119 | withCommonScope (scope)-> 120 | context.partial "admin/views/edit.tmpl", { common: scope, scope: scope } 121 | @.post "#/clients", (context)-> 122 | context.title "Add New Client" 123 | context.params.scope = mergeScope(context.params.scope) 124 | $.ajax 125 | type: "post" 126 | url: "#{api}/clients" 127 | data: 128 | displayName: context.params.displayName 129 | link: context.params.link 130 | imageUrl: context.params.imageUrl 131 | redirectUri: context.params.redirectUri 132 | notes: context.params.notes 133 | scope: context.params.scope 134 | success: (client)-> 135 | app.trigger "notice", "Added new client application #{client.displayName}" 136 | context.redirect "#/" 137 | error: (xhr)-> 138 | withCommonScope (scope)-> 139 | context.params.common = scope 140 | context.partial "admin/views/edit.tmpl", context.params 141 | 142 | # Signout 143 | @.get "#/signout", (context)-> 144 | context.loseAccessToken() 145 | context.redirect "#/" 146 | 147 | # Links that use forms for various methods (i.e. post, delete). 148 | $("a[data-method]").live "click", (evt)-> 149 | evt.preventDefault 150 | link = $(this) 151 | if link.attr("data-confirm") && !confirm(link.attr("data-confirm")) 152 | return false 153 | method = link.attr("data-method") || "get" 154 | form = $("
", { style: "display:none", method: method, action: link.attr("href") }) 155 | if method != "get" && method != "post" 156 | form.append($("")) 157 | app.$element().append form 158 | form.submit() 159 | false 160 | 161 | # Error/notice at top of screen 162 | noticeTimeout = null 163 | app.bind "notice", (evt, message)-> 164 | if !message || message.trim() == "" 165 | message = "Got an error, but don't know why" 166 | $("#notice").text(message).fadeIn("fast") 167 | if noticeTimeout 168 | clearTimeout noticeTimeout 169 | noticeTimeout = null 170 | noticeTimeout = setTimeout ()-> 171 | noticeTimeout = null 172 | $("#notice").fadeOut("slow") 173 | , 5000 174 | $("#notice").live "click", ()-> $(@).fadeOut("slow") 175 | 176 | 177 | # Adds thousands separator to integer or float (can also pass formatted string 178 | # if you care about precision). 179 | $.thousands = (integer)-> 180 | integer.toString().replace(/^(\d+?)((\d{3})+)$/g, (x,a,b)-> a + b.replace(/(\d{3})/g, ",$1") ). 181 | replace(/\.((\d{3})+)(\d+)$/g, (x,a,b,c)-> "." + a.replace(/(\d{3})/g, "$1,") + c ) 182 | 183 | # Returns abbr element with short form of the date (e.g. "Nov 21 2010"). THe 184 | # title attribute provides the full date/time instance, so you can see more 185 | # details by hovering over the element. 186 | $.shortdate = (integer)-> 187 | date = new Date(integer * 1000) 188 | "#{date.toDateString().substring(0,10)}" 189 | 190 | # Draw chart inside the specified container element. 191 | # data -- Array of objects, each one having a timestamp (ts) and some value we 192 | # want to chart 193 | # series -- Name of the value we want to chart 194 | $.fn.chart = (data, series)-> 195 | return if typeof pv == "undefined" # no PV in test environment 196 | canvas = $(@) 197 | w = canvas.width() 198 | h = canvas.height() 199 | today = Math.floor(new Date() / 86400000) 200 | x = pv.Scale.linear(today - 60, today + 1).range(0, w) 201 | max = pv.max(data, (d)-> d[series]) 202 | y = pv.Scale.linear(0, pv.max([max, 10])).range(0, h) 203 | 204 | # The root panel. 205 | vis = new pv.Panel().width(w).height(h).bottom(20).left(20).right(10).top(5) 206 | # X-axis ticks. 207 | vis.add(pv.Rule).data(x.ticks()).left(x).strokeStyle("#fff"). 208 | add(pv.Rule).bottom(-5).height(5).strokeStyle("#000"). 209 | anchor("bottom").add(pv.Label).text((d)-> pv.Format.date("%b %d").format(new Date(d * 86400000)) ) 210 | # Y-axis ticks. 211 | vis.add(pv.Rule).data(y.ticks(3)).bottom(y).strokeStyle((d)-> if d then "#ddd" else "#000"). 212 | anchor("left").add(pv.Label).text(y.tickFormat) 213 | # If we only have one data point, can't show a line so show dot instead 214 | if data.length == 1 215 | vis.add(pv.Dot).data(data). 216 | left((d)-> x(new Date(d.ts)) ).bottom((d)-> y(d[series])).radius(3).lineWidth(2) 217 | else 218 | vis.add(pv.Line).data(data).interpolate("linear"). 219 | left((d)-> x(new Date(d.ts)) ).bottom((d)-> y(d[series]) ).lineWidth(3) 220 | vis.canvas(canvas[0]).render() 221 | -------------------------------------------------------------------------------- /lib/rack/oauth2/admin/js/jquery.tmpl.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery Templating Plugin 3 | * NOTE: Created for demonstration purposes. 4 | * Copyright 2010, John Resig 5 | * Dual licensed under the MIT or GPL Version 2 licenses. 6 | */ 7 | (function( jQuery, undefined ){ 8 | var oldManip = jQuery.fn.domManip, tmplItmAtt = "_tmplitem", 9 | newTmplItems = {}, appendToTmplItems, topTmplItem = { key: 0, data: {} }, itemKey = 0, cloneIndex = 0; 10 | 11 | function newTmplItem( options, parentItem, fn, data ) { 12 | // Returns a template item data structure for a new rendered instance of a template (a 'template item'). 13 | // The content field is a hierarchical array of strings and nested items (to be 14 | // removed and replaced by nodes field of dom elements, once inserted in DOM). 15 | var newItem = { 16 | data: data || (parentItem ? parentItem.data : {}), 17 | tmpl: null, 18 | parent: parentItem || null, 19 | nodes: [], 20 | nest: nest 21 | }; 22 | if ( options ) { 23 | jQuery.extend( newItem, options, { nodes: [], parent: parentItem } ); 24 | fn = fn || (typeof options.tmpl === "function" ? options.tmpl : null); 25 | } 26 | if ( fn ) { 27 | // Build the hierarchical content to be used during insertion into DOM 28 | newItem.tmpl = fn; 29 | newItem.content = newItem.tmpl( jQuery, newItem ); 30 | newItem.key = ++itemKey; 31 | // Keep track of new template item, until it is stored as jQuery Data on DOM element 32 | newTmplItems[itemKey] = newItem; 33 | } 34 | return newItem; 35 | } 36 | 37 | // Override appendTo etc., in order to provide support for targeting multiple elements. (This code would disappear if integrated in jquery core). 38 | jQuery.each({ 39 | appendTo: "append", 40 | prependTo: "prepend", 41 | insertBefore: "before", 42 | insertAfter: "after", 43 | replaceAll: "replaceWith" 44 | }, function( name, original ) { 45 | jQuery.fn[ name ] = function( selector ) { 46 | var ret = [], insert = jQuery( selector ), 47 | parent = this.length === 1 && this[0].parentNode; 48 | 49 | appendToTmplItems = newTmplItems || {}; 50 | if ( parent && parent.nodeType === 11 && parent.childNodes.length === 1 && insert.length === 1 ) { 51 | insert[ original ]( this[0] ); 52 | ret = this; 53 | } else { 54 | for ( var i = 0, l = insert.length; i < l; i++ ) { 55 | cloneIndex = i; 56 | var elems = (i > 0 ? this.clone(true) : this).get(); 57 | jQuery.fn[ original ].apply( jQuery(insert[i]), elems ); 58 | ret = ret.concat( elems ); 59 | } 60 | cloneIndex = 0; 61 | ret = this.pushStack( ret, name, insert.selector ); 62 | } 63 | var tmplItems = appendToTmplItems; 64 | appendToTmplItems = null; 65 | jQuery.tmpl.complete( tmplItems ); 66 | return ret; 67 | }; 68 | }); 69 | 70 | jQuery.fn.extend({ 71 | // Use first wrapped element as template markup. 72 | // Return wrapped set of template items, obtained by rendering template against data. 73 | tmpl: function( data, options, parentItem ) { 74 | return jQuery.tmpl( this[0], data, options, parentItem, parentItem === undefined ); 75 | }, 76 | 77 | // Find which rendered template item the first wrapped DOM element belongs to 78 | tmplItem: function() { 79 | return jQuery.tmplItem( this[0] ); 80 | }, 81 | 82 | // Consider the first wrapped element as a template declaration, and get the compiled template or store it as a named template. 83 | templates: function( name ) { 84 | return jQuery.templates( name, this[0] ); 85 | }, 86 | 87 | domManip: function( args, table, callback, options ) { 88 | // This appears to be a bug in the appendTo, etc. implementation 89 | // it should be doing .call() instead of .apply(). See #6227 90 | if ( args[0].nodeType ) { 91 | var dmArgs = jQuery.makeArray( arguments ), argsLength = args.length, i = 0, tmplItem; 92 | while ( i < argsLength && !(tmplItem = jQuery.data( args[i++], "tmplItem" ))) {}; 93 | if ( argsLength > 1 ) { 94 | dmArgs[0] = [jQuery.makeArray( args )]; 95 | } 96 | if ( tmplItem && cloneIndex ) { 97 | dmArgs[2] = function( fragClone ) { 98 | // Handler called by oldManip when rendered template has been inserted into DOM. 99 | jQuery.tmpl.afterManip( this, fragClone, callback ); 100 | } 101 | } 102 | oldManip.apply( this, dmArgs ); 103 | } else { 104 | oldManip.apply( this, arguments ); 105 | } 106 | cloneIndex = 0; 107 | if ( !appendToTmplItems ) { 108 | jQuery.tmpl.complete( newTmplItems ); 109 | } 110 | return this; 111 | } 112 | }); 113 | 114 | jQuery.extend({ 115 | // Return wrapped set of template items, obtained by rendering template against data. 116 | tmpl: function( tmpl, data, options, parentItem ) { 117 | var ret, topLevel = !parentItem; 118 | if ( topLevel ) { 119 | // This is a top-level tmpl call (not from a nested template using {{tmpl}}) 120 | parentItem = topTmplItem; 121 | tmpl = jQuery.templates[tmpl] || jQuery.templates( null, tmpl ); 122 | } else if ( !tmpl ) { 123 | // The template item is already associated with DOM - this is a refresh. 124 | // Re-evaluate rendered template for the parentItem 125 | tmpl = parentItem.tmpl; 126 | newTmplItems[parentItem.key] = parentItem; 127 | parentItem.nodes = []; 128 | // Rebuild, without creating a new template item 129 | return jQuery( build( parentItem, null, parentItem.tmpl( jQuery, parentItem ) )); 130 | } 131 | if ( !tmpl ) { 132 | return []; // Could throw... 133 | } 134 | if ( typeof data === "function" ) { 135 | data = data.call( parentItem.data || {}, parentItem ); 136 | } 137 | ret = jQuery.isArray( data ) ? 138 | jQuery.map( data, function( dataItem ) { 139 | return newTmplItem( options, parentItem, tmpl, dataItem ); 140 | }) : 141 | [ newTmplItem( options, parentItem, tmpl, data ) ]; 142 | 143 | return topLevel ? jQuery( build( parentItem, null, ret ) ) : ret; 144 | }, 145 | 146 | // Return rendered template item for an element. 147 | tmplItem: function( elem ) { 148 | var tmplItem; 149 | if ( elem instanceof jQuery ) { 150 | elem = tmpl[0]; 151 | } 152 | while ( elem && elem.nodeType === 1 && !(tmplItem = jQuery.data( elem, "tmplItem" )) && (elem = elem.parentNode) ) {} 153 | return tmplItem || topTmplItem; 154 | }, 155 | 156 | // Set: 157 | // Use $.templates( name, tmpl ) to cache a named template, 158 | // where tmpl is a template string, a script element or a jQuery instance wrapping a script element, etc. 159 | // Use $( "selector" ).templates( name ) to provide access by name to a script block template declaration. 160 | 161 | // Get: 162 | // Use $.templates( name ) to access a cached template. 163 | // Also $( selectorToScriptBlock ).templates(), or $.templates( null, templateString ) 164 | // will return the compiled template, without adding a name reference. 165 | templates: function( name, tmpl ) { 166 | if (tmpl) { 167 | // Compile template and associate with name 168 | if ( typeof tmpl === "string" ) { 169 | // This is an HTML string being passed directly in. 170 | tmpl = buildTmplFn( tmpl ) 171 | } else if ( tmpl instanceof jQuery ) { 172 | tmpl = tmpl[0] || {}; 173 | } 174 | if ( tmpl.nodeType ) { 175 | // If this is a template block, use cached copy, or generate tmpl function and cache. 176 | tmpl = jQuery.data( tmpl, "tmpl" ) || jQuery.data( tmpl, "tmpl", buildTmplFn( tmpl.innerHTML )); 177 | } 178 | return name ? (jQuery.templates[name] = tmpl) : tmpl; 179 | } 180 | // Return named compiled template 181 | return typeof name !== "string" ? null : 182 | (jQuery.templates[name] || 183 | // If not in map, treat as a selector. 184 | jQuery.templates( null, jQuery( name ))); 185 | }, 186 | 187 | encode: function( text ) { 188 | // Do HTML encoding replacing < > & and ' and " by corresponding entities. 189 | return ("" + text).split("<").join("<").split(">").join(">").split('"').join(""").split("'").join("'"); 190 | } 191 | }); 192 | 193 | jQuery.extend( jQuery.tmpl, { 194 | tags: { 195 | "tmpl": { 196 | _default: { $2: "null" }, 197 | prefix: "if($notnull_1){_=_.concat($item.nest($1,$2));}" 198 | // tmpl target parameter can be of type function, so use $1, not $1a (so not auto detection of functions) 199 | // This means that {{tmpl foo}} treats foo as a template (which IS a function). 200 | // Explicit parens can be used if foo is a function that returns a template: {{tmpl foo()}}. 201 | }, 202 | "each": { 203 | _default: { $2: "$index, $value" }, 204 | prefix: "if($notnull_1){$.each($1a,function($2){with(this){", 205 | suffix: "}});}" 206 | }, 207 | "if": { 208 | prefix: "if(($notnull_1) && $1a){", 209 | suffix: "}" 210 | }, 211 | "else": { 212 | prefix: "}else{" 213 | }, 214 | "html": { 215 | prefix: "if($notnull_1){_.push($1a);}" 216 | }, 217 | "=": { 218 | _default: { $1: "$data" }, 219 | prefix: "if($notnull_1){_.push($.encode($1a));}" 220 | } 221 | }, 222 | 223 | // This stub can be overridden, e.g. in jquery.tmplPlus for providing rendered events 224 | complete: function( items ) { 225 | newTmplItems = {}; 226 | }, 227 | 228 | // Call this from code which overrides domManip, or equivalent 229 | // Manage cloning/storing template items etc. 230 | afterManip: function afterManip( elem, fragClone, callback ) { 231 | // Provides cloned fragment ready for fixup prior to and after insertion into DOM 232 | var content = fragClone.nodeType === 11 ? 233 | jQuery.makeArray(fragClone.childNodes) : 234 | fragClone.nodeType === 1 ? [fragClone] : []; 235 | 236 | // Return fragment to original caller (e.g. append) for DOM insertion 237 | callback.call( elem, fragClone ); 238 | 239 | // Fragment has been inserted:- Add inserted nodes to tmplItem data structure. Replace inserted element annotations by jQuery.data. 240 | storeTmplItems( content ); 241 | cloneIndex++; 242 | } 243 | }); 244 | 245 | //========================== Private helper functions, used by code above ========================== 246 | 247 | function build( tmplItem, parent, content ) { 248 | // Convert hierarchical content into flat string array 249 | // and finally return array of fragments ready for DOM insertion 250 | var frag, ret = jQuery.map( content, function( item ) { 251 | return (typeof item === "string") ? 252 | // Insert template item annotations, to be converted to jQuery.data( "tmplItem" ) when elems are inserted into DOM. 253 | item.replace( /(<\w+)([^>]*)/g, "$1 " + tmplItmAtt + "=\"" + tmplItem.key + "\" $2" ) : 254 | // This is a child template item. Build nested template. 255 | build( item, tmplItem, item.content ); 256 | }); 257 | if ( parent ) { 258 | // nested template 259 | return ret; 260 | } 261 | // top-level template 262 | ret = ret.join(""); 263 | 264 | // Support templates which have initial or final text nodes, or consist only of text 265 | // Also support HTML entities within the HTML markup. 266 | ret.replace( /^\s*([^<\s][^<]*)?(<[\w\W]+>)([^>]*[^>\s])?\s*$/, function( all, before, middle, after) { 267 | frag = jQuery( middle ).get(); // For now use get(), since buildFragment is not current public 268 | // frag = jQuery.buildFragment( [middle] ); // If buildFragment was public, could do these two lines instead 269 | // frag = frag.cacheable ? frag.fragment.cloneNode(true) : frag.fragment; 270 | 271 | storeTmplItems( frag ); 272 | if ( before ) { 273 | frag = unencode( before ).concat(frag); 274 | } 275 | if ( after ) { 276 | frag = frag.concat(unencode( after )); 277 | } 278 | }); 279 | return frag ? frag : unencode( ret ); 280 | } 281 | 282 | function unencode( text ) { 283 | // createTextNode will not render HTML entities correctly 284 | var el = document.createElement( "div" ); 285 | el.innerHTML = text; 286 | return jQuery.makeArray(el.childNodes); 287 | } 288 | 289 | // Generate a reusable function that will serve to render a template against data 290 | function buildTmplFn( markup ) { 291 | return new Function("jQuery","$item", 292 | "var $=jQuery,_=[],$data=$item.data;" + 293 | 294 | // Introduce the data as local variables using with(){} 295 | "with($data){_.push('" + 296 | 297 | // Convert the template into pure JavaScript 298 | $.trim(markup) 299 | .replace( /([\\'])/g, "\\$1" ) 300 | .replace( /[\r\t\n]/g, " " ) 301 | .replace( /\${([^}]*)}/g, "{{= $1}}" ) 302 | .replace( /{{(\/?)(\w+|.)(?:\(((?:.(?!}}))*?)?\))?(?:\s+(.*?)?)?(\((.*?)\))?\s*}}/g, 303 | function( all, slash, type, fnargs, target, parens, args ) { 304 | var cmd = jQuery.tmpl.tags[ type ], def, expr; 305 | if ( !cmd ) { 306 | throw "Template command not found: " + type; 307 | } 308 | def = cmd._default || []; 309 | if ( target ) { 310 | target = unescape( target ); 311 | args = args ? ("," + unescape( args ) + ")") : (parens ? ")" : ""); 312 | if ( parens && target.indexOf(".") > -1 ) { 313 | // Support for target being things like a.toLowerCase(); 314 | // In that case don't call with template item as 'this' pointer. Just evaluate... 315 | target += parens; 316 | args = ""; 317 | } 318 | expr = args ? ("(" + target + ").call($item" + args) : target; 319 | exprAutoFnDetect = args ? expr: "(typeof(" + target + ")==='function'?(" + target + ").call($item):(" + target + "))"; 320 | } else { 321 | expr = def["$1"] || "null"; 322 | } 323 | fnargs = unescape( fnargs ); 324 | return "');" + 325 | cmd[ slash ? "suffix" : "prefix" ] 326 | .split( "$notnull_1" ).join( "typeof(" + target + ")!=='undefined' && (" + target + ")!=null" ) 327 | .split( "$1a" ).join( exprAutoFnDetect ) 328 | .split( "$1" ).join( expr ) 329 | .split( "$2" ).join( fnargs ? 330 | fnargs.replace( /\s*([^\(]+)\s*(\((.*?)\))?/g, function( all, name, parens, params ) { 331 | params = params ? ("," + params + ")") : (parens ? ")" : ""); 332 | return params ? ("(" + name + ").call($item" + params) : all; 333 | }) 334 | : (def["$2"]||"") 335 | ) + 336 | "_.push('"; 337 | }) + 338 | "');}return _;" 339 | ); 340 | } 341 | 342 | function unescape( args ) { 343 | return args ? args.replace( /\\'/g, "'").replace(/\\\\/g, "\\" ) : null; 344 | } 345 | 346 | function nest( tmpl, data, options ) { 347 | // nested template, using {{tmpl}} tag 348 | return jQuery.tmpl( 349 | typeof tmpl === "string" ? jQuery.templates( tmpl ) : tmpl, 350 | data, 351 | options, 352 | this 353 | ); 354 | } 355 | 356 | // Store template items in jQuery.data(), ensuring a unique tmplItem data data structure for each rendered template instance. 357 | function storeTmplItems( content ) { 358 | var keySuffix = "_" + cloneIndex, elem, elems, newClonedItems = {}; 359 | for ( var i = 0, l = content.length; i < l; i++ ) { 360 | if ( (elem = content[i]).nodeType !== 1 ) { 361 | continue; 362 | } 363 | elems = elem.getElementsByTagName("*"); 364 | for ( var j = 0, m = elems.length; j < m; j++) { 365 | processItemKey( elems[j] ); 366 | } 367 | processItemKey( elem ); 368 | } 369 | 370 | function processItemKey( el ) { 371 | var pntKey, pntNode = el, pntItem, pntNodeItem, tmplItem, key; 372 | // Ensure that each rendered template inserted into the DOM has its own template item, 373 | if ( key = el.getAttribute( tmplItmAtt )) { 374 | while ((pntNode = pntNode.parentNode).nodeType === 1 && !(pntKey = pntNode.getAttribute( tmplItmAtt ))) { } 375 | if ( pntKey !== key ) { 376 | // The next ancestor with a _tmplitem expando is on a different key than this one. 377 | // So this is a top-level element within this template item 378 | tmplItem = newTmplItems[key]; 379 | if ( cloneIndex ) { 380 | cloneTmplItem( key ); 381 | } 382 | pntNodeItem = el.parentNode; 383 | pntNodeItem = pntNodeItem.nodeType === 11 ? 0 : (pntNodeItem.getAttribute( tmplItmAtt ) || 0); 384 | } 385 | el.removeAttribute( tmplItmAtt ); 386 | } else if ( cloneIndex && (tmplItem = jQuery.data( el, "tmplItem" )) ) { 387 | // This was a rendered element, cloned during append or appendTo etc. 388 | // TmplItem stored in jQuery data has already been cloned in cloneCopyEvent. We must replace it with a fresh cloned tmplItem. 389 | cloneTmplItem( tmplItem.key ); 390 | newTmplItems[tmplItem.key] = tmplItem; 391 | pntNodeItem = jQuery.data( el.parentNode, "tmplItem" ); 392 | pntNodeItem = pntNodeItem ? pntNodeItem.key : 0; 393 | } 394 | if ( tmplItem ) { 395 | pntItem = tmplItem; 396 | // Find the template item of the parent element 397 | while ( pntItem && pntItem.key != pntNodeItem ) { 398 | // Add this element as a top-level node for this rendered template item, as well as for any 399 | // ancestor items between this item and the item of its parent element 400 | pntItem.nodes.push( el ); 401 | pntItem = pntItem.parent; 402 | } 403 | delete tmplItem.content; // Could keep this available. Currently deleting to reduce API surface area, and memory use... 404 | // Store template item as jQuery data on the element 405 | jQuery.data( el, "tmplItem", tmplItem ); 406 | } 407 | function cloneTmplItem( key ) { 408 | key = key + keySuffix; 409 | tmplItem = newClonedItems[key] 410 | = (newClonedItems[key] || newTmplItem( tmplItem, newTmplItems[tmplItem.parent.key + keySuffix] || tmplItem.parent, null, true )); 411 | } 412 | } 413 | } 414 | })(jQuery); 415 | -------------------------------------------------------------------------------- /lib/rack/oauth2/admin/js/sammy.js: -------------------------------------------------------------------------------- 1 | // -- Sammy -- /sammy.js 2 | // http://code.quirkey.com/sammy 3 | // Version: 0.6.2 4 | // Built: Mon Oct 11 12:41:51 -0700 2010 5 | (function(g,i){var n,f="([^/]+)",j=/:([\w\d]+)/g,k=/\?([^#]*)$/,b=function(o){return Array.prototype.slice.call(o)},c=function(o){return Object.prototype.toString.call(o)==="[object Function]"},l=function(o){return Object.prototype.toString.call(o)==="[object Array]"},h=decodeURIComponent,e=function(o){return o.replace(/&/g,"&").replace(//g,">")},m=function(o){return function(p,q){return this.route.apply(this,[o,p,q])}},a={},d=[];n=function(){var p=b(arguments),q,o;n.apps=n.apps||{};if(p.length===0||p[0]&&c(p[0])){return n.apply(n,["body"].concat(p))}else{if(typeof(o=p.shift())=="string"){q=n.apps[o]||new n.Application();q.element_selector=o;if(p.length>0){g.each(p,function(r,s){q.use(s)})}if(q.element_selector!=o){delete n.apps[o]}n.apps[q.element_selector]=q;return q}}};n.VERSION="0.6.2";n.addLogger=function(o){d.push(o)};n.log=function(){var o=b(arguments);o.unshift("["+Date()+"]");g.each(d,function(q,p){p.apply(n,o)})};if(typeof i.console!="undefined"){if(c(console.log.apply)){n.addLogger(function(){i.console.log.apply(console,arguments)})}else{n.addLogger(function(){i.console.log(arguments)})}}else{if(typeof console!="undefined"){n.addLogger(function(){console.log.apply(console,arguments)})}}g.extend(n,{makeArray:b,isFunction:c,isArray:l});n.Object=function(o){return g.extend(this,o||{})};g.extend(n.Object.prototype,{escapeHTML:e,h:e,toHash:function(){var o={};g.each(this,function(q,p){if(!c(p)){o[q]=p}});return o},toHTML:function(){var o="";g.each(this,function(q,p){if(!c(p)){o+=""+q+" "+p+"
"}});return o},keys:function(o){var p=[];for(var q in this){if(!c(this[q])||!o){p.push(q)}}return p},has:function(o){return this[o]&&g.trim(this[o].toString())!=""},join:function(){var p=b(arguments);var o=p.shift();return p.join(o)},log:function(){n.log.apply(n,arguments)},toString:function(o){var p=[];g.each(this,function(r,q){if(!c(q)||o){p.push('"'+r+'": '+q.toString())}});return"Sammy.Object: {"+p.join(",")+"}"}});n.HashLocationProxy=function(p,o){this.app=p;this.is_native=false;this._startPolling(o)};n.HashLocationProxy.prototype={bind:function(){var o=this,p=this.app;g(i).bind("hashchange."+this.app.eventNamespace(),function(r,q){if(o.is_native===false&&!q){n.log("native hash change exists, using");o.is_native=true;i.clearInterval(n.HashLocationProxy._interval)}p.trigger("location-changed")});if(!n.HashLocationProxy._bindings){n.HashLocationProxy._bindings=0}n.HashLocationProxy._bindings++},unbind:function(){g(i).unbind("hashchange."+this.app.eventNamespace());n.HashLocationProxy._bindings--;if(n.HashLocationProxy._bindings<=0){i.clearInterval(n.HashLocationProxy._interval)}},getLocation:function(){var o=i.location.toString().match(/^[^#]*(#.+)$/);return o?o[1]:""},setLocation:function(o){return(i.location=o)},_startPolling:function(q){var p=this;if(!n.HashLocationProxy._interval){if(!q){q=10}var o=function(){var r=p.getLocation();if(!n.HashLocationProxy._last_location||r!=n.HashLocationProxy._last_location){i.setTimeout(function(){g(i).trigger("hashchange",[true])},13)}n.HashLocationProxy._last_location=r};o();n.HashLocationProxy._interval=i.setInterval(o,q)}}};n.Application=function(o){var p=this;this.routes={};this.listeners=new n.Object({});this.arounds=[];this.befores=[];this.namespace=(new Date()).getTime()+"-"+parseInt(Math.random()*1000,10);this.context_prototype=function(){n.EventContext.apply(this,arguments)};this.context_prototype.prototype=new n.EventContext();if(c(o)){o.apply(this,[this])}if(!this._location_proxy){this.setLocationProxy(new n.HashLocationProxy(this,this.run_interval_every))}if(this.debug){this.bindToAllEvents(function(r,q){p.log(p.toString(),r.cleaned_type,q||{})})}};n.Application.prototype=g.extend({},n.Object.prototype,{ROUTE_VERBS:["get","post","put","delete"],APP_EVENTS:["run","unload","lookup-route","run-route","route-found","event-context-before","event-context-after","changed","error","check-form-submission","redirect","location-changed"],_last_route:null,_location_proxy:null,_running:false,element_selector:"body",debug:false,raise_errors:false,run_interval_every:50,template_engine:null,toString:function(){return"Sammy.Application:"+this.element_selector},$element:function(){return g(this.element_selector)},use:function(){var o=b(arguments),q=o.shift(),p=q||"";try{o.unshift(this);if(typeof q=="string"){p="Sammy."+q;q=n[q]}q.apply(this,o)}catch(r){if(typeof q==="undefined"){this.error("Plugin Error: called use() but plugin ("+p.toString()+") is not defined",r)}else{if(!c(q)){this.error("Plugin Error: called use() but '"+p.toString()+"' is not a function",r)}else{this.error("Plugin Error",r)}}}return this},setLocationProxy:function(o){var p=this._location_proxy;this._location_proxy=o;if(this.isRunning()){if(p){p.unbind()}this._location_proxy.bind()}},route:function(s,p,u){var r=this,t=[],o,q;if(!u&&c(p)){p=s;u=p;s="any"}s=s.toLowerCase();if(p.constructor==String){j.lastIndex=0;while((q=j.exec(p))!==null){t.push(q[1])}p=new RegExp("^"+p.replace(j,f)+"$")}if(typeof u=="string"){u=r[u]}o=function(v){var w={verb:v,path:p,callback:u,param_names:t};r.routes[v]=r.routes[v]||[];r.routes[v].push(w)};if(s==="any"){g.each(this.ROUTE_VERBS,function(x,w){o(w)})}else{o(s)}return this},get:m("get"),post:m("post"),put:m("put"),del:m("delete"),any:m("any"),mapRoutes:function(p){var o=this;g.each(p,function(q,r){o.route.apply(o,r)});return this},eventNamespace:function(){return["sammy-app",this.namespace].join("-")},bind:function(o,q,s){var r=this;if(typeof s=="undefined"){s=q}var p=function(){var v,t,u;v=arguments[0];u=arguments[1];if(u&&u.context){t=u.context;delete u.context}else{t=new r.context_prototype(r,"bind",v.type,u,v.target)}v.cleaned_type=v.type.replace(r.eventNamespace(),"");s.apply(t,[v,u])};if(!this.listeners[o]){this.listeners[o]=[]}this.listeners[o].push(p);if(this.isRunning()){this._listen(o,p)}return this},trigger:function(o,p){this.$element().trigger([o,this.eventNamespace()].join("."),[p]);return this},refresh:function(){this.last_location=null;this.trigger("location-changed");return this},before:function(o,p){if(c(o)){p=o;o={}}this.befores.push([o,p]);return this},after:function(o){return this.bind("event-context-after",o)},around:function(o){this.arounds.push(o);return this},isRunning:function(){return this._running},helpers:function(o){g.extend(this.context_prototype.prototype,o);return this},helper:function(o,p){this.context_prototype.prototype[o]=p;return this},run:function(o){if(this.isRunning()){return false}var p=this;g.each(this.listeners.toHash(),function(q,r){g.each(r,function(t,s){p._listen(q,s)})});this.trigger("run",{start_url:o});this._running=true;this.last_location=null;if(this.getLocation()==""&&typeof o!="undefined"){this.setLocation(o)}this._checkLocation();this._location_proxy.bind();this.bind("location-changed",function(){p._checkLocation()});this.bind("submit",function(r){var q=p._checkFormSubmission(g(r.target).closest("form"));return(q===false)?r.preventDefault():false});g(i).bind("beforeunload",function(){p.unload()});return this.trigger("changed")},unload:function(){if(!this.isRunning()){return false}var o=this;this.trigger("unload");this._location_proxy.unbind();this.$element().unbind("submit").removeClass(o.eventNamespace());g.each(this.listeners.toHash(),function(p,q){g.each(q,function(s,r){o._unlisten(p,r)})});this._running=false;return this},bindToAllEvents:function(p){var o=this;g.each(this.APP_EVENTS,function(q,r){o.bind(r,p)});g.each(this.listeners.keys(true),function(r,q){if(o.APP_EVENTS.indexOf(q)==-1){o.bind(q,p)}});return this},routablePath:function(o){return o.replace(k,"")},lookupRoute:function(r,p){var q=this,o=false;this.trigger("lookup-route",{verb:r,path:p});if(typeof this.routes[r]!="undefined"){g.each(this.routes[r],function(t,s){if(q.routablePath(p).match(s.path)){o=s;return false}})}return o},runRoute:function(q,D,s,v){var r=this,B=this.lookupRoute(q,D),p,y,t,x,C,z,w,A,o;this.log("runRoute",[q,D].join(" "));this.trigger("run-route",{verb:q,path:D,params:s});if(typeof s=="undefined"){s={}}g.extend(s,this._parseQueryString(D));if(B){this.trigger("route-found",{route:B});if((A=B.path.exec(this.routablePath(D)))!==null){A.shift();g.each(A,function(E,F){if(B.param_names[E]){s[B.param_names[E]]=h(F)}else{if(!s.splat){s.splat=[]}s.splat.push(h(F))}})}p=new this.context_prototype(this,q,D,s,v);t=this.arounds.slice(0);C=this.befores.slice(0);w=[p].concat(s.splat);y=function(){var E;while(C.length>0){z=C.shift();if(r.contextMatchesOptions(p,z[0])){E=z[1].apply(p,[p]);if(E===false){return false}}}r.last_route=B;p.trigger("event-context-before",{context:p});E=B.callback.apply(p,w);p.trigger("event-context-after",{context:p});return E};g.each(t.reverse(),function(E,F){var G=y;y=function(){return F.apply(p,[G])}});try{o=y()}catch(u){this.error(["500 Error",q,D].join(" "),u)}return o}else{return this.notFound(q,D)}},contextMatchesOptions:function(r,t,p){var q=t;if(typeof q==="undefined"||q=={}){return true}if(typeof p==="undefined"){p=true}if(typeof q==="string"||c(q.test)){q={path:q}}if(q.only){return this.contextMatchesOptions(r,q.only,true)}else{if(q.except){return this.contextMatchesOptions(r,q.except,false)}}var o=true,s=true;if(q.path){if(c(q.path.test)){o=q.path.test(r.path)}else{o=(q.path.toString()===r.path)}}if(q.verb){s=q.verb===r.verb}return p?(s&&o):!(s&&o)},getLocation:function(){return this._location_proxy.getLocation()},setLocation:function(o){return this._location_proxy.setLocation(o)},swap:function(o){return this.$element().html(o)},templateCache:function(o,p){if(typeof p!="undefined"){return a[o]=p}else{return a[o]}},clearTemplateCache:function(){return a={}},notFound:function(q,p){var o=this.error(["404 Not Found",q,p].join(" "));return(q==="get")?o:true},error:function(p,o){if(!o){o=new Error()}o.message=[p,o.message].join(" ");this.trigger("error",{message:o.message,error:o});if(this.raise_errors){throw (o)}else{this.log(o.message,o)}},_checkLocation:function(){var o,p;o=this.getLocation();if(!this.last_location||this.last_location[0]!="get"||this.last_location[1]!=o){this.last_location=["get",o];p=this.runRoute("get",o)}return p},_getFormVerb:function(q){var p=g(q),r,o;o=p.find('input[name="_method"]');if(o.length>0){r=o.val()}if(!r){r=p[0].getAttribute("method")}return g.trim(r.toString().toLowerCase())},_checkFormSubmission:function(q){var o,r,t,s,p;this.trigger("check-form-submission",{form:q});o=g(q);r=o.attr("action");t=this._getFormVerb(o);if(!t||t==""){t="get"}this.log("_checkFormSubmission",o,r,t);if(t==="get"){this.setLocation(r+"?"+o.serialize());p=false}else{s=g.extend({},this._parseFormParams(o));p=this.runRoute(t,r,s,q.get(0))}return(typeof p=="undefined")?false:p},_parseFormParams:function(o){var r={},q=o.serializeArray(),p;for(p=0;p0){this.then(this.callbacks.shift())}},load:function(o,p,r){var q=this;return this.then(function(){var s,t,v,u;if(c(p)){r=p;p={}}else{p=g.extend({},p)}if(r){this.then(r)}if(typeof o==="string"){v=(o.match(/\.json$/)||p.json);s=((v&&p.cache===true)||p.cache!==false);q.next_engine=q.event_context.engineFor(o);delete p.cache;delete p.json;if(p.engine){q.next_engine=p.engine;delete p.engine}if(s&&(t=this.event_context.app.templateCache(o))){return t}this.wait();g.ajax(g.extend({url:o,data:{},dataType:v?"json":null,type:"get",success:function(w){if(s){q.event_context.app.templateCache(o,w)}q.next(w)}},p));return false}else{if(o.nodeType){return o.innerHTML}if(o.selector){q.next_engine=o.attr("data-engine");if(p.clone===false){return o.remove()[0].innerHTML.toString()}else{return o[0].innerHTML.toString()}}}})},render:function(o,p,q){if(c(o)&&!p){return this.then(o)}else{if(!p&&this.content){p=this.content}return this.load(o).interpolate(p,o).then(q)}},partial:function(o,p){return this.render(o,p).swap()},send:function(){var q=this,p=b(arguments),o=p.shift();if(l(p[0])){p=p[0]}return this.then(function(r){p.push(function(s){q.next(s)});q.wait();o.apply(o,p);return false})},collect:function(s,r,o){var q=this;var p=function(){if(c(s)){r=s;s=this.content}var t=[],u=false;g.each(s,function(v,x){var w=r.apply(q,[v,x]);if(w.jquery&&w.length==1){w=w[0];u=true}t.push(w);return w});return u?t:t.join("")};return o?p():this.then(p)},renderEach:function(o,p,q,r){if(l(p)){r=q;q=p;p=null}return this.load(o).then(function(t){var s=this;if(!q){q=l(this.previous_content)?this.previous_content:[]}if(r){g.each(q,function(u,w){var x={},v=this.next_engine||o;p?(x[p]=w):(x=w);r(w,s.event_context.interpolate(t,x,v))})}else{return this.collect(q,function(u,w){var x={},v=this.next_engine||o;p?(x[p]=w):(x=w);return this.event_context.interpolate(t,x,v)},true)}})},interpolate:function(r,q,o){var p=this;return this.then(function(t,s){if(!r&&s){r=s}if(this.next_engine){q=this.next_engine;this.next_engine=false}var u=p.event_context.interpolate(t,r,q);return o?s+u:u})},swap:function(){return this.then(function(o){this.event_context.swap(o)}).trigger("changed",{})},appendTo:function(o){return this.then(function(p){g(o).append(p)}).trigger("changed",{})},prependTo:function(o){return this.then(function(p){g(o).prepend(p)}).trigger("changed",{})},replace:function(o){return this.then(function(p){g(o).html(p)}).trigger("changed",{})},trigger:function(o,p){return this.then(function(q){if(typeof p=="undefined"){p={content:q}}this.event_context.trigger(o,p)})}});n.EventContext=function(s,r,p,q,o){this.app=s;this.verb=r;this.path=p;this.params=new n.Object(q);this.target=o};n.EventContext.prototype=g.extend({},n.Object.prototype,{$element:function(){return this.app.$element()},engineFor:function(q){var p=this,o;if(c(q)){return q}q=q.toString();if((o=q.match(/\.([^\.]+)$/))){q=o[1]}if(q&&c(p[q])){return p[q]}if(p.app.template_engine){return this.engineFor(p.app.template_engine)}return function(r,s){return r}},interpolate:function(p,q,o){return this.engineFor(o).apply(this,[p,q])},render:function(o,p,q){return new n.RenderContext(this).render(o,p,q)},renderEach:function(o,p,q,r){return new n.RenderContext(this).renderEach(o,p,q,r)},load:function(o,p,q){return new n.RenderContext(this).load(o,p,q)},partial:function(o,p){return new n.RenderContext(this).partial(o,p)},send:function(){var o=new n.RenderContext(this);return o.send.apply(o,arguments)},redirect:function(){var q,p=b(arguments),o=this.app.getLocation();if(p.length>1){p.unshift("/");q=this.join.apply(this,p)}else{q=p[0]}this.trigger("redirect",{to:q});this.app.last_location=[this.verb,this.path];this.app.setLocation(q);if(o==q){this.app.trigger("location-changed")}},trigger:function(o,p){if(typeof p=="undefined"){p={}}if(!p.context){p.context=this}return this.app.trigger(o,p)},eventNamespace:function(){return this.app.eventNamespace()},swap:function(o){return this.app.swap(o)},notFound:function(){return this.app.notFound(this.verb,this.path)},json:function(o){return g.parseJSON(o)},toString:function(){return"Sammy.EventContext: "+[this.verb,this.path,this.params].join(" ")}});g.sammy=i.Sammy=n})(jQuery,window); -------------------------------------------------------------------------------- /lib/rack/oauth2/admin/js/sammy.json.js: -------------------------------------------------------------------------------- 1 | // -- Sammy -- /plugins/sammy.json.js 2 | // http://code.quirkey.com/sammy 3 | // Version: 0.6.2 4 | // Built: Mon Oct 11 12:41:43 -0700 2010 5 | (function($){if(!window.JSON){window.JSON={}}(function(){function f(n){return n<10?"0"+n:n}if(typeof Date.prototype.toJSON!=="function"){Date.prototype.toJSON=function(key){return this.getUTCFullYear()+"-"+f(this.getUTCMonth()+1)+"-"+f(this.getUTCDate())+"T"+f(this.getUTCHours())+":"+f(this.getUTCMinutes())+":"+f(this.getUTCSeconds())+"Z"};String.prototype.toJSON=Number.prototype.toJSON=Boolean.prototype.toJSON=function(key){return this.valueOf()}}var cx=/[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,escapable=/[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,gap,indent,meta={"\b":"\\b","\t":"\\t","\n":"\\n","\f":"\\f","\r":"\\r",'"':'\\"',"\\":"\\\\"},rep;function quote(string){escapable.lastIndex=0;return escapable.test(string)?'"'+string.replace(escapable,function(a){var c=meta[a];return typeof c==="string"?c:"\\u"+("0000"+a.charCodeAt(0).toString(16)).slice(-4)})+'"':'"'+string+'"'}function str(key,holder){var i,k,v,length,mind=gap,partial,value=holder[key];if(value&&typeof value==="object"&&typeof value.toJSON==="function"){value=value.toJSON(key)}if(typeof rep==="function"){value=rep.call(holder,key,value)}switch(typeof value){case"string":return quote(value);case"number":return isFinite(value)?String(value):"null";case"boolean":case"null":return String(value);case"object":if(!value){return"null"}gap+=indent;partial=[];if(Object.prototype.toString.apply(value)==="[object Array]"){length=value.length;for(i=0;i)[^>]*$|\{\{\! /,p={},e={},y,x={key:0,data:{}},w=0,q=0,g=[];function k(B,A,D,E){var C={data:E||(A?A.data:{}),_wrap:A?A._wrap:null,tmpl:null,parent:A||null,nodes:[],calls:c,nest:b,wrap:n,html:r,update:z};if(B){i.extend(C,B,{nodes:[],parent:A})}if(D){C.tmpl=D;C._ctnt=C._ctnt||C.tmpl(i,C);C.key=++w;(g.length?e:p)[w]=C}return C}i.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(A,B){i.fn[A]=function(C){var F=[],I=i(C),E,G,D,J,H=this.length===1&&this[0].parentNode;y=p||{};if(H&&H.nodeType===11&&H.childNodes.length===1&&I.length===1){I[B](this[0]);F=this}else{for(G=0,D=I.length;G0?this.clone(true):this).get();i.fn[B].apply(i(I[G]),E);F=F.concat(E)}q=0;F=this.pushStack(F,A,I.selector)}J=y;y=null;i.tmpl.complete(J);return F}});i.fn.extend({tmpl:function(C,B,A){return i.tmpl(this[0],C,B,A)},tmplItem:function(){return i.tmplItem(this[0])},template:function(A){return i.template(A,this[0])},domManip:function(C,G,H,B){if(C[0]&&C[0].nodeType){var F=i.makeArray(arguments),E=C.length,D=0,A;while(D1){F[0]=[i.makeArray(C)]}if(A&&q){F[2]=function(I){i.tmpl.afterManip(this,I,H)}}t.apply(this,F)}else{t.apply(this,arguments)}q=0;if(!y){i.tmpl.complete(p)}return this}});i.extend({tmpl:function(C,F,E,B){var D,A=!B;if(A){B=x;C=i.template[C]||i.template(null,C);e={}}else{if(!C){C=B.tmpl;p[B.key]=B;B.nodes=[];if(B.wrapped){s(B,B.wrapped)}return i(m(B,null,B.tmpl(i,B)))}}if(!C){return[]}if(typeof F==="function"){F=F.call(B||{})}if(E&&E.wrapped){s(E,E.wrapped)}D=i.isArray(F)?i.map(F,function(G){return G?k(E,B,C,G):null}):[k(E,B,C,F)];return A?i(m(B,null,D)):D},tmplItem:function(B){var A;if(B instanceof i){B=B[0]}while(B&&B.nodeType===1&&!(A=i.data(B,"tmplItem"))&&(B=B.parentNode)){}return A||x},template:function(B,A){if(A){if(typeof A==="string"){A=l(A)}else{if(A instanceof i){A=A[0]||{}}}if(A.nodeType){A=i.data(A,"tmpl")||i.data(A,"tmpl",l(A.innerHTML))}return typeof B==="string"?(i.template[B]=A):A}return B?(typeof B!=="string"?i.template(null,B):(i.template[B]||i.template(null,u.test(B)?B:i(B)))):null},encode:function(A){return(""+A).split("<").join("<").split(">").join(">").split('"').join(""").split("'").join("'")}});i.extend(i.tmpl,{tag:{tmpl:{_default:{$2:"null"},open:"if($notnull_1){_=_.concat($item.nest($1,$2));}"},wrap:{_default:{$2:"null"},open:"$item.calls(_,$1,$2);_=[];",close:"call=$item.calls();_=call._.concat($item.wrap(call,_));"},each:{_default:{$2:"$index, $value"},open:"if($notnull_1){$.each($1a,function($2){with(this){",close:"}});}"},"if":{open:"if(($notnull_1) && $1a){",close:"}"},"else":{_default:{$1:"true"},open:"}else if(($notnull_1) && $1a){"},html:{open:"if($notnull_1){_.push($1a);}"},"=":{_default:{$1:"$data"},open:"if($notnull_1){_.push($.encode($1a));}"},"!":{open:""}},complete:function(A){p={}},afterManip:function v(C,A,D){var B=A.nodeType===11?i.makeArray(A.childNodes):A.nodeType===1?[A]:[];D.call(C,A);o(B);q++}});function m(A,E,C){var D,B=C?i.map(C,function(F){return(typeof F==="string")?(A.key?F.replace(/(<\w+)(?=[\s>])(?![^>]*_tmplitem)([^>]*)/g,"$1 "+h+'="'+A.key+'" $2'):F):m(F,A,F._ctnt)}):A;if(E){return B}B=B.join("");B.replace(/^\s*([^<\s][^<]*)?(<[\w\W]+>)([^>]*[^>\s])?\s*$/,function(G,H,F,I){D=i(F).get();o(D);if(H){D=a(H).concat(D)}if(I){D=D.concat(a(I))}});return D?D:a(B)}function a(B){var A=document.createElement("div");A.innerHTML=B;return i.makeArray(A.childNodes)}function l(A){return new Function("jQuery","$item","var $=jQuery,call,_=[],$data=$item.data;with($data){_.push('"+i.trim(A).replace(/([\\'])/g,"\\$1").replace(/[\r\t\n]/g," ").replace(/\$\{([^\}]*)\}/g,"{{= $1}}").replace(/\{\{(\/?)(\w+|.)(?:\(((?:[^\}]|\}(?!\}))*?)?\))?(?:\s+(.*?)?)?(\(((?:[^\}]|\}(?!\}))*?)\))?\s*\}\}/g,function(I,C,G,D,E,J,F){var L=i.tmpl.tag[G],B,H,K;if(!L){throw"Template command not found: "+G}B=L._default||[];if(J&&!/\w$/.test(E)){E+=J;J=""}if(E){E=j(E);F=F?(","+j(F)+")"):(J?")":"");H=J?(E.indexOf(".")>-1?E+J:("("+E+").call($item"+F)):E;K=J?H:"(typeof("+E+")==='function'?("+E+").call($item):("+E+"))"}else{K=H=B.$1||"null"}D=j(D);return"');"+L[C?"close":"open"].split("$notnull_1").join(E?"typeof("+E+")!=='undefined' && ("+E+")!=null":"true").split("$1a").join(K).split("$1").join(H).split("$2").join(D?D.replace(/\s*([^\(]+)\s*(\((.*?)\))?/g,function(N,M,O,P){P=P?(","+P+")"):(O?")":"");return P?("("+M+").call($item"+P):N}):(B.$2||""))+"_.push('"})+"');}return _;")}function s(B,A){B._wrap=m(B,true,i.isArray(A)?A:[u.test(A)?A:i(A).html()]).join("")}function j(A){return A?A.replace(/\\'/g,"'").replace(/\\\\/g,"\\"):null}function d(A){var B=document.createElement("div");B.appendChild(A.cloneNode(true));return B.innerHTML}function o(G){var I="_"+q,B,A,E={},F,D,C;for(F=0,D=G.length;F=0;C--){H(A[C])}H(B)}function H(O){var L,N=O,M,J,K;if((K=O.getAttribute(h))){while(N.parentNode&&(N=N.parentNode).nodeType===1&&!(L=N.getAttribute(h))){}if(L!==K){N=N.parentNode?(N.nodeType===11?0:(N.getAttribute(h)||0)):0;if(!(J=p[K])){J=e[K];J=k(J,p[N]||e[N],null,true);J.key=++w;p[w]=J}if(q){P(K)}}O.removeAttribute(h)}else{if(q&&(J=i.data(O,"tmplItem"))){P(J.key);p[J.key]=J;N=i.data(O.parentNode,"tmplItem");N=N?N.key:0}}if(J){M=J;while(M&&M.key!=N){M.nodes.push(O);M=M.parent}delete J._ctnt;delete J._wrap;i.data(O,"tmplItem",J)}function P(Q){Q=Q+I;J=E[Q]=(E[Q]||k(J,p[J.parent.key+I]||J.parent,null,true))}}}function c(C,A,D,B){if(!C){return g.pop()}g.push({_:C,tmpl:A,item:this,data:D,options:B})}function b(A,C,B){return i.tmpl(i.template(A),C,B,this)}function n(C,A){var B=C.options||{};B.wrapped=A;return i.tmpl(i.template(C.tmpl),C.data,B,C.item)}function r(B,C){var A=this._wrap;return i.map(i(i.isArray(A)?A.join(""):A).filter(B||"*"),function(D){return C?D.innerText||D.textContent:D.outerHTML||d(D)})}function z(){var A=this.nodes;i.tmpl(null,null,null,this).insertBefore(A[0]);i(A).remove()}Sammy=Sammy||{};Sammy.Tmpl=function(C,A){var B=function(E,F,D){if(typeof D=="undefined"){D=E}if(!i.template[D]){i.template(D,E)}return i.tmpl(D,i.extend({},this,F))};if(!A){A="tmpl"}C.helper(A,B)}})(jQuery); 6 | -------------------------------------------------------------------------------- /lib/rack/oauth2/admin/views/client.tmpl: -------------------------------------------------------------------------------- 1 |
2 |
3 |

{{if imageUrl}}{{/if}} ${displayName}

4 |
5 | Edit 6 | {{if !revoked}} 7 | Revoke 8 | {{/if}} 9 | Delete 10 |
11 |
Site: ${link}
12 |
13 | Created {{html $.shortdate(revoked)}} 14 | {{if revoked}}Revoked {{html $.shortdate(revoked)}}{{/if}} 15 |
16 | {{each notes}}

${this}

{{/each}} 17 |
18 |
19 |
20 |
    21 |
  • ${$.thousands(tokens.total)}Granted
  • 22 |
  • ${$.thousands(tokens.week)}This Week
  • 23 |
  • ${$.thousands(tokens.revoked)}Revoked (Week)
  • 24 |
25 |
26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | {{each tokens.list}} 37 | 38 | 39 | 40 | 41 | 42 | 43 | 50 | 51 | {{/each}} 52 | 53 |
TokenIdentityScopeCreatedAccessedRevoked
${token}{{if link}}${identity}{{else}}${identity}{{/if}}${scope}{{html $.shortdate(created)}}{{if last_access}}{{html $.shortdate(last_access)}}{{/if}} 44 | {{if revoked}} 45 | {{html $.shortdate(revoked)}} 46 | {{else}} 47 | Revoke 48 | {{/if}} 49 |
54 | 58 |
59 | -------------------------------------------------------------------------------- /lib/rack/oauth2/admin/views/clients.tmpl: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
    5 |
  • ${$.thousands(tokens.total)}Granted
  • 6 |
  • ${$.thousands(tokens.week)}This Week
  • 7 |
  • ${$.thousands(tokens.revoked)}Revoked (Week)
  • 8 |
9 |
10 | Add New Client 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | {{each clients}} 19 | 20 | 26 | 34 | 35 | 36 | 37 | {{/each}} 38 |
ApplicationID/SecretCreatedRevoked
21 | 22 | {{if imageUrl}}{{/if}} 23 | ${displayName.trim() == "" ? "untitled" : displayName} 24 | 25 | 27 | Reveal 28 |
29 |
ID
${id}
30 |
Secret
${secret}
31 |
Redirect
${redirectUri}
32 |
33 |
{{html $.shortdate(created)}}{{if revoked}}{{html $.shortdate(revoked)}}{{/if}}
39 |
40 | 53 | -------------------------------------------------------------------------------- /lib/rack/oauth2/admin/views/edit.tmpl: -------------------------------------------------------------------------------- 1 | {{if id}} 2 | {{else}}{{/if}} 3 |
4 |

Identification

5 | 6 | 10 | 14 | 18 | 22 | 26 | {{if id}} 27 | {{else}}{{/if}} 28 |
29 |
30 |

Scope

31 | {{each common}} 32 | 33 | {{/each}} 34 | {{each scope}} 35 | {{if common.indexOf(this.toString()) < 0}} 36 | 37 | {{/if}} 38 | {{/each}} 39 | 43 |
44 |
45 | 46 | 81 | -------------------------------------------------------------------------------- /lib/rack/oauth2/admin/views/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | OAuth Console 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 28 |
29 |
30 | 33 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /lib/rack/oauth2/admin/views/no_access.tmpl: -------------------------------------------------------------------------------- 1 |
2 |

${error}

3 |

You can try to authenticate again

4 |
5 | -------------------------------------------------------------------------------- /lib/rack/oauth2/models.rb: -------------------------------------------------------------------------------- 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 | 10 | class << self 11 | # Create new instance of the klass and populate its attributes. 12 | def new_instance(klass, fields) 13 | return unless fields 14 | instance = klass.new 15 | fields.each do |name, value| 16 | instance.instance_variable_set :"@#{name}", value 17 | end 18 | instance 19 | end 20 | 21 | # Long, random and hexy. 22 | def secure_random 23 | OpenSSL::Random.random_bytes(32).unpack("H*")[0] 24 | end 25 | 26 | # @private 27 | def create_indexes(&block) 28 | if block 29 | @create_indexes ||= [] 30 | @create_indexes << block 31 | elsif @create_indexes 32 | @create_indexes.each do |block| 33 | block.call 34 | end 35 | @create_indexes = nil 36 | end 37 | end 38 | 39 | # A Mongo::DB object. 40 | def database 41 | @database ||= Server.options.database 42 | raise "No database Configured. You must configure it using Server.options.database = Mongo::Connection.new()[db_name]" unless @database 43 | raise "You set Server.database to #{@database.class}, should be a Mongo::DB object" unless Mongo::DB === @database 44 | @database 45 | end 46 | end 47 | 48 | end 49 | end 50 | end 51 | 52 | 53 | require "rack/oauth2/models/client" 54 | require "rack/oauth2/models/auth_request" 55 | require "rack/oauth2/models/access_grant" 56 | require "rack/oauth2/models/access_token" 57 | require "rack/oauth2/models/issuer" 58 | -------------------------------------------------------------------------------- /lib/rack/oauth2/models/access_grant.rb: -------------------------------------------------------------------------------- 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 8 | class << self 9 | # Find AccessGrant from authentication code. 10 | def from_code(code) 11 | Server.new_instance self, collection.find_one({ :_id=>code, :revoked=>nil }) 12 | end 13 | 14 | # Create a new access grant. 15 | def create(identity, client, scope, redirect_uri = nil, expires = nil) 16 | raise ArgumentError, "Identity must be String or Integer" unless String === identity || Integer === identity 17 | scope = Utils.normalize_scope(scope) & client.scope # Only allowed scope 18 | expires_at = Time.now.to_i + (expires || 300) 19 | fields = { :_id=>Server.secure_random, :identity=>identity, :scope=>scope, 20 | :client_id=>client.id, :redirect_uri=>client.redirect_uri || redirect_uri, 21 | :created_at=>Time.now.to_i, :expires_at=>expires_at, :granted_at=>nil, 22 | :access_token=>nil, :revoked=>nil } 23 | collection.insert fields 24 | Server.new_instance self, fields 25 | end 26 | 27 | def collection 28 | prefix = Server.options[:collection_prefix] 29 | Server.database["#{prefix}.access_grants"] 30 | end 31 | end 32 | 33 | # Authorization code. We are nothing without it. 34 | attr_reader :_id 35 | alias :code :_id 36 | # The identity we authorized access to. 37 | attr_reader :identity 38 | # Client that was granted this access token. 39 | attr_reader :client_id 40 | # Redirect URI for this grant. 41 | attr_reader :redirect_uri 42 | # The scope requested in this grant. 43 | attr_reader :scope 44 | # Does what it says on the label. 45 | attr_reader :created_at 46 | # Tells us when (and if) access token was created. 47 | attr_accessor :granted_at 48 | # Tells us when this grant expires. 49 | attr_accessor :expires_at 50 | # Access token created from this grant. Set and spent. 51 | attr_accessor :access_token 52 | # Timestamp if revoked. 53 | attr_accessor :revoked 54 | 55 | # Authorize access and return new access token. 56 | # 57 | # Access grant can only be redeemed once, but client can make multiple 58 | # requests to obtain it, so we need to make sure only first request is 59 | # successful in returning access token, futher requests raise 60 | # InvalidGrantError. 61 | def authorize!(expires_in = nil) 62 | raise InvalidGrantError, "You can't use the same access grant twice" if self.access_token || self.revoked 63 | client = Client.find(client_id) or raise InvalidGrantError 64 | access_token = AccessToken.get_token_for(identity, client, scope, expires_in) 65 | self.access_token = access_token.token 66 | self.granted_at = Time.now.to_i 67 | self.class.collection.update({ :_id=>code, :access_token=>nil, :revoked=>nil }, { :$set=>{ :granted_at=>granted_at, :access_token=>access_token.token } }, :safe=>true) 68 | reload = self.class.collection.find_one({ :_id=>code, :revoked=>nil }, { :fields=>%w{access_token} }) 69 | raise InvalidGrantError unless reload && reload["access_token"] == access_token.token 70 | return access_token 71 | end 72 | 73 | def revoke! 74 | self.revoked = Time.now.to_i 75 | self.class.collection.update({ :_id=>code, :revoked=>nil }, { :$set=>{ :revoked=>revoked } }) 76 | end 77 | 78 | Server.create_indexes do 79 | # Used to revoke all pending access grants when revoking client. 80 | collection.create_index [[:client_id, Mongo::ASCENDING]] 81 | end 82 | end 83 | 84 | end 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /lib/rack/oauth2/models/access_token.rb: -------------------------------------------------------------------------------- 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 10 | class << self 11 | 12 | # Find AccessToken from token. Does not return revoked tokens. 13 | def from_token(token) 14 | Server.new_instance self, collection.find_one({ :_id=>token, :revoked=>nil }) 15 | end 16 | 17 | # Get an access token (create new one if necessary). 18 | # 19 | # You can set optional expiration in seconds. If zero or nil, token 20 | # never expires. 21 | def get_token_for(identity, client, scope, expires = nil) 22 | raise ArgumentError, "Identity must be String or Integer" unless String === identity || Integer === identity 23 | scope = Utils.normalize_scope(scope) & client.scope # Only allowed scope 24 | 25 | token = collection.find_one({ 26 | :$or=>[{:expires_at=>nil}, {:expires_at=>{:$gt=>Time.now.to_i}}], 27 | :identity=>identity, :scope=>scope, 28 | :client_id=>client.id, :revoked=>nil}) 29 | 30 | unless token 31 | return create_token_for(client, scope, identity, expires) 32 | end 33 | Server.new_instance self, token 34 | end 35 | 36 | # Creates a new AccessToken for the given client and scope. 37 | def create_token_for(client, scope, identity = nil, expires = nil) 38 | expires_at = Time.now.to_i + expires if expires && expires != 0 39 | token = { :_id=>Server.secure_random, :scope=>scope, 40 | :client_id=>client.id, :created_at=>Time.now.to_i, 41 | :expires_at=>expires_at, :revoked=>nil, 42 | :last_access=>Time.now.to_i, :prev_access=>Time.now.to_i } 43 | 44 | token[:identity] = identity if identity 45 | collection.insert token 46 | Client.collection.update({ :_id=>client.id }, { :$inc=>{ :tokens_granted=>1 } }) 47 | Server.new_instance self, token 48 | end 49 | 50 | # Find all AccessTokens for an identity. 51 | def from_identity(identity) 52 | collection.find({ :identity=>identity }).map { |fields| Server.new_instance self, fields } 53 | end 54 | 55 | # Returns all access tokens for a given client, Use limit and offset 56 | # to return a subset of tokens, sorted by creation date. 57 | def for_client(client_id, offset = 0, limit = 100) 58 | client_id = BSON::ObjectId(client_id.to_s) 59 | collection.find({ :client_id=>client_id }, { :sort=>[[:created_at, Mongo::ASCENDING]], :skip=>offset, :limit=>limit }). 60 | map { |token| Server.new_instance self, token } 61 | end 62 | 63 | # Returns count of access tokens. 64 | # 65 | # @param [Hash] filter Count only a subset of access tokens 66 | # @option filter [Integer] days Only count that many days (since now) 67 | # @option filter [Boolean] revoked Only count revoked (true) or non-revoked (false) tokens; count all tokens if nil 68 | # @option filter [String, ObjectId] client_id Only tokens grant to this client 69 | def count(filter = {}) 70 | select = {} 71 | if filter[:days] 72 | now = Time.now.to_i 73 | range = { :$gt=>now - filter[:days] * 86400, :$lte=>now } 74 | select[ filter[:revoked] ? :revoked : :created_at ] = range 75 | elsif filter.has_key?(:revoked) 76 | select[:revoked] = filter[:revoked] ? { :$ne=>nil } : { :$eq=>nil } 77 | end 78 | select[:client_id] = BSON::ObjectId(filter[:client_id].to_s) if filter[:client_id] 79 | collection.find(select).count 80 | end 81 | 82 | def historical(filter = {}) 83 | days = filter[:days] || 60 84 | select = { :$gt=> { :created_at=>Time.now - 86400 * days } } 85 | select = {} 86 | if filter[:client_id] 87 | select[:client_id] = BSON::ObjectId(filter[:client_id].to_s) 88 | end 89 | raw = Server::AccessToken.collection.group("function (token) { return { ts: Math.floor(token.created_at / 86400) } }", 90 | select, { :granted=>0 }, "function (token, state) { state.granted++ }") 91 | raw.sort { |a, b| a["ts"] - b["ts"] } 92 | end 93 | 94 | def collection 95 | prefix = Server.options[:collection_prefix] 96 | Server.database["#{prefix}.access_tokens"] 97 | end 98 | end 99 | 100 | # Access token. As unique as they come. 101 | attr_reader :_id 102 | alias :token :_id 103 | # The identity we authorized access to. 104 | attr_reader :identity 105 | # Client that was granted this access token. 106 | attr_reader :client_id 107 | # The scope granted to this token. 108 | attr_reader :scope 109 | # When token was granted. 110 | attr_reader :created_at 111 | # When token expires for good. 112 | attr_reader :expires_at 113 | # Timestamp if revoked. 114 | attr_accessor :revoked 115 | # Timestamp of last access using this token, rounded up to hour. 116 | attr_accessor :last_access 117 | # Timestamp of previous access using this token, rounded up to hour. 118 | attr_accessor :prev_access 119 | 120 | # Updates the last access timestamp. 121 | def access! 122 | today = (Time.now.to_i / 3600) * 3600 123 | if last_access.nil? || last_access < today 124 | AccessToken.collection.update({ :_id=>token }, { :$set=>{ :last_access=>today, :prev_access=>last_access } }) 125 | self.last_access = today 126 | end 127 | end 128 | 129 | # Revokes this access token. 130 | def revoke! 131 | self.revoked = Time.now.to_i 132 | AccessToken.collection.update({ :_id=>token }, { :$set=>{ :revoked=>revoked } }) 133 | Client.collection.update({ :_id=>client_id }, { :$inc=>{ :tokens_revoked=>1 } }) 134 | end 135 | 136 | Server.create_indexes do 137 | # Used to revoke all pending access grants when revoking client. 138 | collection.create_index [[:client_id, Mongo::ASCENDING]] 139 | # Used to get/revoke access tokens for an identity, also to find and 140 | # return existing access token. 141 | collection.create_index [[:identity, Mongo::ASCENDING]] 142 | end 143 | end 144 | 145 | end 146 | end 147 | end 148 | -------------------------------------------------------------------------------- /lib/rack/oauth2/models/auth_request.rb: -------------------------------------------------------------------------------- 1 | module Rack 2 | module OAuth2 3 | class Server 4 | 5 | # Authorization request. Represents request on behalf of client to access 6 | # particular scope. Use this to keep state from incoming authorization 7 | # request to grant/deny redirect. 8 | class AuthRequest 9 | class << self 10 | # Find AuthRequest from identifier. 11 | def find(request_id) 12 | id = BSON::ObjectId(request_id.to_s) 13 | Server.new_instance self, collection.find_one(id) 14 | rescue BSON::InvalidObjectId 15 | end 16 | 17 | # Create a new authorization request. This holds state, so in addition 18 | # to client ID and scope, we need to know the URL to redirect back to 19 | # and any state value to pass back in that redirect. 20 | def create(client, scope, redirect_uri, response_type, state) 21 | scope = Utils.normalize_scope(scope) & client.scope # Only allowed scope 22 | fields = { :client_id=>client.id, :scope=>scope, :redirect_uri=>client.redirect_uri || redirect_uri, 23 | :response_type=>response_type, :state=>state, 24 | :grant_code=>nil, :authorized_at=>nil, 25 | :created_at=>Time.now.to_i, :revoked=>nil } 26 | fields[:_id] = collection.insert(fields) 27 | Server.new_instance self, fields 28 | end 29 | 30 | def collection 31 | prefix = Server.options[:collection_prefix] 32 | Server.database["#{prefix}.auth_requests"] 33 | end 34 | end 35 | 36 | # Request identifier. We let the database pick this one out. 37 | attr_reader :_id 38 | alias :id :_id 39 | # Client making this request. 40 | attr_reader :client_id 41 | # scope of this request: array of names. 42 | attr_reader :scope 43 | # Redirect back to this URL. 44 | attr_reader :redirect_uri 45 | # Client requested we return state on redirect. 46 | attr_reader :state 47 | # Does what it says on the label. 48 | attr_reader :created_at 49 | # Response type: either code or token. 50 | attr_reader :response_type 51 | # If granted, the access grant code. 52 | attr_accessor :grant_code 53 | # If granted, the access token. 54 | attr_accessor :access_token 55 | # Keeping track of things. 56 | attr_accessor :authorized_at 57 | # Timestamp if revoked. 58 | attr_accessor :revoked 59 | 60 | # Grant access to the specified identity. 61 | def grant!(identity, expires_in = nil) 62 | raise ArgumentError, "Must supply a identity" unless identity 63 | return if revoked 64 | client = Client.find(client_id) or return 65 | self.authorized_at = Time.now.to_i 66 | if response_type == "code" # Requested authorization code 67 | access_grant = AccessGrant.create(identity, client, scope, redirect_uri) 68 | self.grant_code = access_grant.code 69 | self.class.collection.update({ :_id=>id, :revoked=>nil }, { :$set=>{ :grant_code=>access_grant.code, :authorized_at=>authorized_at } }) 70 | else # Requested access token 71 | access_token = AccessToken.get_token_for(identity, client, scope, expires_in) 72 | self.access_token = access_token.token 73 | self.class.collection.update({ :_id=>id, :revoked=>nil, :access_token=>nil }, { :$set=>{ :access_token=>access_token.token, :authorized_at=>authorized_at } }) 74 | end 75 | true 76 | end 77 | 78 | # Deny access. 79 | def deny! 80 | self.authorized_at = Time.now.to_i 81 | self.class.collection.update({ :_id=>id }, { :$set=>{ :authorized_at=>authorized_at } }) 82 | end 83 | 84 | Server.create_indexes do 85 | # Used to revoke all pending access grants when revoking client. 86 | collection.create_index [[:client_id, Mongo::ASCENDING]] 87 | end 88 | 89 | end 90 | 91 | end 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /lib/rack/oauth2/models/client.rb: -------------------------------------------------------------------------------- 1 | module Rack 2 | module OAuth2 3 | class Server 4 | 5 | class Client 6 | 7 | class << self 8 | # Authenticate a client request. This method takes three arguments, 9 | # Find Client from client identifier. 10 | def find(client_id) 11 | id = BSON::ObjectId(client_id.to_s) 12 | Server.new_instance self, collection.find_one(id) 13 | rescue BSON::InvalidObjectId 14 | end 15 | 16 | # Create a new client. Client provides the following properties: 17 | # # :display_name -- Name to show (e.g. UberClient) 18 | # # :link -- Link to client Web site (e.g. http://uberclient.dot) 19 | # # :image_url -- URL of image to show alongside display name 20 | # # :redirect_uri -- Registered redirect URI. 21 | # # :scope -- List of names the client is allowed to request. 22 | # # :notes -- Free form text. 23 | # 24 | # This method does not validate any of these fields, in fact, you're 25 | # not required to set them, use them, or use them as suggested. Using 26 | # them as suggested would result in better user experience. Don't ask 27 | # how we learned that. 28 | def create(args) 29 | redirect_uri = Server::Utils.parse_redirect_uri(args[:redirect_uri]).to_s if args[:redirect_uri] 30 | scope = Server::Utils.normalize_scope(args[:scope]) 31 | fields = { :display_name=>args[:display_name], :link=>args[:link], 32 | :image_url=>args[:image_url], :redirect_uri=>redirect_uri, 33 | :notes=>args[:notes].to_s, :scope=>scope, 34 | :created_at=>Time.now.to_i, :revoked=>nil } 35 | if args[:id] && args[:secret] 36 | fields[:_id], fields[:secret] = BSON::ObjectId(args[:id].to_s), args[:secret] 37 | collection.insert(fields, :safe=>true) 38 | else 39 | fields[:secret] = Server.secure_random 40 | fields[:_id] = collection.insert(fields) 41 | end 42 | Server.new_instance self, fields 43 | end 44 | 45 | # Lookup client by ID, display name or URL. 46 | def lookup(field) 47 | id = BSON::ObjectId(field.to_s) 48 | Server.new_instance self, collection.find_one(id) 49 | rescue BSON::InvalidObjectId 50 | Server.new_instance self, collection.find_one({ :display_name=>field }) || collection.find_one({ :link=>field }) 51 | end 52 | 53 | # Returns all the clients in the database, sorted alphabetically. 54 | def all 55 | collection.find({}, { :sort=>[[:display_name, Mongo::ASCENDING]] }). 56 | map { |fields| Server.new_instance self, fields } 57 | end 58 | 59 | # Deletes client with given identifier (also, all related records). 60 | def delete(client_id) 61 | id = BSON::ObjectId(client_id.to_s) 62 | Client.collection.remove({ :_id=>id }) 63 | AuthRequest.collection.remove({ :client_id=>id }) 64 | AccessGrant.collection.remove({ :client_id=>id }) 65 | AccessToken.collection.remove({ :client_id=>id }) 66 | end 67 | 68 | def collection 69 | prefix = Server.options[:collection_prefix] 70 | Server.database["#{prefix}.clients"] 71 | end 72 | end 73 | 74 | # Client identifier. 75 | attr_reader :_id 76 | alias :id :_id 77 | # Client secret: random, long, and hexy. 78 | attr_reader :secret 79 | # User see this. 80 | attr_reader :display_name 81 | # Link to client's Web site. 82 | attr_reader :link 83 | # Preferred image URL for this icon. 84 | attr_reader :image_url 85 | # Redirect URL. Supplied by the client if they want to restrict redirect 86 | # URLs (better security). 87 | attr_reader :redirect_uri 88 | # List of scope the client is allowed to request. 89 | attr_reader :scope 90 | # Free form fields for internal use. 91 | attr_reader :notes 92 | # Does what it says on the label. 93 | attr_reader :created_at 94 | # Timestamp if revoked. 95 | attr_accessor :revoked 96 | # Counts how many access tokens were granted. 97 | attr_reader :tokens_granted 98 | # Counts how many access tokens were revoked. 99 | attr_reader :tokens_revoked 100 | 101 | # Revoke all authorization requests, access grants and access tokens for 102 | # this client. Ward off the evil. 103 | def revoke! 104 | self.revoked = Time.now.to_i 105 | Client.collection.update({ :_id=>id }, { :$set=>{ :revoked=>revoked } }) 106 | AuthRequest.collection.update({ :client_id=>id }, { :$set=>{ :revoked=>revoked } }) 107 | AccessGrant.collection.update({ :client_id=>id }, { :$set=>{ :revoked=>revoked } }) 108 | AccessToken.collection.update({ :client_id=>id }, { :$set=>{ :revoked=>revoked } }) 109 | end 110 | 111 | def update(args) 112 | fields = [:display_name, :link, :image_url, :notes].inject({}) { |h,k| v = args[k]; h[k] = v if v; h } 113 | fields[:redirect_uri] = Server::Utils.parse_redirect_uri(args[:redirect_uri]).to_s if args[:redirect_uri] 114 | fields[:scope] = Server::Utils.normalize_scope(args[:scope]) 115 | self.class.collection.update({ :_id=>id }, { :$set=>fields }) 116 | self.class.find(id) 117 | end 118 | 119 | Server.create_indexes do 120 | # For quickly returning clients sorted by display name, or finding 121 | # client from a URL. 122 | collection.create_index [[:display_name, Mongo::ASCENDING]] 123 | collection.create_index [[:link, Mongo::ASCENDING]] 124 | end 125 | end 126 | 127 | end 128 | end 129 | end 130 | -------------------------------------------------------------------------------- /lib/rack/oauth2/models/issuer.rb: -------------------------------------------------------------------------------- 1 | module Rack 2 | module OAuth2 3 | class Server 4 | # A third party that issues assertions 5 | # http://tools.ietf.org/html/draft-ietf-oauth-assertions-01#section-5.1 6 | class Issuer 7 | class << self 8 | 9 | # returns the Issuer object for the given identifier 10 | def from_identifier(identifier) 11 | Server.new_instance self, collection.find_one({:_id=>identifier}) 12 | end 13 | 14 | # Create a new Issuer. 15 | def create(args) 16 | fields = {} 17 | [:hmac_secret, :public_key, :notes].each do |key| 18 | fields[key] = args[key] if args.has_key?(key) 19 | end 20 | fields[:created_at] = Time.now.to_i 21 | fields[:updated_at] = Time.now.to_i 22 | fields[:_id] = args[:identifier] 23 | collection.insert(fields, :safe=>true) 24 | Server.new_instance self, fields 25 | end 26 | 27 | 28 | def collection 29 | prefix = Server.options[:collection_prefix] 30 | Server.database["#{prefix}.issuers"] 31 | end 32 | end 33 | 34 | # The unique identifier of this Issuer. String or URI 35 | attr_reader :_id 36 | alias :identifier :_id 37 | # shared secret used for verifying HMAC signatures 38 | attr_reader :hmac_secret 39 | # public key used for verifying RSA signatures 40 | attr_reader :public_key 41 | # notes about this Issuer 42 | attr_reader :notes 43 | 44 | 45 | def update(args) 46 | fields = [:hmac_secret, :public_key, :notes].inject({}) {|h,k| v = args[k]; h[k] = v if v; h} 47 | self.class.collection.update({:_id => identifier }, {:$set => fields}) 48 | self.class.from_identifier(identifier) 49 | end 50 | 51 | Server.create_indexes do 52 | # Used to revoke all pending access grants when revoking client. 53 | collection.create_index [[:identifier, Mongo::ASCENDING]] 54 | end 55 | end 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /lib/rack/oauth2/rails.rb: -------------------------------------------------------------------------------- 1 | require "rack/oauth2/server" 2 | 3 | module Rack 4 | module OAuth2 5 | 6 | # Rails support. 7 | # 8 | # Adds oauth instance method that returns Rack::OAuth2::Helper, see there for 9 | # more details. 10 | # 11 | # Adds oauth_required filter method. Use this filter with actions that require 12 | # authentication, and with actions that require client to have a specific 13 | # access scope. 14 | # 15 | # Adds oauth setting you can use to configure the module (e.g. setting 16 | # available scope, see example). 17 | # 18 | # @example config/environment.rb 19 | # require "rack/oauth2/rails" 20 | # 21 | # Rails::Initializer.run do |config| 22 | # config.oauth[:scope] = %w{read write} 23 | # config.oauth[:authenticator] = lambda do |username, password| 24 | # User.authenticated username, password 25 | # end 26 | # . . . 27 | # end 28 | # 29 | # @example app/controllers/my_controller.rb 30 | # class MyController < ApplicationController 31 | # 32 | # oauth_required :only=>:show 33 | # oauth_required :only=>:update, :scope=>"write" 34 | # 35 | # . . . 36 | # 37 | # protected 38 | # def current_user 39 | # @current_user ||= User.find(oauth.identity) if oauth.authenticated? 40 | # end 41 | # end 42 | # 43 | # @see Helpers 44 | # @see Filters 45 | # @see Configuration 46 | module Rails 47 | 48 | # Helper methods available to controller instance and views. 49 | module Helpers 50 | # Returns the OAuth helper. 51 | # 52 | # @return [Server::Helper] 53 | def oauth 54 | @oauth ||= Rack::OAuth2::Server::Helper.new(request, response) 55 | end 56 | 57 | # Filter that denies access if the request is not authenticated. If you 58 | # do not specify a scope, the class method oauth_required will use this 59 | # filter; you can set the filter in a parent class and skip it in child 60 | # classes that need special handling. 61 | def oauth_required 62 | head oauth.no_access! unless oauth.authenticated? 63 | end 64 | end 65 | 66 | # Filter methods available in controller. 67 | module Filters 68 | 69 | # Adds before filter to require authentication on all the listed paths. 70 | # Use the :scope option if client must also have access to that scope. 71 | # 72 | # @param [Hash] options Accepts before_filter options like :only and 73 | # :except, and the :scope option. 74 | def oauth_required(options = {}) 75 | if scope = options.delete(:scope) 76 | before_filter options do |controller| 77 | if controller.oauth.authenticated? 78 | if !controller.oauth.scope.include?(scope) 79 | controller.send :head, controller.oauth.no_scope!(scope) 80 | end 81 | else 82 | controller.send :head, controller.oauth.no_access! 83 | end 84 | end 85 | else 86 | before_filter :oauth_required, options 87 | end 88 | end 89 | end 90 | 91 | # Configuration methods available in config/environment.rb. 92 | module Configuration 93 | 94 | # Rack module settings. 95 | # 96 | # @return [Hash] Settings 97 | def oauth 98 | @oauth ||= Server.options 99 | end 100 | end 101 | 102 | end 103 | 104 | end 105 | end 106 | -------------------------------------------------------------------------------- /lib/rack/oauth2/server/admin.rb: -------------------------------------------------------------------------------- 1 | require "sinatra/base" 2 | begin 3 | require "sinatra/version" 4 | rescue LoadError 5 | # Sinatra < 1.3 6 | end 7 | require "json" 8 | require "rack/oauth2/server" 9 | require "rack/oauth2/sinatra" 10 | 11 | module Rack 12 | module OAuth2 13 | class Server 14 | class Admin < ::Sinatra::Base 15 | 16 | class << self 17 | 18 | # Rack module that mounts the specified class on the specified path, 19 | # and passes all other request to the application. 20 | class Mount 21 | class << self 22 | def mount(klass, path) 23 | @klass = klass 24 | @path = path 25 | @match = /^#{Regexp.escape(path)}(\/.*|$)?/ 26 | end 27 | 28 | attr_reader :klass, :path, :match 29 | end 30 | 31 | def initialize(app) 32 | @pass = app 33 | @admin = self.class.klass.new 34 | end 35 | 36 | def call(env) 37 | path = env["PATH_INFO"].to_s 38 | script_name = env['SCRIPT_NAME'] 39 | if path =~ self.class.match && rest = $1 40 | env.merge! "SCRIPT_NAME"=>(script_name + self.class.path), "PATH_INFO"=>rest 41 | return @admin.call(env) 42 | else 43 | return @pass.call(env) 44 | end 45 | end 46 | end 47 | 48 | # Returns Rack handle that mounts Admin on the specified path, and 49 | # forwards all other requests back to the application. 50 | # 51 | # @param [String, nil] path The path to mount on, defaults to 52 | # /oauth/admin 53 | # @return [Object] Rack module 54 | # 55 | # @example To include Web admin in Rails 2.x app: 56 | # config.middleware.use Rack::OAuth2::Server::Admin.mount 57 | def mount(path = "/oauth/admin") 58 | mount = Class.new(Mount) 59 | mount.mount Admin, path 60 | mount 61 | end 62 | 63 | end 64 | 65 | 66 | # Client application identified, require to authenticate. 67 | set :client_id, nil 68 | # Client application secret, required to authenticate. 69 | set :client_secret, nil 70 | # Endpoint for requesing authorization, defaults to /oauth/admin. 71 | set :authorize, nil 72 | # Will map an access token identity into a URL in your application, 73 | # using the substitution value "{id}", e.g. 74 | # "http://example.com/users/#{id}") 75 | set :template_url, nil 76 | # Forces all requests to use HTTPS (true by default except in 77 | # development mode). 78 | set :force_ssl, !development? && !test? 79 | # Common scope shown and added by default to new clients. 80 | set :scope, [] 81 | 82 | 83 | set :logger, ::Rails.logger if defined?(::Rails) 84 | # Number of tokens to return in each page. 85 | set :tokens_per_page, 100 86 | if ::Sinatra.const_defined?("VERSION") && Gem::Version.new(::Sinatra::VERSION) >= Gem::Version.new("1.3.0") 87 | set :public_folder, ::File.dirname(__FILE__) + "/../admin" 88 | else 89 | set :public, ::File.dirname(__FILE__) + "/../admin" 90 | end 91 | 92 | set :method_override, true 93 | mime_type :js, "text/javascript" 94 | mime_type :tmpl, "text/x-jquery-template" 95 | 96 | register Rack::OAuth2::Sinatra 97 | 98 | # Force HTTPS except for development environment. 99 | before do 100 | redirect request.url.sub(/^http:/, "https:") if settings.force_ssl && request.scheme != "https" 101 | end 102 | 103 | 104 | # -- Static content -- 105 | 106 | # It's a single-page app, this is that single page. 107 | get "/" do 108 | if ::Sinatra.const_defined?("VERSION") && Gem::Version.new(::Sinatra::VERSION) >= Gem::Version.new("1.3.0") 109 | send_file settings.public_folder + "/views/index.html" 110 | else 111 | send_file settings.public + "/views/index.html" 112 | end 113 | end 114 | 115 | # Service JavaScript, CSS and jQuery templates from the gem. 116 | %w{js css views}.each do |path| 117 | get "/#{path}/:name" do 118 | if ::Sinatra.const_defined?("VERSION") && Gem::Version.new(::Sinatra::VERSION) >= Gem::Version.new("1.3.0") 119 | send_file settings.public_folder + "/#{path}/" + params[:name] 120 | else 121 | send_file settings.public + "/#{path}/" + params[:name] 122 | end 123 | end 124 | end 125 | 126 | 127 | # -- Getting an access token -- 128 | 129 | # To get an OAuth token, you need client ID and secret, two values we 130 | # didn't pass on to the JavaScript code, so it has no way to request 131 | # authorization directly. Instead, it redirects to this URL which in turn 132 | # redirects to the authorization endpoint. This redirect does accept the 133 | # state parameter, which will be returned after authorization. 134 | get "/authorize" do 135 | redirect_uri = "#{request.scheme}://#{request.host}:#{request.port}#{request.script_name}" 136 | query = { :client_id=>settings.client_id, :client_secret=>settings.client_secret, :state=>params[:state], 137 | :response_type=>"token", :scope=>"oauth-admin", :redirect_uri=>redirect_uri } 138 | auth_url = settings.authorize || "#{request.scheme}://#{request.host}:#{request.port}/oauth/authorize" 139 | redirect "#{auth_url}?#{Rack::Utils.build_query(query)}" 140 | end 141 | 142 | 143 | # -- API -- 144 | 145 | oauth_required "/api/clients", "/api/client/:id", "/api/client/:id/revoke", "/api/token/:token/revoke", :scope=>"oauth-admin" 146 | 147 | get "/api/clients" do 148 | content_type "application/json" 149 | json = { :list=>Server::Client.all.map { |client| client_as_json(client) }, 150 | :scope=>Server::Utils.normalize_scope(settings.scope), 151 | :history=>"#{request.script_name}/api/clients/history", 152 | :tokens=>{ :total=>Server::AccessToken.count, :week=>Server::AccessToken.count(:days=>7), 153 | :revoked=>Server::AccessToken.count(:days=>7, :revoked=>true) } } 154 | json.to_json 155 | end 156 | 157 | get "/api/clients/history" do 158 | content_type "application/json" 159 | { :data=>Server::AccessToken.historical }.to_json 160 | end 161 | 162 | post "/api/clients" do 163 | begin 164 | client = Server::Client.create(validate_params(params)) 165 | redirect "#{request.script_name}/api/client/#{client.id}" 166 | rescue 167 | halt 400, $!.message 168 | end 169 | end 170 | 171 | get "/api/client/:id" do 172 | content_type "application/json" 173 | client = Server::Client.find(params[:id]) 174 | json = client_as_json(client, true) 175 | 176 | page = [params[:page].to_i, 1].max 177 | offset = (page - 1) * settings.tokens_per_page 178 | total = Server::AccessToken.count(:client_id=>client.id) 179 | tokens = Server::AccessToken.for_client(params[:id], offset, settings.tokens_per_page) 180 | json[:tokens] = { :list=>tokens.map { |token| token_as_json(token) } } 181 | json[:tokens][:total] = total 182 | json[:tokens][:page] = page 183 | json[:tokens][:next] = "#{request.script_name}/client/#{params[:id]}?page=#{page + 1}" if total > page * settings.tokens_per_page 184 | json[:tokens][:previous] = "#{request.script_name}/client/#{params[:id]}?page=#{page - 1}" if page > 1 185 | json[:tokens][:total] = Server::AccessToken.count(:client_id=>client.id) 186 | json[:tokens][:week] = Server::AccessToken.count(:client_id=>client.id, :days=>7) 187 | json[:tokens][:revoked] = Server::AccessToken.count(:client_id=>client.id, :days=>7, :revoked=>true) 188 | 189 | json.to_json 190 | end 191 | 192 | get "/api/client/:id/history" do 193 | content_type "application/json" 194 | client = Server::Client.find(params[:id]) 195 | { :data=>Server::AccessToken.historical(:client_id=>client.id) }.to_json 196 | end 197 | 198 | put "/api/client/:id" do 199 | client = Server::Client.find(params[:id]) 200 | begin 201 | client.update validate_params(params) 202 | redirect "#{request.script_name}/api/client/#{client.id}" 203 | rescue 204 | halt 400, $!.message 205 | end 206 | end 207 | 208 | delete "/api/client/:id" do 209 | Server::Client.delete(params[:id]) 210 | 200 211 | end 212 | 213 | post "/api/client/:id/revoke" do 214 | client = Server::Client.find(params[:id]) 215 | client.revoke! 216 | 200 217 | end 218 | 219 | post "/api/token/:token/revoke" do 220 | token = Server::AccessToken.from_token(params[:token]) 221 | token.revoke! 222 | 200 223 | end 224 | 225 | helpers do 226 | def validate_params(params) 227 | display_name = params[:displayName].to_s.strip 228 | halt 400, "Missing display name" if display_name.empty? 229 | link = URI.parse(params[:link].to_s.strip).normalize rescue nil 230 | halt 400, "Link is not a URL (must be http://....)" unless link 231 | halt 400, "Link must be an absolute URL with HTTP/S scheme" unless link.absolute? && %{http https}.include?(link.scheme) 232 | redirect_uri = URI.parse(params[:redirectUri].to_s.strip).normalize rescue nil 233 | halt 400, "Redirect URL is not a URL (must be http://....)" unless redirect_uri 234 | halt 400, "Redirect URL must be an absolute URL with HTTP/S scheme" unless 235 | redirect_uri.absolute? && %{http https}.include?(redirect_uri.scheme) 236 | unless params[:imageUrl].nil? || params[:imageUrl].to_s.empty? 237 | image_url = URI.parse(params[:imageUrl].to_s.strip).normalize rescue nil 238 | halt 400, "Image URL must be an absolute URL with HTTP/S scheme" unless 239 | image_url.absolute? && %{http https}.include?(image_url.scheme) 240 | end 241 | scope = Server::Utils.normalize_scope(params[:scope]) 242 | { :display_name=>display_name, :link=>link.to_s, :image_url=>image_url.to_s, 243 | :redirect_uri=>redirect_uri.to_s, :scope=>scope, :notes=>params[:notes] } 244 | end 245 | 246 | def client_as_json(client, with_stats = false) 247 | { "id"=>client.id.to_s, "secret"=>client.secret, :redirectUri=>client.redirect_uri, 248 | :displayName=>client.display_name, :link=>client.link, :imageUrl=>client.image_url, 249 | :notes=>client.notes, :scope=>client.scope, 250 | :url=>"#{request.script_name}/api/client/#{client.id}", 251 | :revoke=>"#{request.script_name}/api/client/#{client.id}/revoke", 252 | :history=>"#{request.script_name}/api/client/#{client.id}/history", 253 | :created=>client.created_at, :revoked=>client.revoked } 254 | end 255 | 256 | def token_as_json(token) 257 | { :token=>token.token, :identity=>token.identity, :scope=>token.scope, :created=>token.created_at, 258 | :expired=>token.expires_at, :revoked=>token.revoked, 259 | :link=>settings.template_url && settings.template_url.gsub("{id}", token.identity), 260 | :last_access=>token.last_access, 261 | :revoke=>"#{request.script_name}/api/token/#{token.token}/revoke" } 262 | end 263 | end 264 | 265 | end 266 | end 267 | end 268 | end 269 | -------------------------------------------------------------------------------- /lib/rack/oauth2/server/errors.rb: -------------------------------------------------------------------------------- 1 | module Rack 2 | module OAuth2 3 | class Server 4 | 5 | # Base class for all OAuth errors. These map to error codes in the spec. 6 | class OAuthError < StandardError 7 | 8 | def initialize(code, message) 9 | super message 10 | @code = code.to_sym 11 | end 12 | 13 | # The OAuth error code. 14 | attr_reader :code 15 | end 16 | 17 | # The end-user or authorization server denied the request. 18 | class AccessDeniedError < OAuthError 19 | def initialize 20 | super :access_denied, "You are now allowed to access this resource." 21 | end 22 | end 23 | 24 | # Access token expired, client expected to request new one using refresh 25 | # token. 26 | class ExpiredTokenError < OAuthError 27 | def initialize 28 | super :expired_token, "The access token has expired." 29 | end 30 | end 31 | 32 | # The client identifier provided is invalid, the client failed to 33 | # authenticate, the client did not include its credentials, provided 34 | # multiple client credentials, or used unsupported credentials type. 35 | class InvalidClientError < OAuthError 36 | def initialize 37 | super :invalid_client, "Client ID and client secret do not match." 38 | end 39 | end 40 | 41 | # The provided access grant is invalid, expired, or revoked (e.g. invalid 42 | # assertion, expired authorization token, bad end-user password credentials, 43 | # or mismatching authorization code and redirection URI). 44 | class InvalidGrantError < OAuthError 45 | def initialize(message = nil) 46 | super :invalid_grant, message || "This access grant is no longer valid." 47 | end 48 | end 49 | 50 | # Invalid_request, the request is missing a required parameter, includes an 51 | # unsupported parameter or parameter value, repeats the same parameter, uses 52 | # more than one method for including an access token, or is otherwise 53 | # malformed. 54 | class InvalidRequestError < OAuthError 55 | def initialize(message) 56 | super :invalid_request, message || "The request has the wrong parameters." 57 | end 58 | end 59 | 60 | # The requested scope is invalid, unknown, or malformed. 61 | class InvalidScopeError < OAuthError 62 | def initialize 63 | super :invalid_scope, "The requested scope is not supported." 64 | end 65 | end 66 | 67 | # Access token expired, client cannot refresh and needs new authorization. 68 | class InvalidTokenError < OAuthError 69 | def initialize 70 | super :invalid_token, "The access token is no longer valid." 71 | end 72 | end 73 | 74 | # The redirection URI provided does not match a pre-registered value. 75 | class RedirectUriMismatchError < OAuthError 76 | def initialize 77 | super :redirect_uri_mismatch, "Must use the same redirect URI you registered with us." 78 | end 79 | end 80 | 81 | # The authenticated client is not authorized to use the access grant type provided. 82 | class UnauthorizedClientError < OAuthError 83 | def initialize 84 | super :unauthorized_client, "You are not allowed to access this resource." 85 | end 86 | end 87 | 88 | # This access grant type is not supported by this server. 89 | class UnsupportedGrantType < OAuthError 90 | def initialize 91 | super :unsupported_grant_type, "This access grant type is not supported by this server." 92 | end 93 | end 94 | 95 | # The requested response type is not supported by the authorization server. 96 | class UnsupportedResponseTypeError < OAuthError 97 | def initialize 98 | super :unsupported_response_type, "The requested response type is not supported." 99 | end 100 | end 101 | 102 | end 103 | end 104 | end 105 | -------------------------------------------------------------------------------- /lib/rack/oauth2/server/helper.rb: -------------------------------------------------------------------------------- 1 | module Rack 2 | module OAuth2 3 | class Server 4 | 5 | # Helper methods that provide access to the OAuth state during the 6 | # authorization flow, and from authenticated requests. For example: 7 | # 8 | # def show 9 | # logger.info "#{oauth.client.display_name} accessing #{oauth.scope}" 10 | # end 11 | class Helper 12 | 13 | def initialize(request, response) 14 | @request, @response = request, response 15 | end 16 | 17 | # Returns the access token. Only applies if client authenticated. 18 | # 19 | # @return [String, nil] Access token, if authenticated 20 | def access_token 21 | @access_token ||= @request.env["oauth.access_token"] 22 | end 23 | 24 | # True if client authenticated. 25 | # 26 | # @return [true, false] True if authenticated 27 | def authenticated? 28 | !!access_token 29 | end 30 | 31 | # Returns the authenticated identity. Only applies if client 32 | # authenticated. 33 | # 34 | # @return [String, nil] Identity, if authenticated 35 | def identity 36 | @identity ||= @request.env["oauth.identity"] 37 | end 38 | 39 | # Returns the Client object associated with this request. Available if 40 | # client authenticated, or while processing authorization request. 41 | # 42 | # @return [Client, nil] Client if authenticated, or while authorizing 43 | def client 44 | if access_token 45 | @client ||= Server.get_client(Server.get_access_token(access_token).client_id) 46 | elsif authorization 47 | @client ||= Server.get_client(Server.get_auth_request(authorization).client_id) 48 | end 49 | end 50 | 51 | # Returns scope associated with this request. Available if client 52 | # authenticated, or while processing authorization request. 53 | # 54 | # @return [Array, nil] Scope names, e.g ["read, "write"] 55 | def scope 56 | if access_token 57 | @scope ||= Server.get_access_token(access_token).scope 58 | elsif authorization 59 | @scope ||= Server.get_auth_request(authorization).scope 60 | end 61 | end 62 | 63 | # Rejects the request and returns 401 (Unauthorized). You can just 64 | # return 401, but this also sets the WWW-Authenticate header the right 65 | # value. 66 | # 67 | # @return 401 68 | def no_access! 69 | @response["oauth.no_access"] = "true" 70 | @response.status = 401 71 | end 72 | 73 | # Rejects the request and returns 403 (Forbidden). You can just 74 | # return 403, but this also sets the WWW-Authenticate header the right 75 | # value. Indicates which scope the client needs to make this request. 76 | # 77 | # @param [String] scope The missing scope, e.g. "read" 78 | # @return 403 79 | def no_scope!(scope) 80 | @response["oauth.no_scope"] = scope.to_s 81 | @response.status = 403 82 | end 83 | 84 | # Returns the authorization request handle. Available when starting an 85 | # authorization request (i.e. /oauth/authorize). 86 | # 87 | # @return [String] Authorization handle 88 | def authorization 89 | @request_id ||= @request.env["oauth.authorization"] || @request.params["authorization"] 90 | end 91 | 92 | # Sets the authorization request handle. Use this during the 93 | # authorization flow. 94 | # 95 | # @param [String] authorization handle 96 | def authorization=(authorization) 97 | @scope, @client = nil 98 | @request_id = authorization 99 | end 100 | 101 | # Grant authorization request. Call this at the end of the authorization 102 | # flow to signal that the user has authorized the client to access the 103 | # specified identity. Don't render anything else. Argument required if 104 | # authorization handle is not passed in the request parameter 105 | # +authorization+. 106 | # 107 | # @param [String, nil] authorization Authorization handle 108 | # @param [String] identity Identity string 109 | # @return 200 110 | def grant!(auth, identity = nil) 111 | auth, identity = authorization, auth unless identity 112 | @response["oauth.authorization"] = auth.to_s 113 | @response["oauth.identity"] = identity.to_s 114 | @response.status = 200 115 | end 116 | 117 | # Deny authorization request. Call this at the end of the authorization 118 | # flow to signal that the user has not authorized the client. Don't 119 | # render anything else. Argument required if authorization handle is not 120 | # passed in the request parameter +authorization+. 121 | # 122 | # @param [String, nil] auth Authorization handle 123 | # @return 401 124 | def deny!(auth = nil) 125 | auth ||= authorization 126 | @response["oauth.authorization"] = auth.to_s 127 | @response.status = 403 128 | end 129 | 130 | # Returns all access tokens associated with this identity. 131 | # 132 | # @param [String] identity Identity string 133 | # @return [Array] 134 | def list_access_tokens(identity) 135 | Rack::OAuth2::Server.list_access_tokens(identity) 136 | end 137 | 138 | def inspect 139 | authorization ? "Authorization request for #{scope.join(",")} on behalf of #{client.display_name}" : 140 | authenticated? ? "Authenticated as #{identity}" : nil 141 | end 142 | 143 | end 144 | 145 | end 146 | end 147 | end 148 | -------------------------------------------------------------------------------- /lib/rack/oauth2/server/practice.rb: -------------------------------------------------------------------------------- 1 | require "rack/oauth2/server/admin" 2 | 3 | module Rack 4 | module OAuth2 5 | class Server 6 | 7 | class Practice < ::Sinatra::Base 8 | register Rack::OAuth2::Sinatra 9 | 10 | get "/" do 11 | <<-HTML 12 | 13 | 14 | OAuth 2.0 Practice Server 15 | 16 | 17 |

Welcome to OAuth 2.0 Practice Server

18 |

This practice server is for testing your OAuth 2.0 client library.

19 |
20 |
Authorization end-point:
21 |
http://#{request.host}:#{request.port}/oauth/authorize
22 |
Access token end-point: 23 |
http://#{request.host}:#{request.port}/oauth/access_token
24 |
Resource requiring authentication:
25 |
http://#{request.host}:#{request.port}/secret
26 |
Resource requiring authorization and scope "sudo":
27 |
http://#{request.host}:#{request.port}/make
28 |
29 |

The scope can be "nobody", "sudo", "oauth-admin" or combination of the three.

30 |

You can manage client applications and tokens from the OAuth console.

31 | 32 | 33 | HTML 34 | end 35 | 36 | # -- Simple authorization -- 37 | 38 | get "/oauth/authorize" do 39 | <<-HTML 40 | 41 | 42 | OAuth 2.0 Practice Server 43 | 44 | 45 |

#{oauth.client.display_name} wants to access your account with the scope #{oauth.scope.join(", ")}

46 |
47 | 48 | 49 |
50 |
51 | 52 | 53 |
54 | 55 | 56 | HTML 57 | end 58 | post "/oauth/grant" do 59 | oauth.grant! "Superman" 60 | end 61 | post "/oauth/deny" do 62 | oauth.deny! 63 | end 64 | 65 | # -- Protected resources -- 66 | 67 | oauth_required "/secret" 68 | get "/private" do 69 | "You're awesome!" 70 | end 71 | 72 | oauth_required "/make", :scope=>"sudo" 73 | get "/write" do 74 | "Sandwhich" 75 | end 76 | end 77 | end 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /lib/rack/oauth2/server/railtie.rb: -------------------------------------------------------------------------------- 1 | require "rack/oauth2/server" 2 | require "rack/oauth2/rails" 3 | require "rails" 4 | 5 | module Rack 6 | module OAuth2 7 | class Server 8 | # Rails 3.x integration. 9 | class Railtie < ::Rails::Railtie # :nodoc: 10 | config.oauth = Server.options 11 | 12 | initializer "rack-oauth2-server" do |app| 13 | app.middleware.use ::Rack::OAuth2::Server, app.config.oauth 14 | config.oauth.logger ||= ::Rails.logger 15 | class ::ActionController::Base 16 | helper ::Rack::OAuth2::Rails::Helpers 17 | include ::Rack::OAuth2::Rails::Helpers 18 | extend ::Rack::OAuth2::Rails::Filters 19 | end 20 | end 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/rack/oauth2/server/utils.rb: -------------------------------------------------------------------------------- 1 | module Rack 2 | module OAuth2 3 | class Server 4 | 5 | module Utils 6 | module_function 7 | 8 | # Parses the redirect URL, normalizes it and returns a URI object. 9 | # 10 | # Raises InvalidRequestError if not an absolute HTTP/S URL. 11 | def parse_redirect_uri(redirect_uri) 12 | raise InvalidRequestError, "Missing redirect URL" unless redirect_uri 13 | uri = URI.parse(redirect_uri).normalize rescue nil 14 | raise InvalidRequestError, "Redirect URL looks fishy to me" unless uri 15 | raise InvalidRequestError, "Redirect URL must be absolute URL" unless uri.absolute? && uri.host 16 | uri 17 | end 18 | 19 | # Given scope as either array or string, return array of same names, 20 | # unique and sorted. 21 | def normalize_scope(scope) 22 | (Array === scope ? scope.join(" ") : scope || "").split(/\s+/).compact.uniq.sort 23 | end 24 | 25 | end 26 | 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/rack/oauth2/sinatra.rb: -------------------------------------------------------------------------------- 1 | require "rack/oauth2/server" 2 | 3 | module Rack 4 | module OAuth2 5 | 6 | # Sinatra support. 7 | # 8 | # Adds oauth instance method that returns Rack::OAuth2::Helper, see there for 9 | # more details. 10 | # 11 | # Adds oauth_required class method. Use this filter with paths that require 12 | # authentication, and with paths that require client to have a specific 13 | # access scope. 14 | # 15 | # Adds oauth setting you can use to configure the module (e.g. setting 16 | # available scope, see example). 17 | # 18 | # @example 19 | # require "rack/oauth2/sinatra" 20 | # class MyApp < Sinatra::Base 21 | # register Rack::OAuth2::Sinatra 22 | # oauth[:scope] = %w{read write} 23 | # 24 | # oauth_required "/api" 25 | # oauth_required "/api/edit", :scope=>"write" 26 | # 27 | # before { @user = User.find(oauth.identity) if oauth.authenticated? } 28 | # end 29 | # 30 | # @see Helpers 31 | module Sinatra 32 | 33 | # Adds before filter to require authentication on all the listed paths. 34 | # Use the :scope option if client must also have access to that scope. 35 | # 36 | # @param [String, ...] path One or more paths that require authentication 37 | # @param [optional, Hash] options Currently only :scope is supported. 38 | def oauth_required(*args) 39 | options = args.pop if Hash === args.last 40 | scope = options[:scope] if options 41 | args.each do |path| 42 | before path do 43 | if oauth.authenticated? 44 | if scope && !oauth.scope.include?(scope) 45 | halt oauth.no_scope! scope 46 | end 47 | else 48 | halt oauth.no_access! 49 | end 50 | end 51 | end 52 | end 53 | 54 | module Helpers 55 | # Returns the OAuth helper. 56 | # 57 | # @return [Server::Helper] 58 | def oauth 59 | @oauth ||= Server::Helper.new(request, response) 60 | end 61 | end 62 | 63 | def self.registered(base) 64 | base.helpers Helpers 65 | base.set :oauth, Server.options 66 | base.use Server, base.settings.oauth 67 | end 68 | 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /rack-oauth2-server.gemspec: -------------------------------------------------------------------------------- 1 | $: << File.dirname(__FILE__) + "/lib" 2 | 3 | Gem::Specification.new do |spec| 4 | spec.name = "rack-oauth2-server" 5 | spec.version = IO.read("VERSION") 6 | spec.author = "Assaf Arkin" 7 | spec.email = "assaf@labnotes.org" 8 | spec.homepage = "http://github.com/assaf/#{spec.name}" 9 | spec.summary = "OAuth 2.0 Authorization Server as a Rack module" 10 | spec.description = "Because you don't allow strangers into your app, and OAuth 2.0 is the new awesome." 11 | spec.post_install_message = "To get started, run the command oauth2-server" 12 | 13 | spec.files = Dir["{bin,lib,rails,test}/**/*", "CHANGELOG", "VERSION", "MIT-LICENSE", "README.md", "Rakefile", "Gemfile", "*.gemspec"] 14 | spec.executable = "oauth2-server" 15 | 16 | spec.extra_rdoc_files = "README.md", "CHANGELOG" 17 | spec.rdoc_options = "--title", "rack-oauth2-server #{spec.version}", "--main", "README.md", 18 | "--webcvs", "http://github.com/assaf/#{spec.name}" 19 | spec.license = "MIT" 20 | 21 | spec.required_ruby_version = '>= 1.8.7' 22 | spec.add_dependency "rack", "~>1.4.5" 23 | spec.add_dependency "mongo", "~>1" 24 | spec.add_dependency "bson_ext" 25 | spec.add_dependency "sinatra", "~>1.3" 26 | spec.add_dependency "json" 27 | spec.add_dependency "jwt", "~>0.1.8" 28 | spec.add_dependency "iconv" 29 | spec.add_development_dependency 'rake', '~>10.0.4' 30 | spec.add_development_dependency 'rack-test', '~>0.6.2' 31 | spec.add_development_dependency 'shoulda', '~>3.4.0' 32 | spec.add_development_dependency 'timecop', '~>0.5.9.1' 33 | spec.add_development_dependency 'ap', '~>0.1.1' 34 | spec.add_development_dependency 'crack', '~>0.3.2' 35 | spec.add_development_dependency 'rails', '~>3.2' 36 | end 37 | -------------------------------------------------------------------------------- /rails/init.rb: -------------------------------------------------------------------------------- 1 | # Rails 2.x initialization. 2 | require "rack/oauth2/rails" 3 | 4 | config.extend ::Rack::OAuth2::Rails::Configuration 5 | config.oauth.logger ||= Rails.logger 6 | config.middleware.use ::Rack::OAuth2::Server, config.oauth 7 | class ActionController::Base 8 | helper ::Rack::OAuth2::Rails::Helpers 9 | include ::Rack::OAuth2::Rails::Helpers 10 | extend ::Rack::OAuth2::Rails::Filters 11 | end 12 | -------------------------------------------------------------------------------- /spec/admin-spec.coffee: -------------------------------------------------------------------------------- 1 | vows = require("vows") 2 | assert = require("assert") 3 | zombie = require("zombie") 4 | 5 | 6 | vows.describe("Sign In").addBatch( 7 | "connect": 8 | topic: -> 9 | zombie.visit "http://localhost:8080/oauth/admin", @callback 10 | "should redirect to sign-in page": (browser)-> assert.equal browser.text("button"), "GrantDeny" 11 | "grant request": 12 | topic: (browser)-> 13 | browser.pressButton "Grant", @callback 14 | "should redirect back to oauth admin": (browser)-> assert.equal browser.location, "http://localhost:8080/oauth/admin#/" 15 | "should be looking at all clients": (browser)-> assert.equal browser.text("title"), "OAuth Admin - All Clients" 16 | "should have table with clients": (browser)-> assert.ok browser.html("table.clients")[0] 17 | "should have one active client": (browser)-> assert.ok browser.html("tr.active").length > 0 18 | "active clients": 19 | topic: (browser)-> browser.querySelectorAll("tr.active").toArray() 20 | "should include the practice server": (clients)-> 21 | assert.length clients.filter((e)-> e.querySelector(".name").textContent.trim() == "Practice OAuth Console"), 1 22 | "practice server": 23 | topic: (clients, browser)-> 24 | clients.filter((e)-> e.querySelector(".name").textContent.trim() == "Practice OAuth Console")[0] 25 | "name": 26 | topic: (client)-> client.querySelector(".name") 27 | "should show service image": (name)-> 28 | assert.ok name.querySelector("img[src='http://localhost:8080/oauth/admin/images/oauth-2.png']") 29 | "secrets": 30 | topic: (client)-> client.querySelector(".secrets") 31 | "should show client ID": (secrets)-> 32 | assert.ok secrets.querySelector("dt:contains('ID') + dd:contains('4d0ee1633321e869ad000001')") 33 | "should show client secret": (secrets)-> 34 | assert.ok secrets.querySelector("dt:contains('Secret') + dd:contains('74e3f0e33203d79f5e2e404e81daab23929a0112b9bea9afbfff7433bbfaa9cb')") 35 | "should show redirect URI": (secrets)-> 36 | assert.ok secrets.querySelector("dt:contains('Redirect') + dd:contains('http://localhost:8080/oauth/admin')") 37 | 38 | ).export(module); 39 | -------------------------------------------------------------------------------- /test/admin/api_test.rb: -------------------------------------------------------------------------------- 1 | require "test/setup" 2 | 3 | class AdminApiTest < Test::Unit::TestCase 4 | module Helpers 5 | def should_fail_authentication 6 | should "respond with status 401 (Unauthorized)" do 7 | assert_equal 401, last_response.status 8 | end 9 | end 10 | 11 | def should_forbid_access 12 | should "respond with status 403 (Forbidden)" do 13 | assert_equal 403, last_response.status 14 | end 15 | end 16 | end 17 | extend Helpers 18 | 19 | 20 | def without_scope 21 | token = Server.token_for("Superman", client.id, "nobody", 0) 22 | header "Authorization", "OAuth #{token}" 23 | end 24 | 25 | def with_scope 26 | token = Server.token_for("Superman", client.id, "oauth-admin", 0) 27 | header "Authorization", "OAuth #{token}" 28 | end 29 | 30 | def json 31 | JSON.parse(last_response.body) 32 | end 33 | 34 | 35 | context "force SSL" do 36 | setup do 37 | Server::Admin.force_ssl = true 38 | with_scope 39 | end 40 | 41 | context "HTTP request" do 42 | setup { get "/oauth/admin/api/clients" } 43 | 44 | should "redirect to HTTPS" do 45 | assert_equal 302, last_response.status 46 | assert_equal "https://example.org/oauth/admin/api/clients", last_response.location 47 | end 48 | end 49 | 50 | context "HTTPS request" do 51 | setup { get "https://example.org/oauth/admin/api/clients" } 52 | 53 | should "serve request" do 54 | assert_equal 200, last_response.status 55 | assert Array === json["list"] 56 | end 57 | end 58 | 59 | teardown { Server::Admin.force_ssl = false } 60 | end 61 | 62 | 63 | # -- /oauth/admin/api/clients 64 | 65 | context "all clients" do 66 | context "without authentication" do 67 | setup { get "/oauth/admin/api/clients" } 68 | should_fail_authentication 69 | end 70 | 71 | context "without scope" do 72 | setup { without_scope ; get "/oauth/admin/api/clients" } 73 | should_forbid_access 74 | end 75 | 76 | context "proper request" do 77 | setup { with_scope ; get "/oauth/admin/api/clients" } 78 | should "return OK" do 79 | assert_equal 200, last_response.status 80 | end 81 | should "return JSON document" do 82 | assert_equal "application/json", last_response.content_type.split(";").first 83 | end 84 | should "return list of clients" do 85 | assert Array === json["list"] 86 | end 87 | should "return known scope" do 88 | assert_equal %w{read write}, json["scope"] 89 | end 90 | end 91 | 92 | context "client list" do 93 | setup do 94 | with_scope 95 | get "/oauth/admin/api/clients" 96 | @first = json["list"].first 97 | end 98 | 99 | should "provide client identifier" do 100 | assert_equal client.id.to_s, @first["id"] 101 | end 102 | should "provide client secret" do 103 | assert_equal client.secret, @first["secret"] 104 | end 105 | should "provide redirect URI" do 106 | assert_equal client.redirect_uri, @first["redirectUri"] 107 | end 108 | should "provide display name" do 109 | assert_equal client.display_name, @first["displayName"] 110 | end 111 | should "provide site URL" do 112 | assert_equal client.link, @first["link"] 113 | end 114 | should "provide image URL" do 115 | assert_equal client.image_url, @first["imageUrl"] 116 | end 117 | should "provide created timestamp" do 118 | assert_equal client.created_at.to_i, @first["created"] 119 | end 120 | should "provide link to client resource"do 121 | assert_equal ["/oauth/admin/api/client", client.id].join("/"), @first["url"] 122 | end 123 | should "provide link to revoke resource"do 124 | assert_equal ["/oauth/admin/api/client", client.id, "revoke"].join("/"), @first["revoke"] 125 | end 126 | should "provide scope for client" do 127 | assert_equal %w{oauth-admin read write}, @first["scope"] 128 | end 129 | should "tell if not revoked" do 130 | assert @first["revoked"].nil? 131 | end 132 | end 133 | 134 | context "revoked client" do 135 | setup do 136 | client.revoke! 137 | with_scope 138 | get "/oauth/admin/api/clients" 139 | @first = json["list"].first 140 | end 141 | 142 | should "provide revoked timestamp" do 143 | assert_equal client.revoked.to_i, @first["revoked"] 144 | end 145 | end 146 | 147 | context "tokens" do 148 | setup do 149 | tokens = [] 150 | 1.upto(10).map do |days| 151 | Timecop.travel -days*86400 do 152 | tokens << Server.token_for("Superman#{days}", client.id) 153 | end 154 | end 155 | # Revoke one token today (within past 7 days), one 10 days ago (beyond) 156 | Timecop.travel -7 * 86400 do 157 | Server.get_access_token(tokens[0]).revoke! 158 | end 159 | Server.get_access_token(tokens[1]).revoke! 160 | with_scope ; get "/oauth/admin/api/clients" 161 | end 162 | 163 | should "return total number of tokens" do 164 | assert_equal 11, json["tokens"]["total"] 165 | end 166 | should "return number of tokens created past week" do 167 | assert_equal 7, json["tokens"]["week"] 168 | end 169 | should "return number of revoked token past week" do 170 | assert_equal 1, json["tokens"]["revoked"] 171 | end 172 | end 173 | end 174 | 175 | 176 | # -- /oauth/admin/api/client/:id 177 | 178 | context "single client" do 179 | context "without authentication" do 180 | setup { get "/oauth/admin/api/client/#{client.id}" } 181 | should_fail_authentication 182 | end 183 | 184 | context "without scope" do 185 | setup { without_scope ; get "/oauth/admin/api/client/#{client.id}" } 186 | should_forbid_access 187 | end 188 | 189 | context "with scope" do 190 | setup { with_scope ; get "/oauth/admin/api/client/#{client.id}" } 191 | 192 | should "return OK" do 193 | assert_equal 200, last_response.status 194 | end 195 | should "return JSON document" do 196 | assert_equal "application/json", last_response.content_type.split(";").first 197 | end 198 | should "provide client identifier" do 199 | assert_equal client.id.to_s, json["id"] 200 | end 201 | should "provide client secret" do 202 | assert_equal client.secret, json["secret"] 203 | end 204 | should "provide redirect URI" do 205 | assert_equal client.redirect_uri, json["redirectUri"] 206 | end 207 | should "provide display name" do 208 | assert_equal client.display_name, json["displayName"] 209 | end 210 | should "provide site URL" do 211 | assert_equal client.link, json["link"] 212 | end 213 | should "provide image URL" do 214 | assert_equal client.image_url, json["imageUrl"] 215 | end 216 | should "provide created timestamp" do 217 | assert_equal client.created_at.to_i, json["created"] 218 | end 219 | should "provide link to client resource"do 220 | assert_equal ["/oauth/admin/api/client", client.id].join("/"), json["url"] 221 | end 222 | should "provide link to revoke resource"do 223 | assert_equal ["/oauth/admin/api/client", client.id, "revoke"].join("/"), json["revoke"] 224 | end 225 | end 226 | end 227 | 228 | end 229 | -------------------------------------------------------------------------------- /test/admin/ui_test.rb: -------------------------------------------------------------------------------- 1 | require "test/setup" 2 | 3 | class AdminUiTest < Test::Unit::TestCase 4 | context "/" do 5 | setup { get "/oauth/admin" } 6 | should "return OK" do 7 | assert_equal 200, last_response.status 8 | end 9 | should "return HTML page" do 10 | assert_match "", last_response.body 11 | end 12 | end 13 | 14 | context "force SSL" do 15 | setup { Server::Admin.force_ssl = true } 16 | 17 | context "HTTP request" do 18 | setup { get "/oauth/admin" } 19 | 20 | should "redirect to HTTPS" do 21 | assert_equal 302, last_response.status 22 | assert_match "https://example.org/oauth/admin", last_response.location 23 | end 24 | end 25 | 26 | context "HTTPS request" do 27 | setup { get "https://example.org/oauth/admin" } 28 | 29 | should "serve request" do 30 | assert_equal 200, last_response.status 31 | assert_match "", last_response.body 32 | end 33 | end 34 | 35 | teardown { Server::Admin.force_ssl = false } 36 | end 37 | 38 | end 39 | -------------------------------------------------------------------------------- /test/oauth/access_token_test.rb: -------------------------------------------------------------------------------- 1 | require "test/setup" 2 | 3 | 4 | # 5. Accessing a Protected Resource 5 | class AccessTokenTest < Test::Unit::TestCase 6 | module Helpers 7 | 8 | def should_return_resource(content) 9 | should "respond with status 200" do 10 | assert_equal 200, last_response.status 11 | end 12 | should "respond with resource name" do 13 | assert_equal content, last_response.body 14 | end 15 | end 16 | 17 | def should_fail_authentication(error = nil) 18 | should "respond with status 401 (Unauthorized)" do 19 | assert_equal 401, last_response.status 20 | end 21 | should "respond with authentication method OAuth" do 22 | assert_equal "OAuth", last_response["WWW-Authenticate"].split.first 23 | end 24 | should "respond with realm" do 25 | assert_match " realm=\"example.org\"", last_response["WWW-Authenticate"] 26 | end 27 | if error 28 | should "respond with error code #{error}" do 29 | assert_match " error=\"#{error}\"", last_response["WWW-Authenticate"] 30 | end 31 | else 32 | should "not respond with error code" do 33 | assert !last_response["WWW-Authenticate"]["error="] 34 | end 35 | end 36 | end 37 | 38 | end 39 | extend Helpers 40 | 41 | 42 | def setup 43 | super 44 | # Get authorization code. 45 | params = { :redirect_uri=>client.redirect_uri, :client_id=>client.id, :client_secret=>client.secret, :response_type=>"code", 46 | :scope=>"read write", :state=>"bring this back" } 47 | get "/oauth/authorize?" + Rack::Utils.build_query(params) 48 | get last_response["Location"] if last_response.status == 303 49 | authorization = last_response.body[/authorization:\s*(\S+)/, 1] 50 | post "/oauth/grant", :authorization=>authorization 51 | code = Rack::Utils.parse_query(URI.parse(last_response["Location"]).query)["code"] 52 | # Get access token 53 | basic_authorize client.id, client.secret 54 | post "/oauth/access_token", :scope=>"read write", :grant_type=>"authorization_code", :code=>code, :redirect_uri=>client.redirect_uri 55 | @token = JSON.parse(last_response.body)["access_token"] 56 | header "Authorization", nil 57 | end 58 | 59 | def with_token(token = @token) 60 | header "Authorization", "OAuth #{token}" 61 | end 62 | 63 | def expire 64 | Rack::OAuth2::Server::AccessToken.collection.update({ :_id => @token }, { :$set=> { :expires_at => (Time.now - 1).to_i } }) 65 | end 66 | 67 | def with_expired_token 68 | expire 69 | header "Authorization", "OAuth #{@token}" 70 | end 71 | 72 | 73 | # 5. Accessing a Protected Resource 74 | 75 | context "public resource" do 76 | context "no authorization" do 77 | setup { get "/public" } 78 | should_return_resource "HAI" 79 | end 80 | 81 | context "with authorization" do 82 | setup do 83 | with_token 84 | get "/public" 85 | end 86 | should_return_resource "HAI from Batman" 87 | end 88 | end 89 | 90 | context "private resource" do 91 | context "no authorization" do 92 | setup { get "/private" } 93 | should_fail_authentication 94 | end 95 | 96 | context "expired authorization" do 97 | setup do 98 | with_expired_token 99 | get "/private" 100 | end 101 | should_fail_authentication :expired_token 102 | end 103 | 104 | context "HTTP authentication" do 105 | context "valid token" do 106 | setup do 107 | with_token 108 | get "/private" 109 | end 110 | should_return_resource "Shhhh" 111 | end 112 | 113 | context "unknown token" do 114 | setup do 115 | with_token "dingdong" 116 | get "/private" 117 | end 118 | should_fail_authentication :invalid_token 119 | end 120 | 121 | context "revoked HTTP token" do 122 | setup do 123 | Server::AccessToken.from_token(@token).revoke! 124 | with_token 125 | get "/private" 126 | end 127 | should_fail_authentication :invalid_token 128 | end 129 | 130 | context "revoked client" do 131 | setup do 132 | client.revoke! 133 | with_token 134 | get "/private" 135 | end 136 | should_fail_authentication :invalid_token 137 | end 138 | end 139 | 140 | # 5.1.2. URI Query Parameter 141 | 142 | context "query parameter" do 143 | context "default mode" do 144 | setup { get "/private?oauth_token=#{@token}" } 145 | should_fail_authentication 146 | end 147 | 148 | context "enabled" do 149 | setup do 150 | config.param_authentication = true 151 | end 152 | 153 | context "valid token" do 154 | setup { get "/private?oauth_token=#{@token}" } 155 | should_return_resource "Shhhh" 156 | end 157 | 158 | context "invalid token" do 159 | setup { get "/private?oauth_token=dingdong" } 160 | should_fail_authentication :invalid_token 161 | end 162 | 163 | teardown do 164 | config.param_authentication = false 165 | end 166 | end 167 | end 168 | end 169 | 170 | context "POST" do 171 | context "no authorization" do 172 | setup { post "/change" } 173 | should_fail_authentication 174 | end 175 | 176 | context "HTTP authentication" do 177 | context "valid token" do 178 | setup do 179 | with_token 180 | post "/change" 181 | end 182 | should_return_resource "Woot!" 183 | end 184 | 185 | context "unknown token" do 186 | setup do 187 | with_token "dingdong" 188 | post "/change" 189 | end 190 | should_fail_authentication :invalid_token 191 | end 192 | 193 | end 194 | 195 | # 5.1.3. Form-Encoded Body Parameter 196 | 197 | context "body parameter" do 198 | context "default mode" do 199 | setup { post "/change", :oauth_token=>@token } 200 | should_fail_authentication 201 | end 202 | 203 | context "enabled" do 204 | setup do 205 | config.param_authentication = true 206 | end 207 | 208 | context "valid token" do 209 | setup { post "/change", :oauth_token=>@token } 210 | should_return_resource "Woot!" 211 | end 212 | 213 | context "invalid token" do 214 | setup { post "/change", :oauth_token=>"dingdong" } 215 | should_fail_authentication :invalid_token 216 | end 217 | 218 | teardown do 219 | config.param_authentication = false 220 | end 221 | end 222 | end 223 | end 224 | 225 | 226 | context "insufficient scope" do 227 | context "valid token" do 228 | setup do 229 | with_token 230 | get "/calc" 231 | end 232 | 233 | should "respond with status 403 (Forbidden)" do 234 | assert_equal 403, last_response.status 235 | end 236 | should "respond with authentication method OAuth" do 237 | assert_equal "OAuth", last_response["WWW-Authenticate"].split.first 238 | end 239 | should "respond with realm" do 240 | assert_match " realm=\"example.org\"", last_response["WWW-Authenticate"] 241 | end 242 | should "respond with error code insufficient_scope" do 243 | assert_match " error=\"insufficient_scope\"", last_response["WWW-Authenticate"] 244 | end 245 | should "respond with scope name" do 246 | assert_match " scope=\"math\"", last_response["WWW-Authenticate"] 247 | end 248 | should "respond with proper headers" do 249 | Rack::Lint.new(nil).check_headers(last_response.headers) 250 | end 251 | end 252 | end 253 | 254 | 255 | context "setting resource" do 256 | context "authenticated" do 257 | setup do 258 | with_token 259 | get "/user" 260 | end 261 | 262 | should "render user name" do 263 | assert_equal "Batman", last_response.body 264 | end 265 | end 266 | 267 | context "not authenticated" do 268 | setup do 269 | get "/user" 270 | end 271 | 272 | should "not render user name" do 273 | assert last_response.body.empty? 274 | end 275 | end 276 | end 277 | 278 | context "list tokens" do 279 | setup do 280 | @other = Server.token_for("foobar", client.id, "read") 281 | get "/list_tokens" 282 | end 283 | 284 | should "return access token" do 285 | assert_contains last_response.body.split, @token 286 | end 287 | 288 | should "not return other resource's token" do 289 | assert !last_response.body.split.include?(@other) 290 | end 291 | end 292 | 293 | context "tokens have an expire date" do 294 | setup do 295 | @other_token = Rack::OAuth2::Server::AccessToken.from_token(@token) 296 | end 297 | 298 | should "expire in a day" do 299 | a_day_later = (Time.now + (60 * 60 * 24) + 1).to_i 300 | assert @other_token.expires_at < a_day_later 301 | end 302 | 303 | end 304 | 305 | 306 | context "with specific host" do 307 | context "right host" do 308 | setup do 309 | get "http://example.org/public" 310 | end 311 | # Right host, but not authenticated 312 | should_return_resource "HAI" 313 | end 314 | 315 | context "wrong host" do 316 | setup do 317 | with_token 318 | get "http://wrong.org/public" 319 | end 320 | # Wrong host, not checking credentials 321 | should_return_resource "HAI" 322 | end 323 | end 324 | 325 | 326 | context "with specific path" do 327 | setup { config.path = "/private" } 328 | 329 | context "outside path" do 330 | setup { with_token ; get "http://example.org/public" } 331 | # Not authenticated 332 | should_return_resource "HAI" 333 | end 334 | 335 | context "inside path" do 336 | setup { with_token ; get "http://example.org/private" } 337 | # Authenticated 338 | should_return_resource "Shhhh" 339 | end 340 | 341 | teardown { config.path = nil } 342 | end 343 | end 344 | -------------------------------------------------------------------------------- /test/oauth/authorization_test.rb: -------------------------------------------------------------------------------- 1 | require "test/setup" 2 | 3 | 4 | # 3. Obtaining End-User Authorization 5 | class AuthorizationTest < Test::Unit::TestCase 6 | module Helpers 7 | 8 | def should_redirect_with_error(error) 9 | should "respond with status code 302 (Found)" do 10 | assert_equal 302, last_response.status 11 | end 12 | should "redirect back to redirect_uri" do 13 | assert_equal URI.parse(last_response["Location"]).host, "uberclient.dot" 14 | end 15 | should "redirect with error code #{error}" do 16 | assert_equal error.to_s, Rack::Utils.parse_query(URI.parse(last_response["Location"]).query)["error"] 17 | end 18 | should "redirect with state parameter" do 19 | assert_equal "bring this back", Rack::Utils.parse_query(URI.parse(last_response["Location"]).query)["state"] 20 | end 21 | end 22 | 23 | def should_ask_user_for_authorization(&block) 24 | should "inform user about client" do 25 | response = last_response.body.split("\n").inject({}) { |h,l| n,v = l.split(/:\s*/) ; h[n.downcase] = v ; h } 26 | assert_equal "UberClient", response["client"] 27 | end 28 | should "inform user about scope" do 29 | response = last_response.body.split("\n").inject({}) { |h,l| n,v = l.split(/:\s*/) ; h[n.downcase] = v ; h } 30 | assert_equal "read, write", response["scope"] 31 | end 32 | end 33 | 34 | end 35 | extend Helpers 36 | 37 | def setup 38 | super 39 | @params = { :redirect_uri=>client.redirect_uri, :client_id=>client.id, :client_secret=>client.secret, :response_type=>"code", 40 | :scope=>"read write", :state=>"bring this back" } 41 | end 42 | 43 | def request_authorization(changes = nil) 44 | get "/oauth/authorize?" + Rack::Utils.build_query(@params.merge(changes || {})) 45 | get last_response["Location"] if last_response.status == 303 46 | end 47 | 48 | def authorization 49 | last_response.body[/authorization:\s*(\S+)/, 1] 50 | end 51 | 52 | 53 | # Checks before we request user for authorization. 54 | # 3.2. Error Response 55 | 56 | context "no redirect URI" do 57 | setup { request_authorization :redirect_uri=>nil } 58 | should "return status 400" do 59 | assert_equal 400, last_response.status 60 | end 61 | end 62 | 63 | context "invalid redirect URI" do 64 | setup { request_authorization :redirect_uri=>"http:not-valid" } 65 | should "return status 400" do 66 | assert_equal 400, last_response.status 67 | end 68 | end 69 | 70 | context "no client ID" do 71 | setup { request_authorization :client_id=>nil } 72 | should_redirect_with_error :invalid_client 73 | end 74 | 75 | context "invalid client ID" do 76 | setup { request_authorization :client_id=>"foobar" } 77 | should_redirect_with_error :invalid_client 78 | end 79 | 80 | context "client ID but no such client" do 81 | setup { request_authorization :client_id=>"4cc7bc483321e814b8000000" } 82 | should_redirect_with_error :invalid_client 83 | end 84 | 85 | context "mismatched redirect URI" do 86 | setup { request_authorization :redirect_uri=>"http://uberclient.dot/oz" } 87 | should_redirect_with_error :redirect_uri_mismatch 88 | end 89 | 90 | context "revoked client" do 91 | setup do 92 | client.revoke! 93 | request_authorization 94 | end 95 | should_redirect_with_error :invalid_client 96 | end 97 | 98 | context "no response type" do 99 | setup { request_authorization :response_type=>nil } 100 | should_redirect_with_error :unsupported_response_type 101 | end 102 | 103 | context "unknown response type" do 104 | setup { request_authorization :response_type=>"foobar" } 105 | should_redirect_with_error :unsupported_response_type 106 | end 107 | 108 | context "unsupported scope" do 109 | setup do 110 | request_authorization :scope=>"read write math" 111 | end 112 | should_redirect_with_error :invalid_scope 113 | end 114 | 115 | 116 | # 3.1. Authorization Response 117 | 118 | context "expecting authorization code" do 119 | setup do 120 | @params[:response_type] = "code" 121 | request_authorization 122 | end 123 | should_ask_user_for_authorization 124 | 125 | context "and granted" do 126 | setup { post "/oauth/grant", :authorization=>authorization } 127 | 128 | should "redirect" do 129 | assert_equal 302, last_response.status 130 | end 131 | should "redirect back to client" do 132 | uri = URI.parse(last_response["Location"]) 133 | assert_equal "uberclient.dot", uri.host 134 | assert_equal "/callback", uri.path 135 | end 136 | 137 | context "redirect URL query parameters" do 138 | setup { @return = Rack::Utils.parse_query(URI.parse(last_response["Location"]).query) } 139 | 140 | should "include authorization code" do 141 | assert_match /[a-f0-9]{32}/i, @return["code"] 142 | end 143 | 144 | should "include original scope" do 145 | assert_equal "read write", @return["scope"] 146 | end 147 | 148 | should "include state from requet" do 149 | assert_equal "bring this back", @return["state"] 150 | end 151 | end 152 | end 153 | 154 | context "and denied" do 155 | setup { post "/oauth/deny", :authorization=>authorization } 156 | 157 | should "redirect" do 158 | assert_equal 302, last_response.status 159 | end 160 | should "redirect back to client" do 161 | uri = URI.parse(last_response["Location"]) 162 | assert_equal "uberclient.dot", uri.host 163 | assert_equal "/callback", uri.path 164 | end 165 | 166 | context "redirect URL" do 167 | setup { @return = Rack::Utils.parse_query(URI.parse(last_response["Location"]).query) } 168 | 169 | should "not include authorization code" do 170 | assert !@return["code"] 171 | end 172 | 173 | should "include error code" do 174 | assert_equal "access_denied", @return["error"] 175 | end 176 | 177 | should "include state from requet" do 178 | assert_equal "bring this back", @return["state"] 179 | end 180 | end 181 | end 182 | end 183 | 184 | 185 | context "expecting access token" do 186 | setup do 187 | @params[:response_type] = "token" 188 | request_authorization 189 | end 190 | should_ask_user_for_authorization 191 | 192 | context "and granted" do 193 | setup { post "/oauth/grant", :authorization=>authorization } 194 | 195 | should "redirect" do 196 | assert_equal 302, last_response.status 197 | end 198 | should "redirect back to client" do 199 | uri = URI.parse(last_response["Location"]) 200 | assert_equal "uberclient.dot", uri.host 201 | assert_equal "/callback", uri.path 202 | end 203 | 204 | context "redirect URL fragment identifier" do 205 | setup { @return = Rack::Utils.parse_query(URI.parse(last_response["Location"]).fragment) } 206 | 207 | should "include access token" do 208 | assert_match /[a-f0-9]{32}/i, @return["access_token"] 209 | end 210 | 211 | should "include original scope" do 212 | assert_equal "read write", @return["scope"] 213 | end 214 | 215 | should "include state from requet" do 216 | assert_equal "bring this back", @return["state"] 217 | end 218 | end 219 | end 220 | 221 | context "and denied" do 222 | setup { post "/oauth/deny", :authorization=>authorization } 223 | 224 | should "redirect" do 225 | assert_equal 302, last_response.status 226 | end 227 | should "redirect back to client" do 228 | uri = URI.parse(last_response["Location"]) 229 | assert_equal "uberclient.dot", uri.host 230 | assert_equal "/callback", uri.path 231 | end 232 | 233 | context "redirect URL" do 234 | setup { @return = Rack::Utils.parse_query(URI.parse(last_response["Location"]).fragment) } 235 | 236 | should "not include authorization code" do 237 | assert !@return["code"] 238 | end 239 | 240 | should "include error code" do 241 | assert_equal "access_denied", @return["error"] 242 | end 243 | 244 | should "include state from requet" do 245 | assert_equal "bring this back", @return["state"] 246 | end 247 | end 248 | end 249 | end 250 | 251 | 252 | # Using existing authorization request 253 | 254 | context "with authorization request" do 255 | setup do 256 | request_authorization 257 | get "/oauth/authorize?" + Rack::Utils.build_query(:authorization=>authorization) 258 | end 259 | 260 | should_ask_user_for_authorization 261 | end 262 | 263 | context "with invalid authorization request" do 264 | setup do 265 | request_authorization 266 | get "/oauth/authorize?" + Rack::Utils.build_query(:authorization=>"foobar") 267 | end 268 | 269 | should "return status 400" do 270 | assert_equal 400, last_response.status 271 | end 272 | end 273 | 274 | context "with revoked authorization request" do 275 | setup do 276 | request_authorization 277 | response = last_response.body.split("\n").inject({}) { |h,l| n,v = l.split(/:\s*/) ; h[n.downcase] = v ; h } 278 | client.revoke! 279 | get "/oauth/authorize?" + Rack::Utils.build_query(:authorization=>response["authorization"]) 280 | end 281 | 282 | should "return status 400" do 283 | assert_equal 400, last_response.status 284 | end 285 | end 286 | 287 | 288 | # Edge cases 289 | 290 | context "unregistered redirect URI" do 291 | setup do 292 | Rack::OAuth2::Server::Client.collection.update({ :_id=>client._id }, { :$set=>{ :redirect_uri=>nil } }) 293 | request_authorization :redirect_uri=>"http://uberclient.dot/oz" 294 | end 295 | should_ask_user_for_authorization 296 | end 297 | 298 | end 299 | -------------------------------------------------------------------------------- /test/oauth/server_methods_test.rb: -------------------------------------------------------------------------------- 1 | require "test/setup" 2 | 3 | 4 | # Tests the Server API 5 | class ServerTest < Test::Unit::TestCase 6 | def setup 7 | super 8 | end 9 | 10 | context "configuration" do 11 | should "set oauth.database" do 12 | assert_equal DATABASE, Server.database 13 | end 14 | 15 | should "set oauth.host" do 16 | assert_equal "example.org", Server.options.host 17 | end 18 | 19 | should "set oauth.collection_prefix" do 20 | assert_equal "oauth2_prefix", Server.options.collection_prefix 21 | end 22 | end 23 | 24 | context "get_auth_request" do 25 | setup { @request = Server::AuthRequest.create(client, client.scope.join(" "), client.redirect_uri, "token", nil) } 26 | should "return authorization request" do 27 | assert_equal @request.id, Server.get_auth_request(@request.id).id 28 | end 29 | 30 | should "return nil if no request found" do 31 | assert !Server.get_auth_request("4ce2488e3321e87ac1000004") 32 | end 33 | end 34 | 35 | 36 | context "get_client" do 37 | should "return authorization request" do 38 | assert_equal client.display_name, Server.get_client(client.id).display_name 39 | end 40 | 41 | should "return nil if no client found" do 42 | assert !Server.get_client("4ce2488e3321e87ac1000004") 43 | end 44 | end 45 | 46 | 47 | context "register" do 48 | context "no client ID" do 49 | setup do 50 | @client = Server.register(:display_name=>"MyApp", :link=>"http://example.org", :image_url=>"http://example.org/favicon.ico", 51 | :redirect_uri=>"http://example.org/oauth/callback", :scope=>%w{read write}) 52 | end 53 | 54 | should "create new client" do 55 | assert_equal 2, Server::Client.collection.count 56 | assert_contains Server::Client.all.map(&:id), @client.id 57 | end 58 | 59 | should "set display name" do 60 | assert_equal "MyApp", Server.get_client(@client.id).display_name 61 | end 62 | 63 | should "set link" do 64 | assert_equal "http://example.org", Server.get_client(@client.id).link 65 | end 66 | 67 | should "set image URL" do 68 | assert_equal "http://example.org/favicon.ico", Server.get_client(@client.id).image_url 69 | end 70 | 71 | should "set redirect URI" do 72 | assert_equal "http://example.org/oauth/callback", Server.get_client(@client.id).redirect_uri 73 | end 74 | 75 | should "set scope" do 76 | assert_equal %w{read write}, Server.get_client(@client.id).scope 77 | end 78 | 79 | should "assign client an ID" do 80 | assert_match /[0-9a-f]{24}/, @client.id.to_s 81 | end 82 | 83 | should "assign client a secret" do 84 | assert_match /[0-9a-f]{64}/, @client.secret 85 | end 86 | end 87 | 88 | context "with client ID" do 89 | 90 | context "no such client" do 91 | setup do 92 | @client = Server.register(:id=>"4ce24c423321e88ac5000015", :secret=>"foobar", :display_name=>"MyApp") 93 | end 94 | 95 | should "create new client" do 96 | assert_equal 2, Server::Client.collection.count 97 | end 98 | 99 | should "should assign it the client identifier" do 100 | assert_equal "4ce24c423321e88ac5000015", @client.id.to_s 101 | end 102 | 103 | should "should assign it the client secret" do 104 | assert_equal "foobar", @client.secret 105 | end 106 | 107 | should "should assign it the other properties" do 108 | assert_equal "MyApp", @client.display_name 109 | end 110 | end 111 | 112 | context "existing client" do 113 | setup do 114 | Server.register(:id=>"4ce24c423321e88ac5000015", :secret=>"foobar", :display_name=>"MyApp") 115 | @client = Server.register(:id=>"4ce24c423321e88ac5000015", :secret=>"foobar", :display_name=>"Rock Star") 116 | end 117 | 118 | should "not create new client" do 119 | assert_equal 2, Server::Client.collection.count 120 | end 121 | 122 | should "should not change the client identifier" do 123 | assert_equal "4ce24c423321e88ac5000015", @client.id.to_s 124 | end 125 | 126 | should "should not change the client secret" do 127 | assert_equal "foobar", @client.secret 128 | end 129 | 130 | should "should change all the other properties" do 131 | assert_equal "Rock Star", @client.display_name 132 | end 133 | end 134 | 135 | context "secret mismatch" do 136 | setup do 137 | Server.register(:id=>"4ce24c423321e88ac5000015", :secret=>"foobar", :display_name=>"MyApp") 138 | end 139 | 140 | should "raise error" do 141 | assert_raises RuntimeError do 142 | Server.register(:id=>"4ce24c423321e88ac5000015", :secret=>"wrong", :display_name=>"MyApp") 143 | end 144 | end 145 | end 146 | 147 | end 148 | end 149 | 150 | 151 | context "access_grant" do 152 | setup do 153 | code = Server.access_grant("Batman", client.id, %w{read}) 154 | basic_authorize client.id, client.secret 155 | post "/oauth/access_token", :scope=>"read", :grant_type=>"authorization_code", :code=>code, :redirect_uri=>client.redirect_uri 156 | @token = JSON.parse(last_response.body)["access_token"] 157 | end 158 | 159 | should "resolve into an access token" do 160 | assert Server.get_access_token(@token) 161 | end 162 | 163 | should "resolve into access token with grant identity" do 164 | assert_equal "Batman", Server.get_access_token(@token).identity 165 | end 166 | 167 | should "resolve into access token with grant scope" do 168 | assert_equal %w{read}, Server.get_access_token(@token).scope 169 | end 170 | 171 | should "resolve into access token with grant client" do 172 | assert_equal client.id, Server.get_access_token(@token).client_id 173 | end 174 | 175 | context "with no scope" do 176 | setup { @code = Server.access_grant("Batman", client.id) } 177 | 178 | should "pick client scope" do 179 | assert_equal %w{oauth-admin read write}, Server::AccessGrant.from_code(@code).scope 180 | end 181 | end 182 | 183 | context "no expiration" do 184 | setup do 185 | @code = Server.access_grant("Batman", client.id) 186 | end 187 | 188 | should "not expire in a minute" do 189 | Timecop.travel 60 do 190 | basic_authorize client.id, client.secret 191 | post "/oauth/access_token", :scope=>"read", :grant_type=>"authorization_code", :code=>@code, :redirect_uri=>client.redirect_uri 192 | assert_equal 200, last_response.status 193 | end 194 | end 195 | 196 | should "expire after 5 minutes" do 197 | Timecop.travel 300 do 198 | basic_authorize client.id, client.secret 199 | post "/oauth/access_token", :scope=>"read", :grant_type=>"authorization_code", :code=>@code, :redirect_uri=>client.redirect_uri 200 | assert_equal 400, last_response.status 201 | end 202 | end 203 | end 204 | 205 | context "expiration set" do 206 | setup do 207 | @code = Server.access_grant("Batman", client.id, nil, 1800) 208 | end 209 | 210 | should "not expire prematurely" do 211 | Timecop.travel 1750 do 212 | basic_authorize client.id, client.secret 213 | post "/oauth/access_token", :scope=>"read", :grant_type=>"authorization_code", :code=>@code, :redirect_uri=>client.redirect_uri 214 | assert_equal 200, last_response.status 215 | end 216 | end 217 | 218 | should "expire after specified seconds" do 219 | Timecop.travel 1800 do 220 | basic_authorize client.id, client.secret 221 | post "/oauth/access_token", :scope=>"read", :grant_type=>"authorization_code", :code=>@code, :redirect_uri=>client.redirect_uri 222 | assert_equal 400, last_response.status 223 | end 224 | end 225 | end 226 | 227 | end 228 | 229 | 230 | context "get_access_token" do 231 | setup { @token = Server.token_for("Batman", client.id, %w{read}) } 232 | should "return authorization request" do 233 | assert_equal @token, Server.get_access_token(@token).token 234 | end 235 | 236 | should "return nil if no client found" do 237 | assert !Server.get_access_token("4ce2488e3321e87ac1000004") 238 | end 239 | 240 | context "with no scope" do 241 | setup { @token = Server.token_for("Batman", client.id) } 242 | 243 | should "pick client scope" do 244 | assert_equal %w{oauth-admin read write}, Server::AccessToken.from_token(@token).scope 245 | end 246 | end 247 | end 248 | 249 | 250 | context "token_for" do 251 | setup { @token = Server.token_for("Batman", client.id, %w{read write}) } 252 | 253 | should "return access token" do 254 | assert_match /[0-9a-f]{32}/, @token 255 | end 256 | 257 | should "associate token with client" do 258 | assert_equal client.id, Server.get_access_token(@token).client_id 259 | end 260 | 261 | should "associate token with identity" do 262 | assert_equal "Batman", Server.get_access_token(@token).identity 263 | end 264 | 265 | should "associate token with scope" do 266 | assert_equal %w{read write}, Server.get_access_token(@token).scope 267 | end 268 | 269 | should "return same token for same parameters" do 270 | assert_equal @token, Server.token_for("Batman", client.id, %w{write read}) 271 | end 272 | 273 | should "return different token for different identity" do 274 | assert @token != Server.token_for("Superman", client.id, %w{read write}) 275 | end 276 | 277 | should "return different token for different client" do 278 | client = Server.register(:display_name=>"MyApp") 279 | assert @token != Server.token_for("Batman", client.id, %w{read write}) 280 | end 281 | 282 | should "return different token for different scope" do 283 | assert @token != Server.token_for("Batman", client.id, %w{read}) 284 | end 285 | 286 | should 'expire token after the specified amount of time' do 287 | Server::AccessToken.collection.drop 288 | token = Server.token_for("Batman", client.id, %w{read write}, 60) 289 | 290 | Timecop.travel 120 do 291 | assert token != Server.token_for("Batman", client.id, %w{read write}) 292 | end 293 | end 294 | end 295 | 296 | context "list access tokens" do 297 | setup do 298 | @one = Server.token_for("Batman", client.id, %w{read}) 299 | @two = Server.token_for("Superman", client.id, %w{read}) 300 | @three = Server.token_for("Batman", client.id, %w{write}) 301 | end 302 | 303 | should "return all tokens for identity" do 304 | assert_contains Server.list_access_tokens("Batman").map(&:token), @one 305 | assert_contains Server.list_access_tokens("Batman").map(&:token), @three 306 | end 307 | 308 | should "not return tokens for other identities" do 309 | assert !Server.list_access_tokens("Batman").map(&:token).include?(@two) 310 | end 311 | 312 | end 313 | 314 | context "issuers" do 315 | setup do 316 | @issuer = Server.register_issuer(:identifier => "http://www.hmacissuer.com", :hmac_secret => "foo", :notes => "Test HMAC Issuer") 317 | end 318 | 319 | should "return a known issuer" do 320 | issuer = Server.get_issuer("http://www.hmacissuer.com") 321 | assert issuer 322 | assert issuer.hmac_secret == "foo" 323 | assert issuer.notes == "Test HMAC Issuer" 324 | end 325 | end 326 | end 327 | -------------------------------------------------------------------------------- /test/oauth/server_test.rb: -------------------------------------------------------------------------------- 1 | require "test/setup" 2 | 3 | class ServerTest < Test::Unit::TestCase 4 | 5 | context "setup server" do 6 | setup { @client = Server.register(:display_name=>"UberClient", :redirect_uri=>"http://uberclient.dot/callback", :scope=>%w{read write oauth-admin}) } 7 | should "have parameters" do 8 | assert_equal "http://uberclient.dot/callback", @client.redirect_uri 9 | assert_equal "UberClient", @client.display_name 10 | assert_same_elements %w(read write oauth-admin), @client.scope 11 | end 12 | end 13 | 14 | end 15 | -------------------------------------------------------------------------------- /test/rails2/app/controllers/api_controller.rb: -------------------------------------------------------------------------------- 1 | class ApiController < ApplicationController 2 | 3 | oauth_required :only=>[:private, :change] 4 | oauth_required :only=>[:calc], :scope=>"math" 5 | 6 | def public 7 | if oauth.authenticated? 8 | render :text=>"HAI from #{oauth.identity}" 9 | else 10 | render :text=>"HAI" 11 | end 12 | end 13 | 14 | def private 15 | render :text=>"Shhhh" 16 | end 17 | 18 | def change 19 | render :text=>"Woot!" 20 | end 21 | 22 | def calc 23 | render :text=>"2+2=4" 24 | end 25 | 26 | def list_tokens 27 | render :text=>oauth.list_access_tokens("Batman").map(&:token).join(" ") 28 | end 29 | 30 | def user 31 | render :text=>current_user.to_s 32 | end 33 | 34 | protected 35 | 36 | def current_user 37 | @current_user ||= oauth.identity if oauth.authenticated? 38 | end 39 | 40 | end 41 | -------------------------------------------------------------------------------- /test/rails2/app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | end 3 | -------------------------------------------------------------------------------- /test/rails2/app/controllers/oauth_controller.rb: -------------------------------------------------------------------------------- 1 | class OauthController < ApplicationController 2 | before_filter do |c| 3 | c.send :head, c.oauth.deny! if c.oauth.scope.include?("time-travel") # Only Superman can do that 4 | end 5 | 6 | def authorize 7 | render :text=>"client: #{oauth.client.display_name}\nscope: #{oauth.scope.join(", ")}\nauthorization: #{oauth.authorization}" 8 | end 9 | 10 | def grant 11 | head oauth.grant!(params["authorization"], "Batman") 12 | end 13 | 14 | def deny 15 | head oauth.deny!(params["authorization"]) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /test/rails2/config/environment.rb: -------------------------------------------------------------------------------- 1 | class << Rails 2 | def vendor_rails? 3 | false 4 | end 5 | end 6 | 7 | require "yaml" 8 | YAML::ENGINE.yamler= "syck" if defined?(YAML::ENGINE) # see http://stackoverflow.com/questions/4980877/rails-error-couldnt-parse-yaml 9 | 10 | Rails::Initializer.run do |config| 11 | config.frameworks = [ :action_controller ] 12 | config.action_controller.session = { :key=>"_myapp_session", :secret=>"Stay hungry. Stay foolish. -- Steve Jobs" } 13 | 14 | config.after_initialize do 15 | config.oauth.database = DATABASE 16 | config.oauth.host = "example.org" 17 | config.oauth.collection_prefix = "oauth2_prefix" 18 | config.oauth.authenticator = lambda do |username, password| 19 | "Batman" if username == "cowbell" && password == "more" 20 | end 21 | end 22 | config.middleware.use Rack::OAuth2::Server::Admin.mount 23 | end 24 | -------------------------------------------------------------------------------- /test/rails2/config/environments/test.rb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/assaf/rack-oauth2-server/91c0dd8a57754b37bbb779b95233b502129def75/test/rails2/config/environments/test.rb -------------------------------------------------------------------------------- /test/rails2/config/routes.rb: -------------------------------------------------------------------------------- 1 | ActionController::Routing::Routes.draw do |map| 2 | # Authorization flow. 3 | map.with_options :controller=>"oauth" do |oauth| 4 | oauth.connect "oauth/authorize", :action=>"authorize" 5 | oauth.connect "oauth/grant", :action=>"grant" 6 | oauth.connect "oauth/deny", :action=>"deny" 7 | end 8 | 9 | # Resources we want to protect 10 | map.with_options :controller=>"api" do |api| 11 | api.connection ":action" 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /test/rails3/app/controllers/api_controller.rb: -------------------------------------------------------------------------------- 1 | class ApiController < ApplicationController 2 | 3 | oauth_required :only=>[:private, :change] 4 | oauth_required :only=>[:calc], :scope=>"math" 5 | 6 | def public 7 | if oauth.authenticated? 8 | render :text=>"HAI from #{oauth.identity}" 9 | else 10 | render :text=>"HAI" 11 | end 12 | end 13 | 14 | def private 15 | render :text=>"Shhhh" 16 | end 17 | 18 | def change 19 | render :text=>"Woot!" 20 | end 21 | 22 | def calc 23 | render :text=>"2+2=4" 24 | end 25 | 26 | def list_tokens 27 | render :text=>oauth.list_access_tokens("Batman").map(&:token).join(" ") 28 | end 29 | 30 | def user 31 | render :text=>current_user.to_s 32 | end 33 | 34 | protected 35 | 36 | def current_user 37 | @current_user ||= oauth.identity if oauth.authenticated? 38 | end 39 | 40 | end 41 | -------------------------------------------------------------------------------- /test/rails3/app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | end 3 | -------------------------------------------------------------------------------- /test/rails3/app/controllers/oauth_controller.rb: -------------------------------------------------------------------------------- 1 | class OauthController < ApplicationController 2 | before_filter do |c| 3 | c.send :head, c.oauth.deny! if c.oauth.scope.include?("time-travel") # Only Superman can do that 4 | end 5 | 6 | def authorize 7 | render :text=>"client: #{oauth.client.display_name}\nscope: #{oauth.scope.join(", ")}\nauthorization: #{oauth.authorization}" 8 | end 9 | 10 | def grant 11 | head oauth.grant!(params["authorization"], "Batman") 12 | end 13 | 14 | def deny 15 | head oauth.deny!(params["authorization"]) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /test/rails3/config/application.rb: -------------------------------------------------------------------------------- 1 | require "action_controller/railtie" 2 | module MyApp 3 | class Application < Rails::Application 4 | config.session_store :cookie_store, :key=>"_my_app_session" 5 | config.secret_token = "Stay hungry. Stay foolish. -- Steve Jobs" 6 | config.active_support.deprecation = :stderr 7 | 8 | config.after_initialize do 9 | config.oauth.database = DATABASE 10 | config.oauth.host = "example.org" 11 | config.oauth.collection_prefix = "oauth2_prefix" 12 | config.oauth.authenticator = lambda do |username, password| 13 | "Batman" if username == "cowbell" && password == "more" 14 | end 15 | config.middleware.use Rack::OAuth2::Server::Admin.mount 16 | end 17 | end 18 | end 19 | Rails.application.config.root = File.dirname(__FILE__) + "/.." 20 | require Rails.root + "config/routes" 21 | -------------------------------------------------------------------------------- /test/rails3/config/environment.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../application', __FILE__) 2 | MyApp::Application.initialize! 3 | -------------------------------------------------------------------------------- /test/rails3/config/routes.rb: -------------------------------------------------------------------------------- 1 | MyApp::Application.routes.draw do 2 | # Authorization flow. 3 | match "oauth/authorize" => "oauth#authorize" 4 | match "oauth/grant" => "oauth#grant" 5 | match "oauth/deny" => "oauth#deny" 6 | 7 | # Resources we want to protect 8 | match ":action"=>"api" 9 | 10 | mount Rack::OAuth2::Server::Admin, :at=>"oauth/admin" 11 | 12 | end 13 | -------------------------------------------------------------------------------- /test/setup.rb: -------------------------------------------------------------------------------- 1 | require "bundler" 2 | Bundler.setup 3 | require "test/unit" 4 | require "rack/test" 5 | require "shoulda" 6 | require "timecop" 7 | require "ap" 8 | require "json" 9 | require "logger" 10 | $: << File.dirname(__FILE__) + "/../lib" 11 | $: << File.expand_path(File.dirname(__FILE__) + "/..") 12 | require "rack/oauth2/server" 13 | require "rack/oauth2/server/admin" 14 | 15 | 16 | ENV["RACK_ENV"] = "test" 17 | ENV["DB"] = "rack_oauth2_server_test" 18 | DATABASE = Mongo::Connection.new[ENV["DB"]] 19 | FRAMEWORK = ENV["FRAMEWORK"] || "sinatra" 20 | 21 | 22 | $logger = Logger.new("test.log") 23 | $logger.level = Logger::DEBUG 24 | Rack::OAuth2::Server::Admin.configure do |config| 25 | config.set :logger, $logger 26 | config.set :logging, true 27 | config.set :raise_errors, true 28 | config.set :dump_errors, true 29 | config.oauth.expires_in = 86400 # a day 30 | config.oauth.logger = $logger 31 | end 32 | 33 | 34 | case FRAMEWORK 35 | when "sinatra", nil 36 | 37 | require "sinatra/base" 38 | puts "Testing with Sinatra #{Sinatra::VERSION}" 39 | require File.dirname(__FILE__) + "/sinatra/my_app" 40 | 41 | class Test::Unit::TestCase 42 | def app 43 | Rack::Builder.new do 44 | map("/oauth/admin") { run Server::Admin } 45 | map("/") { run MyApp } 46 | end 47 | end 48 | 49 | def config 50 | MyApp.oauth 51 | end 52 | end 53 | 54 | when "rails" 55 | 56 | RAILS_ENV = "test" 57 | RAILS_ROOT = File.dirname(__FILE__) + "/rails3" 58 | begin 59 | require "rails" 60 | rescue LoadError 61 | end 62 | 63 | if defined?(Rails::Railtie) 64 | # Rails 3.x 65 | require "rack/oauth2/server/railtie" 66 | require File.dirname(__FILE__) + "/rails3/config/environment" 67 | puts "Testing with Rails #{Rails.version}" 68 | 69 | class Test::Unit::TestCase 70 | def app 71 | ::Rails.application 72 | end 73 | 74 | def config 75 | ::Rails.configuration.oauth 76 | end 77 | end 78 | 79 | else 80 | # Rails 2.x 81 | RAILS_ROOT = File.dirname(__FILE__) + "/rails2" 82 | require "initializer" 83 | require "action_controller" 84 | require File.dirname(__FILE__) + "/rails2/config/environment" 85 | puts "Testing with Rails #{Rails.version}" 86 | 87 | class Test::Unit::TestCase 88 | def app 89 | ActionController::Dispatcher.new 90 | end 91 | 92 | def config 93 | ::Rails.configuration.oauth 94 | end 95 | end 96 | end 97 | 98 | else 99 | puts "Unknown framework #{FRAMEWORK}" 100 | exit -1 101 | end 102 | 103 | 104 | class Test::Unit::TestCase 105 | include Rack::Test::Methods 106 | include Rack::OAuth2 107 | 108 | def setup 109 | Server::Admin.scope = %{read write} 110 | @client = Server.register(:display_name=>"UberClient", :redirect_uri=>"http://uberclient.dot/callback", :scope=>%w{read write oauth-admin}) 111 | end 112 | 113 | attr_reader :client, :end_user 114 | 115 | def teardown 116 | Server::Client.collection.drop 117 | Server::AuthRequest.collection.drop 118 | Server::AccessGrant.collection.drop 119 | Server::AccessToken.collection.drop 120 | Server::Issuer.collection.drop 121 | end 122 | end 123 | -------------------------------------------------------------------------------- /test/sinatra/my_app.rb: -------------------------------------------------------------------------------- 1 | require "rack/oauth2/sinatra" 2 | 3 | class MyApp < Sinatra::Base 4 | use Rack::Logger 5 | set :sessions, true 6 | set :show_exceptions, false 7 | 8 | register Rack::OAuth2::Sinatra 9 | oauth.authenticator = lambda do |username, password| 10 | "Batman" if username == "cowbell" && password == "more" 11 | end 12 | oauth.host = "example.org" 13 | oauth.database = DATABASE 14 | oauth.collection_prefix = "oauth2_prefix" 15 | 16 | # 3. Obtaining End-User Authorization 17 | 18 | before "/oauth/*" do 19 | halt oauth.deny! if oauth.scope.include?("time-travel") # Only Superman can do that 20 | end 21 | 22 | get "/oauth/authorize" do 23 | "client: #{oauth.client.display_name}\nscope: #{oauth.scope.join(", ")}\nauthorization: #{oauth.authorization}" 24 | end 25 | 26 | post "/oauth/grant" do 27 | oauth.grant! "Batman" 28 | end 29 | 30 | post "/oauth/deny" do 31 | oauth.deny! 32 | end 33 | 34 | 35 | # 5. Accessing a Protected Resource 36 | 37 | before { @user = oauth.identity if oauth.authenticated? } 38 | 39 | get "/public" do 40 | if oauth.authenticated? 41 | "HAI from #{oauth.identity}" 42 | else 43 | "HAI" 44 | end 45 | end 46 | 47 | oauth_required "/private", "/change" 48 | 49 | get "/private" do 50 | "Shhhh" 51 | end 52 | 53 | post "/change" do 54 | "Woot!" 55 | end 56 | 57 | oauth_required "/calc", :scope=>"math" 58 | 59 | get "/calc" do 60 | end 61 | 62 | get "/user" do 63 | @user 64 | end 65 | 66 | get "/list_tokens" do 67 | oauth.list_access_tokens("Batman").map(&:token).join(" ") 68 | end 69 | 70 | end 71 | --------------------------------------------------------------------------------