summaryrefslogtreecommitdiffstats
path: root/extensions/GitHubAuth/lib
diff options
context:
space:
mode:
Diffstat (limited to 'extensions/GitHubAuth/lib')
-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
6 files changed, 466 insertions, 0 deletions
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;