warden-github 0.12.1 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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