Skip to content

Commit 898d784

Browse files
authored
Merge pull request #1116 from CakeDC/feature/magic-link
Feature/magic link
2 parents 9e21a37 + 228524c commit 898d784

30 files changed

+738
-67
lines changed

.github/workflows/ci.yml

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ jobs:
1313
strategy:
1414
fail-fast: false
1515
matrix:
16-
php-version: ['8.1', '8.2', '8.3']
16+
php-version: ['8.1', '8.2', '8.3', '8.4']
1717
db-type: [sqlite, mysql, pgsql]
1818
prefer-lowest: ['']
1919

@@ -45,7 +45,7 @@ jobs:
4545
run: echo "::set-output name=date::$(date +'%Y-%m')"
4646

4747
- name: Cache composer dependencies
48-
uses: actions/cache@v1
48+
uses: actions/cache@v4
4949
with:
5050
path: ${{ steps.composer-cache.outputs.dir }}
5151
key: ${{ runner.os }}-composer-${{ steps.key-date.outputs.date }}-${{ hashFiles('composer.json') }}-${{ matrix.prefer-lowest }}
@@ -59,22 +59,22 @@ jobs:
5959
fi
6060
6161
- name: Setup problem matchers for PHPUnit
62-
if: matrix.php-version == '8.1' && matrix.db-type == 'mysql'
62+
if: matrix.php-version == '8.2' && matrix.db-type == 'mysql'
6363
run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"
6464

6565
- name: Run PHPUnit
6666
run: |
6767
if [[ ${{ matrix.db-type }} == 'sqlite' ]]; then export DB_URL='sqlite:///:memory:'; fi
6868
if [[ ${{ matrix.db-type }} == 'mysql' ]]; then export DB_URL='mysql://root:root@127.0.0.1/cakephp?encoding=utf8'; fi
6969
if [[ ${{ matrix.db-type }} == 'pgsql' ]]; then export DB_URL='postgres://postgres:postgres@127.0.0.1/postgres'; fi
70-
if [[ ${{ matrix.php-version }} == '8.1' ]]; then
70+
if [[ ${{ matrix.php-version }} == '8.2' ]]; then
7171
export CODECOVERAGE=1 && vendor/bin/phpunit --display-deprecations --display-incomplete --display-skipped --coverage-clover=coverage.xml
7272
else
7373
vendor/bin/phpunit
7474
fi
7575
7676
- name: Submit code coverage
77-
if: matrix.php-version == '8.1'
77+
if: matrix.php-version == '8.2'
7878
uses: codecov/codecov-action@v1
7979

8080
cs-stan:
@@ -87,7 +87,7 @@ jobs:
8787
- name: Setup PHP
8888
uses: shivammathur/setup-php@v2
8989
with:
90-
php-version: '8.1'
90+
php-version: '8.2'
9191
extensions: mbstring, intl, apcu
9292
coverage: none
9393

@@ -100,7 +100,7 @@ jobs:
100100
run: echo "::set-output name=date::$(date +'%Y-%m')"
101101

102102
- name: Cache composer dependencies
103-
uses: actions/cache@v1
103+
uses: actions/cache@v4
104104
with:
105105
path: ${{ steps.composer-cache.outputs.dir }}
106106
key: ${{ runner.os }}-composer-${{ steps.key-date.outputs.date }}-${{ hashFiles('composer.json') }}-${{ matrix.prefer-lowest }}

Docs/Documentation/Installation.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,16 @@ page for more details.
3232
If you want to use Google Authenticator features...
3333

3434
```
35-
composer require robthree/twofactorauth:"^1.6"
35+
composer require robthree/twofactorauth:@stable
36+
composer require endroid/qr-code:@stable
3637
```
38+
NOTE: The plugin uses `endroid/qr-code` as the QR code provider by default. You can use any other changing the configuration key:
3739

38-
NOTE: you'll need to enable `OneTimePasswordAuthenticator.login` in your config/users.php file:
40+
```php
41+
'OneTimePasswordAuthenticator.qrcodeprovider' => YOUR_PROVIDER,
42+
```
43+
44+
To finish configuration, you'll need to enable `OneTimePasswordAuthenticator.login` in your config/users.php file:
3945

4046
```php
4147
'OneTimePasswordAuthenticator.login' => true,

Docs/Documentation/MagicLink.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
Magic Link
2+
===============================
3+
The plugin offers an easy way to add one-click login capabilities (through a link sent to user email)
4+
5+
6+
Installation Requirement
7+
------------------------
8+
There are no package requirements for using this feature. `Users.Email.required` setting must be set to true
9+
10+
By default the feature is enabled. The default configuration is:
11+
12+
```php
13+
'OneTimeLogin' => [
14+
'enabled' => true,
15+
'tokenLifeTime' => 600,
16+
'DeliveryHandlers' => [
17+
'Email' => [
18+
'className' => \CakeDC\Users\Model\Behavior\OneTimeDelivery\EmailDelivery::class
19+
],
20+
],
21+
],
22+
```
23+
* `tokenLifeTime`: 60 minutes by default. You can set how many seconds you want your token to be valid.
24+
* `DelveryHandlers`: Email delivery is included but it can be easily extended implementing `\CakeDC\Users\Model\Behavior\OneTimeDelivery\DeliveryInterface` (i.e SmsDelivery, PushDelivery, etc)
25+
26+
Enabling
27+
--------
28+
29+
The feature is enabled by default but you can disable it application-wide and enable via Middleware (or any other way) for specific situations using:
30+
31+
```php
32+
Configure::write('OneTimeLogin.enabled', true),
33+
```
34+
35+
Disabling
36+
---------
37+
You can disable it by adding this in your config/users.php file:
38+
39+
```php
40+
'OneTimeLogin.enabled' => false,
41+
```
42+
43+
How does it work
44+
----------------
45+
When the user access the login page, there is a new button `Send me a login link`. On click, the user will be redirected to a page to enter his email address. Once it is submitted, the user will receive an email with the link to automatically login.
46+
47+
Two-factor authentication
48+
----------------
49+
The two-factor authentication is skipped by default for this feature since the user must actively click on a link sent to his email address.
50+
51+
If you want to enable it by adding this in your config/users.php file:
52+
53+
```php
54+
'Auth.Authenticators.OneTimeToken.skipTwoFactorVerify' => false,
55+
```
56+
57+
ReCaptcha
58+
----------------
59+
ReCaptcha will be added automatically to the request login link form if `Users.reCaptcha.login` is enabled. We strongly recommend having ReCaptcha enabled, because it's a public form that could be targeted by an attacker to send multiple requests.
60+

Docs/Home.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ Documentation
2222
* [Social Authentication](Documentation/SocialAuthentication.md)
2323
* [Two Factor Authenticator](Documentation/Two-Factor-Authenticator.md)
2424
* [Webauthn Two-Factor Authentication (Yubico Key compatible)](Documentation/WebauthnTwoFactorAuthenticator.md)
25+
* [Magic Link](Documentation/MagicLink.md)
2526
* [UserHelper](Documentation/UserHelper.md)
2627
* [AuthLinkHelper](Documentation/AuthLinkHelper.md)
2728
* [Events](Documentation/Events.md)

composer.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
"require": {
3131
"php": ">=8.1",
3232
"cakephp/cakephp": "^5.0",
33-
"cakedc/auth": "^10.0",
33+
"cakedc/auth": "^10.1",
3434
"cakephp/authorization": "^3.0",
3535
"cakephp/authentication": "^3.0"
3636
},
@@ -42,7 +42,8 @@
4242
"league/oauth2-linkedin": "@stable",
4343
"luchianenco/oauth2-amazon": "^1.1",
4444
"google/recaptcha": "@stable",
45-
"robthree/twofactorauth": "^2.0",
45+
"robthree/twofactorauth": "^3.0 || ^2.0",
46+
"endroid/qr-code": "^6.0 || ^5.0",
4647
"league/oauth1-client": "^1.7",
4748
"cakephp/cakephp-codesniffer": "^5.0",
4849
"web-auth/webauthn-lib": "^4.4",
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
use Migrations\AbstractMigration;
5+
6+
class AddLoginTokenToUsers extends AbstractMigration
7+
{
8+
/**
9+
* Change Method.
10+
*
11+
* More information on this method is available here:
12+
* https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method
13+
* @return void
14+
*/
15+
public function change(): void
16+
{
17+
$table = $this->table('users');
18+
$table->addColumn('login_token', 'string', [
19+
'default' => null,
20+
'limit' => 32,
21+
'null' => true,
22+
])->addColumn('login_token_date', 'datetime', [
23+
'default' => null,
24+
'null' => true,
25+
])->addColumn('token_send_requested', 'boolean', [
26+
'default' => false,
27+
'null' => false,
28+
])->addIndex('login_token');
29+
$table->update();
30+
}
31+
}

config/permissions.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,9 @@
7979
'webauthn2faRegisterOptions',
8080
'webauthn2faAuthenticate',
8181
'webauthn2faAuthenticateOptions',
82+
'requestLoginLink',
83+
'sendLoginLink',
84+
'singleTokenLogin',
8285
],
8386
'bypassAuth' => true,
8487
],

config/users.php

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@
160160
// The algorithm used
161161
'algorithm' => enum_exists(\RobThree\Auth\Algorithm::class) ? \RobThree\Auth\Algorithm::Sha1 : null,
162162
// QR-code provider (more on this later)
163-
'qrcodeprovider' => null,
163+
'qrcodeprovider' => class_exists('\RobThree\Auth\Providers\Qr\EndroidQrCodeProvider') ? (new \RobThree\Auth\Providers\Qr\EndroidQrCodeProvider()) : null,
164164
// Random Number Generator provider (more on this later)
165165
'rngprovider' => null,
166166
],
@@ -174,6 +174,19 @@
174174
\CakeDC\Auth\Authentication\TwoFactorProcessor\Webauthn2faProcessor::class,
175175
\CakeDC\Auth\Authentication\TwoFactorProcessor\OneTimePasswordProcessor::class,
176176
],
177+
/**
178+
* @see https://github.com/CakeDC/users/blob/14.next-cake5/Docs/Documentation/MagicLink.md
179+
*/
180+
'OneTimeLogin' => [
181+
'enabled' => true,
182+
'thresholdTimeout' => 60,
183+
'tokenLifeTime' => 600,
184+
'DeliveryHandlers' => [
185+
'Email' => [
186+
'className' => \CakeDC\Users\Model\Behavior\OneTimeDelivery\EmailDelivery::class
187+
]
188+
]
189+
],
177190
// default configuration used to auto-load the Auth Component, override to change the way Auth works
178191
'Auth' => [
179192
'Authentication' => [
@@ -219,6 +232,13 @@
219232
'className' => 'CakeDC/Users.SocialPendingEmail',
220233
'skipTwoFactorVerify' => true,
221234
],
235+
'OneTimeToken' => [
236+
'className' => 'CakeDC/Auth.OneTimeToken',
237+
'skipTwoFactorVerify' => true,
238+
'loginUrl' => [
239+
'/login',
240+
],
241+
]
222242
],
223243
'Identifiers' => [
224244
'Password' => [

phpstan-baseline.neon

Lines changed: 0 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,6 @@ parameters:
1010
count: 2
1111
path: src/Controller/Component/LoginComponent.php
1212

13-
-
14-
message: "#^Access to an undefined property CakeDC\\\\Users\\\\Controller\\\\UsersController\\:\\:\\$Authentication\\.$#"
15-
count: 2
16-
path: src/Controller/UsersController.php
17-
1813
-
1914
message: "#^Access to an undefined property CakeDC\\\\Users\\\\Controller\\\\UsersController\\:\\:\\$OneTimePasswordAuthenticator\\.$#"
2015
count: 3
@@ -105,11 +100,6 @@ parameters:
105100
count: 1
106101
path: src/Model/Behavior/BaseTokenBehavior.php
107102

108-
-
109-
message: "#^Access to an undefined property Cake\\\\Datasource\\\\EntityInterface\\:\\:\\$social_accounts\\.$#"
110-
count: 2
111-
path: src/Model/Behavior/LinkSocialBehavior.php
112-
113103
-
114104
message: "#^Access to an undefined property Cake\\\\ORM\\\\Table\\:\\:\\$SocialAccounts\\.$#"
115105
count: 5
@@ -120,21 +110,6 @@ parameters:
120110
count: 1
121111
path: src/Model/Behavior/LinkSocialBehavior.php
122112

123-
-
124-
message: "#^Access to an undefined property Cake\\\\Datasource\\\\EntityInterface\\:\\:\\$password\\.$#"
125-
count: 1
126-
path: src/Model/Behavior/PasswordBehavior.php
127-
128-
-
129-
message: "#^Access to an undefined property Cake\\\\Datasource\\\\EntityInterface\\:\\:\\$password_confirm\\.$#"
130-
count: 1
131-
path: src/Model/Behavior/PasswordBehavior.php
132-
133-
-
134-
message: "#^Call to an undefined method Cake\\\\Datasource\\\\EntityInterface\\:\\:checkPassword\\(\\)\\.$#"
135-
count: 1
136-
path: src/Model/Behavior/PasswordBehavior.php
137-
138113
-
139114
message: "#^Call to an undefined method Cake\\\\ORM\\\\Table\\:\\:findByUsernameOrEmail\\(\\)\\.$#"
140115
count: 1
@@ -209,9 +184,3 @@ parameters:
209184
message: "#^Method CakeDC\\\\Users\\\\Model\\\\Entity\\\\User\\:\\:_setTos\\(\\) should return bool but returns string\\.$#"
210185
count: 1
211186
path: src/Model/Entity/User.php
212-
213-
-
214-
message: "#^Parameter \\#2 \\$usersTable of class CakeDC\\\\Users\\\\Webauthn\\\\Repository\\\\UserCredentialSourceRepository constructor expects CakeDC\\\\Users\\\\Model\\\\Table\\\\UsersTable\\|null, Cake\\\\ORM\\\\Table given\\.$#"
215-
count: 1
216-
path: src/Webauthn/BaseAdapter.php
217-

src/Command/UsersDeleteUserCommand.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ public function execute(Arguments $args, ConsoleIo $io)
4646
*/
4747
$UsersTable = $this->getTableLocator()->get('Users');
4848
/**
49-
* @var \Cake\Datasource\EntityInterface $user
49+
* @var \CakeDC\Users\Model\Entity\User $user
5050
*/
5151
$user = $UsersTable->find()->where(['username' => $username])->firstOrFail();
5252
if (isset($UsersTable->SocialAccounts)) {

0 commit comments

Comments
 (0)