Failed Conditions
Pull Request — master (#16)
by Adrien
05:43
created

HasOtp::isOtp()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 1
c 1
b 0
f 0
dl 0
loc 3
ccs 2
cts 2
cp 1
rs 10
cc 1
nc 1
nop 0
crap 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Ecodev\Felix\Model\Traits;
6
7
use Doctrine\ORM\Mapping as ORM;
8
use Exception;
9
use GraphQL\Doctrine\Annotation as API;
10
use OTPHP;
11
12
/**
13
 * Trait for a user with second factor authentication using OTP.
14
 */
15
trait HasOtp
16
{
17
    /**
18
     * @var bool
19
     *
20
     * @ORM\Column(type="boolean", options={"default" = 0})
21
     */
22
    private $otp = false;
23
24
    /**
25
     * @API\Exclude
26
     *
27
     * @ORM\Column(type="string", length=255, nullable=true)
28
     */
29
    private ?string $otpUri = null;
30
31
    /**
32
     * Must be implemented by the user class to use the login as the key label.
33
     */
34
    abstract public function getLogin(): ?string;
35
36
    /**
37
     * Enable 2FA for the user
38
     * This should be only enabled after otpUri has been generated, stored and verified.
39
     *
40
     * @API\Exclude
41
     */
42 1
    public function setOtp(bool $otp): void
43
    {
44 1
        if ($otp === true && !empty($this->otpUri)) {
45 1
            throw new Exception('Cannot enable OTP without a secret');
46
        }
47
48 1
        $this->otp = $otp;
49
    }
50
51
    /**
52
     * Whether the user has 2FA enabled.
53
     */
54 2
    public function isOtp(): bool
55
    {
56 2
        return $this->otp;
57
    }
58
59
    /**
60
     * Returns the OTP provisionning URI (to display QR code).
61
     *
62
     * @API\Exclude
63
     */
64 2
    public function getOtpUri(): ?string
65
    {
66 2
        return $this->otpUri;
67
    }
68
69
    /**
70
     * Generate and store a new OTP secret.
71
     *
72
     * @param string $issuer identify the service that provided the OTP (application or host name)
73
     * @param int $period number of seconds an OTP will be valid
74
     * @param string $digest digest algorithm: sha1 or sha256, but Google Authenticator only works with sha1
75
     * @param int $digits length of the OTP numeric code
76
     *
77
     * @API\Exclude
78
     */
79 2
    public function createOtpSecret(string $issuer, int $period = 30, string $digest = 'sha1', int $digits = 6): void
80
    {
81 2
        $this->revokeOtpSecret();
82
83 2
        $totp = OTPHP\TOTP::create(null, $period, $digest, $digits);
84 2
        $label = $this->getLogin();
85 2
        if (!$label) {
86
            throw new Exception('User must have a login to initialize OTP');
87
        }
88 2
        $totp->setLabel($label);
89 2
        $totp->setIssuer($issuer);
90 2
        $this->otpUri = $totp->getProvisioningUri();
91
    }
92
93
    /**
94
     * Revoke the existing OTP secret
95
     * This will also disable 2FA.
96
     *
97
     * @API\Exclude
98
     */
99 2
    public function revokeOtpSecret(): void
100
    {
101 2
        $this->otp = false;
102 2
        $this->otpUri = null;
103
    }
104
105
    /**
106
     * Verify an OTP received from the user.
107
     *
108
     * @API\Exclude
109
     */
110
    public function verifyOtp(string $received): bool
111
    {
112
        if (empty($this->otpUri)) {
113
            return false;
114
        }
115
        $otp = OTPHP\Factory::loadFromProvisioningUri($this->otpUri);
116
117
        return $otp->verify($received);
118
    }
119
}
120