๋ฐ์ํ
๊ฐ์
ํ์ฌ์์ ์์ ํ๋ ํ๋ก์ ํธ๋ก ์ธํ์ฌ OTP๋ฅผ ๋์ ํด์ผ ํ๊ธฐ ๋๋ฌธ์ ์ฌ๋ฌ TOTP ์์ฑ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ๋ณด๋ ์ค ๋ฌธ์ํ๊ฐ ์ ๋๊ธฐ๋ ํ์๊ณ , ์ฌ์ฉ๊ฐ๋ ์ข์๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ๋ฐ ํ๊ณ ๋ก ๊ธฐ์ตํ๊ณ ์ ํ๋ค.
- OTPHP - A PHP One Time Password Libray๋ฅผ ์ฌ์ฉ
- ์ ํจํค์ง์ ์ต์ ๋ฒ์ ์์๋ PHP ๋ฒ์ ์ต์ 8.1 ์ด์ ํ์๋ก ํ๋ค.
TOTP (Time-based one-time password, TOTP)?
- ์๊ฐ ๊ธฐ๋ฐ ์ผํ์ฉ ๋น๋ฐ๋ฒํธ๋ ํ์ฌ ์๊ฐ์ ๊ณ ์ ์ฑ์ ์์ฒ์ผ๋ก ์ฌ์ฉํ์ฌ ์ผํ์ฉ ๋น๋ฐ๋ฒํธ(OTP)๋ฅผ ์์ฑํ๋ ์๊ณ ๋ฆฌ์ฆ
<?php
declare(strict_types=1);
namespace OTPHP;
use function assert;
use InvalidArgumentException;
use function is_int;
/**
* @see \OTPHP\Test\TOTPTest
*/
final class TOTP extends OTP implements TOTPInterface
{
public static function create(
null|string $secret = null,
int $period = self::DEFAULT_PERIOD,
string $digest = self::DEFAULT_DIGEST,
int $digits = self::DEFAULT_DIGITS,
int $epoch = self::DEFAULT_EPOCH
): self {
$totp = $secret !== null
? self::createFromSecret($secret)
: self::generate()
;
$totp->setPeriod($period);
$totp->setDigest($digest);
$totp->setDigits($digits);
$totp->setEpoch($epoch);
return $totp;
}
public static function createFromSecret(string $secret): self
{
$totp = new self($secret);
$totp->setPeriod(self::DEFAULT_PERIOD);
$totp->setDigest(self::DEFAULT_DIGEST);
$totp->setDigits(self::DEFAULT_DIGITS);
$totp->setEpoch(self::DEFAULT_EPOCH);
return $totp;
}
public static function generate(): self
{
return self::createFromSecret(self::generateSecret());
}
public function getPeriod(): int
{
$value = $this->getParameter('period');
(is_int($value) && $value > 0) || throw new InvalidArgumentException('Invalid "period" parameter.');
return $value;
}
public function getEpoch(): int
{
$value = $this->getParameter('epoch');
(is_int($value) && $value >= 0) || throw new InvalidArgumentException('Invalid "epoch" parameter.');
return $value;
}
public function expiresIn(): int
{
$period = $this->getPeriod();
return $period - (time() % $this->getPeriod());
}
public function at(int $input): string
{
return $this->generateOTP($this->timecode($input));
}
public function now(): string
{
return $this->at(time());
}
/**
* If no timestamp is provided, the OTP is verified at the actual timestamp. When used, the leeway parameter will
* allow time drift. The passed value is in seconds.
*/
public function verify(string $otp, null|int $timestamp = null, null|int $leeway = null): bool
{
$timestamp ??= time();
$timestamp >= 0 || throw new InvalidArgumentException('Timestamp must be at least 0.');
if ($leeway === null) {
return $this->compareOTP($this->at($timestamp), $otp);
}
$leeway = abs($leeway);
$leeway < $this->getPeriod() || throw new InvalidArgumentException(
'The leeway must be lower than the TOTP period'
);
return $this->compareOTP($this->at($timestamp - $leeway), $otp)
|| $this->compareOTP($this->at($timestamp), $otp)
|| $this->compareOTP($this->at($timestamp + $leeway), $otp);
}
public function getProvisioningUri(): string
{
$params = [];
if ($this->getPeriod() !== 30) {
$params['period'] = $this->getPeriod();
}
if ($this->getEpoch() !== 0) {
$params['epoch'] = $this->getEpoch();
}
return $this->generateURI('totp', $params);
}
public function setPeriod(int $period): void
{
$this->setParameter('period', $period);
}
public function setEpoch(int $epoch): void
{
$this->setParameter('epoch', $epoch);
}
/**
* @return array<non-empty-string, callable>
*/
protected function getParameterMap(): array
{
return array_merge(
parent::getParameterMap(),
[
'period' => static function ($value): int {
(int) $value > 0 || throw new InvalidArgumentException('Period must be at least 1.');
return (int) $value;
},
'epoch' => static function ($value): int {
(int) $value >= 0 || throw new InvalidArgumentException(
'Epoch must be greater than or equal to 0.'
);
return (int) $value;
},
]
);
}
/**
* @param array<non-empty-string, mixed> $options
*/
protected function filterOptions(array &$options): void
{
parent::filterOptions($options);
if (isset($options['epoch']) && $options['epoch'] === 0) {
unset($options['epoch']);
}
ksort($options);
}
/**
* @param 0|positive-int $timestamp
*
* @return 0|positive-int
*/
private function timecode(int $timestamp): int
{
$timecode = (int) floor(($timestamp - $this->getEpoch()) / $this->getPeriod());
assert($timecode >= 0);
return $timecode;
}
}
Laravel ํ๋ก์ ํธ์์ ์ฌ์ฉ ์์
- ์ด๊ธฐ ํ์ ๊ฐ์ ํ QR Code ๋ฐํ
- QR ์ฝ๋๋ฅผ Google Authenticator๋ก Scan ํ ํด๋น ์ซ์ 6์๋ฆฌ ์
๋ ฅ
- 2FA ์ธ์ฆ
- 1๋ฒ์์ ๋ถ์ฌ ๋ Key ๊ฐ์ ์ ๋ ฅ
- 1์ฐจ ์ฝ๋ ์ ๋ ฅ (QR ์ด๋ฏธ์ง์ ์ซ์ ๊ฐ๊ณผ ํด๋น ์ซ์๋ฆฌ 6์๋ฆฌ ๋์ผ ์ฌ๋ถ ํ์ธ)
- 2์ฐจ ์ฝ๋ ์ ๋ ฅ (1์ฐจ ์ฝ๋ ๋ฐํ ํ 30์ด delivery ์๊ฐ ๋ถ์ฌ ํ ์ซ์ 6์๋ฆฌ ํ์ธ)
- 2FA ์ธ์ฆ
- OTP Key ๊ฐ์ ์ ๋ ฅ โก๏ธ ๋ฐ์ดํฐ ๋ฑ๋ก
๋ฐ์ํ