Skip to content

Commit c0d00a5

Browse files
committed
Github Auth Extension
1 parent 1317be9 commit c0d00a5

17 files changed

Lines changed: 771 additions & 32 deletions

.htaccess

Lines changed: 32 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -50,40 +50,40 @@ RewriteRule ^review(.*) page.cgi?id=splinter.html$1 [QSA]
5050
RewriteRule ^user_?profile(.*) page.cgi?id=user_profile.html$1 [QSA]
5151
RewriteRule ^request_defer(.*) page.cgi?id=request_defer.html$1 [QSA]
5252
RewriteRule ^favicon\.ico$ extensions/BMO/web/images/favicon.ico
53-
RewriteRule ^form[\.:]itrequest$ enter_bug.cgi?product=Infrastructure+\%26+Operations&format=itrequest
54-
RewriteRule ^form[\.:](mozlist|poweredby|presentation|trademark|recoverykey)$ enter_bug.cgi?product=mozilla.org&format=$1
55-
RewriteRule ^form[\.:]legal$ enter_bug.cgi?product=Legal&format=legal
56-
RewriteRule ^form[\.:]recruiting$ enter_bug.cgi?product=Recruiting&format=recruiting
57-
RewriteRule ^form[\.:]mozpr$ enter_bug.cgi?product=Mozilla+PR&format=mozpr
58-
RewriteRule ^form[\.:]reps[\.:]mentorship$ enter_bug.cgi?product=Mozilla+Reps&format=mozreps
59-
RewriteRule ^form[\.:]reps[\.:]budget$ enter_bug.cgi?product=Mozilla+Reps&format=remo-budget
60-
RewriteRule ^form[\.:]reps[\.:]swag$ enter_bug.cgi?product=Mozilla+Reps&format=remo-swag
61-
RewriteRule ^form[\.:]reps[\.:]it$ enter_bug.cgi?product=Mozilla+Reps&format=remo-it
62-
RewriteRule ^form[\.:]reps[\.:]payment$ page.cgi?id=remo-form-payment.html
63-
RewriteRule ^form[\.:]csa[\.:]discourse$ enter_bug.cgi?product=Infrastructure+\%26\+Operations&format=csa-discourse
64-
RewriteRule ^form[\.:]employee[\.\-:]incident$ enter_bug.cgi?product=mozilla.org&format=employee-incident
65-
RewriteRule ^form[\.:]brownbag$ https://air.mozilla.org/requests
66-
RewriteRule ^form[\.:]finance$ enter_bug.cgi?product=Finance&format=finance
67-
RewriteRule ^form[\.:]moz[\.\-:]project[\.\-:]review$ enter_bug.cgi?product=mozilla.org&format=moz-project-review
53+
RewriteRule ^form[\.:]itrequest$ enter_bug.cgi?product=Infrastructure+\%26+Operations&format=itrequest [QSA]
54+
RewriteRule ^form[\.:](mozlist|poweredby|presentation|trademark|recoverykey)$ enter_bug.cgi?product=mozilla.org&format=$1 [QSA]
55+
RewriteRule ^form[\.:]legal$ enter_bug.cgi?product=Legal&format=legal [QSA]
56+
RewriteRule ^form[\.:]recruiting$ enter_bug.cgi?product=Recruiting&format=recruiting [QSA]
57+
RewriteRule ^form[\.:]mozpr$ enter_bug.cgi?product=Mozilla+PR&format=mozpr [QSA]
58+
RewriteRule ^form[\.:]reps[\.:]mentorship$ enter_bug.cgi?product=Mozilla+Reps&format=mozreps [QSA]
59+
RewriteRule ^form[\.:]reps[\.:]budget$ enter_bug.cgi?product=Mozilla+Reps&format=remo-budget [QSA]
60+
RewriteRule ^form[\.:]reps[\.:]swag$ enter_bug.cgi?product=Mozilla+Reps&format=remo-swag [QSA]
61+
RewriteRule ^form[\.:]reps[\.:]it$ enter_bug.cgi?product=Mozilla+Reps&format=remo-it [QSA]
62+
RewriteRule ^form[\.:]reps[\.:]payment$ page.cgi?id=remo-form-payment.html [QSA]
63+
RewriteRule ^form[\.:]csa[\.:]discourse$ enter_bug.cgi?product=Infrastructure+\%26\+Operations&format=csa-discourse [QSA]
64+
RewriteRule ^form[\.:]employee[\.\-:]incident$ enter_bug.cgi?product=mozilla.org&format=employee-incident [QSA]
65+
RewriteRule ^form[\.:]brownbag$ https://air.mozilla.org/requests [QSA]
66+
RewriteRule ^form[\.:]finance$ enter_bug.cgi?product=Finance&format=finance [QSA]
67+
RewriteRule ^form[\.:]moz[\.\-:]project[\.\-:]review$ enter_bug.cgi?product=mozilla.org&format=moz-project-review [QSA]
6868
RewriteRule ^form[\.:]docs?$ enter_bug.cgi?product=Developer+Documentation&format=doc [QSA]
69-
RewriteRule ^form[\.:]mdn?$ enter_bug.cgi?product=Mozilla+Developer+Network&format=mdn
70-
RewriteRule ^form[\.:](swag|gear)$ enter_bug.cgi?product=Marketing&format=swag
71-
RewriteRule ^form[\.:]costume$ enter_bug.cgi?product=Marketing&format=costume
69+
RewriteRule ^form[\.:]mdn?$ enter_bug.cgi?product=Mozilla+Developer+Network&format=mdn [QSA]
70+
RewriteRule ^form[\.:](swag|gear)$ enter_bug.cgi?product=Marketing&format=swag [QSA]
71+
RewriteRule ^form[\.:]costume$ enter_bug.cgi?product=Marketing&format=costume [QSA]
7272
RewriteRule ^form[\.:](b2g|fxos)[\.\-:](partner|betaprogram|feature) enter_bug.cgi?product=Firefox+OS&format=fxos-$2 [QSA]
73-
RewriteRule ^form[\.:]ipp$ enter_bug.cgi?product=Internet+Public+Policy&format=ipp
74-
RewriteRule ^form[\.:]creative$ enter_bug.cgi?product=Marketing&format=creative
75-
RewriteRule ^form[\.:]user[\.\-:]engagement$ enter_bug.cgi?product=Marketing&format=user-engagement
76-
RewriteRule ^form[\.:]dev[\.\-:]engagement[\.\-\:]event$ enter_bug.cgi?product=Developer+Engagement&format=dev-engagement-event
77-
RewriteRule ^form[\.:]mobile[\.\-:]compat$ enter_bug.cgi?product=Tech+Evangelism&format=mobile-compat
78-
RewriteRule ^form[\.:]web[\.:]bounty$ enter_bug.cgi?product=mozilla.org&format=web-bounty
79-
RewriteRule ^form[\.:]automative$ enter_bug.cgi?product=Testing&format=automative
80-
RewriteRule ^form[\.:]fxos[\.\-:]preload[\.\-:]app$ enter_bug.cgi?product=Marketplace&format=fxos-preload-app
81-
RewriteRule ^form[\.:]fxos[\.\-:]mcts[\.\-:]waiver$ enter_bug.cgi?product=Firefox+OS&format=fxos-mcts-waiver
82-
RewriteRule ^form[\.:]comm[\.:]newsletter$ enter_bug.cgi?product=Marketing&format=comm-newsletter
83-
RewriteRule ^form[\.:]screen[\.:]share[\.:]whitelist$ enter_bug.cgi?product=Firefox&format=screen-share-whitelist
84-
RewriteRule ^form[\.:]webops[\.\-:]request$ enter_bug.cgi?product=Infrastructure+\%26+Operations&format=webops-request
85-
RewriteRule ^form[\.:]data[\.\-:]compliance$ enter_bug.cgi?product=Data+Compliance&format=data-compliance
86-
RewriteRule ^form[\.:]third[\.\-:]party$ enter_bug.cgi?product=Marketing&format=third-party-apps
73+
RewriteRule ^form[\.:]ipp$ enter_bug.cgi?product=Internet+Public+Policy&format=ipp [QSA]
74+
RewriteRule ^form[\.:]creative$ enter_bug.cgi?product=Marketing&format=creative [QSA]
75+
RewriteRule ^form[\.:]user[\.\-:]engagement$ enter_bug.cgi?product=Marketing&format=user-engagement [QSA]
76+
RewriteRule ^form[\.:]dev[\.\-:]engagement[\.\-\:]event$ enter_bug.cgi?product=Developer+Engagement&format=dev-engagement-event [QSA]
77+
RewriteRule ^form[\.:]mobile[\.\-:]compat$ enter_bug.cgi?product=Tech+Evangelism&format=mobile-compat [QSA]
78+
RewriteRule ^form[\.:]web[\.:]bounty$ enter_bug.cgi?product=mozilla.org&format=web-bounty [QSA]
79+
RewriteRule ^form[\.:]automative$ enter_bug.cgi?product=Testing&format=automative [QSA]
80+
RewriteRule ^form[\.:]fxos[\.\-:]preload[\.\-:]app$ enter_bug.cgi?product=Marketplace&format=fxos-preload-app [QSA]
81+
RewriteRule ^form[\.:]fxos[\.\-:]mcts[\.\-:]waiver$ enter_bug.cgi?product=Firefox+OS&format=fxos-mcts-waiver [QSA]
82+
RewriteRule ^form[\.:]comm[\.:]newsletter$ enter_bug.cgi?product=Marketing&format=comm-newsletter [QSA]
83+
RewriteRule ^form[\.:]screen[\.:]share[\.:]whitelist$ enter_bug.cgi?product=Firefox&format=screen-share-whitelist [QSA]
84+
RewriteRule ^form[\.:]webops[\.\-:]request$ enter_bug.cgi?product=Infrastructure+\%26+Operations&format=webops-request [QSA]
85+
RewriteRule ^form[\.:]data[\.\-:]compliance$ enter_bug.cgi?product=Data+Compliance&format=data-compliance [QSA]
86+
RewriteRule ^form[\.:]third[\.\-:]party$ enter_bug.cgi?product=Marketing&format=third-party-apps [QSA]
8787
RewriteRule ^rest/(.*)$ rest.cgi/$1 [NE]
8888
RewriteRule ^(?:latest|1\.2|1\.3)/(.*)$ extensions/BzAPI/bin/rest.cgi/$1 [NE]
8989
RewriteRule ^bzapi/(.*)$ extensions/BzAPI/bin/rest.cgi/$1 [NE]

extensions/GitHubAuth/Config.pm

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# This Source Code Form is subject to the terms of the Mozilla Public
2+
# License, v. 2.0. If a copy of the MPL was not distributed with this
3+
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
4+
#
5+
# This Source Code Form is "Incompatible With Secondary Licenses", as
6+
# defined by the Mozilla Public License, v. 2.0.
7+
8+
package Bugzilla::Extension::GitHubAuth;
9+
10+
use 5.10.1;
11+
use strict;
12+
13+
use constant NAME => 'GitHubAuth';
14+
15+
use constant REQUIRED_MODULES => [];
16+
17+
use constant OPTIONAL_MODULES => [];
18+
19+
__PACKAGE__->NAME;

extensions/GitHubAuth/Extension.pm

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
# This Source Code Form is subject to the terms of the Mozilla Public
2+
# License, v. 2.0. If a copy of the MPL was not distributed with this
3+
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
4+
#
5+
# This Source Code Form is "Incompatible With Secondary Licenses", as
6+
# defined by the Mozilla Public License, v. 2.0.
7+
8+
package Bugzilla::Extension::GitHubAuth;
9+
10+
use 5.10.1;
11+
use strict;
12+
use parent qw(Bugzilla::Extension);
13+
14+
use Bugzilla::Extension::GitHubAuth::Client;
15+
use Bugzilla::Extension::GitHubAuth::Util qw(target_uri);
16+
17+
use Bugzilla::Error;
18+
use Bugzilla::Util qw(trick_taint);
19+
use List::Util qw(first);
20+
use URI;
21+
use URI::QueryParam;
22+
23+
our $VERSION = '0.01';
24+
25+
BEGIN {
26+
# Monkey-patch can() on Bugzilla::Auth::Login::CGI so that our own fail_nodata gets called.
27+
# Our fail_nodata behaves like CGI's, so this shouldn't be a problem for CGI-based logins.
28+
29+
*Bugzilla::Auth::Login::CGI::can = sub {
30+
my ($stack, $method) = @_;
31+
32+
return undef if $method eq 'fail_nodata';
33+
return $stack->UNIVERSAL::can($method);
34+
};
35+
}
36+
37+
sub install_before_final_checks {
38+
Bugzilla::Group->create({
39+
name => 'no-github-auth',
40+
description => 'Group containing groups whose members may not use GitHubAuth to log in',
41+
isbuggroup => 0,
42+
}) unless Bugzilla::Group->new({ name => 'no-github-auth' });
43+
}
44+
45+
sub template_before_create {
46+
my ($self, $args) = @_;
47+
48+
return if Bugzilla->user->id && !Bugzilla->cgi->param('logout');
49+
50+
$args->{config}{VARIABLES}{github_auth} = {
51+
login => sub {
52+
return Bugzilla::Extension::GitHubAuth::Client->login_uri(target_uri());
53+
},
54+
};
55+
}
56+
57+
sub auth_login_methods {
58+
my ($self, $args) = @_;
59+
my $modules = $args->{'modules'};
60+
if (exists $modules->{'GitHubAuth'}) {
61+
$modules->{'GitHubAuth'} = 'Bugzilla/Extension/GitHubAuth/Login.pm';
62+
}
63+
}
64+
65+
sub auth_verify_methods {
66+
my ($self, $args) = @_;
67+
my $modules = $args->{'modules'};
68+
if (exists $modules->{'GitHubAuth'}) {
69+
$modules->{'GitHubAuth'} = 'Bugzilla/Extension/GitHubAuth/Verify.pm';
70+
}
71+
}
72+
73+
sub config_modify_panels {
74+
my ($self, $args) = @_;
75+
my $auth_panel_params = $args->{panels}{auth}{params};
76+
77+
my $user_info_class = first { $_->{name} eq 'user_info_class' } @$auth_panel_params;
78+
if ($user_info_class) {
79+
push @{ $user_info_class->{choices} }, "GitHubAuth,CGI", "Persona,GitHubAuth,CGI";
80+
}
81+
82+
my $user_verify_class = first { $_->{name} eq 'user_verify_class' } @$auth_panel_params;
83+
if ($user_verify_class) {
84+
unshift @{ $user_verify_class->{choices} }, "GitHubAuth";
85+
}
86+
}
87+
88+
sub config_add_panels {
89+
my ($self, $args) = @_;
90+
my $modules = $args->{panel_modules};
91+
$modules->{GitHubAuth} = "Bugzilla::Extension::GitHubAuth::Config";
92+
}
93+
94+
__PACKAGE__->NAME;
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
# This Source Code Form is subject to the terms of the Mozilla Public
2+
# License, v. 2.0. If a copy of the MPL was not distributed with this
3+
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
4+
#
5+
# This Source Code Form is "Incompatible With Secondary Licenses", as
6+
# defined by the Mozilla Public License, v. 2.0.
7+
8+
package Bugzilla::Extension::GitHubAuth::Client;
9+
use strict;
10+
use warnings;
11+
12+
use JSON qw(decode_json);
13+
use URI;
14+
use URI::QueryParam;
15+
use Digest;
16+
17+
use Bugzilla::Extension::GitHubAuth::Client::Error qw(ThrowUserError ThrowCodeError);
18+
use Bugzilla::Util qw(remote_ip);
19+
20+
use constant DIGEST_HASH => 'SHA1';
21+
22+
use fields qw(user_agent);
23+
24+
use constant {
25+
GH_ACCESS_TOKEN_URI => 'https://github.com/login/oauth/access_token',
26+
GH_AUTHORIZE_URI => 'https://github.com/login/oauth/authorize',
27+
GH_USER_EMAILS_URI => 'https://api.github.com/user/emails',
28+
};
29+
30+
sub new {
31+
my ($class, %init) = @_;
32+
my $self = $class->fields::new();
33+
34+
return $self;
35+
}
36+
37+
sub login_uri {
38+
my ($self, $target) = @_;
39+
40+
$target->query_param(GoAheadAndLogIn => 1);
41+
$target->query_param(github_login => 1);
42+
$target->query_param_delete('logout');
43+
44+
my $uri = URI->new(GH_AUTHORIZE_URI);
45+
46+
$uri->query_form(
47+
client_id => Bugzilla->params->{github_client_id},
48+
scope => 'user:email',
49+
state => $self->get_state($target),
50+
redirect_uri => $target,
51+
);
52+
53+
return $uri;
54+
}
55+
56+
sub get_email_key {
57+
my ($class, $email) = @_;
58+
59+
my $digest = Digest->new(DIGEST_HASH);
60+
$digest->add($email);
61+
$digest->add(remote_ip());
62+
$digest->add(Bugzilla->localconfig->{site_wide_secret});
63+
return $digest->hexdigest;
64+
}
65+
66+
sub get_state {
67+
my ($class, $target) = @_;
68+
my $sorted_target = $target->clone;
69+
$sorted_target->query_form({});
70+
71+
foreach my $key (sort $target->query_param) {
72+
$sorted_target->query_param($key, $target->query_param($key));
73+
}
74+
75+
$sorted_target->query_param_delete("code");
76+
$sorted_target->query_param_delete("state");
77+
$sorted_target->query_param_delete('github_email_key');
78+
$sorted_target->query_param_delete('github_email');
79+
$sorted_target->query_param_delete('GoAheadAndLogIn');
80+
$sorted_target->query_param_delete('github_login');
81+
82+
my $digest = Digest->new(DIGEST_HASH);
83+
$digest->add($sorted_target->as_string);
84+
$digest->add(remote_ip());
85+
$digest->add(Bugzilla->localconfig->{site_wide_secret});
86+
return $digest->hexdigest;
87+
}
88+
89+
sub _handle_response {
90+
my ($self, $response) = @_;
91+
my $data = eval {
92+
decode_json($response->content);
93+
};
94+
if ($@) {
95+
ThrowCodeError("github_bad_response", { message => "Unable to parse json response" });
96+
}
97+
98+
unless ($response->is_success) {
99+
ThrowCodeError("github_error", { response => $response });
100+
}
101+
return $data;
102+
}
103+
104+
sub get_access_token {
105+
my ($self, $code) = @_;
106+
107+
my $response = $self->user_agent->post(
108+
GH_ACCESS_TOKEN_URI,
109+
{ client_id => Bugzilla->params->{github_client_id},
110+
client_secret => Bugzilla->params->{github_client_secret},
111+
code => $code },
112+
Accept => 'application/json',
113+
);
114+
my $data = $self->_handle_response($response);
115+
return $data->{access_token} if exists $data->{access_token};
116+
}
117+
118+
sub get_user_emails {
119+
my ($self, $access_token) = @_;
120+
my $uri = URI->new(GH_USER_EMAILS_URI);
121+
$uri->query_form(access_token => $access_token);
122+
123+
my $response = $self->user_agent->get($uri, Accept => 'application/json');
124+
125+
return $self->_handle_response($response);
126+
}
127+
128+
sub user_agent {
129+
my ($self) = @_;
130+
$self->{user_agent} //= $self->_build_user_agent;
131+
132+
return $self->{user_agent};
133+
}
134+
135+
sub _build_user_agent {
136+
my ($self) = @_;
137+
my $ua = LWP::UserAgent->new( timeout => 10 );
138+
139+
if (Bugzilla->params->{proxy_url}) {
140+
$ua->proxy('https', Bugzilla->params->{proxy_url});
141+
}
142+
143+
return $ua;
144+
}
145+
146+
1;
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# This Source Code Form is subject to the terms of the Mozilla Public
2+
# License, v. 2.0. If a copy of the MPL was not distributed with this
3+
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
4+
#
5+
# This Source Code Form is "Incompatible With Secondary Licenses", as
6+
# defined by the Mozilla Public License, v. 2.0.
7+
8+
package Bugzilla::Extension::GitHubAuth::Client::Error;
9+
10+
use strict;
11+
use warnings;
12+
13+
use Bugzilla::Error ();
14+
15+
use base qw(Exporter);
16+
use fields qw(type error vars);
17+
18+
our @EXPORT = qw(ThrowUserError ThrowCodeError);
19+
our $USE_EXCEPTION_OBJECTS = 0;
20+
21+
sub _new {
22+
my ($class, $type, $error, $vars) = @_;
23+
my $self = $class->fields::new();
24+
$self->{type} = $type;
25+
$self->{error} = $error;
26+
$self->{vars} = $vars // {};
27+
28+
return $self;
29+
}
30+
31+
sub type { $_[0]->{type} }
32+
sub error { $_[0]->{error} }
33+
sub vars { $_[0]->{vars} }
34+
35+
sub ThrowUserError {
36+
if ($USE_EXCEPTION_OBJECTS) {
37+
die __PACKAGE__->_new('user', @_);
38+
}
39+
else {
40+
Bugzilla::Error::ThrowUserError(@_);
41+
}
42+
}
43+
44+
sub ThrowCodeError {
45+
if ($USE_EXCEPTION_OBJECTS) {
46+
die __PACKAGE__->_new('code', @_);
47+
}
48+
else {
49+
Bugzilla::Error::ThrowCodeError(@_);
50+
}
51+
}
52+
53+
1;

0 commit comments

Comments
 (0)