Posted at

Symfony認証でパスワード無しでログインする方法

More than 1 year has passed since last update.

脆弱性の話のようなタイトルですが、例えば管理とユーザーのページが別々に存在していて、運用のために管理画面から任意のユーザーの画面にアクセスしたい場合です。ユーザーのパスワードはハッシュ化されてわからないので、パスワードなしでログインすることになります。

参考にしたのは、SymfonyのGitHubでの「Sharing security context across multiple firewalls #11836」というIssueです。またSymfony3.2での動作を確認してます。


security.yml

次のようなsecurity.ymlがあったとします。Guardを使って認証してますが、おそらくそこは関係ないはず。

AppBundle:User\Userがユーザーのエンティティを表しているとします。

管理者はadminにログインした後、任意のユーザーにログインします。それにはuserというファイヤウォールを突破する必要があります。

security:

providers:
user_db_provider:
entity:
class: AppBundle:User\User
property: mail
firewalls:
admin:
pattern: ^/admin/
# ... 省略
user:
context: user # <- これがログインするコンテクスト
pattern: ^/user/
anonymous: ~
guard:
authenticators:
- app.tanto_authenticator
access_control:
- { path: ^/user/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/user/, roles: ROLE_USER}

ポイントはcontext:で指定している文字列(user)です。


Controllerのコード

管理者用コントローラーのサンプルコードです。

loginActionを使ってuser_idのユーザーにログインします。最初にユーザーのエンティティを取得してから、loginToContextでログインできます。

    /**

* @Config\Route("/admin/user/{user_id}/login", name="admin-user-login")
* @param int $user_id
* @return Response
*/

public function loginAction($user_id)
{
$user = $this->getDoctrine()->getManager()
->getRepository(User::class)->find($user_id);
if (!$user) {
return $this->redirectToUserList('ユーザーを読み込めません');
}
$this->loginToContext($user, 'user');
return $this->redirectToRoute('user-top'); // ユーザー画面にリダイレクト
}
/**
* @param UserInterface $user
* @param string $context
* @param array $roles
*/

private function loginToContext(UserInterface $user, $context, $roles = [])
{
$roles = $roles ?: $user->getRoles();
$token = new UsernamePasswordToken($user, null, $context, $roles);
$session = $this->get('session');
$session->set('_security_'.$context, serialize($token));
$session->save();
}

loginToContextメソッドをそのまま解説すると、



  • UsernamePasswordTokenオブジェクトを作成して、


  • _security_userというセッション名でオブジェクトを保存すると


  • userという名前のcontextにログインできます。

この方法の注意点としては、現在ログイン中の$contextを変更することはできません。あくまでadminにログインしている状態で、userにログインする方法です。


現在のコンテクストを更新する

※番外編

例えば、アクティベーションなどの処理でユーザーのロールを変更する場合です。現在のSecurity.contextで、トークンを再登録することになります。

    /**

* @param UserInterface $user
* @param string $context
* @param array $roles
*/

private function updateContext(UserInterface $user, $context, $roles = [])
{
$roles = $roles ?: $user->getRoles();
$token = new UsernamePasswordToken($user, null, $context, $roles);
$storage = $this->get('security.token_storage');
$storage->getToken()->setAuthenticated(false);
$storage->setToken($token);
}

要は、setAuthenticated(false)として一旦トークンを無効にしてから、setToken($token)で再設定します。先の直接セッションを設定する方法だと、その後のタイミングで元のトークンに戻ってしまうみたいです。


Firewall Context

ところでcontextって何でしょうね?

Symfonyのドキュメントだと「SecurityBundle Configuration ("security"): Firewall Context」というのがあります。

次のようにcontextは省略が可能で、指定されていない場合はファイヤウォール名(この場合はuser)となるとのこと。

security:

firewalls:
user: # コンテキストがないので、この名前が使われる。
pattern: ^/user/
anonymous: ~
guard:
authenticators:
- app.tanto_authenticator


でも結局コンテクストが何かはわからないですね。ログイン情報をセッションに保存するときの名前としか。でもGuardとContextで違うエンティティを使ったりしたら、どんな挙動になるんでしょうね。


ちなみにトークンからコンテクストを取得するには、getProviderKey()が使えます。

    $storage = $this->get('security.token_storage');

$context = $storage->getToken()->getProviderKey();

がAPIとしては存在しないので、使うのは避けたほうが良いかも。

そもそもセッションを直接扱うとか、このあたりの処理はハックな感じがします。