├── LICENSE
├── composer.json
├── sample
├── example.php
├── tmpl
│ ├── ask-for-otp.php
│ ├── loggedin.php
│ ├── login-error.php
│ ├── login.php
│ └── show-qr.php
├── users.dat
└── web
│ ├── Users.php
│ └── index.php
└── src
├── FixedBitNotation.php
├── GoogleAuthenticator.php
├── GoogleAuthenticatorInterface.php
├── GoogleQrUrl.php
└── RuntimeException.php
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2010 Thomas Rabaix
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "sonata-project/google-authenticator",
3 | "description": "Library to integrate Google Authenticator into a PHP project",
4 | "license": "MIT",
5 | "type": "library",
6 | "abandoned": true,
7 | "keywords": [
8 | "google authenticator"
9 | ],
10 | "authors": [
11 | {
12 | "name": "Thomas Rabaix",
13 | "email": "thomas.rabaix@gmail.com"
14 | },
15 | {
16 | "name": "Christian Stocker",
17 | "email": "me@chregu.tv"
18 | },
19 | {
20 | "name": "Andre DeMarre",
21 | "homepage": "http://www.devnetwork.net/viewtopic.php?f=50&t=94989"
22 | }
23 | ],
24 | "homepage": "https://github.com/sonata-project/GoogleAuthenticator",
25 | "require": {
26 | "php": "^7.3 || ^8.0"
27 | },
28 | "require-dev": {
29 | "phpunit/phpunit": "^9.5",
30 | "symfony/phpunit-bridge": "^6.0"
31 | },
32 | "autoload": {
33 | "psr-4": {
34 | "Google\\Authenticator\\": "src/",
35 | "Sonata\\GoogleAuthenticator\\": "src/"
36 | }
37 | },
38 | "autoload-dev": {
39 | "psr-4": {
40 | "Google\\Authenticator\\Tests\\": "tests/",
41 | "Sonata\\GoogleAuthenticator\\Tests\\": "tests/"
42 | }
43 | },
44 | "config": {
45 | "sort-packages": true
46 | },
47 | "extra": {
48 | "branch-alias": {
49 | "dev-master": "2.x-dev"
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/sample/example.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | include_once __DIR__.'/../src/FixedBitNotation.php';
15 | include_once __DIR__.'/../src/GoogleAuthenticator.php';
16 | include_once __DIR__.'/../src/GoogleQrUrl.php';
17 |
18 | $secret = 'XVQ2UIGO75XRUKJO';
19 | $code = '846474';
20 |
21 | $g = new \Sonata\GoogleAuthenticator\GoogleAuthenticator();
22 |
23 | echo 'Current Code is: ';
24 | echo $g->getCode($secret);
25 |
26 | echo "\n";
27 |
28 | echo "Check if $code is valid: ";
29 |
30 | if ($g->checkCode($secret, $code)) {
31 | echo "YES \n";
32 | } else {
33 | echo "NO \n";
34 | }
35 |
36 | $secret = $g->generateSecret();
37 | echo "Get a new Secret: $secret \n";
38 | echo "The QR Code for this secret (to scan with the Google Authenticator App: \n";
39 |
40 | echo \Sonata\GoogleAuthenticator\GoogleQrUrl::generate('chregu', $secret, 'GoogleAuthenticatorExample');
41 | echo "\n";
42 |
--------------------------------------------------------------------------------
/sample/tmpl/ask-for-otp.php:
--------------------------------------------------------------------------------
1 |
2 |
please otp
3 |
4 |
24 |
--------------------------------------------------------------------------------
/sample/tmpl/loggedin.php:
--------------------------------------------------------------------------------
1 |
2 |
3 | Hello getUsername(); ?>
4 |
5 |
8 |
9 |
10 | Show QR Code
11 |
12 |
13 |
16 |
17 |
18 | Logout
19 |
20 |
--------------------------------------------------------------------------------
/sample/tmpl/login-error.php:
--------------------------------------------------------------------------------
1 |
2 | Wrong username or password or token.
3 |
4 |
5 | try again
6 |
7 |
--------------------------------------------------------------------------------
/sample/tmpl/login.php:
--------------------------------------------------------------------------------
1 |
2 | please login
3 |
4 |
9 |
--------------------------------------------------------------------------------
/sample/tmpl/show-qr.php:
--------------------------------------------------------------------------------
1 | Please scan this
2 |
3 | with the Google Authenticator App
4 |
5 |
6 | getUsername(), $secret, 'GoogleAuthenticatorExample');
8 | ?>
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/sample/users.dat:
--------------------------------------------------------------------------------
1 | {"chregu":{"password":"foobar"}}
--------------------------------------------------------------------------------
/sample/web/Users.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | class Users
15 | {
16 | public function __construct(string $file = '../users.dat')
17 | {
18 | $this->userFile = $file;
19 |
20 | $this->users = json_decode(file_get_contents($file), true);
21 | }
22 |
23 | public function hasSession()
24 | {
25 | session_start();
26 | if (isset($_SESSION['username'])) {
27 | return $_SESSION['username'];
28 | }
29 |
30 | return false;
31 | }
32 |
33 | public function storeData(User $user): void
34 | {
35 | $this->users[$user->getUsername()] = $user->getData();
36 | file_put_contents($this->userFile, json_encode($this->users));
37 | }
38 |
39 | public function loadUser($name)
40 | {
41 | if (isset($this->users[$name])) {
42 | return new User($name, $this->users[$name]);
43 | }
44 |
45 | return false;
46 | }
47 | }
48 |
49 | class User
50 | {
51 | public function __construct($user, $data)
52 | {
53 | $this->data = $data;
54 | $this->user = $user;
55 | }
56 |
57 | public function auth($pass)
58 | {
59 | if ($this->data['password'] === $pass) {
60 | return true;
61 | }
62 |
63 | return false;
64 | }
65 |
66 | public function startSession(): void
67 | {
68 | $_SESSION['username'] = $this->user;
69 | }
70 |
71 | public function doLogin(): void
72 | {
73 | session_regenerate_id();
74 | $_SESSION['loggedin'] = true;
75 | $_SESSION['ua'] = $_SERVER['HTTP_USER_AGENT'];
76 | }
77 |
78 | public function doOTP(): void
79 | {
80 | $_SESSION['OTP'] = true;
81 | }
82 |
83 | public function isOTP()
84 | {
85 | if (isset($_SESSION['OTP']) && true === $_SESSION['OTP']) {
86 | return true;
87 | }
88 |
89 | return false;
90 | }
91 |
92 | public function isLoggedIn()
93 | {
94 | if (isset($_SESSION['loggedin']) && true === $_SESSION['loggedin'] &&
95 | isset($_SESSION['ua']) && $_SESSION['ua'] === $_SERVER['HTTP_USER_AGENT']
96 | ) {
97 | return $_SESSION['username'];
98 | }
99 |
100 | return false;
101 | }
102 |
103 | public function getUsername()
104 | {
105 | return $this->user;
106 | }
107 |
108 | public function getSecret()
109 | {
110 | if (isset($this->data['secret'])) {
111 | return $this->data['secret'];
112 | }
113 |
114 | return false;
115 | }
116 |
117 | public function generateSecret()
118 | {
119 | $g = new \Sonata\GoogleAuthenticator\GoogleAuthenticator();
120 | $secret = $g->generateSecret();
121 | $this->data['secret'] = $secret;
122 |
123 | return $secret;
124 | }
125 |
126 | public function getData()
127 | {
128 | return $this->data;
129 | }
130 |
131 | public function setOTPCookie(): void
132 | {
133 | $time = floor(time() / (3600 * 24)); // get day number
134 | //about using the user agent: It's easy to fake it, but it increases the barrier for stealing and reusing cookies nevertheless
135 | // and it doesn't do any harm (except that it's invalid after a browser upgrade, but that may be even intented)
136 | $cookie = $time.':'.hash_hmac('sha1', $this->getUsername().':'.$time.':'.$_SERVER['HTTP_USER_AGENT'], $this->getSecret());
137 | setcookie('otp', $cookie, time() + (30 * 24 * 3600), null, null, null, true);
138 | }
139 |
140 | public function hasValidOTPCookie()
141 | {
142 | // 0 = tomorrow it is invalid
143 | $daysUntilInvalid = 0;
144 | $time = (string) floor((time() / (3600 * 24))); // get day number
145 | if (isset($_COOKIE['otp'])) {
146 | [$otpday, $hash] = explode(':', $_COOKIE['otp']);
147 |
148 | if ($otpday >= $time - $daysUntilInvalid && $hash === hash_hmac('sha1', $this->getUsername().':'.$otpday.':'.$_SERVER['HTTP_USER_AGENT'], $this->getSecret())) {
149 | return true;
150 | }
151 | }
152 |
153 | return false;
154 | }
155 | }
156 |
--------------------------------------------------------------------------------
/sample/web/index.php:
--------------------------------------------------------------------------------
1 |
12 |
13 |
14 |
15 | Google Authenticator in PHP demo
16 |
17 |
18 | hasSession()) {
26 | //load the user data from the json storage.
27 | $user = $users->loadUser($username);
28 | //if he clicked logout, destroy the session and redirect to the startscreen.
29 | if (isset($_GET['logout'])) {
30 | session_destroy();
31 | header('Location: ./');
32 | }
33 | // check if the user is logged in.
34 | if ($user->isLoggedIn()) {
35 | include __DIR__.'/../tmpl/loggedin.php';
36 | //show the QR code if whished so
37 | if (isset($_GET['showqr'])) {
38 | $secret = $user->getSecret();
39 | include __DIR__.'/../tmpl/show-qr.php';
40 | }
41 | }
42 | //if the user is in the OTP phase and submit the OTP.
43 | else {
44 | if ($user->isOTP() && isset($_POST['otp'])) {
45 | $g = new \Google\Authenticator\GoogleAuthenticator();
46 | // check if the submitted token is the right one and log in
47 | if ($g->checkCode($user->getSecret(), $_POST['otp'])) {
48 | // do log-in the user
49 | $user->doLogin();
50 | //if the user clicked the "remember the token" checkbox, set the cookie
51 | if (isset($_POST['remember']) && $_POST['remember']) {
52 | $user->setOTPCookie();
53 | }
54 | include __DIR__.'/../tmpl/loggedin.php';
55 | }
56 | //if the OTP is wrong, destroy the session and tell the user to try again
57 | else {
58 | session_destroy();
59 | include __DIR__.'/../tmpl/login-error.php';
60 | }
61 | }
62 | // if the user is neither logged in nor in the OTP phase, show the login form
63 | else {
64 | session_destroy();
65 | include __DIR__.'/../tmpl/login.php';
66 | }
67 | }
68 | exit();
69 | }
70 | //if the username is set in _POST, then we assume the user filled in the login form.
71 |
72 | if (isset($_POST['username'])) {
73 | // check if we can load the user (ie. the user exists in our db)
74 | $user = $users->loadUser($_POST['username']);
75 | if ($user) {
76 | //try to authenticate the password and start the session if it's correct.
77 | if ($user->auth($_POST['password'])) {
78 | $user->startSession();
79 | //check if the user has a valid OTP cookie, so we don't have to
80 | // ask for the current token and can directly log in
81 | if ($user->hasValidOTPCookie()) {
82 | include __DIR__.'/../tmpl/loggedin.php';
83 | $user->doLogin();
84 | }
85 | // try to get the users' secret from the db,
86 | // if he doesn't have one, generate one, store it and show it.
87 | else {
88 | if (!$user->getSecret()) {
89 | include __DIR__.'/../tmpl/loggedin.php';
90 |
91 | $secret = $user->generateSecret();
92 | $users->storeData($user);
93 | $user->doLogin();
94 | include __DIR__.'/../tmpl/show-qr.php';
95 | }
96 | // if the user neither has a valid OTP cookie nor it's the first login
97 | // ask for the OTP
98 | else {
99 | $user->doOTP();
100 | include __DIR__.'/../tmpl/ask-for-otp.php';
101 | }
102 | }
103 |
104 | exit();
105 | }
106 | }
107 | // if we're here, something went wrong, destroy the session and show a login error
108 | session_destroy();
109 |
110 | include __DIR__.'/../tmpl/login-error.php';
111 | exit();
112 | }
113 |
114 | // if neither a session nor tried to submit the login credentials -> login screen
115 | include __DIR__.'/../tmpl/login.php';
116 |
117 | ?>
118 |
119 |
120 |
--------------------------------------------------------------------------------
/src/FixedBitNotation.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Sonata\GoogleAuthenticator;
15 |
16 | /**
17 | * FixedBitNotation.
18 | *
19 | * The FixedBitNotation class is for binary to text conversion. It
20 | * can handle many encoding schemes, formally defined or not, that
21 | * use a fixed number of bits to encode each character.
22 | *
23 | * @author Andre DeMarre
24 | */
25 | final class FixedBitNotation
26 | {
27 | /**
28 | * @var string
29 | */
30 | private $chars;
31 |
32 | /**
33 | * @var int
34 | */
35 | private $bitsPerCharacter;
36 |
37 | /**
38 | * @var int
39 | */
40 | private $radix;
41 |
42 | /**
43 | * @var bool
44 | */
45 | private $rightPadFinalBits;
46 |
47 | /**
48 | * @var bool
49 | */
50 | private $padFinalGroup;
51 |
52 | /**
53 | * @var string
54 | */
55 | private $padCharacter;
56 |
57 | /**
58 | * @var string[]
59 | */
60 | private $charmap;
61 |
62 | /**
63 | * @param int $bitsPerCharacter Bits to use for each encoded character
64 | * @param string $chars Base character alphabet
65 | * @param bool $rightPadFinalBits How to encode last character
66 | * @param bool $padFinalGroup Add padding to end of encoded output
67 | * @param string $padCharacter Character to use for padding
68 | */
69 | public function __construct(int $bitsPerCharacter, ?string $chars = null, bool $rightPadFinalBits = false, bool $padFinalGroup = false, string $padCharacter = '=')
70 | {
71 | // Ensure validity of $chars
72 | if (!\is_string($chars) || ($charLength = \strlen($chars)) < 2) {
73 | $chars =
74 | '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-,';
75 | $charLength = 64;
76 | }
77 |
78 | // Ensure validity of $bitsPerCharacter
79 | if ($bitsPerCharacter < 1) {
80 | // $bitsPerCharacter must be at least 1
81 | $bitsPerCharacter = 1;
82 | $radix = 2;
83 | } elseif ($charLength < 1 << $bitsPerCharacter) {
84 | // Character length of $chars is too small for $bitsPerCharacter
85 | // Set $bitsPerCharacter to greatest acceptable value
86 | $bitsPerCharacter = 1;
87 | $radix = 2;
88 |
89 | while ($charLength >= ($radix <<= 1) && $bitsPerCharacter < 8) {
90 | ++$bitsPerCharacter;
91 | }
92 |
93 | $radix >>= 1;
94 | } elseif ($bitsPerCharacter > 8) {
95 | // $bitsPerCharacter must not be greater than 8
96 | $bitsPerCharacter = 8;
97 | $radix = 256;
98 | } else {
99 | $radix = 1 << $bitsPerCharacter;
100 | }
101 |
102 | $this->chars = $chars;
103 | $this->bitsPerCharacter = $bitsPerCharacter;
104 | $this->radix = $radix;
105 | $this->rightPadFinalBits = $rightPadFinalBits;
106 | $this->padFinalGroup = $padFinalGroup;
107 | $this->padCharacter = $padCharacter[0];
108 | }
109 |
110 | /**
111 | * Encode a string.
112 | *
113 | * @param string $rawString Binary data to encode
114 | */
115 | public function encode($rawString): string
116 | {
117 | // Unpack string into an array of bytes
118 | $bytes = unpack('C*', $rawString);
119 | $byteCount = \count($bytes);
120 |
121 | $encodedString = '';
122 | $byte = array_shift($bytes);
123 | $bitsRead = 0;
124 |
125 | $chars = $this->chars;
126 | $bitsPerCharacter = $this->bitsPerCharacter;
127 | $rightPadFinalBits = $this->rightPadFinalBits;
128 | $padFinalGroup = $this->padFinalGroup;
129 | $padCharacter = $this->padCharacter;
130 |
131 | // Generate encoded output;
132 | // each loop produces one encoded character
133 | for ($c = 0; $c < $byteCount * 8 / $bitsPerCharacter; ++$c) {
134 | // Get the bits needed for this encoded character
135 | if ($bitsRead + $bitsPerCharacter > 8) {
136 | // Not enough bits remain in this byte for the current
137 | // character
138 | // Save the remaining bits before getting the next byte
139 | $oldBitCount = 8 - $bitsRead;
140 | $oldBits = $byte ^ ($byte >> $oldBitCount << $oldBitCount);
141 | $newBitCount = $bitsPerCharacter - $oldBitCount;
142 |
143 | if (!$bytes) {
144 | // Last bits; match final character and exit loop
145 | if ($rightPadFinalBits) {
146 | $oldBits <<= $newBitCount;
147 | }
148 | $encodedString .= $chars[$oldBits];
149 |
150 | if ($padFinalGroup) {
151 | // Array of the lowest common multiples of
152 | // $bitsPerCharacter and 8, divided by 8
153 | $lcmMap = [1 => 1, 2 => 1, 3 => 3, 4 => 1, 5 => 5, 6 => 3, 7 => 7, 8 => 1];
154 | $bytesPerGroup = $lcmMap[$bitsPerCharacter];
155 | $pads = (int) ($bytesPerGroup * 8 / $bitsPerCharacter
156 | - ceil((\strlen($rawString) % $bytesPerGroup)
157 | * 8 / $bitsPerCharacter));
158 | $encodedString .= str_repeat($padCharacter[0], $pads);
159 | }
160 |
161 | break;
162 | }
163 |
164 | // Get next byte
165 | $byte = array_shift($bytes);
166 | $bitsRead = 0;
167 | } else {
168 | $oldBitCount = 0;
169 | $newBitCount = $bitsPerCharacter;
170 | }
171 |
172 | // Read only the needed bits from this byte
173 | $bits = $byte >> 8 - ($bitsRead + $newBitCount);
174 | $bits ^= $bits >> $newBitCount << $newBitCount;
175 | $bitsRead += $newBitCount;
176 |
177 | if ($oldBitCount) {
178 | // Bits come from seperate bytes, add $oldBits to $bits
179 | $bits = ($oldBits << $newBitCount) | $bits;
180 | }
181 |
182 | $encodedString .= $chars[$bits];
183 | }
184 |
185 | return $encodedString;
186 | }
187 |
188 | /**
189 | * Decode a string.
190 | *
191 | * @param string $encodedString Data to decode
192 | * @param bool $caseSensitive
193 | * @param bool $strict Returns null if $encodedString contains
194 | * an undecodable character
195 | */
196 | public function decode($encodedString, $caseSensitive = true, $strict = false): string
197 | {
198 | if (!$encodedString || !\is_string($encodedString)) {
199 | // Empty string, nothing to decode
200 | return '';
201 | }
202 |
203 | $chars = $this->chars;
204 | $bitsPerCharacter = $this->bitsPerCharacter;
205 | $radix = $this->radix;
206 | $rightPadFinalBits = $this->rightPadFinalBits;
207 | $padCharacter = $this->padCharacter;
208 |
209 | // Get index of encoded characters
210 | if ($this->charmap) {
211 | $charmap = $this->charmap;
212 | } else {
213 | $charmap = [];
214 |
215 | for ($i = 0; $i < $radix; ++$i) {
216 | $charmap[$chars[$i]] = $i;
217 | }
218 |
219 | $this->charmap = $charmap;
220 | }
221 |
222 | // The last encoded character is $encodedString[$lastNotatedIndex]
223 | $lastNotatedIndex = \strlen($encodedString) - 1;
224 |
225 | // Remove trailing padding characters
226 | while ($encodedString[$lastNotatedIndex] === $padCharacter[0]) {
227 | $encodedString = substr($encodedString, 0, $lastNotatedIndex);
228 | --$lastNotatedIndex;
229 | }
230 |
231 | $rawString = '';
232 | $byte = 0;
233 | $bitsWritten = 0;
234 |
235 | // Convert each encoded character to a series of unencoded bits
236 | for ($c = 0; $c <= $lastNotatedIndex; ++$c) {
237 | if (!isset($charmap[$encodedString[$c]]) && !$caseSensitive) {
238 | // Encoded character was not found; try other case
239 | if (isset($charmap[$cUpper = strtoupper($encodedString[$c])])) {
240 | $charmap[$encodedString[$c]] = $charmap[$cUpper];
241 | } elseif (isset($charmap[$cLower = strtolower($encodedString[$c])])) {
242 | $charmap[$encodedString[$c]] = $charmap[$cLower];
243 | }
244 | }
245 |
246 | if (isset($charmap[$encodedString[$c]])) {
247 | $bitsNeeded = 8 - $bitsWritten;
248 | $unusedBitCount = $bitsPerCharacter - $bitsNeeded;
249 |
250 | // Get the new bits ready
251 | if ($bitsNeeded > $bitsPerCharacter) {
252 | // New bits aren't enough to complete a byte; shift them
253 | // left into position
254 | $newBits = $charmap[$encodedString[$c]] << $bitsNeeded
255 | - $bitsPerCharacter;
256 | $bitsWritten += $bitsPerCharacter;
257 | } elseif ($c !== $lastNotatedIndex || $rightPadFinalBits) {
258 | // Zero or more too many bits to complete a byte;
259 | // shift right
260 | $newBits = $charmap[$encodedString[$c]] >> $unusedBitCount;
261 | $bitsWritten = 8; //$bitsWritten += $bitsNeeded;
262 | } else {
263 | // Final bits don't need to be shifted
264 | $newBits = $charmap[$encodedString[$c]];
265 | $bitsWritten = 8;
266 | }
267 |
268 | $byte |= $newBits;
269 |
270 | if (8 === $bitsWritten || $c === $lastNotatedIndex) {
271 | // Byte is ready to be written
272 | $rawString .= pack('C', $byte);
273 |
274 | if ($c !== $lastNotatedIndex) {
275 | // Start the next byte
276 | $bitsWritten = $unusedBitCount;
277 | $byte = ($charmap[$encodedString[$c]]
278 | ^ ($newBits << $unusedBitCount)) << 8 - $bitsWritten;
279 | }
280 | }
281 | } elseif ($strict) {
282 | // Unable to decode character; abort
283 | return null;
284 | }
285 | }
286 |
287 | return $rawString;
288 | }
289 | }
290 |
291 | // NEXT_MAJOR: Remove class alias
292 | class_alias('Sonata\GoogleAuthenticator\FixedBitNotation', 'Google\Authenticator\FixedBitNotation', false);
293 |
--------------------------------------------------------------------------------
/src/GoogleAuthenticator.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Sonata\GoogleAuthenticator;
15 |
16 | /**
17 | * @see https://github.com/google/google-authenticator/wiki/Key-Uri-Format
18 | */
19 | final class GoogleAuthenticator implements GoogleAuthenticatorInterface
20 | {
21 | /**
22 | * @var int
23 | */
24 | private $passCodeLength;
25 |
26 | /**
27 | * @var int
28 | */
29 | private $secretLength;
30 |
31 | /**
32 | * @var int
33 | */
34 | private $pinModulo;
35 |
36 | /**
37 | * @var \DateTimeInterface
38 | */
39 | private $instanceTime;
40 |
41 | /**
42 | * @var int
43 | */
44 | private $codePeriod;
45 |
46 | /**
47 | * @var int
48 | */
49 | private $periodSize = 30;
50 |
51 | public function __construct(int $passCodeLength = 6, int $secretLength = 10, ?\DateTimeInterface $instanceTime = null, int $codePeriod = 30)
52 | {
53 | /*
54 | * codePeriod is the duration in seconds that the code is valid.
55 | * periodSize is the length of a period to calculate periods since Unix epoch.
56 | * periodSize cannot be larger than the codePeriod.
57 | */
58 |
59 | $this->passCodeLength = $passCodeLength;
60 | $this->secretLength = $secretLength;
61 | $this->codePeriod = $codePeriod;
62 | $this->periodSize = $codePeriod < $this->periodSize ? $codePeriod : $this->periodSize;
63 | $this->pinModulo = 10 ** $passCodeLength;
64 | $this->instanceTime = $instanceTime ?? new \DateTimeImmutable();
65 | }
66 |
67 | /**
68 | * @param string $secret
69 | * @param string $code
70 | * @param int $discrepancy
71 | */
72 | public function checkCode($secret, $code, $discrepancy = 1): bool
73 | {
74 | /**
75 | * Discrepancy is the factor of periodSize ($discrepancy * $periodSize) allowed on either side of the
76 | * given codePeriod. For example, if a code with codePeriod = 60 is generated at 10:00:00, a discrepancy
77 | * of 1 will allow a periodSize of 30 seconds on either side of the codePeriod resulting in a valid code
78 | * from 09:59:30 to 10:00:29.
79 | *
80 | * The result of each comparison is stored as a timestamp here instead of using a guard clause
81 | * (https://refactoring.com/catalog/replaceNestedConditionalWithGuardClauses.html). This is to implement
82 | * constant time comparison to make side-channel attacks harder. See
83 | * https://cryptocoding.net/index.php/Coding_rules#Compare_secret_strings_in_constant_time for details.
84 | * Each comparison uses hash_equals() instead of an operator to implement constant time equality comparison
85 | * for each code.
86 | */
87 | $periods = floor($this->codePeriod / $this->periodSize);
88 |
89 | $result = 0;
90 | for ($i = -$discrepancy; $i < $periods + $discrepancy; ++$i) {
91 | $dateTime = new \DateTimeImmutable('@'.($this->instanceTime->getTimestamp() - ($i * $this->periodSize)));
92 | $result = hash_equals($this->getCode($secret, $dateTime), $code) ? $dateTime->getTimestamp() : $result;
93 | }
94 |
95 | return $result > 0;
96 | }
97 |
98 | /**
99 | * NEXT_MAJOR: add the interface typehint to $time and remove deprecation.
100 | *
101 | * @param string $secret
102 | * @param float|string|int|\DateTimeInterface|null $time
103 | */
104 | public function getCode($secret, /* \DateTimeInterface */ $time = null): string
105 | {
106 | if (null === $time) {
107 | $time = $this->instanceTime;
108 | }
109 |
110 | if ($time instanceof \DateTimeInterface) {
111 | $timeForCode = floor($time->getTimestamp() / $this->periodSize);
112 | } else {
113 | @trigger_error(
114 | 'Passing anything other than null or a DateTimeInterface to $time is deprecated as of 2.0 '.
115 | 'and will not be possible as of 3.0.',
116 | \E_USER_DEPRECATED
117 | );
118 | $timeForCode = $time;
119 | }
120 |
121 | $base32 = new FixedBitNotation(5, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567', true, true);
122 | $secret = $base32->decode($secret);
123 |
124 | $timeForCode = str_pad(pack('N', $timeForCode), 8, \chr(0), \STR_PAD_LEFT);
125 |
126 | $hash = hash_hmac('sha1', $timeForCode, $secret, true);
127 | $offset = \ord(substr($hash, -1));
128 | $offset &= 0xF;
129 |
130 | $truncatedHash = $this->hashToInt($hash, $offset) & 0x7FFFFFFF;
131 |
132 | return str_pad((string) ($truncatedHash % $this->pinModulo), $this->passCodeLength, '0', \STR_PAD_LEFT);
133 | }
134 |
135 | /**
136 | * NEXT_MAJOR: Remove this method.
137 | *
138 | * @param string $user
139 | * @param string $hostname
140 | * @param string $secret
141 | *
142 | * @deprecated deprecated as of 2.1 and will be removed in 3.0. Use Sonata\GoogleAuthenticator\GoogleQrUrl::generate() instead.
143 | */
144 | public function getUrl($user, $hostname, $secret): string
145 | {
146 | @trigger_error(sprintf(
147 | 'Using %s() is deprecated as of 2.1 and will be removed in 3.0. '.
148 | 'Use Sonata\GoogleAuthenticator\GoogleQrUrl::generate() instead.',
149 | __METHOD__
150 | ), \E_USER_DEPRECATED);
151 |
152 | $issuer = \func_get_args()[3] ?? null;
153 | $accountName = sprintf('%s@%s', $user, $hostname);
154 |
155 | // manually concat the issuer to avoid a change in URL
156 | $url = GoogleQrUrl::generate($accountName, $secret);
157 |
158 | if ($issuer) {
159 | $url .= '%26issuer%3D'.$issuer;
160 | }
161 |
162 | return $url;
163 | }
164 |
165 | public function generateSecret(): string
166 | {
167 | return (new FixedBitNotation(5, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567', true, true))
168 | ->encode(random_bytes($this->secretLength));
169 | }
170 |
171 | private function hashToInt(string $bytes, int $start): int
172 | {
173 | return unpack('N', substr(substr($bytes, $start), 0, 4))[1];
174 | }
175 | }
176 |
177 | // NEXT_MAJOR: Remove class alias
178 | class_alias('Sonata\GoogleAuthenticator\GoogleAuthenticator', 'Google\Authenticator\GoogleAuthenticator', false);
179 |
--------------------------------------------------------------------------------
/src/GoogleAuthenticatorInterface.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Sonata\GoogleAuthenticator;
15 |
16 | interface GoogleAuthenticatorInterface
17 | {
18 | /**
19 | * @param string $secret
20 | * @param string $code
21 | */
22 | public function checkCode($secret, $code, $discrepancy = 1): bool;
23 |
24 | /**
25 | * NEXT_MAJOR: add the interface typehint to $time and remove deprecation.
26 | *
27 | * @param string $secret
28 | * @param float|string|int|\DateTimeInterface|null $time
29 | */
30 | public function getCode($secret, /* \DateTimeInterface */ $time = null): string;
31 |
32 | /**
33 | * NEXT_MAJOR: Remove this method.
34 | *
35 | * @param string $user
36 | * @param string $hostname
37 | * @param string $secret
38 | *
39 | * @deprecated deprecated as of 2.1 and will be removed in 3.0. Use Sonata\GoogleAuthenticator\GoogleQrUrl::generate() instead.
40 | */
41 | public function getUrl($user, $hostname, $secret): string;
42 |
43 | public function generateSecret(): string;
44 | }
45 |
--------------------------------------------------------------------------------
/src/GoogleQrUrl.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Sonata\GoogleAuthenticator;
15 |
16 | /**
17 | * Responsible for QR image url generation.
18 | *
19 | * @see http://goqr.me/api/
20 | * @see http://goqr.me/api/doc/
21 | * @see https://github.com/google/google-authenticator/wiki/Key-Uri-Format
22 | *
23 | * @author Iltar van der Berg
24 | */
25 | final class GoogleQrUrl
26 | {
27 | /**
28 | * Private by design.
29 | */
30 | private function __construct()
31 | {
32 | }
33 |
34 | /**
35 | * Generates a URL that is used to show a QR code.
36 | *
37 | * Account names may not contain a double colon (:). Valid account name
38 | * examples:
39 | * - "John.Doe@gmail.com"
40 | * - "John Doe"
41 | * - "John_Doe_976"
42 | *
43 | * The Issuer may not contain a double colon (:). The issuer is recommended
44 | * to pass along. If used, it will also be appended before the accountName.
45 | *
46 | * The previous examples with the issuer "Acme inc" would result in label:
47 | * - "Acme inc:John.Doe@gmail.com"
48 | * - "Acme inc:John Doe"
49 | * - "Acme inc:John_Doe_976"
50 | *
51 | * The contents of the label, issuer and secret will be encoded to generate
52 | * a valid URL.
53 | *
54 | * @param string $accountName The account name to show and identify
55 | * @param string $secret The secret is the generated secret unique to that user
56 | * @param string|null $issuer Where you log in to
57 | * @param int $size Image size in pixels, 200 will make it 200x200
58 | */
59 | public static function generate(string $accountName, string $secret, ?string $issuer = null, int $size = 200): string
60 | {
61 | if ('' === $accountName || false !== strpos($accountName, ':')) {
62 | throw RuntimeException::InvalidAccountName($accountName);
63 | }
64 |
65 | if ('' === $secret) {
66 | throw RuntimeException::InvalidSecret();
67 | }
68 |
69 | $label = $accountName;
70 | $otpauthString = 'otpauth://totp/%s?secret=%s';
71 |
72 | if (null !== $issuer) {
73 | if ('' === $issuer || false !== strpos($issuer, ':')) {
74 | throw RuntimeException::InvalidIssuer($issuer);
75 | }
76 |
77 | // use both the issuer parameter and label prefix as recommended by Google for BC reasons
78 | $label = $issuer.':'.$label;
79 | $otpauthString .= '&issuer=%s';
80 | }
81 |
82 | $otpauthString = rawurlencode(sprintf($otpauthString, $label, $secret, $issuer));
83 |
84 | return sprintf(
85 | 'https://api.qrserver.com/v1/create-qr-code/?size=%1$dx%1$d&data=%2$s&ecc=M',
86 | $size,
87 | $otpauthString
88 | );
89 | }
90 | }
91 |
92 | // NEXT_MAJOR: Remove class alias
93 | class_alias('Sonata\GoogleAuthenticator\GoogleQrUrl', 'Google\Authenticator\GoogleQrUrl', false);
94 |
--------------------------------------------------------------------------------
/src/RuntimeException.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | * For the full copyright and license information, please view the LICENSE
11 | * file that was distributed with this source code.
12 | */
13 |
14 | namespace Sonata\GoogleAuthenticator;
15 |
16 | /**
17 | * Contains runtime exception templates.
18 | *
19 | * @author Iltar van der Berg
20 | */
21 | final class RuntimeException extends \RuntimeException
22 | {
23 | public static function InvalidAccountName(string $accountName): self
24 | {
25 | return new self(sprintf(
26 | 'The account name may not contain a double colon (:) and may not be an empty string. Given "%s".',
27 | $accountName
28 | ));
29 | }
30 |
31 | public static function InvalidIssuer(string $issuer): self
32 | {
33 | return new self(sprintf(
34 | 'The issuer name may not contain a double colon (:) and may not be an empty string. Given "%s".',
35 | $issuer
36 | ));
37 | }
38 |
39 | public static function InvalidSecret(): self
40 | {
41 | return new self('The secret name may not be an empty string.');
42 | }
43 | }
44 |
45 | // NEXT_MAJOR: Remove class alias
46 | class_alias('Sonata\GoogleAuthenticator\RuntimeException', 'Google\Authenticator\RuntimeException', false);
47 |
--------------------------------------------------------------------------------