warden-github 0.12.1 → 0.13.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -6,3 +6,5 @@ Gemfile.lock
6
6
  vendor/gems
7
7
  *.gem
8
8
  .rbenv-version
9
+ bin/
10
+ tags
data/Gemfile CHANGED
@@ -1,9 +1,7 @@
1
1
  source :rubygems
2
2
 
3
- gem 'ruby-debug19', :platforms => :ruby_19
4
- gem 'ruby-debug', :platforms => :ruby_18
3
+ gem 'debugger', :platforms => :ruby_19, :require => false
4
+ gem 'ruby-debug', :platforms => :ruby_18, :require => false
5
5
 
6
6
  # Specify your gem's dependencies in warden-github.gemspec
7
7
  gemspec
8
-
9
- # vim:ft=ruby
data/README.md CHANGED
@@ -1,11 +1,11 @@
1
1
  warden-github
2
2
  =============
3
3
 
4
- A [warden](http://github.com/hassox/warden) strategy that provides oauth authentication to github. Find out more about enabling your application at github's [oauth quickstart](http://gist.github.com/419219).
4
+ A [warden](/hassox/warden) strategy that provides oauth authentication to github. Find out more about enabling your application at github's [oauth quickstart](http://gist.github.com/419219).
5
5
 
6
- To test it out on localhost set your callback url to 'http://localhost:9292/auth/github/callback'
6
+ To test it out on localhost set your callback url to 'http://localhost:9292/'.
7
7
 
8
- There's an example app in [spec/app.rb](/atmos/warden-github/blob/master/spec/app.rb).
8
+ There's an example app in [example/app.rb](/atmos/warden-github/blob/master/example/app.rb).
9
9
 
10
10
  Using with GitHub Enterprise
11
11
  ============================
@@ -14,6 +14,5 @@ Export the `OCTOKIT_API_ENDPOINT` environmental variable to the URL of your ente
14
14
 
15
15
  The Extension in Action
16
16
  =======================
17
- % gem install bundler
18
- % bundle install
19
- % GITHUB_CLIENT_ID="<from GH>" GITHUB_CLIENT_SECRET="<from GH>" bundle exec rackup -p9393 -E none
17
+
18
+ % GITHUB_CLIENT_ID="<from GH>" GITHUB_CLIENT_SECRET="<from GH>" bundle exec rackup
data/config.ru CHANGED
@@ -8,13 +8,13 @@ rescue LoadError
8
8
  Bundler.setup
9
9
  end
10
10
 
11
- Bundler.require(:runtime, :test)
12
- require "ruby-debug"
11
+ begin
12
+ require 'debugger'
13
+ rescue LoadError
14
+ require 'ruby-debug'
15
+ end
13
16
 
14
- $LOAD_PATH << File.dirname(__FILE__) + '/lib'
15
- require File.expand_path(File.join(File.dirname(__FILE__), 'lib', 'warden-github'))
16
- require File.expand_path(File.join(File.dirname(__FILE__), 'spec', 'app'))
17
+ require 'warden/github'
18
+ require File.expand_path('../example/app', __FILE__)
17
19
 
18
20
  run Example.app
19
-
20
- # vim:ft=ruby
data/example/app.rb ADDED
@@ -0,0 +1,98 @@
1
+ require 'sinatra'
2
+ require 'yajl/json_gem'
3
+
4
+ module Example
5
+ class App < Sinatra::Base
6
+ enable :sessions
7
+ enable :raise_errors
8
+ disable :show_exceptions
9
+
10
+ use Warden::Manager do |manager|
11
+ manager.default_strategies :github
12
+ manager.failure_app = BadAuthentication
13
+
14
+ manager[:github_client_id] = ENV['GITHUB_CLIENT_ID'] || 'ee9aa24b64d82c21535a'
15
+ manager[:github_secret] = ENV['GITHUB_CLIENT_SECRET'] || 'ed8ff0c54067aefb808dab1ca265865405d08d6f'
16
+
17
+ manager[:github_scopes] = ''
18
+ end
19
+
20
+ helpers do
21
+ def ensure_authenticated
22
+ unless env['warden'].authenticate!
23
+ throw(:warden)
24
+ end
25
+ end
26
+
27
+ def user
28
+ env['warden'].user
29
+ end
30
+ end
31
+
32
+ get '/' do
33
+ if user
34
+ <<-EOS
35
+ <h2>Hello #{user.name}!</h2>
36
+ <ul>
37
+ <li><a href='/profile'>View profile</a></li>
38
+ <li><a href='/logout'>Sign out</a></li>
39
+ </ul>
40
+ EOS
41
+ else
42
+ <<-EOS
43
+ <h2>Hello stranger!</h2>
44
+ <ul>
45
+ <li><a href='/profile'>View profile</a> (implicit sign in)</li>
46
+ <li><a href='/login'>Sign in</a> (explicit sign in)</li>
47
+ </ul>
48
+ EOS
49
+ end
50
+ end
51
+
52
+ get '/profile' do
53
+ ensure_authenticated
54
+ <<-EOS
55
+ <h2>Hello #{user.name}!</h2>
56
+ <ul>
57
+ <li><a href='/'>Home</a></li>
58
+ <li><a href='/logout'>Sign out</a></li>
59
+ </ul>
60
+ <h3>Profile</h3>
61
+ <h4>Rails Org Member: #{user.organization_member?('rails')}.</h4>
62
+ <h4>Publicized Rails Org Member: #{user.organization_public_member?('rails')}.</h4>
63
+ <h4>Rails Committer Team Member: #{user.team_member?(632)}.</h4>
64
+ EOS
65
+ end
66
+
67
+ get '/login' do
68
+ ensure_authenticated
69
+ redirect '/'
70
+ end
71
+
72
+ get '/logout' do
73
+ env['warden'].logout
74
+ redirect '/'
75
+ end
76
+
77
+ get '/debug' do
78
+ content_type :text
79
+ env['rack.session'].to_yaml
80
+ end
81
+ end
82
+
83
+ class BadAuthentication < Sinatra::Base
84
+ get '/unauthenticated' do
85
+ status 403
86
+ <<-EOS
87
+ <h2>Unable to authenticate, sorry bud.</h2>
88
+ <p>#{env['warden'].message}</p>
89
+ EOS
90
+ end
91
+ end
92
+
93
+ def self.app
94
+ @app ||= Rack::Builder.new do
95
+ run App
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,10 @@
1
+ require 'warden'
2
+
3
+ require 'warden/github/user'
4
+ require 'warden/github/oauth'
5
+ require 'warden/github/version'
6
+ require 'warden/github/strategy'
7
+ require 'warden/github/hook'
8
+
9
+ require 'yajl'
10
+ require 'securerandom'
@@ -0,0 +1,6 @@
1
+ Warden::Manager.after_authentication do |user, auth, opts|
2
+ scope = opts.fetch(:scope)
3
+ strategy = auth.winning_strategies[scope]
4
+
5
+ strategy.finalize_flow! if strategy.class == Warden::GitHub::Strategy
6
+ end
@@ -0,0 +1,97 @@
1
+ require 'uri'
2
+ require 'net/https'
3
+
4
+ module Warden
5
+ module GitHub
6
+ class OAuth
7
+ BadVerificationCode = Class.new(StandardError)
8
+
9
+ attr_reader :code,
10
+ :state,
11
+ :scope,
12
+ :client_secret,
13
+ :client_id,
14
+ :redirect_uri
15
+
16
+ def initialize(attrs={})
17
+ @code = attrs[:code]
18
+ @state = attrs[:state]
19
+ @scope = attrs[:scope]
20
+ @client_id = attrs.fetch(:client_id)
21
+ @client_secret = attrs.fetch(:client_secret)
22
+ @redirect_uri = attrs.fetch(:redirect_uri)
23
+ end
24
+
25
+ def authorize_uri
26
+ @authorize_uri ||= build_uri(
27
+ '/login/oauth/authorize',
28
+ :client_id => client_id,
29
+ :redirect_uri => redirect_uri,
30
+ :scope => scope,
31
+ :state => state)
32
+ end
33
+
34
+ def access_token
35
+ @access_token ||= load_access_token
36
+ end
37
+
38
+ private
39
+
40
+ def load_access_token
41
+ http = Net::HTTP.new(access_token_uri.host, access_token_uri.port)
42
+ http.use_ssl = access_token_uri.scheme == 'https'
43
+
44
+ request = Net::HTTP::Post.new(access_token_uri.path)
45
+ request.body = access_token_uri.query
46
+
47
+ response = http.request(request)
48
+ decode_params(response.body).fetch('access_token')
49
+ rescue IndexError
50
+ fail BadVerificationCode, 'Bad verification code'
51
+ end
52
+
53
+ def access_token_uri
54
+ @access_token_uri ||= build_uri(
55
+ '/login/oauth/access_token',
56
+ :client_id => client_id,
57
+ :client_secret => client_secret,
58
+ :code => code)
59
+ end
60
+
61
+ def build_uri(path, params)
62
+ URI(Octokit::Configuration::DEFAULT_WEB_ENDPOINT).tap do |uri|
63
+ uri.path = path
64
+ uri.query = encode_params(normalize_params(params))
65
+ end
66
+ end
67
+
68
+ def normalize_params(params)
69
+ params.reject { |_,v| v.nil? || v == '' }
70
+ end
71
+
72
+ def encode_params(params)
73
+ if URI.respond_to? :encode_www_form
74
+ return URI.encode_www_form(params)
75
+ end
76
+
77
+ params.map { |*kv|
78
+ kv.flatten.map { |i|
79
+ URI.encode(i.to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
80
+ }.join('=')
81
+ }.join('&')
82
+ end
83
+
84
+ def decode_params(params)
85
+ if URI.respond_to? :decode_www_form
86
+ return Hash[URI.decode_www_form(params)]
87
+ end
88
+
89
+ Hash[
90
+ params.split('&').map { |i|
91
+ i.split('=').map { |i| URI.decode(i) }
92
+ }
93
+ ]
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,122 @@
1
+ module Warden
2
+ module GitHub
3
+ class Strategy < ::Warden::Strategies::Base
4
+ SESSION_KEY = 'warden.github.oauth'
5
+
6
+ # The first time this is called, the flow gets set up, stored in the
7
+ # session and the user gets redirected to GitHub to perform the login.
8
+ #
9
+ # When this is called a second time, the flow gets evaluated, the code
10
+ # gets exchanged for a token, and the user gets loaded and passed to
11
+ # warden.
12
+ #
13
+ # If anything goes wrong, the flow is aborted and reset, and warden gets
14
+ # notified about the failure.
15
+ #
16
+ # Once the user gets set, warden invokes the after_authentication callback
17
+ # that handles the redirect to the originally requested url and cleans up
18
+ # the flow. Note that this is done in a hook because setting a user
19
+ # (through #success!) and redirecting (through #redirect!) inside the
20
+ # #authenticate! method are mutual exclusive.
21
+ def authenticate!
22
+ if in_flow?
23
+ continue_flow!
24
+ else
25
+ begin_flow!
26
+ end
27
+ end
28
+
29
+ def in_flow?
30
+ !custom_session.empty? &&
31
+ params['state'] &&
32
+ (params['code'] || params['error'])
33
+ end
34
+
35
+ # This is called by the after_authentication hook which is invoked after
36
+ # invoking #success!.
37
+ def finalize_flow!
38
+ redirect!(custom_session['return_to'])
39
+ teardown_flow
40
+ throw(:warden)
41
+ end
42
+
43
+ private
44
+
45
+ def begin_flow!
46
+ custom_session['state'] = state
47
+ custom_session['return_to'] = request.url
48
+ redirect!(oauth.authorize_uri.to_s)
49
+ throw(:warden)
50
+ end
51
+
52
+ def continue_flow!
53
+ validate_flow!
54
+ success!(load_user)
55
+ end
56
+
57
+ def abort_flow!(message)
58
+ teardown_flow
59
+ fail!(message)
60
+ throw(:warden)
61
+ end
62
+
63
+ def teardown_flow
64
+ session.delete(SESSION_KEY)
65
+ end
66
+
67
+ def validate_flow!
68
+ if params['state'] != state
69
+ abort_flow!('State mismatch')
70
+ elsif (error = params['error']) && !error.blank?
71
+ abort_flow!(error.gsub(/_/, ' '))
72
+ end
73
+ end
74
+
75
+ def custom_session
76
+ session[SESSION_KEY] ||= {}
77
+ end
78
+
79
+ def load_user
80
+ User.load(oauth.access_token)
81
+ rescue OAuth::BadVerificationCode => e
82
+ abort_flow!(e.message)
83
+ end
84
+
85
+ def state
86
+ @state ||= custom_session['state'] || SecureRandom.hex(20)
87
+ end
88
+
89
+ def oauth
90
+ @oauth ||= OAuth.new(
91
+ :code => params['code'],
92
+ :state => state,
93
+ :scope => env['warden'].config[:github_scopes],
94
+ :client_id => env['warden'].config[:github_client_id],
95
+ :client_secret => env['warden'].config[:github_secret],
96
+ :redirect_uri => redirect_uri)
97
+ end
98
+
99
+ def redirect_uri
100
+ absolute_uri(request, callback_path, env['HTTP_X_FORWARDED_PROTO'])
101
+ end
102
+
103
+ def callback_path
104
+ env['warden'].config[:github_callback_url] || request.path
105
+ end
106
+
107
+ def absolute_uri(request, suffix = nil, proto = "http")
108
+ port_part = case request.scheme
109
+ when "http"
110
+ request.port == 80 ? "" : ":#{request.port}"
111
+ when "https"
112
+ request.port == 443 ? "" : ":#{request.port}"
113
+ end
114
+
115
+ proto = "http" if proto.nil?
116
+ "#{proto}://#{request.host}#{port_part}#{suffix}"
117
+ end
118
+ end
119
+ end
120
+ end
121
+
122
+ Warden::Strategies.add(:github, Warden::GitHub::Strategy)
@@ -0,0 +1,81 @@
1
+ require 'octokit'
2
+
3
+ module Warden
4
+ module GitHub
5
+ class User < Struct.new(:attribs, :token)
6
+ ATTRIBUTES = %w[id login name gravatar_id email company].freeze
7
+
8
+ def self.load(access_token)
9
+ api = Octokit::Client.new(:oauth_token => access_token)
10
+ data = Hash[api.user.to_hash.select { |k,_| ATTRIBUTES.include?(k) }]
11
+
12
+ new(data, access_token)
13
+ end
14
+
15
+ def marshal_dump
16
+ Hash[members.zip(values)]
17
+ end
18
+
19
+ def marshal_load(hash)
20
+ hash.each { |k,v| send("#{k}=", v) }
21
+ end
22
+
23
+ ATTRIBUTES.each do |name|
24
+ define_method(name) { attribs[name] }
25
+ end
26
+
27
+ # See if the user is a public member of the named organization
28
+ #
29
+ # name - the organization name
30
+ #
31
+ # Returns: true if the user is publicized as an org member
32
+ def organization_public_member?(org_name)
33
+ api.organization_public_member?(org_name, login)
34
+ end
35
+
36
+ # Backwards compatibility:
37
+ alias_method :publicized_organization_member?, :organization_public_member?
38
+
39
+ # See if the user is a member of the named organization
40
+ #
41
+ # name - the organization name
42
+ #
43
+ # Returns: true if the user has access, false otherwise
44
+ def organization_member?(org_name)
45
+ api.organization_member?(org_name, login)
46
+ end
47
+
48
+ # See if the user is a member of the team id
49
+ #
50
+ # team_id - the team's id
51
+ #
52
+ # Returns: true if the user has access, false otherwise
53
+ def team_member?(team_id)
54
+ # TODO: Use next line as method body once pengwynn/octokit#206 is public.
55
+ # api.team_member?(team_id, login)
56
+
57
+ # If the user is able to query the team member
58
+ # A user is only able to query for team members if they're a member.
59
+ # Thus, if querying does succeed, they will be in the list and checking
60
+ # the list won't be necessary.
61
+ api.team_members(team_id)
62
+ true
63
+ rescue Octokit::NotFound
64
+ false
65
+ end
66
+
67
+ # Access the GitHub API from Octokit
68
+ #
69
+ # Octokit is a robust client library for the GitHub API
70
+ # https://github.com/pengwynn/octokit
71
+ #
72
+ # Returns a cached client object for easy use
73
+ def api
74
+ # Don't cache instance for now because of a ruby marshaling bug present
75
+ # in MRI 1.9.3 (Bug #7627) that causes instance variables to be
76
+ # marshaled even when explicitly specifying #marshal_dump.
77
+ Octokit::Client.new(:login => login, :oauth_token => token)
78
+ end
79
+ end
80
+ end
81
+ end