The required configuration in hydra.conf:
enable_google_login = 1 google_client_id = 238429sdjkds….apps.googleusercontent.com
and optionally persona_allowed_domains to restrict to one or more domains.
3QWDDLBR5DGFK5Y3TDMK55R2SCHRHFVO2KW2BMZGIYRTIQEZC45AC
GS4SFHCPF76AX2U4NLLJGUISF6Y4AHYWEW3GLTDCKVDDXLSIGXUQC
Z4NL5TWBVTPUTDYAE7YH7C5OPWLF5JKGFEDLYWD5OX4TISW67CUAC
IFY7BYPSDD4FA7LYXENS7DUTGKOKZSS6U5C2LVC26UOJAX3GIMXQC
HL6ZYWHFSOSTGCUEP2K554F5OY4RWAHJYS2MA2V3JMZIG262LFKAC
2DHE2ZAKR4AU7OE6E5CYNFWVGQXLHEX5LFKVU43PMBVX3QW6RHFAC
XOOSZ3WSJVMXS2ZMAS64QYUGAIBOMU5754QDSRB2AYDKGVNNH2FAC
6K5PBUUN4GQAMOVX5BS6YYMHJ3PIF2PPZBTIEQ4R7BQNHC23GS3AC
T4LLYESZ2HUXSLKZ6GNBLVWUVG7R5IDFHYHYO773QIZ6QTOOXR2AC
J5UVLXOK6EDIL5I7VKWH4V2QDS4DPD7FHRK6XBWSXFRQS4JKXFZQC
XJRJ4J7M6BC433TBLWHHKX7UYYCFX6M7ZQLUEYYTREPCSM6M3RDQC
JFW656FT5JSDCA5NBNFGTJABIR2LR5VRDRYI7KF54PYNFDSL4DMAC
LZVO64YG43JD7YMZSCTZNOBS5ROZA4FMPKJW2YOMHX2V5PTGBVWQC
LSZLZHJYGXZTCNH4JUXU7W23MW5PBVM4OBMWRRVNEDROMIBUVQNAC
36ZTCZ4FDV6ILURQEIGFHCP57ALZH6OWYNN3MBXG2QXZSBULBPMAC
2CZSW5S53UW7ACDNT5T3UQNJKFBCXCBYLQ5CL6GVCCOMKBGXZIEQC
QVIQAYZTTDRODPFP6HCWOETJ2OYLECU34FD4K5YKL7ENLG6HI3GAC
D44B24QC6NCED6DVUYP2IJJEVBG2JNBKPBRRSLI5UXQTKA23DJQQC
D5QIOJGPKQJIYBUCSC3MFJ3TXLPNZ2XMI37GXMFRVRFWWR2VMTFAC
4JPNFWRBFCFER74NDL3XZXNQ25YQCPD7E4XKYKF7VCWIESE56G4AC
JARRBLZDQ2JZWY7IUVPTOT7WJMBPMLFLF2MGLVGOYROAAISYGLSAC
PFB5ZUQWW67FRYY54QGTAVTWYMUKXLXRA2XCY3IWK6HZNE6DDLFAC
OEPUOUNBNTHTFZVDXREGBQCKFRCWMVP2MDVK4OA47VK2DBKEWVYAC
P75LFRF4GMC5KSIHC4XDBFRYIO36XUABQ2BN3EJXL42764BFAZUAC
VVRM3EGCPCYA3GQEGZUEMOGEEYTMMUZK46UFU3R46AKZC62ZEMRQC
WDKFN4B2M7BUF4S7X6YA5AKHOJKHLG65LWNQVOSIBTKFEZELBMKAC
QL55ECJ6KMMBUOWQ6LKSOVN7L43CH4S6SPE2AQ3VX3KSGC32RP4AC
DV43UILUJNNU4DJMQ5NIZ2TY5Y4NOPQZXXQQJNKINUKA2VBAJ2QAC
MQMF2LBWWPW2SOZKC3O7P4TBJJ3V6RBVQ52OYA4KCGXS7G6SHWQAC
SZYDW2DG5Z7BR3ICKWDXVUNSMCDSXMYZUB6FQ4W2B2FVZJD6PULQC
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;
# If persona_allowed_domains is set, check if the email address returned is on these domains.
# When not configured, allow all domains.
# If persona_allowed_domains is set, check if the email address
# returned is on these domains. When not configured, allow all
# domains.
if (!$user) {
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 {
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);
}
[% 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 %]
You are signed in as <tt>[% HTML.escape(c.user.username) %]</tt>[% IF c.user.type == 'persona' %] via Persona[% END %].
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 %].
<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>
<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 %]
[% PROCESS auth.tt %]
[% IF personaEnabled %]
[% WRAPPER makeSubMenu title="Sign in" %]
[% 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 %]
<input type="text" class="span3" name="emailaddress" [% IF !create && user.type == 'persona' %]disabled="disabled"[% END %] [%+ HTML.attributes(value => user.emailaddress) %]/>
<input type="text" class="span3" name="emailaddress" [% IF !create && user.username.search('@') %]disabled="disabled"[% END %] [%+ HTML.attributes(value => user.emailaddress) %]/>