summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDylan William Hardison <dylan@hardison.net>2015-03-04 04:27:15 +0100
committerDylan William Hardison <dylan@hardison.net>2015-04-03 00:21:08 +0200
commitc0d00a57eebb31d3e2cdef0ebb9219ebe2fe2bab (patch)
treee6c6eb07c7348e086f1ed6de54de829e806dda05
parent1317be9bb70dbb945fa82d0d1bd8548c83c86ebe (diff)
downloadbugzilla-c0d00a57eebb31d3e2cdef0ebb9219ebe2fe2bab.tar.gz
bugzilla-c0d00a57eebb31d3e2cdef0ebb9219ebe2fe2bab.tar.xz
Github Auth Extension
-rw-r--r--.htaccess64
-rw-r--r--extensions/GitHubAuth/Config.pm19
-rw-r--r--extensions/GitHubAuth/Extension.pm94
-rw-r--r--extensions/GitHubAuth/lib/Client.pm146
-rw-r--r--extensions/GitHubAuth/lib/Client/Error.pm53
-rw-r--r--extensions/GitHubAuth/lib/Config.pm36
-rw-r--r--extensions/GitHubAuth/lib/Login.pm173
-rw-r--r--extensions/GitHubAuth/lib/Util.pm35
-rw-r--r--extensions/GitHubAuth/lib/Verify.pm23
-rw-r--r--extensions/GitHubAuth/template/en/default/account/auth/github-verify-account.html.tmpl48
-rw-r--r--extensions/GitHubAuth/template/en/default/admin/params/githubauth.html.tmpl19
-rw-r--r--extensions/GitHubAuth/template/en/default/hook/account/auth/login-additional_methods.html.tmpl18
-rw-r--r--extensions/GitHubAuth/template/en/default/hook/account/auth/login-small-additional_methods.html.tmpl26
-rw-r--r--extensions/GitHubAuth/template/en/default/hook/global/code-error-errors.html.tmpl25
-rw-r--r--extensions/GitHubAuth/template/en/default/hook/global/user-error-errors.html.tmpl24
-rw-r--r--extensions/GitHubAuth/web/images/github_sign_in.pngbin0 -> 4780 bytes
-rw-r--r--extensions/GitHubAuth/web/images/sign_in.pngbin0 -> 2577 bytes
17 files changed, 771 insertions, 32 deletions
diff --git a/.htaccess b/.htaccess
index 5de1c259f..7358461c3 100644
--- a/.htaccess
+++ b/.htaccess
@@ -50,40 +50,40 @@ RewriteRule ^review(.*) page.cgi?id=splinter.html$1 [QSA]
RewriteRule ^user_?profile(.*) page.cgi?id=user_profile.html$1 [QSA]
RewriteRule ^request_defer(.*) page.cgi?id=request_defer.html$1 [QSA]
RewriteRule ^favicon\.ico$ extensions/BMO/web/images/favicon.ico
-RewriteRule ^form[\.:]itrequest$ enter_bug.cgi?product=Infrastructure+\%26+Operations&format=itrequest
-RewriteRule ^form[\.:](mozlist|poweredby|presentation|trademark|recoverykey)$ enter_bug.cgi?product=mozilla.org&format=$1
-RewriteRule ^form[\.:]legal$ enter_bug.cgi?product=Legal&format=legal
-RewriteRule ^form[\.:]recruiting$ enter_bug.cgi?product=Recruiting&format=recruiting
-RewriteRule ^form[\.:]mozpr$ enter_bug.cgi?product=Mozilla+PR&format=mozpr
-RewriteRule ^form[\.:]reps[\.:]mentorship$ enter_bug.cgi?product=Mozilla+Reps&format=mozreps
-RewriteRule ^form[\.:]reps[\.:]budget$ enter_bug.cgi?product=Mozilla+Reps&format=remo-budget
-RewriteRule ^form[\.:]reps[\.:]swag$ enter_bug.cgi?product=Mozilla+Reps&format=remo-swag
-RewriteRule ^form[\.:]reps[\.:]it$ enter_bug.cgi?product=Mozilla+Reps&format=remo-it
-RewriteRule ^form[\.:]reps[\.:]payment$ page.cgi?id=remo-form-payment.html
-RewriteRule ^form[\.:]csa[\.:]discourse$ enter_bug.cgi?product=Infrastructure+\%26\+Operations&format=csa-discourse
-RewriteRule ^form[\.:]employee[\.\-:]incident$ enter_bug.cgi?product=mozilla.org&format=employee-incident
-RewriteRule ^form[\.:]brownbag$ https://air.mozilla.org/requests
-RewriteRule ^form[\.:]finance$ enter_bug.cgi?product=Finance&format=finance
-RewriteRule ^form[\.:]moz[\.\-:]project[\.\-:]review$ enter_bug.cgi?product=mozilla.org&format=moz-project-review
+RewriteRule ^form[\.:]itrequest$ enter_bug.cgi?product=Infrastructure+\%26+Operations&format=itrequest [QSA]
+RewriteRule ^form[\.:](mozlist|poweredby|presentation|trademark|recoverykey)$ enter_bug.cgi?product=mozilla.org&format=$1 [QSA]
+RewriteRule ^form[\.:]legal$ enter_bug.cgi?product=Legal&format=legal [QSA]
+RewriteRule ^form[\.:]recruiting$ enter_bug.cgi?product=Recruiting&format=recruiting [QSA]
+RewriteRule ^form[\.:]mozpr$ enter_bug.cgi?product=Mozilla+PR&format=mozpr [QSA]
+RewriteRule ^form[\.:]reps[\.:]mentorship$ enter_bug.cgi?product=Mozilla+Reps&format=mozreps [QSA]
+RewriteRule ^form[\.:]reps[\.:]budget$ enter_bug.cgi?product=Mozilla+Reps&format=remo-budget [QSA]
+RewriteRule ^form[\.:]reps[\.:]swag$ enter_bug.cgi?product=Mozilla+Reps&format=remo-swag [QSA]
+RewriteRule ^form[\.:]reps[\.:]it$ enter_bug.cgi?product=Mozilla+Reps&format=remo-it [QSA]
+RewriteRule ^form[\.:]reps[\.:]payment$ page.cgi?id=remo-form-payment.html [QSA]
+RewriteRule ^form[\.:]csa[\.:]discourse$ enter_bug.cgi?product=Infrastructure+\%26\+Operations&format=csa-discourse [QSA]
+RewriteRule ^form[\.:]employee[\.\-:]incident$ enter_bug.cgi?product=mozilla.org&format=employee-incident [QSA]
+RewriteRule ^form[\.:]brownbag$ https://air.mozilla.org/requests [QSA]
+RewriteRule ^form[\.:]finance$ enter_bug.cgi?product=Finance&format=finance [QSA]
+RewriteRule ^form[\.:]moz[\.\-:]project[\.\-:]review$ enter_bug.cgi?product=mozilla.org&format=moz-project-review [QSA]
RewriteRule ^form[\.:]docs?$ enter_bug.cgi?product=Developer+Documentation&format=doc [QSA]
-RewriteRule ^form[\.:]mdn?$ enter_bug.cgi?product=Mozilla+Developer+Network&format=mdn
-RewriteRule ^form[\.:](swag|gear)$ enter_bug.cgi?product=Marketing&format=swag
-RewriteRule ^form[\.:]costume$ enter_bug.cgi?product=Marketing&format=costume
+RewriteRule ^form[\.:]mdn?$ enter_bug.cgi?product=Mozilla+Developer+Network&format=mdn [QSA]
+RewriteRule ^form[\.:](swag|gear)$ enter_bug.cgi?product=Marketing&format=swag [QSA]
+RewriteRule ^form[\.:]costume$ enter_bug.cgi?product=Marketing&format=costume [QSA]
RewriteRule ^form[\.:](b2g|fxos)[\.\-:](partner|betaprogram|feature) enter_bug.cgi?product=Firefox+OS&format=fxos-$2 [QSA]
-RewriteRule ^form[\.:]ipp$ enter_bug.cgi?product=Internet+Public+Policy&format=ipp
-RewriteRule ^form[\.:]creative$ enter_bug.cgi?product=Marketing&format=creative
-RewriteRule ^form[\.:]user[\.\-:]engagement$ enter_bug.cgi?product=Marketing&format=user-engagement
-RewriteRule ^form[\.:]dev[\.\-:]engagement[\.\-\:]event$ enter_bug.cgi?product=Developer+Engagement&format=dev-engagement-event
-RewriteRule ^form[\.:]mobile[\.\-:]compat$ enter_bug.cgi?product=Tech+Evangelism&format=mobile-compat
-RewriteRule ^form[\.:]web[\.:]bounty$ enter_bug.cgi?product=mozilla.org&format=web-bounty
-RewriteRule ^form[\.:]automative$ enter_bug.cgi?product=Testing&format=automative
-RewriteRule ^form[\.:]fxos[\.\-:]preload[\.\-:]app$ enter_bug.cgi?product=Marketplace&format=fxos-preload-app
-RewriteRule ^form[\.:]fxos[\.\-:]mcts[\.\-:]waiver$ enter_bug.cgi?product=Firefox+OS&format=fxos-mcts-waiver
-RewriteRule ^form[\.:]comm[\.:]newsletter$ enter_bug.cgi?product=Marketing&format=comm-newsletter
-RewriteRule ^form[\.:]screen[\.:]share[\.:]whitelist$ enter_bug.cgi?product=Firefox&format=screen-share-whitelist
-RewriteRule ^form[\.:]webops[\.\-:]request$ enter_bug.cgi?product=Infrastructure+\%26+Operations&format=webops-request
-RewriteRule ^form[\.:]data[\.\-:]compliance$ enter_bug.cgi?product=Data+Compliance&format=data-compliance
-RewriteRule ^form[\.:]third[\.\-:]party$ enter_bug.cgi?product=Marketing&format=third-party-apps
+RewriteRule ^form[\.:]ipp$ enter_bug.cgi?product=Internet+Public+Policy&format=ipp [QSA]
+RewriteRule ^form[\.:]creative$ enter_bug.cgi?product=Marketing&format=creative [QSA]
+RewriteRule ^form[\.:]user[\.\-:]engagement$ enter_bug.cgi?product=Marketing&format=user-engagement [QSA]
+RewriteRule ^form[\.:]dev[\.\-:]engagement[\.\-\:]event$ enter_bug.cgi?product=Developer+Engagement&format=dev-engagement-event [QSA]
+RewriteRule ^form[\.:]mobile[\.\-:]compat$ enter_bug.cgi?product=Tech+Evangelism&format=mobile-compat [QSA]
+RewriteRule ^form[\.:]web[\.:]bounty$ enter_bug.cgi?product=mozilla.org&format=web-bounty [QSA]
+RewriteRule ^form[\.:]automative$ enter_bug.cgi?product=Testing&format=automative [QSA]
+RewriteRule ^form[\.:]fxos[\.\-:]preload[\.\-:]app$ enter_bug.cgi?product=Marketplace&format=fxos-preload-app [QSA]
+RewriteRule ^form[\.:]fxos[\.\-:]mcts[\.\-:]waiver$ enter_bug.cgi?product=Firefox+OS&format=fxos-mcts-waiver [QSA]
+RewriteRule ^form[\.:]comm[\.:]newsletter$ enter_bug.cgi?product=Marketing&format=comm-newsletter [QSA]
+RewriteRule ^form[\.:]screen[\.:]share[\.:]whitelist$ enter_bug.cgi?product=Firefox&format=screen-share-whitelist [QSA]
+RewriteRule ^form[\.:]webops[\.\-:]request$ enter_bug.cgi?product=Infrastructure+\%26+Operations&format=webops-request [QSA]
+RewriteRule ^form[\.:]data[\.\-:]compliance$ enter_bug.cgi?product=Data+Compliance&format=data-compliance [QSA]
+RewriteRule ^form[\.:]third[\.\-:]party$ enter_bug.cgi?product=Marketing&format=third-party-apps [QSA]
RewriteRule ^rest/(.*)$ rest.cgi/$1 [NE]
RewriteRule ^(?:latest|1\.2|1\.3)/(.*)$ extensions/BzAPI/bin/rest.cgi/$1 [NE]
RewriteRule ^bzapi/(.*)$ extensions/BzAPI/bin/rest.cgi/$1 [NE]
diff --git a/extensions/GitHubAuth/Config.pm b/extensions/GitHubAuth/Config.pm
new file mode 100644
index 000000000..88186a91e
--- /dev/null
+++ b/extensions/GitHubAuth/Config.pm
@@ -0,0 +1,19 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::GitHubAuth;
+
+use 5.10.1;
+use strict;
+
+use constant NAME => 'GitHubAuth';
+
+use constant REQUIRED_MODULES => [];
+
+use constant OPTIONAL_MODULES => [];
+
+__PACKAGE__->NAME;
diff --git a/extensions/GitHubAuth/Extension.pm b/extensions/GitHubAuth/Extension.pm
new file mode 100644
index 000000000..dee927165
--- /dev/null
+++ b/extensions/GitHubAuth/Extension.pm
@@ -0,0 +1,94 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::GitHubAuth;
+
+use 5.10.1;
+use strict;
+use parent qw(Bugzilla::Extension);
+
+use Bugzilla::Extension::GitHubAuth::Client;
+use Bugzilla::Extension::GitHubAuth::Util qw(target_uri);
+
+use Bugzilla::Error;
+use Bugzilla::Util qw(trick_taint);
+use List::Util qw(first);
+use URI;
+use URI::QueryParam;
+
+our $VERSION = '0.01';
+
+BEGIN {
+ # Monkey-patch can() on Bugzilla::Auth::Login::CGI so that our own fail_nodata gets called.
+ # Our fail_nodata behaves like CGI's, so this shouldn't be a problem for CGI-based logins.
+
+ *Bugzilla::Auth::Login::CGI::can = sub {
+ my ($stack, $method) = @_;
+
+ return undef if $method eq 'fail_nodata';
+ return $stack->UNIVERSAL::can($method);
+ };
+}
+
+sub install_before_final_checks {
+ Bugzilla::Group->create({
+ name => 'no-github-auth',
+ description => 'Group containing groups whose members may not use GitHubAuth to log in',
+ isbuggroup => 0,
+ }) unless Bugzilla::Group->new({ name => 'no-github-auth' });
+}
+
+sub template_before_create {
+ my ($self, $args) = @_;
+
+ return if Bugzilla->user->id && !Bugzilla->cgi->param('logout');
+
+ $args->{config}{VARIABLES}{github_auth} = {
+ login => sub {
+ return Bugzilla::Extension::GitHubAuth::Client->login_uri(target_uri());
+ },
+ };
+}
+
+sub auth_login_methods {
+ my ($self, $args) = @_;
+ my $modules = $args->{'modules'};
+ if (exists $modules->{'GitHubAuth'}) {
+ $modules->{'GitHubAuth'} = 'Bugzilla/Extension/GitHubAuth/Login.pm';
+ }
+}
+
+sub auth_verify_methods {
+ my ($self, $args) = @_;
+ my $modules = $args->{'modules'};
+ if (exists $modules->{'GitHubAuth'}) {
+ $modules->{'GitHubAuth'} = 'Bugzilla/Extension/GitHubAuth/Verify.pm';
+ }
+}
+
+sub config_modify_panels {
+ my ($self, $args) = @_;
+ my $auth_panel_params = $args->{panels}{auth}{params};
+
+ my $user_info_class = first { $_->{name} eq 'user_info_class' } @$auth_panel_params;
+ if ($user_info_class) {
+ push @{ $user_info_class->{choices} }, "GitHubAuth,CGI", "Persona,GitHubAuth,CGI";
+ }
+
+ my $user_verify_class = first { $_->{name} eq 'user_verify_class' } @$auth_panel_params;
+ if ($user_verify_class) {
+ unshift @{ $user_verify_class->{choices} }, "GitHubAuth";
+ }
+}
+
+sub config_add_panels {
+ my ($self, $args) = @_;
+ my $modules = $args->{panel_modules};
+ $modules->{GitHubAuth} = "Bugzilla::Extension::GitHubAuth::Config";
+}
+
+__PACKAGE__->NAME;
diff --git a/extensions/GitHubAuth/lib/Client.pm b/extensions/GitHubAuth/lib/Client.pm
new file mode 100644
index 000000000..896e82eff
--- /dev/null
+++ b/extensions/GitHubAuth/lib/Client.pm
@@ -0,0 +1,146 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::GitHubAuth::Client;
+use strict;
+use warnings;
+
+use JSON qw(decode_json);
+use URI;
+use URI::QueryParam;
+use Digest;
+
+use Bugzilla::Extension::GitHubAuth::Client::Error qw(ThrowUserError ThrowCodeError);
+use Bugzilla::Util qw(remote_ip);
+
+use constant DIGEST_HASH => 'SHA1';
+
+use fields qw(user_agent);
+
+use constant {
+ GH_ACCESS_TOKEN_URI => 'https://github.com/login/oauth/access_token',
+ GH_AUTHORIZE_URI => 'https://github.com/login/oauth/authorize',
+ GH_USER_EMAILS_URI => 'https://api.github.com/user/emails',
+};
+
+sub new {
+ my ($class, %init) = @_;
+ my $self = $class->fields::new();
+
+ return $self;
+}
+
+sub login_uri {
+ my ($self, $target) = @_;
+
+ $target->query_param(GoAheadAndLogIn => 1);
+ $target->query_param(github_login => 1);
+ $target->query_param_delete('logout');
+
+ my $uri = URI->new(GH_AUTHORIZE_URI);
+
+ $uri->query_form(
+ client_id => Bugzilla->params->{github_client_id},
+ scope => 'user:email',
+ state => $self->get_state($target),
+ redirect_uri => $target,
+ );
+
+ return $uri;
+}
+
+sub get_email_key {
+ my ($class, $email) = @_;
+
+ my $digest = Digest->new(DIGEST_HASH);
+ $digest->add($email);
+ $digest->add(remote_ip());
+ $digest->add(Bugzilla->localconfig->{site_wide_secret});
+ return $digest->hexdigest;
+}
+
+sub get_state {
+ my ($class, $target) = @_;
+ my $sorted_target = $target->clone;
+ $sorted_target->query_form({});
+
+ foreach my $key (sort $target->query_param) {
+ $sorted_target->query_param($key, $target->query_param($key));
+ }
+
+ $sorted_target->query_param_delete("code");
+ $sorted_target->query_param_delete("state");
+ $sorted_target->query_param_delete('github_email_key');
+ $sorted_target->query_param_delete('github_email');
+ $sorted_target->query_param_delete('GoAheadAndLogIn');
+ $sorted_target->query_param_delete('github_login');
+
+ my $digest = Digest->new(DIGEST_HASH);
+ $digest->add($sorted_target->as_string);
+ $digest->add(remote_ip());
+ $digest->add(Bugzilla->localconfig->{site_wide_secret});
+ return $digest->hexdigest;
+}
+
+sub _handle_response {
+ my ($self, $response) = @_;
+ my $data = eval {
+ decode_json($response->content);
+ };
+ if ($@) {
+ ThrowCodeError("github_bad_response", { message => "Unable to parse json response" });
+ }
+
+ unless ($response->is_success) {
+ ThrowCodeError("github_error", { response => $response });
+ }
+ return $data;
+}
+
+sub get_access_token {
+ my ($self, $code) = @_;
+
+ my $response = $self->user_agent->post(
+ GH_ACCESS_TOKEN_URI,
+ { client_id => Bugzilla->params->{github_client_id},
+ client_secret => Bugzilla->params->{github_client_secret},
+ code => $code },
+ Accept => 'application/json',
+ );
+ my $data = $self->_handle_response($response);
+ return $data->{access_token} if exists $data->{access_token};
+}
+
+sub get_user_emails {
+ my ($self, $access_token) = @_;
+ my $uri = URI->new(GH_USER_EMAILS_URI);
+ $uri->query_form(access_token => $access_token);
+
+ my $response = $self->user_agent->get($uri, Accept => 'application/json');
+
+ return $self->_handle_response($response);
+}
+
+sub user_agent {
+ my ($self) = @_;
+ $self->{user_agent} //= $self->_build_user_agent;
+
+ return $self->{user_agent};
+}
+
+sub _build_user_agent {
+ my ($self) = @_;
+ my $ua = LWP::UserAgent->new( timeout => 10 );
+
+ if (Bugzilla->params->{proxy_url}) {
+ $ua->proxy('https', Bugzilla->params->{proxy_url});
+ }
+
+ return $ua;
+}
+
+1;
diff --git a/extensions/GitHubAuth/lib/Client/Error.pm b/extensions/GitHubAuth/lib/Client/Error.pm
new file mode 100644
index 000000000..358df6938
--- /dev/null
+++ b/extensions/GitHubAuth/lib/Client/Error.pm
@@ -0,0 +1,53 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::GitHubAuth::Client::Error;
+
+use strict;
+use warnings;
+
+use Bugzilla::Error ();
+
+use base qw(Exporter);
+use fields qw(type error vars);
+
+our @EXPORT = qw(ThrowUserError ThrowCodeError);
+our $USE_EXCEPTION_OBJECTS = 0;
+
+sub _new {
+ my ($class, $type, $error, $vars) = @_;
+ my $self = $class->fields::new();
+ $self->{type} = $type;
+ $self->{error} = $error;
+ $self->{vars} = $vars // {};
+
+ return $self;
+}
+
+sub type { $_[0]->{type} }
+sub error { $_[0]->{error} }
+sub vars { $_[0]->{vars} }
+
+sub ThrowUserError {
+ if ($USE_EXCEPTION_OBJECTS) {
+ die __PACKAGE__->_new('user', @_);
+ }
+ else {
+ Bugzilla::Error::ThrowUserError(@_);
+ }
+}
+
+sub ThrowCodeError {
+ if ($USE_EXCEPTION_OBJECTS) {
+ die __PACKAGE__->_new('code', @_);
+ }
+ else {
+ Bugzilla::Error::ThrowCodeError(@_);
+ }
+}
+
+1;
diff --git a/extensions/GitHubAuth/lib/Config.pm b/extensions/GitHubAuth/lib/Config.pm
new file mode 100644
index 000000000..0dc78531b
--- /dev/null
+++ b/extensions/GitHubAuth/lib/Config.pm
@@ -0,0 +1,36 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::GitHubAuth::Config;
+
+use strict;
+use warnings;
+
+use Bugzilla::Config::Common;
+
+our $sortkey = 1350;
+
+sub get_param_list {
+ my ($class) = @_;
+
+ my @params = (
+ {
+ name => 'github_client_id',
+ type => 't',
+ default => '',
+ },
+ {
+ name => 'github_client_secret',
+ type => 't',
+ default => '',
+ },
+ );
+
+ return @params;
+}
+
+1;
diff --git a/extensions/GitHubAuth/lib/Login.pm b/extensions/GitHubAuth/lib/Login.pm
new file mode 100644
index 000000000..c83456274
--- /dev/null
+++ b/extensions/GitHubAuth/lib/Login.pm
@@ -0,0 +1,173 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::GitHubAuth::Login;
+use strict;
+use warnings;
+use base qw(Bugzilla::Auth::Login);
+use fields qw(github_failure);
+
+use Scalar::Util qw(blessed);
+
+use Bugzilla::Constants qw(AUTH_NODATA AUTH_ERROR USAGE_MODE_BROWSER );
+use Bugzilla::Util qw(trick_taint correct_urlbase);
+use Bugzilla::Extension::GitHubAuth::Client;
+use Bugzilla::Extension::GitHubAuth::Client::Error ();
+use Bugzilla::Extension::GitHubAuth::Util qw(target_uri);
+use Bugzilla::Error;
+
+use constant { requires_verification => 1,
+ is_automatic => 1,
+ user_can_create_account => 1 };
+
+sub get_login_info {
+ my ($self) = @_;
+ my $cgi = Bugzilla->cgi;
+ my $github_login = $cgi->param('github_login');
+ my $github_email = $cgi->param('github_email');
+ my $github_email_key = $cgi->param('github_email_key');
+
+ return { failure => AUTH_NODATA } unless $github_login;
+
+ if ($github_email_key && $github_email) {
+ trick_taint($github_email);
+ trick_taint($github_email_key);
+ return $self->_get_login_info_from_email($github_email, $github_email_key);
+ }
+ else {
+ return $self->_get_login_info_from_github();
+ }
+}
+
+sub _get_login_info_from_github {
+ my ($self) = @_;
+ my $cgi = Bugzilla->cgi;
+ my $template = Bugzilla->template;
+ my $state = $cgi->param('state');
+ my $code = $cgi->param('code');
+
+ return { failure => AUTH_ERROR, error => 'github_missing_code' } unless $code;
+ return { failure => AUTH_ERROR, error => 'github_invalid_state' } unless $state;
+
+ trick_taint($code);
+ trick_taint($state);
+
+ my $target = target_uri();
+ my $client = Bugzilla::Extension::GitHubAuth::Client->new;
+ if ($state ne $client->get_state($target)) {
+ return { failure => AUTH_ERROR, error => 'github_invalid_state' };
+ }
+
+ my ($access_token, $emails);
+ eval {
+ # The following variable lets us catch and return (rather than throw) errors
+ # from our github client code, as required by the Auth API.
+ local $Bugzilla::Extension::GitHubAuth::Client::Error::USE_EXCEPTION_OBJECTS = 1;
+ $access_token = $client->get_access_token($code);
+ $emails = $client->get_user_emails($access_token);
+ };
+ my $e = $@;
+ if (blessed $e && $e->isa('Bugzilla::Extension::GitHubAuth::Client::Error')) {
+ my $key = $e->type eq 'user' ? 'user_error' : 'error';
+ return { failure => AUTH_ERROR, $key => $e->error, details => $e->vars };
+ }
+ elsif ($e) {
+ die $e;
+ }
+
+ my @emails = map { $_->{email} } grep { $_->{verified} } @$emails;
+
+ my $choose_email = sub {
+ my ($email) = @_;
+ my $uri = $target->clone;
+ my $key = Bugzilla::Extension::GitHubAuth::Client->get_email_key($email);
+ $uri->query_param(github_email => $email);
+ $uri->query_param(github_email_key => $key);
+ return $uri;
+ };
+
+ my @bugzilla_users;
+ my @github_emails;
+ foreach my $email (@emails) {
+ my $user = Bugzilla::User->new({name => $email, cache => 1});
+ if ($user) {
+ push @bugzilla_users, $user;
+ }
+ else {
+ push @github_emails, $email;
+ }
+ }
+ my @allowed_bugzilla_users = grep { not $_->in_group('no-github-auth') } @bugzilla_users;
+
+ if (@allowed_bugzilla_users == 1) {
+ my ($user) = @allowed_bugzilla_users;
+ return { username => $user->login, user_id => $user->id, github_auth => 1 };
+ }
+ elsif (@allowed_bugzilla_users > 1) {
+ $self->{github_failure} = {
+ template => 'account/auth/github-verify-account.html.tmpl',
+ vars => {
+ bugzilla_users => \@allowed_bugzilla_users,
+ choose_email => $choose_email,
+ },
+ };
+ return { failure => AUTH_NODATA };
+ }
+ elsif (@allowed_bugzilla_users == 0 && @bugzilla_users > 0 && @github_emails == 0) {
+ return { failure => AUTH_ERROR,
+ user_error => 'github_auth_account_too_powerful' };
+ }
+ elsif (@github_emails) {
+ $self->{github_failure} = {
+ template => 'account/auth/github-verify-account.html.tmpl',
+ vars => {
+ github_emails => \@github_emails,
+ choose_email => $choose_email,
+ },
+ };
+ return { failure => AUTH_NODATA };
+ }
+ else {
+ return { failure => AUTH_ERROR, user_error => 'github_no_emails' };
+ }
+}
+
+sub _get_login_info_from_email {
+ my ($self, $github_email, $github_email_key) = @_;
+ my $cgi = Bugzilla->cgi;
+
+ my $key = Bugzilla::Extension::GitHubAuth::Client->get_email_key($github_email);
+ unless ($github_email_key eq $key) {
+ return { failure => AUTH_ERROR,
+ user_error => 'github_invalid_email',
+ { email => $github_email }};
+ }
+
+ my $user = Bugzilla::User->new({name => $github_email, cache => 1});
+ return { failure => AUTH_ERROR,
+ user_error => 'github_auth_account_too_powerful' } if $user && $user->in_group('no-github-auth');
+
+ return { username => $github_email, github_auth => 1 };
+}
+
+sub fail_nodata {
+ my ($self) = @_;
+ my $cgi = Bugzilla->cgi;
+ my $template = Bugzilla->template;
+
+ ThrowUserError('login_required') if Bugzilla->usage_mode != USAGE_MODE_BROWSER;
+
+ my $file = $self->{github_failure}{template} // "account/auth/login.html.tmpl";
+ my $vars = $self->{github_failure}{vars} // { target => $cgi->url(-relative=>1) };
+
+ print $cgi->header();
+ $template->process($file, $vars) or ThrowTemplateError($template->error());
+ exit;
+}
+
+
+1;
diff --git a/extensions/GitHubAuth/lib/Util.pm b/extensions/GitHubAuth/lib/Util.pm
new file mode 100644
index 000000000..bda76a420
--- /dev/null
+++ b/extensions/GitHubAuth/lib/Util.pm
@@ -0,0 +1,35 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::GitHubAuth::Util;
+
+use strict;
+use warnings;
+
+use Bugzilla::Util qw(correct_urlbase);
+use URI;
+
+use base qw(Exporter);
+our @EXPORT = qw( target_uri );
+
+
+# this is like correct_urlbase() except it returns the *requested* uri, before http url rewrites have been applied.
+# needed to generate github's redirect_uri.
+sub target_uri {
+ my $cgi = Bugzilla->cgi;
+ my $base = URI->new(correct_urlbase());
+ if (my $request_uri = $cgi->request_uri) {
+ $base->path('');
+ $request_uri =~ s!^/+!!;
+ return URI->new($base . "/" . $request_uri);
+ }
+ else {
+ return URI->new(correct_urlbase() . $cgi->url(-relative => 1, query => ));
+ }
+}
+
+1;
diff --git a/extensions/GitHubAuth/lib/Verify.pm b/extensions/GitHubAuth/lib/Verify.pm
new file mode 100644
index 000000000..ee66c8904
--- /dev/null
+++ b/extensions/GitHubAuth/lib/Verify.pm
@@ -0,0 +1,23 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::GitHubAuth::Verify;
+use strict;
+use warnings;
+use base qw(Bugzilla::Auth::Verify);
+
+use Bugzilla::Constants qw( AUTH_NO_SUCH_USER );
+
+sub check_credentials {
+ my ($self, $login_data) = @_;
+
+ return { failure => AUTH_NO_SUCH_USER } unless $login_data->{github_auth};
+
+ return $login_data;
+}
+
+1;
diff --git a/extensions/GitHubAuth/template/en/default/account/auth/github-verify-account.html.tmpl b/extensions/GitHubAuth/template/en/default/account/auth/github-verify-account.html.tmpl
new file mode 100644
index 000000000..ba528b390
--- /dev/null
+++ b/extensions/GitHubAuth/template/en/default/account/auth/github-verify-account.html.tmpl
@@ -0,0 +1,48 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[%# Input: github_emails, bugzilla_emails.
+ # This template will not be called when bugzilla_emails.size == 1
+ # or when both github_emails.size == 0 && bugzilla_emails.size == 0
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+[% PROCESS global/header.html.tmpl
+ title = "Choose Account"
+%]
+
+[% IF bugzilla_users %]
+ <h1>Choose Account</h1>
+
+ <p>It seems that more than one [% terms.Bugzilla %] account connected to
+ your GitHub account. You may choose to login by clicking the link below.</p>
+
+ <ul class="bugzilla_emails">
+ [% FOREACH bz_user IN bugzilla_users %]
+ <li><a href="[% choose_email(bz_user.email) FILTER html %]">[% bz_user.email FILTER html %]</a></li>
+ [% END %]
+ </ul>
+[% ELSE %]
+ <h1>Account Not In [% terms.Bugzilla %]</h1>
+
+ [% IF github_emails.size == 1 %]
+ <p>The email '[% github_emails.0 FILTER html %]' was not found in [% terms.Bugzilla %] and will need to be created to log in.</p>
+
+ <a href="[% choose_email(github_emails.0) FILTER html %]">Create Account</a>
+ [% ELSE %]
+ <p>You have multiple email addresses associated with your GitHub account.
+ Which one should be used to create your [% terms.Bugzilla %] account?</p>
+
+ [% FOREACH email IN github_emails %]
+ <li><a href="[% choose_email(email) FILTER html %]">[% email FILTER html %]</a></li>
+ [% END %]
+ [% END %]
+[% END %]
+
+[% PROCESS global/footer.html.tmpl %]
diff --git a/extensions/GitHubAuth/template/en/default/admin/params/githubauth.html.tmpl b/extensions/GitHubAuth/template/en/default/admin/params/githubauth.html.tmpl
new file mode 100644
index 000000000..56f2475c3
--- /dev/null
+++ b/extensions/GitHubAuth/template/en/default/admin/params/githubauth.html.tmpl
@@ -0,0 +1,19 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[%
+ title = "GitHubAuth"
+ desc = "Configure GitHubAuth Authentication"
+%]
+
+[%
+ param_descs = {
+ github_client_id => "Your GitHub Client ID",
+ github_client_secret => "Your GitHub Client Secret"
+ }
+%]
diff --git a/extensions/GitHubAuth/template/en/default/hook/account/auth/login-additional_methods.html.tmpl b/extensions/GitHubAuth/template/en/default/hook/account/auth/login-additional_methods.html.tmpl
new file mode 100644
index 000000000..26eb8d63b
--- /dev/null
+++ b/extensions/GitHubAuth/template/en/default/hook/account/auth/login-additional_methods.html.tmpl
@@ -0,0 +1,18 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF Param('user_info_class').split(',').contains('GitHubAuth') %]
+ <p>
+ <a href="[% github_auth.login FILTER html %]">
+ <img src="extensions/GitHubAuth/web/images/github_sign_in.png"
+ alt="Sign in with GitHub"
+ title="Sign in with GitHub"
+ width="185" height="25">
+ </a>
+ </p>
+[% END %]
diff --git a/extensions/GitHubAuth/template/en/default/hook/account/auth/login-small-additional_methods.html.tmpl b/extensions/GitHubAuth/template/en/default/hook/account/auth/login-small-additional_methods.html.tmpl
new file mode 100644
index 000000000..6c4582b70
--- /dev/null
+++ b/extensions/GitHubAuth/template/en/default/hook/account/auth/login-small-additional_methods.html.tmpl
@@ -0,0 +1,26 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF Param('user_info_class').split(',').contains('GitHubAuth') %]
+ <script type="text/javascript">
+ YAHOO.util.Event.addListener('login_link[% qs_suffix FILTER js %]','click', function () {
+ var login_link = YAHOO.util.Dom.get('github_mini_login[% qs_suffix FILTER js %]');
+ YAHOO.util.Dom.removeClass(login_link, 'bz_default_hidden');
+ });
+ YAHOO.util.Event.addListener('hide_mini_login[% qs_suffix FILTER js %]','click', function () {
+ var login_link = YAHOO.util.Dom.get('github_mini_login[% qs_suffix FILTER js %]');
+ YAHOO.util.Dom.addClass(login_link, 'bz_default_hidden');
+ });
+ </script>
+ <span id="github_mini_login[% qs_suffix FILTER html %]" class="bz_default_hidden">
+ <a href="[% github_auth.login FILTER html %]">
+ <img src="extensions/GitHubAuth/web/images/sign_in.png" height="22" width="75" align="absmiddle"
+ alt="Sign in with GitHub"
+ title="Sign in with GitHub"></a> or
+ </span>
+[% END %]
diff --git a/extensions/GitHubAuth/template/en/default/hook/global/code-error-errors.html.tmpl b/extensions/GitHubAuth/template/en/default/hook/global/code-error-errors.html.tmpl
new file mode 100644
index 000000000..5f6672e2b
--- /dev/null
+++ b/extensions/GitHubAuth/template/en/default/hook/global/code-error-errors.html.tmpl
@@ -0,0 +1,25 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF error == "github_invalid_state" %]
+ [% title = "Invalid State Parameter" %]
+ An invalid <em>state</em> parameter was passed to the GitHub OAuth2 callback.
+
+[% ELSIF error == "github_missing_code" %]
+ [% title = "Missing GitHub Auth Code" %]
+ Expected a <em>code</em> parameter in the GitHub OAuth2 callback.
+
+[% ELSIF error == "github_bad_response" %]
+ [% title = "Bad Response from GitHub" %]
+ Received unexpected response from GitHub: [% message FILTER html %]
+
+[% ELSIF error == "github_error" %]
+ [% title = "GitHub Error" %]
+ GitHub returned an error: [% response.message FILTER html %]
+
+[% END %]
diff --git a/extensions/GitHubAuth/template/en/default/hook/global/user-error-errors.html.tmpl b/extensions/GitHubAuth/template/en/default/hook/global/user-error-errors.html.tmpl
new file mode 100644
index 000000000..61a08367d
--- /dev/null
+++ b/extensions/GitHubAuth/template/en/default/hook/global/user-error-errors.html.tmpl
@@ -0,0 +1,24 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF error == "github_no_emails" %]
+ Your GitHub account cannot be used because [% terms.Bugzilla %] cannot see any email addresses. Either your GitHub account
+ has no verified email addresses or [% terms.Bugzilla %] is not authorized to see them.
+
+[% ELSIF error == "github_invalid_email" %]
+ Your GitHub account email '[% email FILTER html %]' is not valid.
+
+[% ELSIF error == "github_auth_account_too_powerful" %]
+ [% title = "Account Too Powerful" %]
+ Your account is a member of a group which is not permitted to use GitHub to
+ log in. Please log in with your [% terms.Bugzilla %] username and password.
+ <br><br>
+ (GitHub logins are disabled for accounts which are members of certain
+ particularly sensitive groups, while we gain experience with the technology.)
+
+[% END %]
diff --git a/extensions/GitHubAuth/web/images/github_sign_in.png b/extensions/GitHubAuth/web/images/github_sign_in.png
new file mode 100644
index 000000000..f32356f01
--- /dev/null
+++ b/extensions/GitHubAuth/web/images/github_sign_in.png
Binary files differ
diff --git a/extensions/GitHubAuth/web/images/sign_in.png b/extensions/GitHubAuth/web/images/sign_in.png
new file mode 100644
index 000000000..9df6058e5
--- /dev/null
+++ b/extensions/GitHubAuth/web/images/sign_in.png
Binary files differ