From 5c3f1b93027cd4a6acfc16b7c616aa07a233482c Mon Sep 17 00:00:00 2001 From: Linus Heckemann Date: Fri, 4 Feb 2022 19:54:43 +0100 Subject: [PATCH 1/2] Implement generic OIDC-based authentication Co-Authored-By: Maximilian Bosch --- flake.nix | 20 +-------- perl-packages.nix | 77 ++++++++++++++++++++++++++++++++ src/lib/Hydra/Controller/Root.pm | 2 + src/lib/Hydra/Controller/User.pm | 71 ++++++++++++++++++++++++++++- src/root/topbar.tt | 4 ++ 5 files changed, 155 insertions(+), 19 deletions(-) create mode 100644 perl-packages.nix diff --git a/flake.nix b/flake.nix index 6bbec9b03..eac9a3b84 100644 --- a/flake.nix +++ b/flake.nix @@ -42,24 +42,7 @@ overlays.default = final: prev: { # Add LDAP dependencies that aren't currently found within nixpkgs. - perlPackages = prev.perlPackages // { - - PrometheusTiny = final.perlPackages.buildPerlPackage { - pname = "Prometheus-Tiny"; - version = "0.007"; - src = final.fetchurl { - url = "mirror://cpan/authors/id/R/RO/ROBN/Prometheus-Tiny-0.007.tar.gz"; - sha256 = "0ef8b226a2025cdde4df80129dd319aa29e884e653c17dc96f4823d985c028ec"; - }; - buildInputs = with final.perlPackages; [ HTTPMessage Plack TestException ]; - meta = { - homepage = "https://github.com/robn/Prometheus-Tiny"; - description = "A tiny Prometheus client"; - license = with final.lib.licenses; [ artistic1 gpl1Plus ]; - }; - }; - - }; + perlPackages = prev.perlPackages // import ./perl-packages.nix prev; hydra = with final; let perlDeps = buildEnv { @@ -112,6 +95,7 @@ NetAmazonS3 NetPrometheus NetStatsd + OIDCLite PadWalker ParallelForkManager PerlCriticCommunity diff --git a/perl-packages.nix b/perl-packages.nix new file mode 100644 index 000000000..4008d6ba9 --- /dev/null +++ b/perl-packages.nix @@ -0,0 +1,77 @@ +prev: +with prev.perlPackages; +let inherit (prev) lib fetchurl; +in rec { + ClassErrorHandler = buildPerlPackage { + pname = "Class-ErrorHandler"; + version = "0.04"; + src = fetchurl { + url = "mirror://cpan/authors/id/T/TO/TOKUHIROM/Class-ErrorHandler-0.04.tar.gz"; + sha256 = "342d2dcfc797a20bee8179b1b96b85c0ae7a5b48827359523cd8c74c3e704502"; + }; + meta = { + homepage = "https://github.com/tokuhirom/Class-ErrorHandler"; + description = "Base class for error handling"; + license = with lib.licenses; [ artistic1 gpl1Plus ]; + }; + }; + OIDCLite = buildPerlModule { + pname = "OIDC-Lite"; + version = "0.10"; + src = fetchurl { + url = "mirror://cpan/authors/id/R/RI/RITOU/OIDC-Lite-0.10.tar.gz"; + sha256 = "529096272a160d8cd947bec79e01b48639db93726432b4d93039a7507421245a"; + }; + buildInputs = [ CryptOpenSSLRSA TestMockLWPConditional TestMockObject ]; + propagatedBuildInputs = [ ClassAccessor DataDump JSONWebToken JSONXS OAuthLite2 ParamsValidate ]; + meta = { + homepage = "https://github.com/ritou/p5-oidc-lite"; + description = "OpenID Connect Library"; + license = with lib.licenses; [ artistic1 gpl1Plus ]; + }; + }; + OAuthLite = buildPerlPackage { + pname = "OAuth-Lite"; + version = "1.35"; + src = fetchurl { + url = "mirror://cpan/authors/id/L/LY/LYOKATO/OAuth-Lite-1.35.tar.gz"; + sha256 = "740528f8345bcb8849c1e3bfc91510b3c7f9df6255af09987d4175c1dea43c5e"; + }; + propagatedBuildInputs = [ ClassAccessor ClassDataAccessor ClassErrorHandler CryptOpenSSLRSA CryptOpenSSLRandom LWP ListMoreUtils UNIVERSALrequire URI ]; + meta = { + description = "OAuth framework"; + license = with lib.licenses; [ artistic1 gpl1Plus ]; + }; + }; + TestMockLWPConditional = buildPerlModule { + pname = "Test-Mock-LWP-Conditional"; + version = "0.04"; + src = fetchurl { + url = "mirror://cpan/authors/id/M/MA/MASAKI/Test-Mock-LWP-Conditional-0.04.tar.gz"; + sha256 = "8817129488f1eae4896aae59b8e09e94f720fdd697a73aef13241e8123940667"; + }; + buildInputs = [ ModuleBuildTiny TestFakeHTTPD TestUseAllModules TestTCP TestSharedFork ]; + propagatedBuildInputs = [ ClassMethodModifiers LWP MathRandomSecure SubInstall ]; + meta = { + homepage = "https://github.com/masaki/Test-Mock-LWP-Conditional"; + description = "Stubbing on LWP request"; + license = with lib.licenses; [ artistic1 gpl1Plus ]; + }; + }; + OAuthLite2 = buildPerlModule { + pname = "OAuth-Lite2"; + version = "0.11"; + src = fetchurl { + url = "mirror://cpan/authors/id/R/RI/RITOU/OAuth-Lite2-0.11.tar.gz"; + sha256 = "01417ec28acefd25a839bdb4b846056036ae122c181dab907e48e0bdb938686a"; + }; + buildInputs = [ ModuleBuildTiny ]; + propagatedBuildInputs = [ ClassAccessor ClassErrorHandler DataDump HashMultiValue IOString JSONXS LWP ParamsValidate Plack StringRandom TryTiny URI XMLLibXML ]; + meta = { + homepage = "https://github.com/ritou/p5-oauth-lite2"; + description = "OAuth 2.0 Library"; + license = with lib.licenses; [ artistic1 gpl1Plus ]; + }; + }; + +} diff --git a/src/lib/Hydra/Controller/Root.pm b/src/lib/Hydra/Controller/Root.pm index c6843d296..63ecdcdd5 100644 --- a/src/lib/Hydra/Controller/Root.pm +++ b/src/lib/Hydra/Controller/Root.pm @@ -33,6 +33,8 @@ sub noLoginNeeded { return $whitelisted || $c->request->path eq "api/push-github" || $c->request->path eq "google-login" || + $c->request->path eq "oidc-login" || + $c->request->path eq "oidc-redirect" || $c->request->path eq "github-redirect" || $c->request->path eq "github-login" || $c->request->path eq "login" || diff --git a/src/lib/Hydra/Controller/User.pm b/src/lib/Hydra/Controller/User.pm index 9e7d96e5a..346cc293b 100644 --- a/src/lib/Hydra/Controller/User.pm +++ b/src/lib/Hydra/Controller/User.pm @@ -11,14 +11,26 @@ use Hydra::Config qw(getLDAPConfigAmbient); use Hydra::Helper::Nix; use Hydra::Helper::CatalystUtils; use Hydra::Helper::Email; +use OIDC::Lite::Client::WebServer; use LWP::UserAgent; use JSON::MaybeXS; use HTML::Entities; +use UUID4::Tiny; use Encode qw(decode); __PACKAGE__->config->{namespace} = ''; +sub get_oidc_client { + my $c = shift; + return OIDC::Lite::Client::WebServer->new( + id => $c->config->{oidc_client_id}, + secret => $c->config->{oidc_client_secret}, + authorize_uri => $c->config->{oidc_auth_uri}, + access_token_uri => $c->config->{oidc_token_uri}, + ); +} + sub login :Local :Args(0) :ActionClass('REST') { } @@ -92,7 +104,7 @@ sub doLDAPLogin { } sub doEmailLogin { - my ($self, $c, $type, $email, $fullName) = @_; + my ($self, $c, $type, $email, $fullName, $username) = @_; die "No email address provided.\n" unless defined $email; @@ -100,6 +112,8 @@ sub doEmailLogin { # in URLs. die "Illegal email address.\n" unless $email =~ /^[a-zA-Z0-9\.\-\_]+@[a-zA-Z0-9\.\-\_]+$/; + $username = $email unless defined $username; + # If allowed_domains is set, check if the email address # returned is on these domains. When not configured, allow all # domains. @@ -143,6 +157,38 @@ sub doEmailLogin { } +sub oidc_login :Path('/oidc-login') Args(0) { + my ($self, $c) = @_; + + error($c, "Logging in via OIDC is not enabled.", 404) unless $c->config->{enable_oidc_login}; + my $oidc_client = get_oidc_client($c); + + error($c, q{OIDC state mismatch. Is this a CSRF attack?}) unless $c->session->{oidc_state} && $c->session->{oidc_state} eq $c->request->param("state"); + + my $code = $c->request->param("code"); + my $token_response = $oidc_client->get_access_token( + code => $code, + redirect_uri => $c->uri_for('/oidc-login'), + ) or error($c, $oidc_client->errstr); + + $c->session->{access_token} = $token_response->access_token; + $c->session->{expires_at} = time() + $token_response->expires_in; + $c->session->{refresh_token} = $token_response->refresh_token; + + my $res = LWP::UserAgent->new->get( + $c->config->{oidc_userinfo_uri}, + Authorization => sprintf(q{Bearer %s}, $token_response->access_token) + ); + + my $claims = decode_json($res->decoded_content) or error($c, q{Could not decode claims.}, 401); + + error($c, "Email address must be verified.", 401) unless $claims->{email_verified}; + + doEmailLogin($self, $c, "oidc", $claims->{email}, $claims->{name} // undef, sprintf("oidc:$claims->{sub}")); + + $c->res->redirect($c->uri_for($c->res->cookies->{'after_oidc'})); +} + sub google_login :Path('/google-login') Args(0) { my ($self, $c) = @_; requirePost($c); @@ -226,6 +272,29 @@ sub github_redirect :Path('/github-redirect') Args(0) { $c->res->redirect("https://github.com/login/oauth/authorize?client_id=$client_id&scope=user:email"); } +sub oidc_redirect :Path('/oidc-redirect') Args(0) { + + my ($self, $c) = @_; + + my $after = "/" . $c->req->params->{after}; + + $c->res->cookies->{'after_oidc'} = { + name => 'after_oidc', + value => $after, + }; + + my $oidc_client = get_oidc_client($c); + my $state = UUID4::Tiny::create_uuid_string(); + $c->session->{oidc_state} = $state; + my $redirect_url = $oidc_client->uri_to_redirect( + redirect_uri => $c->uri_for('/oidc-login'), + scope => q{openid}, + state => $state, + ); + + $c->res->redirect( $redirect_url ); +} + sub captcha :Local Args(0) { my ($self, $c) = @_; diff --git a/src/root/topbar.tt b/src/root/topbar.tt index 1771222d7..e261dafed 100644 --- a/src/root/topbar.tt +++ b/src/root/topbar.tt @@ -143,6 +143,10 @@ Sign in with GitHub [% END %] + [% IF c.config.enable_oidc_login %] + Sign in with OIDC + + [% END %] Sign in with a Hydra account [% END %] [% END %] From 9a92fef1e2826dd69d3016622b5432503b012830 Mon Sep 17 00:00:00 2001 From: Linus Heckemann Date: Sun, 20 Aug 2023 19:38:49 +0200 Subject: [PATCH 2/2] Set convenient variables for db access in shellHook --- flake.nix | 3 +++ 1 file changed, 3 insertions(+) diff --git a/flake.nix b/flake.nix index eac9a3b84..58cf445ac 100644 --- a/flake.nix +++ b/flake.nix @@ -198,6 +198,9 @@ mkdir -p .hydra-data export HYDRA_DATA="$(pwd)/.hydra-data" export HYDRA_DBI='dbi:Pg:dbname=hydra;host=localhost;port=64444' + export PGPORT=64444 + export PGDATABASE=hydra + export PGHOST=localhost popd >/dev/null '';