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 +2 -0
- data/Gemfile +2 -4
- data/README.md +5 -6
- data/config.ru +7 -7
- data/example/app.rb +98 -0
- data/lib/warden/github.rb +10 -0
- data/lib/warden/github/hook.rb +6 -0
- data/lib/warden/github/oauth.rb +97 -0
- data/lib/warden/github/strategy.rb +122 -0
- data/lib/warden/github/user.rb +81 -0
- data/lib/warden/github/version.rb +5 -0
- data/spec/fixtures/user.json +42 -0
- data/spec/integration/oauth_spec.rb +131 -0
- data/spec/spec_helper.rb +8 -3
- data/spec/unit/oauth_spec.rb +68 -0
- data/spec/unit/user_spec.rb +112 -0
- data/warden-github.gemspec +8 -11
- metadata +73 -89
- data/lib/warden-github.rb +0 -14
- data/lib/warden-github/proxy.rb +0 -45
- data/lib/warden-github/strategy.rb +0 -83
- data/lib/warden-github/user.rb +0 -159
- data/lib/warden-github/version.rb +0 -5
- data/spec/app.rb +0 -71
- data/spec/oauth_spec.rb +0 -18
- data/spec/proxy_spec.rb +0 -34
- data/spec/user_spec.rb +0 -14
data/.gitignore
CHANGED
data/Gemfile
CHANGED
@@ -1,9 +1,7 @@
|
|
1
1
|
source :rubygems
|
2
2
|
|
3
|
-
gem '
|
4
|
-
gem 'ruby-debug',
|
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](
|
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/
|
6
|
+
To test it out on localhost set your callback url to 'http://localhost:9292/'.
|
7
7
|
|
8
|
-
There's an example app in [
|
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
|
-
|
18
|
-
% bundle
|
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
|
-
|
12
|
-
require
|
11
|
+
begin
|
12
|
+
require 'debugger'
|
13
|
+
rescue LoadError
|
14
|
+
require 'ruby-debug'
|
15
|
+
end
|
13
16
|
|
14
|
-
|
15
|
-
require File.expand_path(
|
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,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
|