From bbd48911336aa213eecca03b23aa3e9790da2433 Mon Sep 17 00:00:00 2001 From: Jelle Besseling Date: Sat, 26 Dec 2020 17:58:16 +0100 Subject: [PATCH 1/7] Implement GitHub logins Requires the following configuration options enable_github_login = 1 github_client_id github_client_secret Or github_client_secret_file which points to a file with the secret --- src/lib/Hydra/Controller/Root.pm | 2 ++ src/lib/Hydra/Controller/User.pm | 52 ++++++++++++++++++++++++++++++++ src/root/topbar.tt | 4 +++ src/script/hydra-create-user | 18 +++++------ src/sql/hydra.sql | 2 +- 5 files changed, 68 insertions(+), 10 deletions(-) diff --git a/src/lib/Hydra/Controller/Root.pm b/src/lib/Hydra/Controller/Root.pm index a9b0d558..4070184c 100644 --- a/src/lib/Hydra/Controller/Root.pm +++ b/src/lib/Hydra/Controller/Root.pm @@ -30,6 +30,8 @@ sub noLoginNeeded { return $whitelisted || $c->request->path eq "api/push-github" || $c->request->path eq "google-login" || + $c->request->path eq "github-redirect" || + $c->request->path eq "github-login" || $c->request->path eq "login" || $c->request->path eq "logo" || $c->request->path =~ /^static\//; diff --git a/src/lib/Hydra/Controller/User.pm b/src/lib/Hydra/Controller/User.pm index 656ce018..ce48dd44 100644 --- a/src/lib/Hydra/Controller/User.pm +++ b/src/lib/Hydra/Controller/User.pm @@ -4,6 +4,7 @@ use utf8; use strict; use warnings; use base 'Hydra::Base::Controller::REST'; +use File::Slurp; use Crypt::RandPasswd; use Digest::SHA1 qw(sha1_hex); use Hydra::Helper::Nix; @@ -154,6 +155,57 @@ sub google_login :Path('/google-login') Args(0) { doEmailLogin($self, $c, "google", $data->{email}, $data->{name} // undef); } +sub github_login :Path('/github-login') Args(0) { + my ($self, $c) = @_; + + error($c, "Logging in via GitHub is not enabled.") unless $c->config->{enable_github_login}; + my $client_id = $c->config->{github_client_id} or die "github_client_id not configured."; + my $client_secret = $c->config->{github_client_secret} // do { + my $client_secret_file = $c->config->{github_client_secret_file} or die "github_client_secret nor github_client_secret_file is configured."; + my $client_secret = read_file($client_secret_file); + $client_secret =~ s/\s+//; + $client_secret; + }; + die "No github secret configured" unless $client_secret; + + my $ua = new LWP::UserAgent; + my $response = $ua->post( + 'https://github.com/login/oauth/access_token', + { + client_id => $client_id, + client_secret => $client_secret, + code => ($c->req->params->{code} // die "No token."), + }, Accept => 'application/json'); + error($c, "Did not get a response from GitHub.") unless $response->is_success; + + my $data = decode_json($response->decoded_content) or die; + my $access_token = $data->{access_token} // die "No access_token in response from GitHub."; + + $response = $ua->get('https://api.github.com/user', Authorization => "token $access_token"); + error($c, "Did not get a response from GitHub for user info.") unless $response->is_success; + + $data = decode_json($response->decoded_content) or die; + doEmailLogin($self, $c, "github", $data->{email}, $data->{name} // undef); + + $c->res->redirect($c->uri_for($c->res->cookies->{'after_github'})); +} + +sub github_redirect :Path('/github-redirect') Args(0) { + my ($self, $c) = @_; + + error($c, "Logging in via GitHub is not enabled.") unless $c->config->{enable_github_login}; + my $client_id = $c->config->{github_client_id} or die "github_client_id not configured."; + + my $after = "/" . $c->req->params->{after}; + + $c->res->cookies->{'after_github'} = { + name => 'after_github', + value => $after, + }; + + $c->res->redirect("https://github.com/login/oauth/authorize?client_id=$client_id"); +} + sub captcha :Local Args(0) { my ($self, $c) = @_; diff --git a/src/root/topbar.tt b/src/root/topbar.tt index b9b839f9..e0156231 100644 --- a/src/root/topbar.tt +++ b/src/root/topbar.tt @@ -136,6 +136,10 @@
  • Sign in with Google
  • [% END %] + [% IF c.config.enable_github_login %] +
  • Sign in with GitHub
  • +
  • + [% END %]
  • Sign in with a Hydra account
  • diff --git a/src/script/hydra-create-user b/src/script/hydra-create-user index 8d673a05..3fde1aad 100755 --- a/src/script/hydra-create-user +++ b/src/script/hydra-create-user @@ -11,7 +11,7 @@ sub showHelp { print < \$renameFrom, die "$0: one user name required\n" if scalar @ARGV != 1; my $userName = $ARGV[0]; -die "$0: type must be `hydra' or `google'\n" - if defined $type && $type ne "hydra" && $type ne "google"; +die "$0: type must be `hydra', `google' or `github'\n" + if defined $type && $type ne "hydra" && $type ne "google" && $type ne "github"; my $db = Hydra::Model::DB->new(); @@ -67,19 +67,19 @@ $db->txn_do(sub { { username => $userName, type => "hydra", emailaddress => "", password => "!" }); } - die "$0: Google user names must be email addresses\n" - if $user->type eq "google" && $userName !~ /\@/; + die "$0: Google or GitHub user names must be email addresses\n" + if ($user->type eq "google" || $user->type eq "github") && $userName !~ /\@/; $user->update({ type => $type }) if defined $type; $user->update({ fullname => $fullName eq "" ? undef : $fullName }) if defined $fullName; - if ($user->type eq "google") { - die "$0: Google accounts do not have an explicitly set email address.\n" + if ($user->type eq "google" || $user->type eq "github") { + die "$0: Google and GitHub accounts do not have an explicitly set email address.\n" if defined $emailAddress; - die "$0: Google accounts do not have a password.\n" + die "$0: Google and GitHub accounts do not have a password.\n" if defined $password; - die "$0: Google accounts do not have a password.\n" + die "$0: Google and GitHub accounts do not have a password.\n" if defined $passwordHash; $user->update({ emailaddress => $userName, password => "!" }); } else { diff --git a/src/sql/hydra.sql b/src/sql/hydra.sql index 67843a90..46e3827e 100644 --- a/src/sql/hydra.sql +++ b/src/sql/hydra.sql @@ -10,7 +10,7 @@ create table Users ( emailAddress text not null, password text not null, -- sha256 hash emailOnError integer not null default 0, - type text not null default 'hydra', -- either "hydra" or "google" + type text not null default 'hydra', -- either "hydra", "google" or "github" publicDashboard boolean not null default false ); From e88355b3d4cd3b533bf74f765ee35ffcccae8490 Mon Sep 17 00:00:00 2001 From: Jelle Besseling Date: Wed, 30 Dec 2020 21:32:00 +0100 Subject: [PATCH 2/7] Use email api call --- src/lib/Hydra/Controller/User.pm | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/lib/Hydra/Controller/User.pm b/src/lib/Hydra/Controller/User.pm index ce48dd44..15fcefe6 100644 --- a/src/lib/Hydra/Controller/User.pm +++ b/src/lib/Hydra/Controller/User.pm @@ -181,11 +181,23 @@ sub github_login :Path('/github-login') Args(0) { my $data = decode_json($response->decoded_content) or die; my $access_token = $data->{access_token} // die "No access_token in response from GitHub."; - $response = $ua->get('https://api.github.com/user', Authorization => "token $access_token"); - error($c, "Did not get a response from GitHub for user info.") unless $response->is_success; + $response = $ua->get('https://api.github.com/user/emails', Accept => 'application/vnd.github.v3+json', Authorization => "token $access_token"); + error($c, "Did not get a response from GitHub for email info.") unless $response->is_success; $data = decode_json($response->decoded_content) or die; - doEmailLogin($self, $c, "github", $data->{email}, $data->{name} // undef); + my $email; + + foreach my $eml (@{$data}) { + $email = $eml->{email} if $eml->{verified} && $eml->{primary}; + print STDERR "$eml->{email}\n"; + } + + print STDERR "$email\n"; + $response = $ua->get('https://api.github.com/user', Authorization => "token $access_token"); + error($c, "Did not get a response from GitHub for user info.") unless $response->is_success; + $data = decode_json($response->decoded_content) or die; + + doEmailLogin($self, $c, "github", $email, $data->{name} // undef); $c->res->redirect($c->uri_for($c->res->cookies->{'after_github'})); } From 5f4eddbe57d0795e068e23cb16e1efa4f2bd092c Mon Sep 17 00:00:00 2001 From: Jelle Besseling Date: Wed, 30 Dec 2020 18:32:57 +0100 Subject: [PATCH 3/7] Use email scope --- src/lib/Hydra/Controller/User.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/Hydra/Controller/User.pm b/src/lib/Hydra/Controller/User.pm index 15fcefe6..2881429d 100644 --- a/src/lib/Hydra/Controller/User.pm +++ b/src/lib/Hydra/Controller/User.pm @@ -215,7 +215,7 @@ sub github_redirect :Path('/github-redirect') Args(0) { value => $after, }; - $c->res->redirect("https://github.com/login/oauth/authorize?client_id=$client_id"); + $c->res->redirect("https://github.com/login/oauth/authorize?client_id=$client_id&scope=user:email"); } From 19f9d8249f031fdcd7e3a83bdc110c9a049b18ce Mon Sep 17 00:00:00 2001 From: Jelle Besseling Date: Mon, 4 Jan 2021 17:48:37 +0100 Subject: [PATCH 4/7] Update src/lib/Hydra/Controller/User.pm Co-authored-by: Eelco Dolstra --- src/lib/Hydra/Controller/User.pm | 1 - 1 file changed, 1 deletion(-) diff --git a/src/lib/Hydra/Controller/User.pm b/src/lib/Hydra/Controller/User.pm index 2881429d..62f2b84c 100644 --- a/src/lib/Hydra/Controller/User.pm +++ b/src/lib/Hydra/Controller/User.pm @@ -189,7 +189,6 @@ sub github_login :Path('/github-login') Args(0) { foreach my $eml (@{$data}) { $email = $eml->{email} if $eml->{verified} && $eml->{primary}; - print STDERR "$eml->{email}\n"; } print STDERR "$email\n"; From 20d8134936e230600bec19ac622cf8cf56ef8da5 Mon Sep 17 00:00:00 2001 From: Jelle Besseling Date: Mon, 4 Jan 2021 17:48:43 +0100 Subject: [PATCH 5/7] Update src/lib/Hydra/Controller/User.pm Co-authored-by: Eelco Dolstra --- src/lib/Hydra/Controller/User.pm | 1 - 1 file changed, 1 deletion(-) diff --git a/src/lib/Hydra/Controller/User.pm b/src/lib/Hydra/Controller/User.pm index 62f2b84c..2fb72d81 100644 --- a/src/lib/Hydra/Controller/User.pm +++ b/src/lib/Hydra/Controller/User.pm @@ -191,7 +191,6 @@ sub github_login :Path('/github-login') Args(0) { $email = $eml->{email} if $eml->{verified} && $eml->{primary}; } - print STDERR "$email\n"; $response = $ua->get('https://api.github.com/user', Authorization => "token $access_token"); error($c, "Did not get a response from GitHub for user info.") unless $response->is_success; $data = decode_json($response->decoded_content) or die; From c49ca66689c0320e48a08a1f813af1a15ec70411 Mon Sep 17 00:00:00 2001 From: Jelle Besseling Date: Mon, 4 Jan 2021 18:09:05 +0100 Subject: [PATCH 6/7] Die when no email is found --- src/lib/Hydra/Controller/User.pm | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/lib/Hydra/Controller/User.pm b/src/lib/Hydra/Controller/User.pm index 2fb72d81..50bd5a15 100644 --- a/src/lib/Hydra/Controller/User.pm +++ b/src/lib/Hydra/Controller/User.pm @@ -191,6 +191,8 @@ sub github_login :Path('/github-login') Args(0) { $email = $eml->{email} if $eml->{verified} && $eml->{primary}; } + die "No primary email for this GitHub profile" unless $email; + $response = $ua->get('https://api.github.com/user', Authorization => "token $access_token"); error($c, "Did not get a response from GitHub for user info.") unless $response->is_success; $data = decode_json($response->decoded_content) or die; From 43d662f63ad78acfd1b586ac8cecdfa3955a547a Mon Sep 17 00:00:00 2001 From: Jelle Besseling Date: Mon, 4 Jan 2021 18:09:49 +0100 Subject: [PATCH 7/7] Don't use enable_github_login option after all Instead the github_client_id option is used to detect if github logins should be enabled. --- src/lib/Hydra/Controller/User.pm | 2 -- src/root/topbar.tt | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/lib/Hydra/Controller/User.pm b/src/lib/Hydra/Controller/User.pm index 50bd5a15..9cdece8a 100644 --- a/src/lib/Hydra/Controller/User.pm +++ b/src/lib/Hydra/Controller/User.pm @@ -158,7 +158,6 @@ sub google_login :Path('/google-login') Args(0) { sub github_login :Path('/github-login') Args(0) { my ($self, $c) = @_; - error($c, "Logging in via GitHub is not enabled.") unless $c->config->{enable_github_login}; my $client_id = $c->config->{github_client_id} or die "github_client_id not configured."; my $client_secret = $c->config->{github_client_secret} // do { my $client_secret_file = $c->config->{github_client_secret_file} or die "github_client_secret nor github_client_secret_file is configured."; @@ -205,7 +204,6 @@ sub github_login :Path('/github-login') Args(0) { sub github_redirect :Path('/github-redirect') Args(0) { my ($self, $c) = @_; - error($c, "Logging in via GitHub is not enabled.") unless $c->config->{enable_github_login}; my $client_id = $c->config->{github_client_id} or die "github_client_id not configured."; my $after = "/" . $c->req->params->{after}; diff --git a/src/root/topbar.tt b/src/root/topbar.tt index e0156231..f8366917 100644 --- a/src/root/topbar.tt +++ b/src/root/topbar.tt @@ -136,7 +136,7 @@
  • Sign in with Google
  • [% END %] - [% IF c.config.enable_github_login %] + [% IF c.config.github_client_id %]
  • Sign in with GitHub
  • [% END %]