├── .gitignore ├── .rvmrc ├── .travis.yml ├── CHANGELOG ├── Gemfile ├── MIT-LICENSE ├── README.rdoc ├── Rails2 ├── Rails3 ├── Rakefile ├── VERSION ├── bin └── oauth2-server ├── config.ru ├── 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 │ ├── 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 ├── 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 | -------------------------------------------------------------------------------- /.rvmrc: -------------------------------------------------------------------------------- 1 | export RUBYOPT="rubygems" 2 | export RUBYLIB="." 3 | rvm 1.9.2 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | rvm: 2 | - 1.8.7 3 | - 1.9.2 4 | gemfile: 5 | - Gemfile 6 | - Rails2 7 | - Rails3 8 | env: 9 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | 2011-09-13 Version 2.4.2 2 | 3 | Fixed Server::Admin.mount ignoring path (ABrukish). 4 | 5 | Fixed a few issues with documentation (Jesse Miller). 6 | 7 | 8 | 2011-08-01 Version 2.4.1 9 | 10 | Fixes error in oauth2-server command line (Michael Saffitz) 11 | 12 | 13 | 2011-07-28 Version 2.4.0 14 | 15 | Added fourth argument to Server.token_for that allows setting token expiration, 16 | and Server option expires_in that does that same thing for all tokens. 17 | 18 | Set to number of seconds token should be accepted. If nil or zero, access token 19 | never expires. For example: 20 | 21 | config.oauth.expires_in = 1.day 22 | 23 | 24 | 2011-07-18 Version 2.3.0 25 | 26 | Setting oauth.database = in configuration block now works as you would 27 | expect it to. As a side note, this is now a global setting (i.e. shared by all 28 | handlers). 29 | 30 | 31 | 2011-07-13 Version 2.2.2 32 | 33 | Fix for unknown [] for NilClass when database not setup (epinault-ttc) 34 | 35 | Warn people when they forgot to set Server.database or set it to 36 | Mongo::Connection instead of Mongo::DB. 37 | 38 | Fixes the strict url scheme issue (Martin Wawrusch). 39 | 40 | 41 | 2011-04-11 version 2.2.1 42 | 43 | Content type header on redirects (Marc Schwieterman) 44 | 45 | 46 | 2011-02-02 version 2.2.0 47 | 48 | Don't require client_secret when requesting authorization (George Ogata). 49 | 50 | Don't check the redirect_uri if the client does not have one set (George Ogata). 51 | 52 | Look for post params if request is a POST (George Ogata). 53 | 54 | 55 | 2010-12-22 version 2.1.0 56 | 57 | Added support for two-legged OAuth flow (Brian Ploetz) 58 | 59 | Fixed query parameter authorization and allowed access_token to be defined 60 | (Ari) 61 | 62 | 63 | 2010-11-30 version 2.0.1 64 | 65 | Change: username/password authentication with no scope results in access token 66 | with default scope. Makes like easier for everyone. 67 | 68 | 69 | 2010-11-23 version 2.0.0 70 | 71 | MAJOR CHANGE: 72 | Keeping with OAuth 2.0 spec terminology, we'll call it scope all around. Some 73 | places in the API that previously used "scopes" have been changed to "scope". 74 | 75 | OTOH, the scope is not consistently stored and returned as array of names, 76 | previous was stored as comma-separated string, and often returned as such. 77 | Whatever you have stored with pre 2.0 will probably not work with 2.0 and 78 | forward. 79 | 80 | Clients now store their scope, and only those names are allowed in access 81 | tokens. The global setting oauth.scope is no longer in use. Forget about it. 82 | 83 | To migrate from 1.4.x to 2.0: 84 | 85 | oauth2-server migrate --db 86 | 87 | Application client registrations will change from having no scope to having an 88 | empty scope, so you would want to update their registration, either using the 89 | Web console, or from your code: 90 | 91 | Client.all.each { |client| client.update(:scope=>%w{read write}) } 92 | 93 | 94 | Use Rack::OAuth2::Server token_for and access_grant to generate access tokens 95 | and access grants, respectively. These are mighty useful if you're using the 96 | OAuth 2.0 infrastructure, but have different ways for authorizing, e.g. using 97 | access tokens instead of cookies. 98 | 99 | Rack::OAuth2::Server method register to register new client applications and 100 | update existing records. This method is idempotent, so you can use it in rake 101 | db:seed, deploy scripts, migrations, etc. 102 | 103 | If your authenticator accepts four arguments, it will receive, in addition to 104 | username and password, also the client identifier and requested scopes. 105 | 106 | Web console now allows you to set/unset individual scopes for each client 107 | application, and store a note on each client. 108 | 109 | Added Sammy.js OAuth 2.0 plugin. 110 | 111 | 112 | 2010-11-12 version 1.4.6 113 | 114 | Added Railtie support for Rails 3.x and now running tests against both Rails 115 | 2.x and 3.x. 116 | 117 | 118 | 2010-11-11 version 1.4.5 119 | 120 | Cosmetic changes to UI. Added throbber and error messages when AJAX requests go 121 | foul. Header on the left, sign-out on the right, as most people expect it. 122 | Client name is no longer a link to the site, site link shown separately. 123 | 124 | 125 | 2010-11-10 version 1.4.4 126 | 127 | Added a practice server. You can use it to test your OAuth 2.0 client library. 128 | To fire up the practice server: oauth2-server practice 129 | 130 | Bumped up dependencies on Rack 1.1 or later, Sinatra 1.1 or later. 131 | 132 | 133 | 2010-11-09 version 1.4.3 134 | 135 | Renamed Rack::OAuth2::Server::Admin to just Rack::OAuth2::Admin. 136 | 137 | Checked in config.ru, I use this for testing the Web console. 138 | 139 | 140 | 2010-11-09 version 1.4.2 141 | 142 | Fix to commend line tool to properly do authentication. 143 | 144 | Added Sinatra as dependency. 145 | 146 | 147 | 2010-11-09 version 1.4.1 148 | 149 | Fix to command line tool when accessing MongoDB with username/password. 150 | 151 | 152 | 2010-11-09 version 1.4.0 153 | 154 | If authorization handle is passed as request parameter (the recommended way), 155 | then you can call oauth.grant! with a single argument and oauth.deny! with no 156 | arguments. 157 | 158 | You can now call oauth.deny! at any point during the authorization flow, e.g. 159 | automatically deny all requests based on scope and client. 160 | 161 | To deny access, return status code 403 (was, incorrectly 401). Or just use 162 | oauth.deny!. 163 | 164 | Web console gets template_url setting you can use to map access token identity 165 | into a URL in your application. The substitution variable is "{id}". 166 | 167 | Added error page when authorization attempt fails (instead of endless 168 | redirect). 169 | 170 | Fixed mounting of Web console on Rails. If it failed you before, try again. 171 | 172 | Fixed documentation for configuration under Rails, clarify that all the 173 | interesting stuff happens in after_initialize. 174 | 175 | Fixed error responses for response_type=token to use fragment identifier. 176 | 177 | 178 | 2010-11-08 version 1.3.1 179 | 180 | Added command line tool, helps you get started and setup: 181 | $ oauth2-server setup --db my_db 182 | 183 | Added a touch of color to the UI and ability to delete a client. 184 | 185 | You can not sign out of the Web console. 186 | 187 | 188 | 2010-11-07 version 1.3.0 189 | 190 | Added OAuth authorization console. 191 | 192 | Added param_authentication option: turn this on if you need to support 193 | oauth_token query parameter or form field. Disabled by default. 194 | 195 | Added host option: only check requests sent to that host (e.g. only check 196 | requests to api.example.com). 197 | 198 | Added path option: only check requests under this path (e.g. only check 199 | requests for /api/...). 200 | 201 | 202 | 2010-11-03 version 1.2.2 203 | 204 | Store ObjectId references in database. 205 | 206 | 207 | 2010-11-03 version 1.2.1 208 | 209 | Make sure order of scope no longer important for access token lookup. 210 | 211 | 212 | 2010-11-02 version 1.2.0 213 | 214 | You can now redirect to /oauth/authorize with authorization query parameter and 215 | it will do the right thing. 216 | 217 | 218 | 2010-11-02 version 1.1.1 219 | 220 | Fixed missing rails/init.rb. 221 | 222 | 223 | 2010-11-02 version 1.1.0 224 | 225 | Renamed oauth.resource as oauth.identity to remove confusion, besides, it's 226 | more often identity than anything else. 227 | 228 | Added automagic loading under Rails, no need to require special path. 229 | 230 | Added Rack::OAuth2::Server::Options class, easier to user than Hash. 231 | 232 | Added indexes for speedier queries. 233 | 234 | 235 | 2010-11-02 version 1.0.0 236 | 237 | World premiere. 238 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source :rubygems 2 | gemspec 3 | 4 | group :development do 5 | gem "rake" 6 | gem "thin" 7 | gem "yard" 8 | end 9 | 10 | group :test do 11 | gem "awesome_print" 12 | gem "rack-test" 13 | gem "shoulda" 14 | gem "timecop" 15 | end 16 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | = If you want the latest code, report an issue or submit a pull request, please head to this repo: 2 | = https://github.com/assaf/rack-oauth2-server 3 | -------------------------------------------------------------------------------- /Rails2: -------------------------------------------------------------------------------- 1 | source :rubygems 2 | gemspec 3 | gem "awesome_print" 4 | gem "json" 5 | gem "rack-test" 6 | gem "rails", "~>2.3" 7 | gem "shoulda" 8 | gem "timecop" 9 | -------------------------------------------------------------------------------- /Rails3: -------------------------------------------------------------------------------- 1 | source :rubygems 2 | gemspec 3 | gem "awesome_print" 4 | gem "json" 5 | gem "rack-test" 6 | gem "rails", "~>3.0" 7 | gem "shoulda" 8 | gem "timecop" 9 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "rake/testtask" 2 | 3 | spec = Gem::Specification.load(Dir["*.gemspec"].first) 4 | 5 | desc "Install dependencies" 6 | task :setup do 7 | puts "Installing gems for testing with Sinatra ..." 8 | sh "bundle install" 9 | puts "Installing gems for testing with Rails 2.3 ..." 10 | sh "env BUNDLE_GEMFILE=Rails2 bundle install" 11 | puts "Installing gems for testing with Rails 3.x ..." 12 | sh "env BUNDLE_GEMFILE=Rails3 bundle install" 13 | end 14 | 15 | desc "Run this in development mode when updating the CoffeeScript file" 16 | task :coffee do 17 | sh "coffee -w -o lib/rack/oauth2/admin/js/ lib/rack/oauth2/admin/js/application.coffee" 18 | end 19 | 20 | task :compile do 21 | sh "coffee -c -l -o lib/rack/oauth2/admin/js/ lib/rack/oauth2/admin/js/application.coffee" 22 | end 23 | 24 | desc "Build the Gem" 25 | task :build=>:compile do 26 | sh "gem build #{spec.name}.gemspec" 27 | end 28 | 29 | desc "Install #{spec.name} locally" 30 | task :install=>:build do 31 | sudo = "sudo" unless File.writable?( Gem::ConfigMap[:bindir]) 32 | sh "#{sudo} gem install #{spec.name}-#{spec.version}.gem" 33 | end 34 | 35 | desc "Push new release to gemcutter and git tag" 36 | task :push=>["test:all", "build"] do 37 | sh "git push" 38 | puts "Tagging version #{spec.version} .." 39 | sh "git tag v#{spec.version}" 40 | sh "git push --tag" 41 | puts "Building and pushing gem .." 42 | sh "gem push #{spec.name}-#{spec.version}.gem" 43 | end 44 | 45 | desc "Run all tests" 46 | Rake::TestTask.new do |task| 47 | task.test_files = FileList['test/**/*_test.rb'] 48 | if Rake.application.options.trace 49 | #task.warning = true 50 | task.verbose = true 51 | elsif Rake.application.options.silent 52 | task.ruby_opts << "-W0" 53 | else 54 | task.verbose = true 55 | end 56 | task.ruby_opts << "-I." 57 | end 58 | 59 | namespace :test do 60 | task :all=>["test:sinatra", "test:rails2", "test:rails3"] 61 | desc "Run all tests against Sinatra" 62 | task :sinatra do 63 | sh "env BUNDLE_GEMFILE=Gemfile bundle exec rake" 64 | end 65 | desc "Run all tests against Rails 2.3.x" 66 | task :rails2 do 67 | sh "env BUNDLE_GEMFILE=Rails2 bundle exec rake" 68 | end 69 | desc "Run all tests against Rails 3.x" 70 | task :rails3 do 71 | sh "env BUNDLE_GEMFILE=Rails3 bundle exec rake" 72 | end 73 | end 74 | 75 | task :default do 76 | ENV["FRAMEWORK"] = "rails" 77 | begin 78 | require "rails" # check for Rails3 79 | rescue LoadError 80 | begin 81 | require "initializer" # check for Rails2 82 | rescue LoadError 83 | ENV["FRAMEWORK"] = "sinatra" 84 | end 85 | end 86 | task("test").invoke 87 | end 88 | 89 | 90 | begin 91 | require "yard" 92 | YARD::Rake::YardocTask.new do |doc| 93 | doc.files = FileList["lib/**/*.rb"] 94 | end 95 | rescue LoadError 96 | end 97 | 98 | task :clean do 99 | rm_rf %w{doc .yardoc *.gem} 100 | end 101 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 2.4.2 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 | if (i = ARGV.index("--port") || ARGV.index("-p")) && ARGV[i+1] 18 | port = ARGV[i + 1].to_i 19 | ARGV[i,2] = [] 20 | end 21 | 22 | 23 | case ARGV[0] 24 | when "list" 25 | 26 | fail "No database. Use the --db option to tell us which database to use" unless Server.options.database 27 | Server::Client.all.each do |client| 28 | next if client.revoked 29 | print "%-30s\t%s\n" % [client.display_name, client.link] 30 | print " ID %s\tSecret %s\n" % [client.id, client.secret] 31 | print "\n" 32 | end 33 | 34 | when "register" 35 | 36 | fail "No database. Use the --db option to tell us which database to use" unless Server.options.database 37 | begin 38 | print "Application name:\t" 39 | display_name = $stdin.gets 40 | print "Application URL:\t" 41 | link = $stdin.gets 42 | print "Redirect URI:\t\t" 43 | redirect_uri = $stdin.gets 44 | print "Scope (space separated names):\t\t" 45 | scope = $stdin.gets 46 | client = Server.register(:display_name=>display_name, :link=>link, :redirect_uri=>redirect_uri, :scope=>scope) 47 | rescue 48 | puts "\nFailed to register client: #{$!}" 49 | exit -1 50 | end 51 | puts "Registered #{client.display_name}" 52 | puts "ID\t#{client.id}" 53 | puts "Secret\t#{client.secret}" 54 | 55 | when "setup" 56 | 57 | fail "No database. Use the --db option to tell us which database to use" unless Server.options.database 58 | puts "Where would you mount the Web console? This is a URL that must end with /admin," 59 | puts "for example, http://example.com/oauth/admin" 60 | print ": " 61 | uri = URI.parse($stdin.gets) 62 | begin 63 | uri.normalize! 64 | fail "No an HTTP/S URL" unless uri.absolute? && %{http https}.include?(uri.scheme) 65 | fail "Path must end with /admin" unless uri.path[/\/admin$/] 66 | client = Server.register(:display_name=>"OAuth Console", :link=>uri.to_s, :image_url=>"#{uri.to_s}/images/oauth-2.png", 67 | :redirect_uri=>uri.to_s, :scope=>"oauth-admin") 68 | rescue 69 | puts "\nFailed to register client: #{$!}" 70 | exit -1 71 | end 72 | print <<-TEXT 73 | 74 | Next Steps 75 | ========== 76 | 77 | Make sure you ONLY authorize administrators to use the oauth-admin scope. 78 | For example: 79 | 80 | before_filter do 81 | # Only admins allowed to authorize the scope oauth-admin 82 | head oauth.deny! if oauth.scope.include?("oauth-admin") && !current_user.admin? 83 | end 84 | 85 | Rails 2.x, add the following to config/environment.rb: 86 | 87 | config.after_initialize do 88 | config.middleware.use Rack::OAuth2::Server::Admin.mount "#{uri.path}" 89 | Rack::OAuth2::Server::Admin.set :client_id, "#{client.id}" 90 | Rack::OAuth2::Server::Admin.set :client_secret, "#{client.secret}" 91 | end 92 | 93 | Rails 3.x, add the following to config/application.rb: 94 | 95 | config.after_initialize do 96 | Rack::OAuth2::Server::Admin.set :client_id, "#{client.id}" 97 | Rack::OAuth2::Server::Admin.set :client_secret, "#{client.secret}" 98 | end 99 | 100 | And add the follownig to config/routes.rb: 101 | 102 | mount Rack::OAuth2::Server::Admin=>"/oauth/admin" 103 | 104 | Sinatra, Padrino and other Rack applications, mount the console: 105 | 106 | Rack::Builder.new do 107 | map("#{uri.path}") { run Rack::OAuth2::Server::Admin } 108 | map("/") { run MyApp } 109 | end 110 | Rack::OAuth2::Server::Admin.set :client_id, "#{client.id}" 111 | Rack::OAuth2::Server::Admin.set :client_secret, "#{client.secret}" 112 | 113 | The console will authorize access by redirecting to 114 | https://#{uri.host}/oauth/authorize 115 | 116 | If this is not your OAuth 2.0 authorization endpoint, you can change it by 117 | setting the :authorize option. 118 | TEXT 119 | 120 | when "practice" 121 | 122 | require "logger" 123 | begin 124 | require "thin" 125 | rescue LoadError 126 | puts "Needs the Thin Web server. Please gem install thin and run again" 127 | exit -1 128 | end 129 | require "rack/oauth2/server/practice" 130 | 131 | fail "No database. Use the --db option to tell us which database to use" unless Server.options.database 132 | port ||= 8080 133 | admin_url = "http://localhost:#{port}/oauth/admin" 134 | unless client = Server::Client.lookup(admin_url) 135 | client = Server.register(:display_name=>"Practice OAuth Console", :image_url=>"#{admin_url}/images/oauth-2.png", 136 | :link=>admin_url, :redirect_uri=>admin_url, :scope=>"oauth-admin") 137 | end 138 | Server::Admin.configure do |config| 139 | logger = Logger.new(STDOUT) 140 | logger.level = Logger::DEBUG 141 | config.set :client_id, client.id 142 | config.set :client_secret, client.secret 143 | config.set :scope, "nobody sudo" 144 | config.set :logger, logger 145 | config.set :logging, true 146 | config.set :dump_errors, true 147 | config.oauth.logger = logger 148 | end 149 | 150 | Server::Practice.configure do |config| 151 | logger = Logger.new(STDOUT) 152 | logger.level = Logger::DEBUG 153 | config.set :logger, logger 154 | config.set :logging, true 155 | config.set :dump_errors, true 156 | config.oauth.logger = logger 157 | end 158 | 159 | print "\nFiring up the practice server.\nFor instructions, go to http://localhost:#{port}/\n\n\n" 160 | Thin::Server.new "127.0.0.1", port do 161 | map("/") { run Server::Practice.new } 162 | map("/oauth/admin") { run Server::Admin.new } 163 | end.start 164 | 165 | when "migrate" 166 | 167 | fail "No database. Use the --db option to tell us which database to use" unless Server.options.database 168 | puts "Set all clients to this scope (can change later by calling Client.register):" 169 | print ": " 170 | scope = $stdin.gets.strip.split 171 | puts "Updating Client scope to #{scope.join(", ")}" 172 | Server::Client.collection.find({ :scope=>{ :$exists=>false } }, :fields=>[]).each do |client| 173 | update = { :scope=>scope, 174 | :tokens_granted=>Server::AccessToken.count(:client_id=>client["_id"]), 175 | :tokens_revoked=>Server::AccessToken.count(:client_id=>client["_id"], :revoked=>true) } 176 | Server::Client.collection.update({ :_id=>client["_id"] }, { :$set=>update }) 177 | end 178 | [Server::AccessToken, Server::AccessGrant, Server::AuthRequest].each do |mod| 179 | puts "Updating #{mod.name} scope from string to array" 180 | mod.collection.find({ :scope=>{ :$type=>2 } }, :fields=>[]).each do |token| 181 | scope = token["scope"].split 182 | mod.collection.update({ :_id=>token["_id"] }, { :$set=>{ :scope=>scope } }) 183 | end 184 | end 185 | else 186 | 187 | print <<-TEXT 188 | Usage: oauth2-server [options] COMMAND [args] 189 | Version #{Server::VERSION} 190 | 191 | Commands: 192 | list Lists all active clients 193 | migrate Run this when migrating from 1.x to 2.x 194 | practice Runs a dummy OAuth 2.0 server, use this to test your OAuth 2.0 client 195 | register Register a new client application 196 | setup Create new admin account and help you setup the OAuth Web console 197 | 198 | Options: 199 | --db database Database name or connection URL 200 | --port number Port to run admin server, detault is 8080 201 | TEXT 202 | exit -1 203 | 204 | end 205 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/demandforce/rack-oauth2-server/1ae51703342272f23a205c6009aa8ffdff6ce47a/lib/rack/oauth2/admin/images/loading.gif -------------------------------------------------------------------------------- /lib/rack/oauth2/admin/images/oauth-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/demandforce/rack-oauth2-server/1ae51703342272f23a205c6009aa8ffdff6ce47a/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 #{Server.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 | 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 | Server.database["oauth2.access_grants"] 29 | end 30 | end 31 | 32 | # Authorization code. We are nothing without it. 33 | attr_reader :_id 34 | alias :code :_id 35 | # The identity we authorized access to. 36 | attr_reader :identity 37 | # Client that was granted this access token. 38 | attr_reader :client_id 39 | # Redirect URI for this grant. 40 | attr_reader :redirect_uri 41 | # The scope requested in this grant. 42 | attr_reader :scope 43 | # Does what it says on the label. 44 | attr_reader :created_at 45 | # Tells us when (and if) access token was created. 46 | attr_accessor :granted_at 47 | # Tells us when this grant expires. 48 | attr_accessor :expires_at 49 | # Access token created from this grant. Set and spent. 50 | attr_accessor :access_token 51 | # Timestamp if revoked. 52 | attr_accessor :revoked 53 | 54 | # Authorize access and return new access token. 55 | # 56 | # Access grant can only be redeemed once, but client can make multiple 57 | # requests to obtain it, so we need to make sure only first request is 58 | # successful in returning access token, futher requests raise 59 | # InvalidGrantError. 60 | def authorize!(expires_in = nil) 61 | raise InvalidGrantError, "You can't use the same access grant twice" if self.access_token || self.revoked 62 | client = Client.find(client_id) or raise InvalidGrantError 63 | access_token = AccessToken.get_token_for(identity, client, scope, expires_in) 64 | self.access_token = access_token.token 65 | self.granted_at = Time.now.to_i 66 | self.class.collection.update({ :_id=>code, :access_token=>nil, :revoked=>nil }, { :$set=>{ :granted_at=>granted_at, :access_token=>access_token.token } }, :safe=>true) 67 | reload = self.class.collection.find_one({ :_id=>code, :revoked=>nil }, { :fields=>%w{access_token} }) 68 | raise InvalidGrantError unless reload && reload["access_token"] == access_token.token 69 | return access_token 70 | end 71 | 72 | def revoke! 73 | self.revoked = Time.now.to_i 74 | self.class.collection.update({ :_id=>code, :revoked=>nil }, { :$set=>{ :revoked=>revoked } }) 75 | end 76 | 77 | Server.create_indexes do 78 | # Used to revoke all pending access grants when revoking client. 79 | collection.create_index [[:client_id, Mongo::ASCENDING]] 80 | end 81 | end 82 | 83 | end 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /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 | unless token = collection.find_one({ :identity=>identity, :scope=>scope, :client_id=>client.id, :revoked=>nil }) 25 | expires_at = Time.now.to_i + expires if expires && expires != 0 26 | token = { :_id=>Server.secure_random, :identity=>identity, :scope=>scope, 27 | :client_id=>client.id, :created_at=>Time.now.to_i, 28 | :expires_at=>expires_at, :revoked=>nil } 29 | collection.insert token 30 | Client.collection.update({ :_id=>client.id }, { :$inc=>{ :tokens_granted=>1 } }) 31 | end 32 | Server.new_instance self, token 33 | end 34 | 35 | # Find all AccessTokens for an identity. 36 | def from_identity(identity) 37 | collection.find({ :identity=>identity }).map { |fields| Server.new_instance self, fields } 38 | end 39 | 40 | # Returns all access tokens for a given client, Use limit and offset 41 | # to return a subset of tokens, sorted by creation date. 42 | def for_client(client_id, offset = 0, limit = 100) 43 | client_id = BSON::ObjectId(client_id.to_s) 44 | collection.find({ :client_id=>client_id }, { :sort=>[[:created_at, Mongo::ASCENDING]], :skip=>offset, :limit=>limit }). 45 | map { |token| Server.new_instance self, token } 46 | end 47 | 48 | # Returns count of access tokens. 49 | # 50 | # @param [Hash] filter Count only a subset of access tokens 51 | # @option filter [Integer] days Only count that many days (since now) 52 | # @option filter [Boolean] revoked Only count revoked (true) or non-revoked (false) tokens; count all tokens if nil 53 | # @option filter [String, ObjectId] client_id Only tokens grant to this client 54 | def count(filter = {}) 55 | select = {} 56 | if filter[:days] 57 | now = Time.now.to_i 58 | range = { :$gt=>now - filter[:days] * 86400, :$lte=>now } 59 | select[ filter[:revoked] ? :revoked : :created_at ] = range 60 | elsif filter.has_key?(:revoked) 61 | select[:revoked] = filter[:revoked] ? { :$ne=>nil } : { :$eq=>nil } 62 | end 63 | select[:client_id] = BSON::ObjectId(filter[:client_id].to_s) if filter[:client_id] 64 | collection.find(select).count 65 | end 66 | 67 | def historical(filter = {}) 68 | days = filter[:days] || 60 69 | select = { :$gt=> { :created_at=>Time.now - 86400 * days } } 70 | select = {} 71 | if filter[:client_id] 72 | select[:client_id] = BSON::ObjectId(filter[:client_id].to_s) 73 | end 74 | raw = Server::AccessToken.collection.group("function (token) { return { ts: Math.floor(token.created_at / 86400) } }", 75 | select, { :granted=>0 }, "function (token, state) { state.granted++ }") 76 | raw.sort { |a, b| a["ts"] - b["ts"] } 77 | end 78 | 79 | def collection 80 | Server.database["oauth2.access_tokens"] 81 | end 82 | end 83 | 84 | # Access token. As unique as they come. 85 | attr_reader :_id 86 | alias :token :_id 87 | # The identity we authorized access to. 88 | attr_reader :identity 89 | # Client that was granted this access token. 90 | attr_reader :client_id 91 | # The scope granted to this token. 92 | attr_reader :scope 93 | # When token was granted. 94 | attr_reader :created_at 95 | # When token expires for good. 96 | attr_reader :expires_at 97 | # Timestamp if revoked. 98 | attr_accessor :revoked 99 | # Timestamp of last access using this token, rounded up to hour. 100 | attr_accessor :last_access 101 | # Timestamp of previous access using this token, rounded up to hour. 102 | attr_accessor :prev_access 103 | 104 | # Updates the last access timestamp. 105 | def access! 106 | today = (Time.now.to_i / 3600) * 3600 107 | if last_access.nil? || last_access < today 108 | AccessToken.collection.update({ :_id=>token }, { :$set=>{ :last_access=>today, :prev_access=>last_access } }) 109 | self.last_access = today 110 | end 111 | end 112 | 113 | # Revokes this access token. 114 | def revoke! 115 | self.revoked = Time.now.to_i 116 | AccessToken.collection.update({ :_id=>token }, { :$set=>{ :revoked=>revoked } }) 117 | Client.collection.update({ :_id=>client_id }, { :$inc=>{ :tokens_revoked=>1 } }) 118 | end 119 | 120 | Server.create_indexes do 121 | # Used to revoke all pending access grants when revoking client. 122 | collection.create_index [[:client_id, Mongo::ASCENDING]] 123 | # Used to get/revoke access tokens for an identity, also to find and 124 | # return existing access token. 125 | collection.create_index [[:identity, Mongo::ASCENDING]] 126 | end 127 | end 128 | 129 | end 130 | end 131 | end 132 | -------------------------------------------------------------------------------- /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 | Server.database["oauth2.auth_requests"] 32 | end 33 | end 34 | 35 | # Request identifier. We let the database pick this one out. 36 | attr_reader :_id 37 | alias :id :_id 38 | # Client making this request. 39 | attr_reader :client_id 40 | # scope of this request: array of names. 41 | attr_reader :scope 42 | # Redirect back to this URL. 43 | attr_reader :redirect_uri 44 | # Client requested we return state on redirect. 45 | attr_reader :state 46 | # Does what it says on the label. 47 | attr_reader :created_at 48 | # Response type: either code or token. 49 | attr_reader :response_type 50 | # If granted, the access grant code. 51 | attr_accessor :grant_code 52 | # If granted, the access token. 53 | attr_accessor :access_token 54 | # Keeping track of things. 55 | attr_accessor :authorized_at 56 | # Timestamp if revoked. 57 | attr_accessor :revoked 58 | 59 | # Grant access to the specified identity. 60 | def grant!(identity, expires_in = nil) 61 | raise ArgumentError, "Must supply a identity" unless identity 62 | return if revoked 63 | client = Client.find(client_id) or return 64 | self.authorized_at = Time.now.to_i 65 | if response_type == "code" # Requested authorization code 66 | access_grant = AccessGrant.create(identity, client, scope, redirect_uri) 67 | self.grant_code = access_grant.code 68 | self.class.collection.update({ :_id=>id, :revoked=>nil }, { :$set=>{ :grant_code=>access_grant.code, :authorized_at=>authorized_at } }) 69 | else # Requested access token 70 | access_token = AccessToken.get_token_for(identity, client, scope, expires_in) 71 | self.access_token = access_token.token 72 | self.class.collection.update({ :_id=>id, :revoked=>nil, :access_token=>nil }, { :$set=>{ :access_token=>access_token.token, :authorized_at=>authorized_at } }) 73 | end 74 | true 75 | end 76 | 77 | # Deny access. 78 | def deny! 79 | self.authorized_at = Time.now.to_i 80 | self.class.collection.update({ :_id=>id }, { :$set=>{ :authorized_at=>authorized_at } }) 81 | end 82 | 83 | Server.create_indexes do 84 | # Used to revoke all pending access grants when revoking client. 85 | collection.create_index [[:client_id, Mongo::ASCENDING]] 86 | end 87 | 88 | end 89 | 90 | end 91 | end 92 | end 93 | -------------------------------------------------------------------------------- /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 | Server.database["oauth2.clients"] 70 | end 71 | end 72 | 73 | # Client identifier. 74 | attr_reader :_id 75 | alias :id :_id 76 | # Client secret: random, long, and hexy. 77 | attr_reader :secret 78 | # User see this. 79 | attr_reader :display_name 80 | # Link to client's Web site. 81 | attr_reader :link 82 | # Preferred image URL for this icon. 83 | attr_reader :image_url 84 | # Redirect URL. Supplied by the client if they want to restrict redirect 85 | # URLs (better security). 86 | attr_reader :redirect_uri 87 | # List of scope the client is allowed to request. 88 | attr_reader :scope 89 | # Free form fields for internal use. 90 | attr_reader :notes 91 | # Does what it says on the label. 92 | attr_reader :created_at 93 | # Timestamp if revoked. 94 | attr_accessor :revoked 95 | # Counts how many access tokens were granted. 96 | attr_reader :tokens_granted 97 | # Counts how many access tokens were revoked. 98 | attr_reader :tokens_revoked 99 | 100 | # Revoke all authorization requests, access grants and access tokens for 101 | # this client. Ward off the evil. 102 | def revoke! 103 | self.revoked = Time.now.to_i 104 | Client.collection.update({ :_id=>id }, { :$set=>{ :revoked=>revoked } }) 105 | AuthRequest.collection.update({ :client_id=>id }, { :$set=>{ :revoked=>revoked } }) 106 | AccessGrant.collection.update({ :client_id=>id }, { :$set=>{ :revoked=>revoked } }) 107 | AccessToken.collection.update({ :client_id=>id }, { :$set=>{ :revoked=>revoked } }) 108 | end 109 | 110 | def update(args) 111 | fields = [:display_name, :link, :image_url, :notes].inject({}) { |h,k| v = args[k]; h[k] = v if v; h } 112 | fields[:redirect_uri] = Server::Utils.parse_redirect_uri(args[:redirect_uri]).to_s if args[:redirect_uri] 113 | fields[:scope] = Server::Utils.normalize_scope(args[:scope]) 114 | self.class.collection.update({ :_id=>id }, { :$set=>fields }) 115 | self.class.find(id) 116 | end 117 | 118 | Server.create_indexes do 119 | # For quickly returning clients sorted by display name, or finding 120 | # client from a URL. 121 | collection.create_index [[:display_name, Mongo::ASCENDING]] 122 | collection.create_index [[:link, Mongo::ASCENDING]] 123 | end 124 | end 125 | 126 | end 127 | end 128 | end 129 | -------------------------------------------------------------------------------- /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 | require "json" 3 | require "rack/oauth2/server" 4 | require "rack/oauth2/sinatra" 5 | 6 | module Rack 7 | module OAuth2 8 | class Server 9 | class Admin < ::Sinatra::Base 10 | 11 | class << self 12 | 13 | # Rack module that mounts the specified class on the specified path, 14 | # and passes all other request to the application. 15 | class Mount 16 | class << self 17 | def mount(klass, path) 18 | @klass = klass 19 | @path = path 20 | @match = /^#{Regexp.escape(path)}(\/.*|$)?/ 21 | end 22 | 23 | attr_reader :klass, :path, :match 24 | end 25 | 26 | def initialize(app) 27 | @pass = app 28 | @admin = self.class.klass.new 29 | end 30 | 31 | def call(env) 32 | path = env["PATH_INFO"].to_s 33 | script_name = env['SCRIPT_NAME'] 34 | if path =~ self.class.match && rest = $1 35 | env.merge! "SCRIPT_NAME"=>(script_name + self.class.path), "PATH_INFO"=>rest 36 | return @admin.call(env) 37 | else 38 | return @pass.call(env) 39 | end 40 | end 41 | end 42 | 43 | # Returns Rack handle that mounts Admin on the specified path, and 44 | # forwards all other requests back to the application. 45 | # 46 | # @param [String, nil] path The path to mount on, defaults to 47 | # /oauth/admin 48 | # @return [Object] Rack module 49 | # 50 | # @example To include Web admin in Rails 2.x app: 51 | # config.middleware.use Rack::OAuth2::Server::Admin.mount 52 | def mount(path = "/oauth/admin") 53 | mount = Class.new(Mount) 54 | mount.mount Admin, path 55 | mount 56 | end 57 | 58 | end 59 | 60 | 61 | # Client application identified, require to authenticate. 62 | set :client_id, nil 63 | # Client application secret, required to authenticate. 64 | set :client_secret, nil 65 | # Endpoint for requesing authorization, defaults to /oauth/admin. 66 | set :authorize, nil 67 | # Will map an access token identity into a URL in your application, 68 | # using the substitution value "{id}", e.g. 69 | # "http://example.com/users/#{id}") 70 | set :template_url, nil 71 | # Forces all requests to use HTTPS (true by default except in 72 | # development mode). 73 | set :force_ssl, !development? && !test? 74 | # Common scope shown and added by default to new clients. 75 | set :scope, [] 76 | 77 | 78 | set :logger, ::Rails.logger if defined?(::Rails) 79 | # Number of tokens to return in each page. 80 | set :tokens_per_page, 100 81 | set :public, ::File.dirname(__FILE__) + "/../admin" 82 | set :method_override, true 83 | mime_type :js, "text/javascript" 84 | mime_type :tmpl, "text/x-jquery-template" 85 | 86 | register Rack::OAuth2::Sinatra 87 | 88 | # Force HTTPS except for development environment. 89 | before do 90 | redirect request.url.sub(/^http:/, "https:") if settings.force_ssl && request.scheme != "https" 91 | end 92 | 93 | 94 | # -- Static content -- 95 | 96 | # It's a single-page app, this is that single page. 97 | get "/" do 98 | send_file settings.public + "/views/index.html" 99 | end 100 | 101 | # Service JavaScript, CSS and jQuery templates from the gem. 102 | %w{js css views}.each do |path| 103 | get "/#{path}/:name" do 104 | send_file settings.public + "/#{path}/" + params[:name] 105 | end 106 | end 107 | 108 | 109 | # -- Getting an access token -- 110 | 111 | # To get an OAuth token, you need client ID and secret, two values we 112 | # didn't pass on to the JavaScript code, so it has no way to request 113 | # authorization directly. Instead, it redirects to this URL which in turn 114 | # redirects to the authorization endpoint. This redirect does accept the 115 | # state parameter, which will be returned after authorization. 116 | get "/authorize" do 117 | redirect_uri = "#{request.scheme}://#{request.host}:#{request.port}#{request.script_name}" 118 | query = { :client_id=>settings.client_id, :client_secret=>settings.client_secret, :state=>params[:state], 119 | :response_type=>"token", :scope=>"oauth-admin", :redirect_uri=>redirect_uri } 120 | auth_url = settings.authorize || "#{request.scheme}://#{request.host}:#{request.port}/oauth/authorize" 121 | redirect "#{auth_url}?#{Rack::Utils.build_query(query)}" 122 | end 123 | 124 | 125 | # -- API -- 126 | 127 | oauth_required "/api/clients", "/api/client/:id", "/api/client/:id/revoke", "/api/token/:token/revoke", :scope=>"oauth-admin" 128 | 129 | get "/api/clients" do 130 | content_type "application/json" 131 | json = { :list=>Server::Client.all.map { |client| client_as_json(client) }, 132 | :scope=>Server::Utils.normalize_scope(settings.scope), 133 | :history=>"#{request.script_name}/api/clients/history", 134 | :tokens=>{ :total=>Server::AccessToken.count, :week=>Server::AccessToken.count(:days=>7), 135 | :revoked=>Server::AccessToken.count(:days=>7, :revoked=>true) } } 136 | json.to_json 137 | end 138 | 139 | get "/api/clients/history" do 140 | content_type "application/json" 141 | { :data=>Server::AccessToken.historical }.to_json 142 | end 143 | 144 | post "/api/clients" do 145 | begin 146 | client = Server::Client.create(validate_params(params)) 147 | redirect "#{request.script_name}/api/client/#{client.id}" 148 | rescue 149 | halt 400, $!.message 150 | end 151 | end 152 | 153 | get "/api/client/:id" do 154 | content_type "application/json" 155 | client = Server::Client.find(params[:id]) 156 | json = client_as_json(client, true) 157 | 158 | page = [params[:page].to_i, 1].max 159 | offset = (page - 1) * settings.tokens_per_page 160 | total = Server::AccessToken.count(:client_id=>client.id) 161 | tokens = Server::AccessToken.for_client(params[:id], offset, settings.tokens_per_page) 162 | json[:tokens] = { :list=>tokens.map { |token| token_as_json(token) } } 163 | json[:tokens][:total] = total 164 | json[:tokens][:page] = page 165 | json[:tokens][:next] = "#{request.script_name}/client/#{params[:id]}?page=#{page + 1}" if total > page * settings.tokens_per_page 166 | json[:tokens][:previous] = "#{request.script_name}/client/#{params[:id]}?page=#{page - 1}" if page > 1 167 | json[:tokens][:total] = Server::AccessToken.count(:client_id=>client.id) 168 | json[:tokens][:week] = Server::AccessToken.count(:client_id=>client.id, :days=>7) 169 | json[:tokens][:revoked] = Server::AccessToken.count(:client_id=>client.id, :days=>7, :revoked=>true) 170 | 171 | json.to_json 172 | end 173 | 174 | get "/api/client/:id/history" do 175 | content_type "application/json" 176 | client = Server::Client.find(params[:id]) 177 | { :data=>Server::AccessToken.historical(:client_id=>client.id) }.to_json 178 | end 179 | 180 | put "/api/client/:id" do 181 | client = Server::Client.find(params[:id]) 182 | begin 183 | client.update validate_params(params) 184 | redirect "#{request.script_name}/api/client/#{client.id}" 185 | rescue 186 | halt 400, $!.message 187 | end 188 | end 189 | 190 | delete "/api/client/:id" do 191 | Server::Client.delete(params[:id]) 192 | 200 193 | end 194 | 195 | post "/api/client/:id/revoke" do 196 | client = Server::Client.find(params[:id]) 197 | client.revoke! 198 | 200 199 | end 200 | 201 | post "/api/token/:token/revoke" do 202 | token = Server::AccessToken.from_token(params[:token]) 203 | token.revoke! 204 | 200 205 | end 206 | 207 | helpers do 208 | def validate_params(params) 209 | display_name = params[:displayName].to_s.strip 210 | halt 400, "Missing display name" if display_name.empty? 211 | link = URI.parse(params[:link].to_s.strip).normalize rescue nil 212 | halt 400, "Link is not a URL (must be http://....)" unless link 213 | halt 400, "Link must be an absolute URL with HTTP/S scheme" unless link.absolute? && %{http https}.include?(link.scheme) 214 | redirect_uri = URI.parse(params[:redirectUri].to_s.strip).normalize rescue nil 215 | halt 400, "Redirect URL is not a URL (must be http://....)" unless redirect_uri 216 | halt 400, "Redirect URL must be an absolute URL with HTTP/S scheme" unless 217 | redirect_uri.absolute? && %{http https}.include?(redirect_uri.scheme) 218 | unless params[:imageUrl].nil? || params[:imageUrl].to_s.empty? 219 | image_url = URI.parse(params[:imageUrl].to_s.strip).normalize rescue nil 220 | halt 400, "Image URL must be an absolute URL with HTTP/S scheme" unless 221 | image_url.absolute? && %{http https}.include?(image_url.scheme) 222 | end 223 | scope = Server::Utils.normalize_scope(params[:scope]) 224 | { :display_name=>display_name, :link=>link.to_s, :image_url=>image_url.to_s, 225 | :redirect_uri=>redirect_uri.to_s, :scope=>scope, :notes=>params[:notes] } 226 | end 227 | 228 | def client_as_json(client, with_stats = false) 229 | { "id"=>client.id.to_s, "secret"=>client.secret, :redirectUri=>client.redirect_uri, 230 | :displayName=>client.display_name, :link=>client.link, :imageUrl=>client.image_url, 231 | :notes=>client.notes, :scope=>client.scope, 232 | :url=>"#{request.script_name}/api/client/#{client.id}", 233 | :revoke=>"#{request.script_name}/api/client/#{client.id}/revoke", 234 | :history=>"#{request.script_name}/api/client/#{client.id}/history", 235 | :created=>client.created_at, :revoked=>client.revoked } 236 | end 237 | 238 | def token_as_json(token) 239 | { :token=>token.token, :identity=>token.identity, :scope=>token.scope, :created=>token.created_at, 240 | :expired=>token.expires_at, :revoked=>token.revoked, 241 | :link=>settings.template_url && settings.template_url.gsub("{id}", token.identity), 242 | :last_access=>token.last_access, 243 | :revoke=>"#{request.script_name}/api/token/#{token.token}/revoke" } 244 | end 245 | end 246 | 247 | end 248 | end 249 | end 250 | end 251 | -------------------------------------------------------------------------------- /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/flowtown/#{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.rdoc", "Rakefile", "Gemfile", "*.gemspec"] 14 | spec.executable = "oauth2-server" 15 | 16 | spec.extra_rdoc_files = "README.rdoc", "CHANGELOG" 17 | spec.rdoc_options = "--title", "rack-oauth2-server #{spec.version}", "--main", "README.rdoc", 18 | "--webcvs", "http://github.com/flowtown/#{spec.name}" 19 | spec.license = "MIT" 20 | 21 | spec.required_ruby_version = '>= 1.8.7' 22 | spec.add_dependency "rack", "~>1.1" 23 | spec.add_dependency "mongo", "~>1" 24 | spec.add_dependency "bson_ext" 25 | spec.add_dependency "sinatra", "~>1.1" 26 | end 27 | -------------------------------------------------------------------------------- /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_equal "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_grant_test.rb: -------------------------------------------------------------------------------- 1 | require "test/setup" 2 | 3 | 4 | # 4. Obtaining an Access Token 5 | class AccessGrantTest < Test::Unit::TestCase 6 | module Helpers 7 | 8 | def should_return_error(error) 9 | should "respond with status 400 (Bad Request)" do 10 | assert_equal 400, last_response.status 11 | end 12 | should "respond with JSON document" do 13 | assert_equal "application/json", last_response.content_type 14 | end 15 | should "respond with error code #{error}" do 16 | assert_equal error.to_s, JSON.parse(last_response.body)["error"] 17 | end 18 | end 19 | 20 | def should_respond_with_authentication_error(error) 21 | should "respond with status 401 (Unauthorized)" do 22 | assert_equal 401, last_response.status 23 | end 24 | should "respond with authentication method OAuth" do 25 | assert_equal "OAuth", last_response["WWW-Authenticate"].split.first 26 | end 27 | should "respond with realm" do 28 | assert_match " realm=\"example.org\"", last_response["WWW-Authenticate"] 29 | end 30 | should "respond with error code #{error}" do 31 | assert_match " error=\"#{error}\"", last_response["WWW-Authenticate"] 32 | end 33 | end 34 | 35 | def should_respond_with_access_token(scope = "read write") 36 | should "respond with status 200" do 37 | assert_equal 200, last_response.status 38 | end 39 | should "respond with JSON document" do 40 | assert_equal "application/json", last_response.content_type 41 | end 42 | should "respond with cache control no-store" do 43 | assert_equal "no-store", last_response["Cache-Control"] 44 | end 45 | should "not respond with error code" do 46 | assert JSON.parse(last_response.body)["error"].nil? 47 | end 48 | should "response with access token" do 49 | assert_match /[a-f0-9]{32}/i, JSON.parse(last_response.body)["access_token"] 50 | end 51 | should "response with scope" do 52 | assert_equal scope || "", JSON.parse(last_response.body)["scope"] 53 | end 54 | end 55 | 56 | 57 | end 58 | extend Helpers 59 | 60 | def setup 61 | super 62 | # Get authorization code. 63 | params = { :redirect_uri=>client.redirect_uri, :client_id=>client.id, :client_secret=>client.secret, :response_type=>"code", 64 | :scope=>"read write", :state=>"bring this back" } 65 | get "/oauth/authorize?" + Rack::Utils.build_query(params) 66 | get last_response["Location"] if last_response.status == 303 67 | authorization = last_response.body[/authorization:\s*(\S+)/, 1] 68 | post "/oauth/grant", :authorization=>authorization 69 | @code = Rack::Utils.parse_query(URI.parse(last_response["Location"]).query)["code"] 70 | end 71 | 72 | def request_none(scope = nil) 73 | basic_authorize client.id, client.secret 74 | # Note: This grant_type becomes "client_credentials" in version 11 of the OAuth 2.0 spec 75 | params = { :grant_type=>"none", :scope=>"read write" } 76 | params[:scope] = scope if scope 77 | post "/oauth/access_token", params 78 | end 79 | 80 | def request_access_token(changes = nil) 81 | params = { :client_id=>client.id, :client_secret=>client.secret, :scope=>"read write", 82 | :grant_type=>"authorization_code", :code=>@code, :redirect_uri=>client.redirect_uri }.merge(changes || {}) 83 | basic_authorize params.delete(:client_id), params.delete(:client_secret) 84 | post "/oauth/access_token", params 85 | end 86 | 87 | def request_with_username_password(username, password, scope = nil) 88 | basic_authorize client.id, client.secret 89 | params = { :grant_type=>"password" } 90 | params[:scope] = scope if scope 91 | params[:username] = username if username 92 | params[:password] = password if password 93 | post "/oauth/access_token", params 94 | end 95 | 96 | 97 | # 4. Obtaining an Access Token 98 | 99 | context "GET request" do 100 | setup { get "/oauth/access_token" } 101 | 102 | should "respond with status 405 (Method Not Allowed)" do 103 | assert_equal 405, last_response.status 104 | end 105 | end 106 | 107 | context "no client ID" do 108 | setup { request_access_token :client_id=>nil } 109 | should_respond_with_authentication_error :invalid_client 110 | end 111 | 112 | context "invalid client ID" do 113 | setup { request_access_token :client_id=>"foobar" } 114 | should_respond_with_authentication_error :invalid_client 115 | end 116 | 117 | context "client ID but no such client" do 118 | setup { request_access_token :client_id=>"4cc7bc483321e814b8000000" } 119 | should_respond_with_authentication_error :invalid_client 120 | end 121 | 122 | context "no client secret" do 123 | setup { request_access_token :client_secret=>nil } 124 | should_respond_with_authentication_error :invalid_client 125 | end 126 | 127 | context "wrong client secret" do 128 | setup { request_access_token :client_secret=>"plain wrong" } 129 | should_respond_with_authentication_error :invalid_client 130 | end 131 | 132 | context "client revoked" do 133 | setup do 134 | client.revoke! 135 | request_access_token 136 | end 137 | should_respond_with_authentication_error :invalid_client 138 | end 139 | 140 | context "unsupported grant type" do 141 | setup { request_access_token :grant_type=>"bogus" } 142 | should_return_error :unsupported_grant_type 143 | end 144 | 145 | # 4.1.1. Authorization Code 146 | 147 | context "no authorization code" do 148 | setup { request_access_token :code=>nil } 149 | should_return_error :invalid_grant 150 | end 151 | 152 | context "unknown authorization code" do 153 | setup { request_access_token :code=>"unknown" } 154 | should_return_error :invalid_grant 155 | end 156 | 157 | context "authorization code for different client" do 158 | setup do 159 | grant = Server::AccessGrant.create("foo bar", Server.register(:scope=>%w{read write}), "read write", nil) 160 | request_access_token :code=>grant.code 161 | end 162 | should_return_error :invalid_grant 163 | end 164 | 165 | context "authorization code revoked" do 166 | setup do 167 | Server::AccessGrant.from_code(@code).revoke! 168 | request_access_token 169 | end 170 | should_return_error :invalid_grant 171 | end 172 | 173 | context "mistmatched redirect URI" do 174 | setup { request_access_token :redirect_uri=>"http://uberclient.dot/oz" } 175 | should_return_error :invalid_grant 176 | end 177 | 178 | context "no redirect URI to match" do 179 | setup do 180 | @client = Server.register(:display_name=>"No rediret", :scope=>"read write") 181 | grant = Server::AccessGrant.create("foo bar", client, "read write", nil) 182 | request_access_token :code=>grant.code, :redirect_uri=>"http://uberclient.dot/oz" 183 | end 184 | should_respond_with_access_token 185 | end 186 | 187 | context "access grant expired" do 188 | setup do 189 | Timecop.travel 300 do 190 | request_access_token 191 | end 192 | end 193 | should_return_error :invalid_grant 194 | end 195 | 196 | context "access grant spent" do 197 | setup do 198 | request_access_token 199 | request_access_token 200 | end 201 | should_return_error :invalid_grant 202 | end 203 | 204 | # 4.1.2. Resource Owner Password Credentials 205 | 206 | context "no username" do 207 | setup { request_with_username_password nil, "more" } 208 | should_return_error :invalid_grant 209 | end 210 | 211 | context "no password" do 212 | setup { request_with_username_password nil, "more" } 213 | should_return_error :invalid_grant 214 | end 215 | 216 | context "not authorized" do 217 | setup { request_with_username_password "cowbell", "less" } 218 | should_return_error :invalid_grant 219 | end 220 | 221 | context "no scope specified" do 222 | setup { request_with_username_password "cowbell", "more" } 223 | should_respond_with_access_token "oauth-admin read write" 224 | end 225 | 226 | context "given scope" do 227 | setup { request_with_username_password "cowbell", "more", "read" } 228 | should_respond_with_access_token "read" 229 | end 230 | 231 | context "unsupported scope" do 232 | setup { request_with_username_password "cowbell", "more", "read write math" } 233 | should_return_error :invalid_scope 234 | end 235 | 236 | context "authenticator with 4 parameters" do 237 | setup do 238 | @old = config.authenticator 239 | config.authenticator = lambda do |username, password, client_id, scope| 240 | @client_id = client_id 241 | @scope = scope 242 | "Batman" 243 | end 244 | request_with_username_password "cowbell", "more", "read" 245 | end 246 | 247 | should_respond_with_access_token "read" 248 | should "receive client identifier" do 249 | assert_equal client.id, @client_id 250 | end 251 | should "receive scope" do 252 | assert_equal %w{read}, @scope 253 | end 254 | 255 | teardown { config.authenticator = @old } 256 | end 257 | 258 | 259 | # 4.2. Access Token Response 260 | 261 | context "using none" do 262 | setup { request_none } 263 | should_respond_with_access_token "read write" 264 | end 265 | 266 | context "using authorization code" do 267 | setup { request_access_token } 268 | should_respond_with_access_token "read write" 269 | end 270 | 271 | context "using username/password" do 272 | setup { request_with_username_password "cowbell", "more", "read" } 273 | should_respond_with_access_token "read" 274 | end 275 | 276 | end 277 | -------------------------------------------------------------------------------- /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 | end 249 | end 250 | 251 | 252 | context "setting resource" do 253 | context "authenticated" do 254 | setup do 255 | with_token 256 | get "/user" 257 | end 258 | 259 | should "render user name" do 260 | assert_equal "Batman", last_response.body 261 | end 262 | end 263 | 264 | context "not authenticated" do 265 | setup do 266 | get "/user" 267 | end 268 | 269 | should "not render user name" do 270 | assert last_response.body.empty? 271 | end 272 | end 273 | end 274 | 275 | context "list tokens" do 276 | setup do 277 | @other = Server.token_for("foobar", client.id, "read") 278 | get "/list_tokens" 279 | end 280 | 281 | should "return access token" do 282 | assert_contains last_response.body.split, @token 283 | end 284 | 285 | should "not return other resource's token" do 286 | assert !last_response.body.split.include?(@other) 287 | end 288 | end 289 | 290 | context "tokens have an expire date" do 291 | setup do 292 | @other_token = Rack::OAuth2::Server::AccessToken.from_token(@token) 293 | end 294 | 295 | should "expire in a day" do 296 | a_day_later = (Time.now + (60 * 60 * 24) + 1).to_i 297 | assert @other_token.expires_at < a_day_later 298 | end 299 | 300 | end 301 | 302 | 303 | context "with specific host" do 304 | context "right host" do 305 | setup do 306 | get "http://example.org/public" 307 | end 308 | # Right host, but not authenticated 309 | should_return_resource "HAI" 310 | end 311 | 312 | context "wrong host" do 313 | setup do 314 | with_token 315 | get "http://wrong.org/public" 316 | end 317 | # Wrong host, not checking credentials 318 | should_return_resource "HAI" 319 | end 320 | end 321 | 322 | 323 | context "with specific path" do 324 | setup { config.path = "/private" } 325 | 326 | context "outside path" do 327 | setup { with_token ; get "http://example.org/public" } 328 | # Not authenticated 329 | should_return_resource "HAI" 330 | end 331 | 332 | context "inside path" do 333 | setup { with_token ; get "http://example.org/private" } 334 | # Authenticated 335 | should_return_resource "Shhhh" 336 | end 337 | 338 | teardown { config.path = nil } 339 | end 340 | end 341 | -------------------------------------------------------------------------------- /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 | end 19 | 20 | context "get_auth_request" do 21 | setup { @request = Server::AuthRequest.create(client, client.scope.join(" "), client.redirect_uri, "token", nil) } 22 | should "return authorization request" do 23 | assert_equal @request.id, Server.get_auth_request(@request.id).id 24 | end 25 | 26 | should "return nil if no request found" do 27 | assert !Server.get_auth_request("4ce2488e3321e87ac1000004") 28 | end 29 | end 30 | 31 | 32 | context "get_client" do 33 | should "return authorization request" do 34 | assert_equal client.display_name, Server.get_client(client.id).display_name 35 | end 36 | 37 | should "return nil if no client found" do 38 | assert !Server.get_client("4ce2488e3321e87ac1000004") 39 | end 40 | end 41 | 42 | 43 | context "register" do 44 | context "no client ID" do 45 | setup do 46 | @client = Server.register(:display_name=>"MyApp", :link=>"http://example.org", :image_url=>"http://example.org/favicon.ico", 47 | :redirect_uri=>"http://example.org/oauth/callback", :scope=>%w{read write}) 48 | end 49 | 50 | should "create new client" do 51 | assert_equal 2, Server::Client.collection.count 52 | assert_contains Server::Client.all.map(&:id), @client.id 53 | end 54 | 55 | should "set display name" do 56 | assert_equal "MyApp", Server.get_client(@client.id).display_name 57 | end 58 | 59 | should "set link" do 60 | assert_equal "http://example.org", Server.get_client(@client.id).link 61 | end 62 | 63 | should "set image URL" do 64 | assert_equal "http://example.org/favicon.ico", Server.get_client(@client.id).image_url 65 | end 66 | 67 | should "set redirect URI" do 68 | assert_equal "http://example.org/oauth/callback", Server.get_client(@client.id).redirect_uri 69 | end 70 | 71 | should "set scope" do 72 | assert_equal %w{read write}, Server.get_client(@client.id).scope 73 | end 74 | 75 | should "assign client an ID" do 76 | assert_match /[0-9a-f]{24}/, @client.id.to_s 77 | end 78 | 79 | should "assign client a secret" do 80 | assert_match /[0-9a-f]{64}/, @client.secret 81 | end 82 | end 83 | 84 | context "with client ID" do 85 | 86 | context "no such client" do 87 | setup do 88 | @client = Server.register(:id=>"4ce24c423321e88ac5000015", :secret=>"foobar", :display_name=>"MyApp") 89 | end 90 | 91 | should "create new client" do 92 | assert_equal 2, Server::Client.collection.count 93 | end 94 | 95 | should "should assign it the client identifier" do 96 | assert_equal "4ce24c423321e88ac5000015", @client.id.to_s 97 | end 98 | 99 | should "should assign it the client secret" do 100 | assert_equal "foobar", @client.secret 101 | end 102 | 103 | should "should assign it the other properties" do 104 | assert_equal "MyApp", @client.display_name 105 | end 106 | end 107 | 108 | context "existing client" do 109 | setup do 110 | Server.register(:id=>"4ce24c423321e88ac5000015", :secret=>"foobar", :display_name=>"MyApp") 111 | @client = Server.register(:id=>"4ce24c423321e88ac5000015", :secret=>"foobar", :display_name=>"Rock Star") 112 | end 113 | 114 | should "not create new client" do 115 | assert_equal 2, Server::Client.collection.count 116 | end 117 | 118 | should "should not change the client identifier" do 119 | assert_equal "4ce24c423321e88ac5000015", @client.id.to_s 120 | end 121 | 122 | should "should not change the client secret" do 123 | assert_equal "foobar", @client.secret 124 | end 125 | 126 | should "should change all the other properties" do 127 | assert_equal "Rock Star", @client.display_name 128 | end 129 | end 130 | 131 | context "secret mismatch" do 132 | setup do 133 | Server.register(:id=>"4ce24c423321e88ac5000015", :secret=>"foobar", :display_name=>"MyApp") 134 | end 135 | 136 | should "raise error" do 137 | assert_raises RuntimeError do 138 | Server.register(:id=>"4ce24c423321e88ac5000015", :secret=>"wrong", :display_name=>"MyApp") 139 | end 140 | end 141 | end 142 | 143 | end 144 | end 145 | 146 | 147 | context "access_grant" do 148 | setup do 149 | code = Server.access_grant("Batman", client.id, %w{read}) 150 | basic_authorize client.id, client.secret 151 | post "/oauth/access_token", :scope=>"read", :grant_type=>"authorization_code", :code=>code, :redirect_uri=>client.redirect_uri 152 | @token = JSON.parse(last_response.body)["access_token"] 153 | end 154 | 155 | should "resolve into an access token" do 156 | assert Server.get_access_token(@token) 157 | end 158 | 159 | should "resolve into access token with grant identity" do 160 | assert_equal "Batman", Server.get_access_token(@token).identity 161 | end 162 | 163 | should "resolve into access token with grant scope" do 164 | assert_equal %w{read}, Server.get_access_token(@token).scope 165 | end 166 | 167 | should "resolve into access token with grant client" do 168 | assert_equal client.id, Server.get_access_token(@token).client_id 169 | end 170 | 171 | context "with no scope" do 172 | setup { @code = Server.access_grant("Batman", client.id) } 173 | 174 | should "pick client scope" do 175 | assert_equal %w{oauth-admin read write}, Server::AccessGrant.from_code(@code).scope 176 | end 177 | end 178 | 179 | context "no expiration" do 180 | setup do 181 | @code = Server.access_grant("Batman", client.id) 182 | end 183 | 184 | should "not expire in a minute" do 185 | Timecop.travel 60 do 186 | basic_authorize client.id, client.secret 187 | post "/oauth/access_token", :scope=>"read", :grant_type=>"authorization_code", :code=>@code, :redirect_uri=>client.redirect_uri 188 | assert_equal 200, last_response.status 189 | end 190 | end 191 | 192 | should "expire after 5 minutes" do 193 | Timecop.travel 300 do 194 | basic_authorize client.id, client.secret 195 | post "/oauth/access_token", :scope=>"read", :grant_type=>"authorization_code", :code=>@code, :redirect_uri=>client.redirect_uri 196 | assert_equal 400, last_response.status 197 | end 198 | end 199 | end 200 | 201 | context "expiration set" do 202 | setup do 203 | @code = Server.access_grant("Batman", client.id, nil, 1800) 204 | end 205 | 206 | should "not expire prematurely" do 207 | Timecop.travel 1750 do 208 | basic_authorize client.id, client.secret 209 | post "/oauth/access_token", :scope=>"read", :grant_type=>"authorization_code", :code=>@code, :redirect_uri=>client.redirect_uri 210 | assert_equal 200, last_response.status 211 | end 212 | end 213 | 214 | should "expire after specified seconds" do 215 | Timecop.travel 1800 do 216 | basic_authorize client.id, client.secret 217 | post "/oauth/access_token", :scope=>"read", :grant_type=>"authorization_code", :code=>@code, :redirect_uri=>client.redirect_uri 218 | assert_equal 400, last_response.status 219 | end 220 | end 221 | end 222 | 223 | end 224 | 225 | 226 | context "get_access_token" do 227 | setup { @token = Server.token_for("Batman", client.id, %w{read}) } 228 | should "return authorization request" do 229 | assert_equal @token, Server.get_access_token(@token).token 230 | end 231 | 232 | should "return nil if no client found" do 233 | assert !Server.get_access_token("4ce2488e3321e87ac1000004") 234 | end 235 | 236 | context "with no scope" do 237 | setup { @token = Server.token_for("Batman", client.id) } 238 | 239 | should "pick client scope" do 240 | assert_equal %w{oauth-admin read write}, Server::AccessToken.from_token(@token).scope 241 | end 242 | end 243 | end 244 | 245 | 246 | context "token_for" do 247 | setup { @token = Server.token_for("Batman", client.id, %w{read write}) } 248 | 249 | should "return access token" do 250 | assert_match /[0-9a-f]{32}/, @token 251 | end 252 | 253 | should "associate token with client" do 254 | assert_equal client.id, Server.get_access_token(@token).client_id 255 | end 256 | 257 | should "associate token with identity" do 258 | assert_equal "Batman", Server.get_access_token(@token).identity 259 | end 260 | 261 | should "associate token with scope" do 262 | assert_equal %w{read write}, Server.get_access_token(@token).scope 263 | end 264 | 265 | should "return same token for same parameters" do 266 | assert_equal @token, Server.token_for("Batman", client.id, %w{write read}) 267 | end 268 | 269 | should "return different token for different identity" do 270 | assert @token != Server.token_for("Superman", client.id, %w{read write}) 271 | end 272 | 273 | should "return different token for different client" do 274 | client = Server.register(:display_name=>"MyApp") 275 | assert @token != Server.token_for("Batman", client.id, %w{read write}) 276 | end 277 | 278 | should "return different token for different scope" do 279 | assert @token != Server.token_for("Batman", client.id, %w{read}) 280 | end 281 | end 282 | 283 | 284 | context "list access tokens" do 285 | setup do 286 | @one = Server.token_for("Batman", client.id, %w{read}) 287 | @two = Server.token_for("Superman", client.id, %w{read}) 288 | @three = Server.token_for("Batman", client.id, %w{write}) 289 | end 290 | 291 | should "return all tokens for identity" do 292 | assert_contains Server.list_access_tokens("Batman").map(&:token), @one 293 | assert_contains Server.list_access_tokens("Batman").map(&:token), @three 294 | end 295 | 296 | should "not return tokens for other identities" do 297 | assert !Server.list_access_tokens("Batman").map(&:token).include?(@two) 298 | end 299 | 300 | end 301 | 302 | end 303 | -------------------------------------------------------------------------------- /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.authenticator = lambda do |username, password| 18 | "Batman" if username == "cowbell" && password == "more" 19 | end 20 | end 21 | config.middleware.use Rack::OAuth2::Server::Admin.mount 22 | end 23 | -------------------------------------------------------------------------------- /test/rails2/config/environments/test.rb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/demandforce/rack-oauth2-server/1ae51703342272f23a205c6009aa8ffdff6ce47a/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.authenticator = lambda do |username, password| 12 | "Batman" if username == "cowbell" && password == "more" 13 | end 14 | config.middleware.use Rack::OAuth2::Server::Admin.mount 15 | end 16 | end 17 | end 18 | Rails.application.config.root = File.dirname(__FILE__) + "/.." 19 | require Rails.root + "config/routes" 20 | -------------------------------------------------------------------------------- /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 | end 121 | end 122 | -------------------------------------------------------------------------------- /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 | 15 | 16 | 17 | # 3. Obtaining End-User Authorization 18 | 19 | before "/oauth/*" do 20 | halt oauth.deny! if oauth.scope.include?("time-travel") # Only Superman can do that 21 | end 22 | 23 | get "/oauth/authorize" do 24 | "client: #{oauth.client.display_name}\nscope: #{oauth.scope.join(", ")}\nauthorization: #{oauth.authorization}" 25 | end 26 | 27 | post "/oauth/grant" do 28 | oauth.grant! "Batman" 29 | end 30 | 31 | post "/oauth/deny" do 32 | oauth.deny! 33 | end 34 | 35 | 36 | # 5. Accessing a Protected Resource 37 | 38 | before { @user = oauth.identity if oauth.authenticated? } 39 | 40 | get "/public" do 41 | if oauth.authenticated? 42 | "HAI from #{oauth.identity}" 43 | else 44 | "HAI" 45 | end 46 | end 47 | 48 | oauth_required "/private", "/change" 49 | 50 | get "/private" do 51 | "Shhhh" 52 | end 53 | 54 | post "/change" do 55 | "Woot!" 56 | end 57 | 58 | oauth_required "/calc", :scope=>"math" 59 | 60 | get "/calc" do 61 | end 62 | 63 | get "/user" do 64 | @user 65 | end 66 | 67 | get "/list_tokens" do 68 | oauth.list_access_tokens("Batman").map(&:token).join(" ") 69 | end 70 | 71 | end 72 | --------------------------------------------------------------------------------