#!/usr/bin/env plackup

use warnings;
use strict;

use Plack::Request;
use Plack::Builder;
use Plack::App::File;
use Authen::U2F qw(u2f_challenge u2f_registration_verify u2f_signature_verify);
use Template;
use JSON;

my $t = Template->new;

# base app. finds a template file, includes the session and any current u2f
# vars in the stash and expands the template
my $base_app = sub {
  my ($env) = @_;
  my $req = Plack::Request->new($env);
  my $session = $req->session;

  my $path = $req->request_uri;
  my ($file) = $path eq '/' ? ('index') : $path =~ m{^/(\w+)$};
  return $req->new_response(404)->finalize unless $file && -r "$file.html.tt2";

  my $template = do { local (@ARGV, $/) = ("$file.html.tt2"); <> };
  my $u2f = defined $env->{u2f} ? $env->{u2f} : {};
  $t->process(\$template, {
    %$session,
    u2f => $u2f,
  }, \my $output) || die $t->error;

  my $res = $req->new_response(200);
  $res->headers([ 'Content-type' => 'text/html' ]);
  $res->body($output);
  return $res->finalize;
};

# signup. on GET, just goes through to the base app to display the signup page.
# on POST, inserts the passed username into the session, which we use as our "I
# am logged in indicator
my $signup_app = sub {
  my ($env) = @_;
  my $req = Plack::Request->new($env);
  my $session = $req->session;

  return $base_app->($env) unless $req->method eq 'POST';

  my $params = $req->parameters;
  $session->{$_} = $params->{$_} for keys %$params;
  my $res = $req->new_response;
  $res->redirect('/', 302);
  return $res->finalize;
};

# logout handler. deletes the username in the session, and then returns to the
# root
my $logout_app = sub {
  my ($env) = @_;
  my $req = Plack::Request->new($env);
  my $session = $req->session;

  delete $session->{username};

  my $res = $req->new_response;
  $res->redirect('/', 302);
  return $res->finalize;
};

# register screen. prepares a registration challenge and then goes to the base
# handler, which will build the page from the register template, which has some
# javascript in it to interact with the U2F device
my $register_app = sub {
  my ($env) = @_;
  my $req = Plack::Request->new($env);
  my $session = $req->session;

  my $app_id = 'https://'.$req->uri->host;

  $session->{challenge} = u2f_challenge;

  my $register_request = {
    appId => $app_id,
    registerRequest => {
      version   => 'U2F_V2',
      challenge => $session->{challenge},
    },
    registeredKeys => [ map {
      +{ version => 'U2F_V2', keyHandle => $_ }
    } keys %{$session->{registered_keys}} ],
  };

  $env->{u2f}{register_request} = encode_json($register_request);

  return $base_app->($env);
};

# save registration. recieves the signed registration challenge and verifies
# it. if it's all good, it gets saved in the session (in a real app, it would
# get saved in the user's persistent data)
my $save_registration_app = sub {
  my ($env) = @_;
  my $req = Plack::Request->new($env);
  my $session = $req->session;

  my $app_id = 'https://'.$req->uri->host;

  my ($handle, $key) = u2f_registration_verify(
    challenge         => $session->{challenge},
    app_id            => $app_id,
    origin            => $app_id,
    registration_data => $req->parameters->{registrationData},
    client_data       => $req->parameters->{clientData},
  );

  $session->{registered_keys}{$handle} = $key;

  my $res = $req->new_response;
  $res->redirect('/', 302);
  return $res->finalize;
};

# login. like signup, stores the username in the session to indicate "I am
# logged in". then redirects to a second handler to do the U2F setup
my $login_app = sub {
  my ($env) = @_;
  my $req = Plack::Request->new($env);
  my $session = $req->session;

  return $base_app->($env) unless $req->method eq 'POST';

  my $params = $req->parameters;
  $session->{$_} = $params->{$_} for keys %$params;
  my $res = $req->new_response;
  $res->redirect('/login_u2f', 302);
  return $res->finalize;
};

# login stage 2, prepare a signing (auth) challenge and then go the the base
# handler to create the page from login_u2f template, which has some javascript
# in it to interacte with the U2F device
my $login_u2f_app = sub {
  my ($env) = @_;
  my $req = Plack::Request->new($env);
  my $session = $req->session;

  my $app_id = 'https://'.$req->uri->host;

  $session->{challenge} = u2f_challenge;

  my $sign_request = {
    appId => $app_id,
    challenge => $session->{challenge},
    registeredKeys => [ map {
      +{ version => 'U2F_V2', keyHandle => $_ }
    } keys %{$session->{registered_keys}} ],
  };

  $env->{u2f}{sign_request} = encode_json($sign_request);

  return $base_app->($env);
};

# finish u2f. recieves the signed auth challenge and verifies it. if it checks
# out, the user is now logged in
my $finish_u2f_app = sub {
  my ($env) = @_;
  my $req = Plack::Request->new($env);
  my $session = $req->session;

  my $app_id = 'https://'.$req->uri->host;

  my $key_handle = $req->parameters->{keyHandle};

  u2f_signature_verify(
    challenge         => $session->{challenge},
    app_id            => $app_id,
    origin            => $app_id,
    key_handle        => $key_handle,
    key               => $session->{registered_keys}{$key_handle},
    signature_data    => $req->parameters->{signatureData},
    client_data       => $req->parameters->{clientData},
  );

  my $res = $req->new_response;
  $res->redirect('/', 302);
  return $res->finalize;
};

builder {
  enable 'Session';

  mount '/u2f-api.js' => Plack::App::File->new(file => 'u2f-api.js')->to_app;

  mount '/signup' => $signup_app;

  mount '/logout' => $logout_app;

  mount '/register'          => $register_app;
  mount '/save_registration' => $save_registration_app;

  mount '/login'      => $login_app;
  mount '/login_u2f'  => $login_u2f_app;
  mount '/finish_u2f' => $finish_u2f_app;

  mount '/' => $base_app;
}