diff options
author | Dylan William Hardison <dylan@hardison.net> | 2015-03-04 04:27:15 +0100 |
---|---|---|
committer | Dylan William Hardison <dylan@hardison.net> | 2015-04-03 00:21:08 +0200 |
commit | c0d00a57eebb31d3e2cdef0ebb9219ebe2fe2bab (patch) | |
tree | e6c6eb07c7348e086f1ed6de54de829e806dda05 | |
parent | 1317be9bb70dbb945fa82d0d1bd8548c83c86ebe (diff) | |
download | bugzilla-c0d00a57eebb31d3e2cdef0ebb9219ebe2fe2bab.tar.gz bugzilla-c0d00a57eebb31d3e2cdef0ebb9219ebe2fe2bab.tar.xz |
Github Auth Extension
17 files changed, 771 insertions, 32 deletions
@@ -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 Binary files differnew file mode 100644 index 000000000..f32356f01 --- /dev/null +++ b/extensions/GitHubAuth/web/images/github_sign_in.png diff --git a/extensions/GitHubAuth/web/images/sign_in.png b/extensions/GitHubAuth/web/images/sign_in.png Binary files differnew file mode 100644 index 000000000..9df6058e5 --- /dev/null +++ b/extensions/GitHubAuth/web/images/sign_in.png |