168 lines
4.4 KiB
PHP
168 lines
4.4 KiB
PHP
<?php
|
|
/**
|
|
* Class Authenticator
|
|
*
|
|
* @created 24.11.2015
|
|
* @author Smiley <smiley@chillerlan.net>
|
|
* @copyright 2015 Smiley
|
|
* @license MIT
|
|
*/
|
|
declare(strict_types=1);
|
|
|
|
namespace chillerlan\Authenticator;
|
|
|
|
use chillerlan\Authenticator\Authenticators\AuthenticatorInterface;
|
|
use chillerlan\Settings\SettingsContainerInterface;
|
|
use InvalidArgumentException;
|
|
use SensitiveParameter;
|
|
use function http_build_query;
|
|
use function rawurlencode;
|
|
use function sprintf;
|
|
use function trim;
|
|
use const PHP_QUERY_RFC3986;
|
|
|
|
/**
|
|
* Yet another Google authenticator implementation!
|
|
*
|
|
* @link https://tools.ietf.org/html/rfc4226
|
|
* @link https://tools.ietf.org/html/rfc6238
|
|
* @link https://github.com/google/google-authenticator
|
|
* @link https://openauthentication.org/specifications-technical-resources/
|
|
* @link https://blog.ircmaxell.com/2014/11/its-all-about-time.html
|
|
*/
|
|
class Authenticator{
|
|
|
|
protected SettingsContainerInterface|AuthenticatorOptions $options;
|
|
protected AuthenticatorInterface $authenticator;
|
|
protected string $mode = AuthenticatorInterface::TOTP;
|
|
|
|
/**
|
|
* Authenticator constructor
|
|
*/
|
|
public function __construct(
|
|
SettingsContainerInterface|AuthenticatorOptions $options = new AuthenticatorOptions,
|
|
string|null $secret = null
|
|
){
|
|
// phpcs:ignore
|
|
$this->setOptions($options);
|
|
|
|
if($secret !== null){
|
|
$this->setSecret($secret);
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* Sets an options instance and invokes an authenticator according to the given mode
|
|
*
|
|
* Please note that this will reset the secret phrase stored with the authenticator instance
|
|
* if a different mode than the current is given.
|
|
*/
|
|
public function setOptions(SettingsContainerInterface|AuthenticatorOptions $options):self{
|
|
$this->options = $options;
|
|
|
|
// invoke a new authenticator interface if necessary
|
|
if(!isset($this->authenticator) || $this->options->mode !== $this->mode){
|
|
$this->mode = $this->options->mode;
|
|
$this->authenticator = new (AuthenticatorInterface::MODES[$this->options->mode])($this->options);
|
|
}
|
|
|
|
$this->authenticator->setOptions($this->options);
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Sets a secret phrase from a Base32 representation
|
|
*
|
|
* @codeCoverageIgnore
|
|
*/
|
|
public function setSecret(#[SensitiveParameter] string $encodedSecret):self{
|
|
$this->authenticator->setSecret($encodedSecret);
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Returns a Base32 representation of the current secret phrase
|
|
*
|
|
* @codeCoverageIgnore
|
|
*/
|
|
public function getSecret():string{
|
|
return $this->authenticator->getSecret();
|
|
}
|
|
|
|
/**
|
|
* Generates a new (secure random) secret phrase
|
|
*
|
|
* @codeCoverageIgnore
|
|
*/
|
|
public function createSecret(int|null $length = null):string{
|
|
return $this->authenticator->createSecret($length);
|
|
}
|
|
|
|
/**
|
|
* Creates a new OTP code with the given secret
|
|
*
|
|
* $data may be
|
|
* - a UNIX timestamp (TOTP)
|
|
* - a counter value (HOTP)
|
|
*
|
|
* @codeCoverageIgnore
|
|
*/
|
|
public function code(int|null $data = null):string{
|
|
return $this->authenticator->code($data);
|
|
}
|
|
|
|
/**
|
|
* Checks the given $code against the secret
|
|
*
|
|
* $data may be
|
|
* - a UNIX timestamp (TOTP)
|
|
* - a counter value (HOTP)
|
|
*
|
|
* @codeCoverageIgnore
|
|
*/
|
|
public function verify(#[SensitiveParameter] string $otp, int|null $data = null):bool{
|
|
return $this->authenticator->verify($otp, $data);
|
|
}
|
|
|
|
/**
|
|
* Creates a URI for use in QR codes for example
|
|
*
|
|
* @link https://github.com/google/google-authenticator/wiki/Key-Uri-Format#parameters
|
|
*
|
|
* @throws \InvalidArgumentException
|
|
*/
|
|
public function getUri(string $label, string $issuer, int|null $hotpCounter = null, bool|null $omitSettings = null):string{
|
|
$label = trim($label);
|
|
$issuer = trim($issuer);
|
|
|
|
if(empty($label) || empty($issuer)){
|
|
throw new InvalidArgumentException('$label and $issuer cannot be empty');
|
|
}
|
|
|
|
$values = [
|
|
'secret' => $this->authenticator->getSecret(),
|
|
'issuer' => $issuer,
|
|
];
|
|
|
|
if($omitSettings !== true){
|
|
$values['digits'] = $this->options->digits;
|
|
$values['algorithm'] = $this->options->algorithm;
|
|
|
|
if($this->mode === AuthenticatorInterface::TOTP){
|
|
$values['period'] = $this->options->period;
|
|
}
|
|
|
|
if($this->mode === AuthenticatorInterface::HOTP && $hotpCounter !== null){
|
|
$values['counter'] = $hotpCounter;
|
|
}
|
|
}
|
|
|
|
$values = http_build_query($values, '', '&', PHP_QUERY_RFC3986);
|
|
|
|
return sprintf('otpauth://%s/%s?%s', $this->mode, rawurlencode($label), $values);
|
|
}
|
|
|
|
}
|