マルチ・フェーズ認証 Plack::Middleware

Google の 2 フェーズ・ログインのように、 2 つ以上のログイン方式を組み合わせて認証を Plack::Middleware::Session を使っておこなう Middleware を試作しました。

tociyuki/libplack-middleware-auth-loginchain-perl

認証は、 最初の認証ページを GET して、 そこへ正しいパスワードを POST し、 リダイレクト先の次の認証ページを GET して、 そこへ正しいパスワードを POST してと順を追わない限り失敗するように作ってあります。 認証の途中で、 トップページ等へ移ってから、 再開しようとしても失敗します。 失敗時は、 最初の認証ページへリダイレクトするようにしています。 すべての認証を登録している順番に成功していき、 最後の認証に成功したら、 ユーザ・アカウントに紐付けてあるページへリダイレクトします。

この Middleware では、 認証中のログイン・セッション中に、 もう一度認証を確認することもできます。 その場合でも、 正しい順で GET と POST をおこなわないと認証に失敗します。 ただし、 ログイン・セッション中に、 認証の途中でトップページ等へ移るときは、 ログイン・セッションを維持します。 その場合でも、 途中で抜けた場所から認証を再開しようとしても、 先頭の認証ページへリダイレクトするのは同じです。

example に、 RFC 6238 時刻ベース・使い捨てパスワードと、 通常の平文パスワードの 2 段階認証だけをおこなう単純な PSGI アプリケーションを置いてあります。 この Middleware は、 認証ロジックとログイン・ページの作成をいっさいおこなわず、 Middleware を使う側で用意しておく流儀にしてあります。 builder の enable 時に、 それらを指定します。

use Plack::Builder;
use Plack::Session;
use MyCrypt;

sub auth_totp {
    my($account, $password, $env) = @_;
    exists $users->{$account}{'totpkey'} or return;
    my $key = $users->{$account}{'totpkey'};
    $password eq MyCrypt->totp_sha1_6(time, $key) or return;
    return {'account' => $account, 'redirect_uri' => "/$account"};
}

sub auth_xcrypt {
    my($account, $password, $env) = @_;
    exists $users->{$account}{'password'} or return;
    my $saltyhash = $users->{$account}{'password'};
    $saltyhash eq MyCrypt->xcrypt($password, $saltyhash) or return;
    return {'account' => $account, 'redirect_uri' => "/$account"};
}

# Plack::Response を返すこと
sub auth_render {
    my($req, $param) = @_;
    return render($req, 200, 'login.html', $param);
}

my $app = {
    my($env) = @_;
    my $session = Plack::Session->new($env);
    # ログイン中、 user.account にアカウント名が入ります。
    my $user_account = $session->get('user.account');
    # user.account に紐付けた user.redirect_uri も同様にセットします。
    my $user_redirect_uri = $session->get('user.redirect_uri');
    # ログイン中、 最後に認証された UNIX 時刻が入ります。
    my $user_auth_time = $session->get('user.auth_time');
    #...
};

builder {
    enable 'Session';
    enable 'Auth::LoginChain',
        login_spec => [
            {'uri' => '/login',
             'authenticator' => \&auth_totp,
             'renderer' => \&auth_render,
             'realm' => 'One-Time Password'},
            {'uri' => '/login2',
             'authenticator' => \&auth_xcrypt,
             'renderer' => \&auth_render,
             'realm' => 'Password'},
        ],
        logout_spec => {
            'uri' => '/logout',
            'redirect_uri' => '/'
        };
    $app;
};