How to use Passwordless Login Link Authentication

Login links, also called "magic links", are a passwordless authentication mechanism. Whenever a user wants to login, a new link is generated and sent to them (e.g. using an email). The link fully authenticates the user in the application when clicking on it.

「マジック リンク」とも呼ばれるログイン リンクは、パスワードのない認証メカニズムです。ユーザーがログインするたびに、新しいリンクが生成されて送信されます (電子メールなどを使用)。リンクをクリックすると、アプリケーションでユーザーが完全に認証されます。

This authentication method can help you eliminate most of the customer support related to authentication (e.g. I forgot my password, how can I change or reset my password, etc.)

この認証方法を使用すると、認証に関連するほとんどのカスタマー サポートをなくすことができます (たとえば、パスワードを忘れた、パスワードを変更またはリセットするにはどうすればよいかなど)。

This guide assumes you have setup security and have created a user object in your application. Follow the main security guide if this is not yet the case.

このガイドでは、セキュリティをセットアップし、アプリケーションでユーザー オブジェクトを作成していることを前提としています。これがまだ当てはまらない場合は、メインのセキュリティ ガイドに従ってください。

The login link authenticator is configured using the login_link option under the firewall. You must configure a check_route and signature_properties when enabling this authenticator:

ログイン リンク オーセンティケータは、ファイアウォールの下で login_link オプションを使用して設定されます。このオーセンティケーターを有効にするときは、check_route と signature_properties を構成する必要があります。
  • YAML
    YAML
  • XML
    XML
  • PHP
    PHP
1
2
3
4
5
6
7
# config/packages/security.yaml
security:
    firewalls:
        main:
            login_link:
                check_route: login_check
                signature_properties: ['id']

The signature_properties are used to create a signed URL. This must contain at least one property of your User object that uniquely identifies this user (e.g. the user ID). Read more about this setting further down below.

signature_properties は、署名付き URL を作成するために使用されます。これには、このユーザーを一意に識別するユーザー オブジェクトのプロパティが少なくとも 1 つ含まれている必要があります (ユーザー ID など)。この設定の詳細については、以下をお読みください。

The check_route must be an existing route and it will be used to generate the login link that will authenticate the user. You don't need a controller (or it can be empty) because the login link authenticator will intercept requests to this route:

check_route は既存のルートである必要があり、ユーザーを認証するログイン リンクを生成するために使用されます。ログインリンクオーセンティケーターがこのルートへのリクエストをインターセプトするため、コントローラーは必要ありません (または空にすることができます)。
  • Attributes
    属性
  • YAML
    YAML
  • XML
    XML
  • PHP
    PHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// src/Controller/SecurityController.php
namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;

class SecurityController extends AbstractController
{
    #[Route('/login_check', name: 'login_check')]
    public function check()
    {
        throw new \LogicException('This code should never be reached');
    }
}

Now that the authenticator is able to check the login links, you must create a page where a user can request a login link and log in to your website.

オーセンティケーターがログイン リンクをチェックできるようになったので、ユーザーがログイン リンクを要求して Web サイトにログインできるページを作成する必要があります。

The login link can be generated using the LoginLinkHandlerInterface. The correct login link handler is autowired for you when type-hinting for this interface:

ログイン リンクは、LoginLinkHandlerInterface を使用して生成できます。このインターフェイスのタイプ ヒントを指定すると、正しいログイン リンク ハンドラが自動配線されます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// src/Controller/SecurityController.php
namespace App\Controller;

use App\Repository\UserRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\LoginLink\LoginLinkHandlerInterface;

class SecurityController extends AbstractController
{
    #[Route('/login', name: 'login')]
    public function requestLoginLink(LoginLinkHandlerInterface $loginLinkHandler, UserRepository $userRepository, Request $request)
    {
        // check if login form is submitted
        if ($request->isMethod('POST')) {
            // load the user in some way (e.g. using the form input)
            $email = $request->request->get('email');
            $user = $userRepository->findOneBy(['email' => $email]);

            // create a login link for $user this returns an instance
            // of LoginLinkDetails
            $loginLinkDetails = $loginLinkHandler->createLoginLink($user);
            $loginLink = $loginLinkDetails->getUrl();

            // ... send the link and return a response (see next section)
        }

        // if it's not submitted, render the "login" form
        return $this->render('security/login.html.twig');
    }

    // ...
}
1
2
3
4
5
6
7
8
9
{# templates/security/login.html.twig #}
{% extends 'base.html.twig' %}

{% block body %}
<form action="{{ path('login') }}" method="POST">
    <input type="email" name="email">
    <button type="submit">Send Login Link</button>
</form>
{% endblock %}

In this controller, the user is submitting their email address to the controller. Based on this property, the correct user is loaded and a login link is created using createLoginLink().

このコントローラーでは、ユーザーが電子メール アドレスをコントローラーに送信しています。このプロパティに基づいて、正しいユーザーが読み込まれ、createLoginLink() を使用してログインリンクが作成されます。

Caution

注意

It is important to send this link to the user and not show it directly, as that would allow anyone to login. For instance, use the mailer component to mail the login link to the user. Or use the component to send an SMS to the user's device.

誰でもログインできるようになるため、このリンクをユーザーに送信し、直接表示しないことが重要です。たとえば、メーラー コンポーネントを使用してログイン リンクをユーザーにメールで送信したり、コンポーネントを使用してユーザーのデバイスに SMS を送信したりします。

Now the link is created, it needs to be sent to the user. Anyone with the link is able to login as this user, so you need to make sure to send it to a known device of them (e.g. using e-mail or SMS).

リンクが作成されたので、ユーザーに送信する必要があります。リンクを知っている人は誰でもこのユーザーとしてログインできるため、既知のデバイスにリンクを送信する必要があります (たとえば、電子メールまたは SMS を使用)。

You can send the link using any library or method. However the login link authenticator provides integration with the Notifier component. Use the special LoginLinkNotification to create a notification and send it to the user's email address or phone number:

任意のライブラリまたはメソッドを使用してリンクを送信できます。ただし、ログイン リンク認証システムは Notifier コンポーネントとの統合を提供します。特別な LoginLinkNotification を使用して通知を作成し、ユーザーの電子メール アドレスまたは電話番号に送信します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// src/Controller/SecurityController.php

// ...
use Symfony\Component\Notifier\NotifierInterface;
use Symfony\Component\Notifier\Recipient\Recipient;
use Symfony\Component\Security\Http\LoginLink\LoginLinkNotification;

class SecurityController extends AbstractController
{
    #[Route('/login', name: 'login')]
    public function requestLoginLink(NotifierInterface $notifier, LoginLinkHandlerInterface $loginLinkHandler, UserRepository $userRepository, Request $request)
    {
        if ($request->isMethod('POST')) {
            $email = $request->request->get('email');
            $user = $userRepository->findOneBy(['email' => $email]);

            $loginLinkDetails = $loginLinkHandler->createLoginLink($user);

            // create a notification based on the login link details
            $notification = new LoginLinkNotification(
                $loginLinkDetails,
                'Welcome to MY WEBSITE!' // email subject
            );
            // create a recipient for this user
            $recipient = new Recipient($user->getEmail());

            // send the notification to the user
            $notifier->send($notification, $recipient);

            // render a "Login link is sent!" page
            return $this->render('security/login_link_sent.html.twig');
        }

        return $this->render('security/login.html.twig');
    }

    // ...
}

Note

ノート

This integration requires the Notifier and Mailer components to be installed and configured. Install all required packages using:

この統合には、Notifier コンポーネントと Mailer コンポーネントをインストールして構成する必要があります。次を使用して、必要なすべてのパッケージをインストールします。
1
2
3
$ composer require symfony/mailer symfony/notifier \
    symfony/twig-bundle twig/extra-bundle \
    twig/cssinliner-extra twig/inky-extra

This will send an email like this to the user:

これにより、次のようなメールがユーザーに送信されます。

Tip

ヒント

You can customize this e-mail template by extending the LoginLinkNotification and configuring another htmlTemplate:

この電子メール テンプレートは、LoginLinkNotification を拡張して別の htmlTemplate を構成することでカスタマイズできます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// src/Notifier/CustomLoginLinkNotification
namespace App\Notifier;

use Symfony\Component\Security\Http\LoginLink\LoginLinkNotification;

class CustomLoginLinkNotification extends LoginLinkNotification
{
    public function asEmailMessage(EmailRecipientInterface $recipient, string $transport = null): ?EmailMessage
    {
        $emailMessage = parent::asEmailMessage($recipient, $transport);

        // get the NotificationEmail object and override the template
        $email = $emailMessage->getMessage();
        $email->htmlTemplate('emails/custom_login_link_email.html.twig');

        return $emailMessage;
    }
}

Then, use this new CustomLoginLinkNotification in the controller instead.

次に、代わりにコントローラーでこの新しい CustomLoginLinkNotification を使用します。

Important Considerations

Login links are a convenient way of authenticating users, but it is also considered less secure than a traditional username and password form. It is not recommended to use login links in security critical applications.

ログイン リンクは、ユーザーを認証する便利な方法ですが、従来のユーザー名とパスワードの形式よりも安全性が低いと考えられています。セキュリティ クリティカルなアプリケーションでログイン リンクを使用することはお勧めしません。

However, the implementation in Symfony does have a couple extension points to make the login links more secure. In this section, the most important configuration decisions are discussed:

ただし、Symfony での実装には、ログイン リンクをより安全にするための拡張ポイントがいくつかあります。このセクションでは、最も重要な構成上の決定事項について説明します。

It is important for login links to have a limited lifetime. This reduces the risk that someone can intercept the link and use it to login as somebody else. By default, Symfony defines a lifetime of 10 minutes (600 seconds). You can customize this using the lifetime option:

ログイン リンクの有効期間を制限することが重要です。これにより、誰かがリンクを傍受し、それを使用して別人としてログインするリスクが軽減されます。デフォルトでは、Symfony は 10 分 (600 秒) のライフタイムを定義します。これは、lifetime オプションを使用してカスタマイズできます。
  • YAML
    YAML
  • XML
    XML
  • PHP
    PHP
1
2
3
4
5
6
7
8
# config/packages/security.yaml
security:
    firewalls:
        main:
            login_link:
                check_route: login_check
                # lifetime in seconds
                lifetime: 300

Tip

ヒント

You can also customize the lifetime per link.

リンクごとの有効期間をカスタマイズすることもできます。

Symfony uses signed URLs to implement login links. The advantage of this is that valid links do not have to be stored in a database. The signed URLs allow Symfony to still invalidate already sent login links when important information changes (e.g. a user's email address).

symfony は署名付き URL を使用してログイン リンクを実装します。この利点は、有効なリンクをデータベースに保存する必要がないことです。署名された URL により、Symfony は、重要な情報 (ユーザーのメールアドレスなど) が変更されたときに、既に送信されたログインリンクを無効にすることができます。

The signed URL contains 3 parameters:

署名付き URL には 3 つのパラメーターが含まれています。
expires
The UNIX timestamp when the link expires.
リンクの有効期限が切れたときの UNIX タイムスタンプ。
user
The value returned from $user->getUserIdentifier() for this user.
このユーザーの $user->getUserIdentifier() から返される値。
hash
A hash of expires, user and any configured signature properties. Whenever these change, the hash changes and previous login links are invalidated.
有効期限、ユーザー、および構成された署名プロパティのハッシュ。これらが変更されるたびに、ハッシュが変更され、以前のログインリンクが無効になります。

You can add more properties to the hash by using the signature_properties option:

signature_properties オプションを使用して、ハッシュにさらにプロパティを追加できます。
  • YAML
    YAML
  • XML
    XML
  • PHP
    PHP
1
2
3
4
5
6
7
# config/packages/security.yaml
security:
    firewalls:
        main:
            login_link:
                check_route: login_check
                signature_properties: [id, email]

The properties are fetched from the user object using the PropertyAccess component (e.g. using getEmail() or a public $email property in this example).

プロパティは、PropertyAccess コンポーネントを使用してユーザー オブジェクトから取得されます (たとえば、この例では getEmail() または public $email プロパティを使用)。

Tip

ヒント

You can also use the signature properties to add very advanced invalidating logic to your login links. For instance, if you store a $lastLinkRequestedAt property on your users that you update in the requestLoginLink() controller, you can invalidate all login links whenever a user requests a new link.

署名のプロパティを使用して、非常に高度な無効化ロジックをログイン リンクに追加することもできます。たとえば、requestLoginLink() コントローラーで更新する $lastLinkRequestedAt プロパティをユーザーに保存すると、ユーザーが新しいリンクを要求するたびに、すべてのログイン リンクを無効にすることができます。

It is a common characteristic of login links to limit the number of times it can be used. Symfony can support this by storing used login links in the cache. Enable this support by setting the max_uses option:

使用できる回数を制限することは、ログイン リンクの一般的な特性です。 symfony は使用されたログインリンクをキャッシュに保存することでこれをサポートできます。 max_uses オプションを設定して、このサポートを有効にします。
  • YAML
    YAML
  • XML
    XML
  • PHP
    PHP
1
2
3
4
5
6
7
8
9
10
11
# config/packages/security.yaml
security:
    firewalls:
        main:
            login_link:
                check_route: login_check
                # only allow the link to be used 3 times
                max_uses: 3

                # optionally, configure the cache pool
                #used_link_cache: 'cache.redis'

Make sure there is enough space left in the cache, otherwise invalid links can no longer be stored (and thus become valid again). Expired invalid links are automatically removed from the cache.

キャッシュに十分なスペースが残っていることを確認してください。そうしないと、無効なリンクを保存できなくなります (したがって、再び有効になります)。期限切れの無効なリンクは、キャッシュから自動的に削除されます。

The cache pools are not cleared by the cache:clear command, but removing var/cache/ manually may remove the cache if the cache component is configured to store its cache in that location. Read the Cache guide for more information.

キャッシュ プールは cache:clear コマンドではクリアされませんが、手動で var/cache/ を削除すると、キャッシュ コンポーネントがその場所にキャッシュを保存するように構成されている場合、キャッシュが削除されることがあります。詳細については、キャッシュ ガイドを参照してください。

When setting max_uses to 1, you must take extra precautions to make it work as expected. Email providers and browsers often load a preview of the links, meaning that the link is already invalidated by the preview loader.

max_uses を 1 に設定する場合は、期待どおりに機能させるために特別な予防措置を講じる必要があります。電子メール プロバイダーとブラウザーは、多くの場合、リンクのプレビューを読み込みます。これは、リンクがプレビュー ローダーによって既に無効になっていることを意味します。

In order to solve this issue, first set the check_post_only option let the authenticator only handle HTTP POST methods:

この問題を解決するには、まず check_post_only オプションを設定して、オーセンティケーターが HTTP POST メソッドのみを処理するようにします。
  • YAML
    YAML
  • XML
    XML
  • PHP
    PHP
1
2
3
4
5
6
7
8
# config/packages/security.yaml
security:
    firewalls:
        main:
            login_link:
                check_route: login_check
                check_post_only: true
                max_uses: 1

Then, use the check_route controller to render a page that lets the user create this POST request (e.g. by clicking a button):

次に、check_route コントローラーを使用して、ユーザーがこの POST リクエストを作成できるページをレンダリングします (たとえば、ボタンをクリックすることによって)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// src/Controller/SecurityController.php
namespace App\Controller;

// ...
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;

class SecurityController extends AbstractController
{
    #[Route('/login_check', name: 'login_check')]
    public function check(Request $request)
    {
        // get the login link query parameters
        $expires = $request->query->get('expires');
        $username = $request->query->get('user');
        $hash = $request->query->get('hash');

        // and render a template with the button
        return $this->render('security/process_login_link.html.twig', [
            'expires' => $expires,
            'user' => $username,
            'hash' => $hash,
        ]);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{# templates/security/process_login_link.html.twig #}
{% extends 'base.html.twig' %}

{% block body %}
    <h2>Hi! You are about to login to ...</h2>

    <!-- for instance, use a form with hidden fields to
         create the POST request --->
    <form action="{{ path('login_check') }}" method="POST">
        <input type="hidden" name="expires" value="{{ expires }}">
        <input type="hidden" name="user" value="{{ user }}">
        <input type="hidden" name="hash" value="{{ hash }}">

        <button type="submit">Continue</button>
    </form>
{% endblock %}

Customizing the Success Handler

Sometimes, the default success handling does not fit your use-case (e.g. when you need to generate and return an API key). To customize how the success handler behaves, create your own handler as a class that implements AuthenticationSuccessHandlerInterface:

デフォルトの成功処理がユースケースに合わない場合があります (たとえば、API キーを生成して返す必要がある場合)。成功ハンドラーの動作をカスタマイズするには、AuthenticationSuccessHandlerInterface を実装するクラスとして独自のハンドラーを作成します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// src/Security/Authentication/AuthenticationSuccessHandler.php
namespace App\Security\Authentication;

use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface;

class AuthenticationSuccessHandler implements AuthenticationSuccessHandlerInterface
{
    public function onAuthenticationSuccess(Request $request, TokenInterface $token): JsonResponse
    {
        $user = $token->getUser();
        $userApiToken = $user->getApiToken();

        return new JsonResponse(['apiToken' => $userApiToken]);
    }
}

Then, configure this service ID as the success_handler:

次に、このサービス ID を success_handler として構成します。
  • YAML
    YAML
  • XML
    XML
  • PHP
    PHP
1
2
3
4
5
6
7
8
9
# config/packages/security.yaml
security:
    firewalls:
        main:
            login_link:
                check_route: login_check
                lifetime: 600
                max_uses: 1
                success_handler: App\Security\Authentication\AuthenticationSuccessHandler

Tip

ヒント

If you want to customize the default failure handling, use the failure_handler option and create a class that implements AuthenticationFailureHandlerInterface.

デフォルトの失敗処理をカスタマイズする場合は、failure_handler オプションを使用して、AuthenticationFailureHandlerInterface を実装するクラスを作成します。

The createLoginLink() method accepts a second optional argument to pass the Request object used when generating the login link. This allows to customize features such as the locale used to generate the link:

createLoginLink() メソッドは、ログイン リンクの生成時に使用される theRequest オブジェクトを渡すための 2 番目のオプション引数を受け入れます。これにより、リンクの生成に使用されるロケールなどの機能をカスタマイズできます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// src/Controller/SecurityController.php
namespace App\Controller;

// ...
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Http\LoginLink\LoginLinkHandlerInterface;

class SecurityController extends AbstractController
{
    #[Route('/login', name: 'login')]
    public function requestLoginLink(LoginLinkHandlerInterface $loginLinkHandler, Request $request)
    {
        // check if login form is submitted
        if ($request->isMethod('POST')) {
            // ... load the user in some way

            // clone and customize Request
            $userRequest = clone $request;
            $userRequest->setLocale($user->getLocale() ?? $request->getDefaultLocale());

            // create a login link for $user (this returns an instance of LoginLinkDetails)
            $loginLinkDetails = $loginLinkHandler->createLoginLink($user, $userRequest);
            $loginLink = $loginLinkDetails->getUrl();

            // ...
        }

        return $this->render('security/login.html.twig');
    }

    // ...
}

By default, generated links use the lifetime configured globally but you can change the lifetime per link using the third argument of the createLoginLink() method:

デフォルトでは、生成されたリンクはグローバルに設定された有効期間を使用しますが、createLoginLink() メソッドの 3 番目の引数を使用して、リンクごとの有効期間を変更できます。
1
2
3
// the third optional argument is the lifetime in seconds
$loginLinkDetails = $loginLinkHandler->createLoginLink($user, null, 60);
$loginLink = $loginLinkDetails->getUrl();

6.2

6.2

The argument to customize the link lifetime was introduced in Symfony 6.2.

リンクの有効期間をカスタマイズする引数は、Symfony 6.2 で導入されました。