Add support for logging in via a Google account

[?]
Jan 13, 2016, 4:32 PM
3QWDDLBR5DGFK5Y3TDMK55R2SCHRHFVO2KW2BMZGIYRTIQEZC45AC

Dependencies

  • [2] GS4SFHCP templates: Use uri_for to reference static paths.
  • [3] Z4NL5TWB Fix audience URL
  • [4] IFY7BYPS User.pm: Handle params from JSON properly
  • [5] HL6ZYWHF Allow configuring a set of domains to allow logins from Persona.
  • [6] 2DHE2ZAK Allow Hydra to run as a private instance by requiring a login.
  • [7] XOOSZ3WS Only include Persona JS when Persona is enabled
  • [8] 36ZTCZ4F Add basic Persona support
  • [9] LZVO64YG Merge in the first bits of the API work
  • [10] XMB4MTRL Show sign in as success
  • [11] 2CZSW5S5 Don't redirect to /login if authentication is required
  • [12] P75LFRF4 Slight cleanup in the Persona sign in code
  • [13] JFW656FT Add a flag to enable Persona support
  • [14] MQMF2LBW Re-enable adding new users via the web interface
  • [15] QVIQAYZT Be paranoid about the Persona email address
  • [16] G4X5IUYJ Remove default logo, replaced by text for now. Hide template in jobset edit.
  • [17] XJRJ4J7M Add user registration
  • [18] MGOGOKQP add tracker html code via HYDRA_TRACKER
  • [19] OEPUOUNB Using twitter bootstrap for more consistent looks for Hydra
  • [20] DV43UILU Don't float the search bar to the right in collapsed mode
  • [21] WDKFN4B2 Make sign in a modal dialog box rather than a separate page
  • [22] YM27DTVT Remove superfluous HYDRA_LOGO environment variable
  • [23] J5UVLXOK * Start of a basic Catalyst web interface.
  • [24] D44B24QC Store the account type ("hydra" or "persona") explicitly in the database
  • [25] 2G63HKCH Fix some wellformedness issues
  • [26] X5BWNTMA Make the logo configurable via hydra.conf
  • [27] PFB5ZUQW Fix legacy login
  • [28] VVRM3EGC Link to both the Persona and legacy sign in
  • [29] QL55ECJ6 - adapted ui for hydra, more in line with nixos.org website
  • [30] JARRBLZD Bootstrapify the Hydra forms (except the project and jobset edit pages)
  • [31] BW6TYQJS Use local copy of the Persona sign in button
  • [32] R6APT7HG Fix hydra_logo setting
  • [*] T4LLYESZ * Nix expression for building Hydra.
  • [*] 6K5PBUUN Use buildEnv to combine Hydra's Perl dependencies
  • [*] LSZLZHJY Allow users to edit their own settings
  • [*] D5QIOJGP * Move everything up one directory.
  • [*] 4JPNFWRB * Use jquery for the logfile manipulation.
  • [*] SZYDW2DG hydra: added some user admin

Change contents

  • edit in release.nix at line 120
    [35.556]
    [35.601]
    CryptJWT
  • edit in src/lib/Hydra/Controller/Root.pm at line 16
    [8.1084]
    [6.0]
  • edit in src/lib/Hydra/Controller/Root.pm at line 22
    [6.87]
    [6.87]
    $c->request->path eq "google-login" ||
  • edit in src/lib/Hydra/Controller/Root.pm at line 40
    [8.196][8.0:78]()
    $c->stash->{personaEnabled} = $c->config->{enable_persona} // "0" eq "1";
  • edit in src/lib/Hydra/Controller/Root.pm at line 73
    [8.21615]
    [8.21615]
  • edit in src/lib/Hydra/Controller/User.pm at line 7
    [8.23630]
    [36.272]
    use Crypt::JWT qw(decode_jwt);
  • edit in src/lib/Hydra/Controller/User.pm at line 47
    [8.24932][8.33:112](),[8.140][8.0:21](),[8.21][8.79:165]()
    sub persona_login :Path('/persona-login') Args(0) {
    my ($self, $c) = @_;
    requirePost($c);
    error($c, "Persona support is not enabled.") unless $c->stash->{personaEnabled};
  • edit in src/lib/Hydra/Controller/User.pm at line 48
    [8.183][4.0:61](),[4.61][8.240:392](),[8.240][8.240:392](),[8.392][3.0:39](),[3.39][8.439:451](),[8.439][8.439:451](),[8.451][8.22:106]()
    my $assertion = $c->stash->{params}->{assertion} or die;
    my $ua = new LWP::UserAgent;
    my $response = $ua->post(
    'https://verifier.login.persona.org/verify',
    { assertion => $assertion,
    audience => $c->uri_for('/')
    });
    error($c, "Did not get a response from Persona.") unless $response->is_success;
  • replacement in src/lib/Hydra/Controller/User.pm at line 49
    [8.1421][8.553:613](),[8.613][8.107:178]()
    my $d = decode_json($response->decoded_content) or die;
    error($c, "Persona says: $d->{reason}") if $d->{status} ne "okay";
    [8.1421]
    [8.24933]
    sub doEmailLogin {
    my ($self, $c, $type, $email, $fullName) = @_;
  • replacement in src/lib/Hydra/Controller/User.pm at line 52
    [8.24934][8.702:738]()
    my $email = $d->{email} or die;
    [8.24934]
    [8.0]
    die "No email address provided.\n" unless defined $email;
  • replacement in src/lib/Hydra/Controller/User.pm at line 56
    [8.85][8.85:178]()
    die "Illegal email address." unless $email =~ /^[a-zA-Z0-9\.\-\_]+@[a-zA-Z0-9\.\-\_]+$/;
    [8.85]
    [5.0]
    die "Illegal email address.\n" unless $email =~ /^[a-zA-Z0-9\.\-\_]+@[a-zA-Z0-9\.\-\_]+$/;
  • replacement in src/lib/Hydra/Controller/User.pm at line 58
    [5.1][5.1:145]()
    # If persona_allowed_domains is set, check if the email address returned is on these domains.
    # When not configured, allow all domains.
    [5.1]
    [5.145]
    # If persona_allowed_domains is set, check if the email address
    # returned is on these domains. When not configured, allow all
    # domains.
  • replacement in src/lib/Hydra/Controller/User.pm at line 62
    [5.216][5.216:251]()
    if ( $allowed_domains ne "") {
    [5.216]
    [5.251]
    if ($allowed_domains ne "") {
  • replacement in src/lib/Hydra/Controller/User.pm at line 70
    [5.505][5.505:576]()
    die "Email address is not allowed to login." unless $email_ok;
    [5.505]
    [5.576]
    error($c, "Your email address does not belong to a domain that is allowed to log in.\n")
    unless $email_ok;
  • replacement in src/lib/Hydra/Controller/User.pm at line 76
    [8.794][8.794:812]()
    if (!$user) {
    [8.794]
    [8.812]
    if ($user) {
    # Automatically upgrade Persona accounts to Google accounts.
    if ($user->type eq "persona" && $type eq "google") {
    $user->update({type => "google"});
    }
    die "You cannot login via login type '$type'.\n" if $user->type ne $type;
    } else {
  • edit in src/lib/Hydra/Controller/User.pm at line 86
    [8.885]
    [8.885]
    , fullname => $fullName,
  • replacement in src/lib/Hydra/Controller/User.pm at line 89
    [8.953][8.0:32]()
    , type => "persona"
    [8.953]
    [8.953]
    , type => $type
  • edit in src/lib/Hydra/Controller/User.pm at line 99
    [8.1115]
    [8.1115]
    sub persona_login :Path('/persona-login') Args(0) {
    my ($self, $c) = @_;
    requirePost($c);
    error($c, "Logging in via Persona is not enabled.") unless $c->config->{enable_persona};
  • edit in src/lib/Hydra/Controller/User.pm at line 107
    [8.1116]
    [8.1116]
    my $assertion = $c->stash->{params}->{assertion} or die;
  • edit in src/lib/Hydra/Controller/User.pm at line 109
    [8.1117]
    [8.1421]
    my $ua = new LWP::UserAgent;
    my $response = $ua->post(
    'https://verifier.login.persona.org/verify',
    { assertion => $assertion,
    audience => $c->uri_for('/')
    });
    error($c, "Did not get a response from Persona.") unless $response->is_success;
    my $d = decode_json($response->decoded_content) or die;
    error($c, "Persona says: $d->{reason}") if $d->{status} ne "okay";
    doEmailLogin($self, $c, "persona", $d->{email}, undef);
    }
    # From https://www.googleapis.com/oauth2/v3/certs. Should probably not
    # hard-code this.
    my $googleKeys = <<'EOF';
    {
    "keys": [
    {
    "kty": "RSA",
    "alg": "RS256",
    "use": "sig",
    "kid": "10685afd5291883ce668345afd77201390406f82",
    "n": "xeNopuszp35W6H1w2Tw4OrSwT8BZ9f7-2PoOyWZmfMmUDmYT2uxrZezDK0YLap5LVmpLNcpZP5Hj67_32NU3my4qfA-SlxuJMUxHWJF7Dqr-QNAqld0SZ_po4qz5ZTHDxNxoZ4iw_T-4lhIBGm0RIZprDDGPI7Vo8qIeIMjZywoh_nq32zB6tnjEUBvHcgay0qXEnQkKkavzHO_c5sLc1qXM0jDQVqyO1enevW2yA_8gP0Qb7014ycN5umCvEHc66c2_iNT-R4zgw8gd1g05n2xwyET8qb_3wi5LqUV-Cri4mJ2xwGY8uynlD2I4jVtOYJusBgNs6AfwyehzsLdwSQ",
    "e": "AQAB"
    },
    {
    "kty": "RSA",
    "alg": "RS256",
    "use": "sig",
    "kid": "5a68fc8a3ec0c30e0be95aa08db99a68a725467f",
    "n": "zmXvUwXYSo8VouhnkURp-3xywch-jPrk7q0gugqC7QIchBPnvdXdS-bj6sr1AqDl_hEDtiLGfiVr3Ft_U022rtHAl5n5NxyybUtZXWyT5yQZM4jopGBajavEUdCl9b4pqb-q_3fVaxUXe7re23sVjI5Bntd-8RYZ70tq-ZvCWBqsnz6lHi9Ditp3CZGWLMMBZlIv3nKnClOrZXL98Jmt7AAod-Gtk65saqnrMwWtBcI_Q-3u23ytywbMLanCeFFNUWlIOgZqyYYkOm-ylLRJzVaZ1THtcWILWCYUgxXjyF9DtXO3a8nct2JhdacD3LzRiPv3sXr31cg4arwUk19JoQ",
    "e": "AQAB"
    }
    ]
    }
    EOF
    sub google_login :Path('/google-login') Args(0) {
    my ($self, $c) = @_;
    requirePost($c);
    error($c, "Logging in via Google is not enabled.") unless $c->config->{enable_google_login};
    my $data = decode_jwt(
    token => ($c->stash->{params}->{id_token} // die "No token."),
    kid_keys => $googleKeys,
    verify_exp => 1,
    );
    die unless $data->{aud} eq $c->config->{google_client_id};
    die unless $data->{iss} eq "accounts.google.com" || $data->{iss} eq "https://accounts.google.com";
    die "Email address is not verified" unless $data->{email_verified};
    # FIXME: verify hosted domain claim?
    doEmailLogin($self, $c, "google", $data->{email}, $data->{name} // undef);
    }
  • file addition: auth.tt (----------)
    [37.1486]
    [% IF c.user_exists %]
    <script>
    function finishSignOut() {
    $.post("[% c.uri_for('/logout') %]")
    .done(function(data) {
    window.location.reload();
    })
    .fail(function() { bootbox.alert("Server request failed!"); });
    }
    function signOut() {
    [% IF c.user.type == 'google' %]
    gapi.load('auth2', function() {
    gapi.auth2.init();
    var auth2 = gapi.auth2.getAuthInstance();
    auth2.then(function () {
    auth2.signOut().then(finishSignOut, finishSignOut);
    });
    });
    [% ELSE %]
    finishSignOut();
    [% END %]
    }
    </script>
    [% ELSE %]
    <div id="hydra-signin" class="modal hide fade" tabindex="-1" role="dialog" aria-hidden="true">
    <form class="form-horizontal">
    <div class="modal-body">
    <div class="control-group">
    <label class="control-label">User name</label>
    <div class="controls">
    <input type="text" class="span3" name="username" value=""/>
    </div>
    </div>
    <div class="control-group">
    <label class="control-label">Password</label>
    <div class="controls">
    <input type="password" class="span3" name="password" value=""/>
    </div>
    </div>
    </div>
    <div class="modal-footer">
    <button id="do-signin" class="btn btn-primary">Sign in</button>
    <button class="btn" data-dismiss="modal" aria-hidden="true">Cancel</button>
    </div>
    </form>
    </div>
    <script>
    function finishSignOut() { }
    $("#do-signin").click(function() {
    requestJSON({
    url: "[% c.uri_for('/login') %]",
    data: $(this).parents("form").serialize(),
    type: 'POST',
    success: function(data) {
    window.location.reload();
    }
    });
    return false;
    });
    </script>
    [% IF c.config.enable_google_login %]
    <script>
    function onGoogleSignIn(googleUser) {
    requestJSON({
    url: "[% c.uri_for('/google-login') %]",
    data: "id_token=" + googleUser.getAuthResponse().id_token,
    type: 'POST',
    success: function(data) {
    window.location.reload();
    }
    });
    return false;
    };
    </script>
    [% END %]
    [% END %]
    [% IF c.config.enable_persona %]
    <script src="https://login.persona.org/include.js"></script>
    <script>
    navigator.id.watch({
    onlogin: function(assertion) {
    requestJSON({
    url: "[% c.uri_for('/persona-login') %]",
    data: "assertion=" + assertion,
    type: 'POST',
    success: function(data) { window.location.reload(); },
    postError: function() { navigator.id.logout(); }
    });
    }
    });
    $("#persona-signin").click(function() {
    navigator.id.request({ siteName: 'Hydra' });
    });
    </script>
    [% END %]
  • edit in src/root/layout.tt at line 38
    [2.2270]
    [38.54]
    [% IF c.config.enable_google_login %]
    <meta name="google-signin-client_id" content="[% c.config.google_client_id %]">
    <script src="https://apis.google.com/js/platform.js" async="1" defer="1"></script>
    [% END %]
  • replacement in src/root/layout.tt at line 102
    [8.9312][8.511:641]()
    You are signed in as <tt>[% HTML.escape(c.user.username) %]</tt>[% IF c.user.type == 'persona' %] via Persona[% END %].
    [8.9312]
    [8.9375]
    You are signed in as <tt>[% HTML.escape(c.user.username) %]</tt>
    [%- IF c.user.type == 'persona' %] via Persona
    [%- ELSIF c.user.type == 'google' %] via Google[% END %].
  • edit in src/root/layout.tt at line 110
    [8.1323][8.1323:1337](),[8.1337][8.218:246](),[8.246][8.166:428](),[8.428][8.449:471](),[8.449][8.449:471](),[8.471][8.7485:7486](),[8.2168][8.7485:7486](),[8.11729][8.7485:7486](),[8.7486][8.642:695](),[8.695][8.525:603](),[8.525][8.525:603](),[8.603][7.0:31](),[7.31][8.618:685](),[8.618][8.618:685](),[8.685][8.2257:2258](),[8.2257][8.2257:2258](),[8.2258][8.686:868](),[8.868][8.35:191](),[8.191][8.429:561](),[8.561][8.259:275](),[8.259][8.259:275](),[8.275][8.1243:1297](),[8.1243][8.1243:1297](),[8.1297][8.2360:2361](),[8.2360][8.2360:2361](),[8.2361][8.1298:1346](),[8.1346][8.0:55](),[8.55][8.1380:1518](),[8.1380][8.1380:1518](),[8.1518][8.422:1400]()
    <script>
    function doLogout() {
    [% IF c.user_exists %]
    $.post("[% c.uri_for('/logout') %]")
    .done(function(data) {
    window.location.reload();
    })
    .fail(function() { bootbox.alert("Server request failed!"); });
    [% END %]
    }
    </script>
    [% IF c.user_exists && c.user.type == 'hydra' %]
    <script>
    $("#persona-signout").click(doLogout);
    </script>
    [% ELSIF personaEnabled %]
    <script src="https://login.persona.org/include.js"></script>
    <script>
    navigator.id.watch({
    loggedInUser: [% c.user_exists ? '"' _ HTML.escape(c.user.username) _ '"' : "null" %],
    onlogin: function(assertion) {
    requestJSON({
    url: "[% c.uri_for('/persona-login') %]",
    data: "assertion=" + assertion,
    type: 'POST',
    success: function(data) { window.location.reload(); },
    postError: function() { navigator.id.logout(); }
    });
    },
    onlogout: doLogout
    });
    $("#persona-signin").click(function() {
    navigator.id.request({ siteName: 'Hydra' });
    });
    $("#persona-signout").click(function() {
    navigator.id.logout();
    });
    </script>
    [% END %]
    [% IF !c.user_exists %]
    <div id="hydra-signin" class="modal hide fade" tabindex="-1" role="dialog" aria-hidden="true">
    <form class="form-horizontal">
    <div class="modal-body">
    <div class="control-group">
    <label class="control-label">User name</label>
    <div class="controls">
    <input type="text" class="span3" name="username" value=""/>
    </div>
    </div>
    <div class="control-group">
    <label class="control-label">Password</label>
    <div class="controls">
    <input type="password" class="span3" name="password" value=""/>
    </div>
    </div>
    </div>
    <div class="modal-footer">
    <button id="do-signin" class="btn btn-primary">Sign in</button>
    <button class="btn" data-dismiss="modal" aria-hidden="true">Cancel</button>
    </div>
    </form>
    </div>
  • replacement in src/root/layout.tt at line 111
    [8.1401][8.1401:1804]()
    <script>
    $("#do-signin").click(function() {
    requestJSON({
    url: "[% c.uri_for('/login') %]",
    data: $(this).parents("form").serialize(),
    type: 'POST',
    success: function(data) {
    window.location.reload();
    }
    });
    return false;
    });
    </script>
    [% END %]
    [8.1401]
    [8.1804]
    [% PROCESS auth.tt %]
  • replacement in src/root/topbar.tt at line 130
    [8.2441][8.2441:2493]()
    <a href="#" id="persona-signout">Sign out</a>
    [8.2441]
    [8.2493]
    <a href="#" onclick="signOut();">Sign out</a>
  • replacement in src/root/topbar.tt at line 133
    [8.311][8.616:692]()
    [% IF personaEnabled %]
    [% WRAPPER makeSubMenu title="Sign in" %]
    [8.311]
    [8.692]
    [% WRAPPER makeSubMenu title="Sign in" %]
    [% IF c.config.enable_google_login %]
    <li>
    <a><div class="g-signin2" data-onsuccess="onGoogleSignIn" data-theme="dark"></div></a>
    </li>
    <li class="divider"></li>
    [% END %]
    [% IF c.config.enable_persona %]
  • edit in src/root/topbar.tt at line 147
    [8.930][8.930:1044]()
    <li>
    <a href="#hydra-signin" data-toggle="modal">Sign in with a Hydra account</a>
    </li>
  • edit in src/root/topbar.tt at line 148
    [8.1060][8.1060:1075]()
    [% ELSE %]
  • replacement in src/root/topbar.tt at line 149
    [8.339][8.1076:1140]()
    <a href="#hydra-signin" data-toggle="modal">Sign in</a>
    [8.339]
    [8.418]
    <a href="#hydra-signin" data-toggle="modal">Sign in with a Hydra account</a>
  • replacement in src/root/user.tt at line 56
    [8.5979][8.1750:1935]()
    <input type="text" class="span3" name="emailaddress" [% IF !create && user.type == 'persona' %]disabled="disabled"[% END %] [%+ HTML.attributes(value => user.emailaddress) %]/>
    [8.5979]
    [8.6099]
    <input type="text" class="span3" name="emailaddress" [% IF !create && user.username.search('@') %]disabled="disabled"[% END %] [%+ HTML.attributes(value => user.emailaddress) %]/>