新規会員登録やパスワードリセットなどで、期間限定のトークンを発行することがあります。普通はトークンの発行時にトークン自身と有効期限をDBに登録し、トークン検証時にレコードの有効期限を確認します。
ですが、さまざまな事情でDBにトークンを記録したくないとか、そもそもDBが使用できないというような場合があります。そういうときにDBを使用せずに期間限定トークンを発行する方法を考えてみました。
クリティカルな用途には向かないと思いますので、実際に採用するかどうかは慎重にご検討ください。
アイデア
アイデアとしては、トークン内にトークン発行日時を埋め込んでしまえばいいというものです。
トークンを解読して発行日時を書き換えられたら意味がないので、トークンは一方向ハッシュ化します。
一方向ハッシュ化したら発行側でもトークンを検証できませんが、そこは現在日時を使って再度トークンを生成しなおすことで対応します。
実装
実装は以下のようになります。
function generate_token($salt)
{
$epoch = time();
return _generate_token($salt, $epoch);
}
function verify_token($salt, $token)
{
$epoch = time();
$new_token = _generate_token($salt, $epoch);
return $token == $new_token;
}
function _generate_token($salt, $epoch)
{
return crypt((string)$epoch, $salt);
}
これで有効期間1秒のトークンが実装できました。
有効期間1秒ではさすがに使い物になりません。有効期間を1時間にする場合、3600秒前までトークン検証を繰り返します。
function verify_token($salt, $token)
{
$epoch = time();
for($i = 0; $i < 3600; $i++) {
$new_token = _generate_token($salt, $epoch - $i);
if($token == $new_token) {
return true;
}
}
return false;
}
検証のたびに3600回もループを回していては負荷が問題になりますので、エポック秒を 3600 で割って使います。
function _generate_token($salt, $epoch)
{
return crypt((string)floor($epoch / 3600), $salt);
}
しかしこれでは有効期間は1秒から3600秒のどれかになってしまいます。12時台であれば、12時0分でも12時30分でも12時59分でも、全部有効期限は12時59分59秒ってことですね。運良く12時直後にトークンを発行したら1時間近く有効ですが、12時59分付近だと有効期限は1分になってしまい、さすがに実用的ではありません。
そこで以下のように修正します。
function verify_token($salt, $token)
{
$epoch = time();
$new_token = _generate_token($salt, $epoch);
if($token == $new_token) {
return true;
}
$new_token = _generate_token($salt, $epoch - 3600);
if($token == $new_token) {
return true;
}
return false;
}
これで有効期間は1時間から1時間59分59秒となりました。1時間有効なトークンと言いつつ最長で2時間近く有効になりますので、かなりいい加減な仕様になります。つまりは、その程度のいい加減さが許される程度の用途にしか使えないということです。
欠点
他にも欠点がありまして、トークンにユーザを特定する情報を埋め込むことができません。通常のトークン実装であれば DB をトークンで検索してレコードに紐づけてあったユーザIDなりを取得すればいいのですが、DB を使用していませんから取得できる情報はユーザから渡されるもののみになります。ですので、ユーザIDなどの情報を別パラメータで引き回しておく必要があります。
https://example.com/?token=xxx // ×
https://example.com/?userid=yyy&token=xxx // 〇