Symfony Advent Calendar 2021 の19日目の記事です。
機能は@ippey_s さんのSymfonyのパスポートでした。
今日はパスポートを使って実際にFirebase Authenticationで認証してみようと思います。
kreait/firebase-bundleを入れる
まずはkreait/firebase-bundleをcompsoerで入れます。
1
|
$ composer require kreait/firebase-bundle
|
あとはサービスアカウントのFirebase Admin SDKから秘密鍵を生成して設定します。
1
2
3
4
5
|
#config/packages/firebase.yaml
kreait_firebase:
projects:
test_app:
credentials: '%kernel.project_dir%/config/firebase_credentials.json'
|
UserInterfaceを継承したクラスを用意する
Symfony\Component\Security\Core\User\UserInterface
を継承したUserClassを作ります。
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
|
<?php
declare(strict_types=1);
namespace App\Firebase;
use Symfony\Component\Security\Core\User\UserInterface;
final class FirebaseUser implements UserInterface
{
public function __construct(private string $id, private string $email)
{
}
public function getId(): string
{
return $this->id;
}
public function getEmail(): string
{
return $this->email;
}
public function getRoles(): array
{
return ['ROLE_USER'];
}
public function getPassword(): string
{
throw new \RuntimeException('No support method');
}
public function getSalt(): string
{
throw new \RuntimeException('No support method');
}
public function eraseCredentials(): void
{
// TODO: Implement eraseCredentials() method.
}
public function getUsername(): string
{
return $this->email;
}
public function getUserIdentifier(): ?string
{
return $this->id;
}
}
|
認証用クラスを作成する
フロントエンドから送られてきたFirebaseのidTokenを検証して、問題なければパスポートを発行するためのクラスを作ります。
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
|
<?php
declare(strict_types=1);
namespace App\Firebase;
use Kreait\Firebase\Auth;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
final class FirebaseTestAuthenticator extends AbstractAuthenticator
{
public function __construct(private Auth $auth)
{
}
public function supports(Request $request): ?bool
{
return null !== $request->headers->get('authorization');
}
public function authenticate(Request $request): PassportInterface
{
$idToken = $this->parseToken($request->headers->get('authorization'));
$token = $this->auth->verifyIdToken($idToken);
if ($token->isExpired(new \DateTimeImmutable())) {
throw new CustomUserMessageAuthenticationException('API Token is expired');
}
return new SelfValidatingPassport(new UserBadge($token->getId(), static function () use ($token) {
$id = (string) $token->claims()->get('sub');
$email = (string) $token->claims()->get('email');
return FirebaseUser($id, $email);
}));
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
return null;
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
{
return new JsonResponse([
'status' => 'error',
], Response::HTTP_UNAUTHORIZED);
}
public function parseToken(string $token): string
{
if (preg_match('/Bearer\s(\S+)/i', $token, $matches)) {
return $matches[1];
}
throw new \RuntimeException('token parse error');
}
}
|
security.yamlで設定する
最後にsecurity.yamlに設定を書きます。
ほぼ今まで通りかなとは思います。
firewallsのmainでstatelessをtrueにするのを忘れないように注意してください。
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
|
security:
# https://symfony.com/doc/current/security/experimental_authenticators.html
enable_authenticator_manager: true
# https://symfony.com/doc/current/security.html#c-hashing-passwords
password_hashers:
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
# https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers
providers:
users_in_memory: { memory: null }
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
lazy: true
stateless: true
custom_authenticators:
1: App\Security\FirebaseUserAuthenticator
# Easy way to control access for large sections of your site
# Note: Only the *first* access control that matches will be used
access_control:
- { path: ^/mansions, roles: PUBLIC_ACCESS }
- { path: ^/status, roles: PUBLIC_ACCESS }
- { path: ^/admin, roles: ROLE_API_ADMIN }
- { path: ^/, roles: ROLE_API_USER }
|
おまけ
Firebase Authで認証してしまうとAPIの動作チェックとかめんどくさくなります。
そこで自分はdev環境ではdev用のAuthenticatorを用意して環境変数でいい感じにダミーな値が設定出来るようにしています。
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
|
<?php
declare(strict_types=1);
namespace App\Dev\Firebase;
use App\Firebase\FirebaseUser;
use App\Security\FirebaseUserAuthenticator;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
final class DebugFirebaseUserAuthenticator extends AbstractAuthenticator
{
public function __construct(
private FirebaseUserAuthenticator $parentAuthenticator,
private bool $debugFirebaseAuth,
private string $debugFirebaseUserId,
private string $debugFirebaseEmail,
private bool $debugFirebaseAdmin
) {
}
public function supports(Request $request): ?bool
{
if (false === $this->debugFirebaseAuth) {
return $this->parentAuthenticator->supports($request);
}
return true;
}
public function authenticate(Request $request): PassportInterface
{
if (false === $this->debugFirebaseAuth) {
return $this->parentAuthenticator->authenticate($request);
}
return new SelfValidatingPassport(
new UserBadge(
$this->debugFirebaseUserId,
fn () => new FirebaseUser(
$this->debugFirebaseUserId,
$this->debugFirebaseEmail,
$this->debugFirebaseAdmin
)
)
);
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
if (false === $this->debugFirebaseAuth) {
return $this->parentAuthenticator->onAuthenticationSuccess($request, $token, $firewallName);
}
return null;
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
{
if (false === $this->debugFirebaseAuth) {
return $this->parentAuthenticator->onAuthenticationFailure($request, $exception);
}
return new JsonResponse([
'status' => 'error',
], Response::HTTP_UNAUTHORIZED);
}
}
|
とはconfig/packages/dev/security.yaml
に以下のように書きます。
1
2
3
4
5
6
7
|
security:
firewalls:
main:
lazy: true
stateless: true
custom_authenticators:
1: App\Dev\Firebase\DebugFirebaseUserAuthenticator
|
最後にservices.yamlを作成して以下のように設定します。
1
2
3
4
5
6
7
|
App\Dev\Firebase\DebugFirebaseUserAuthenticator:
arguments:
$debugFirebaseAuth: '%env(bool:AUTH_DEBUG_FIREBASE)%'
$debugFirebaseUserId: '%env(string:AUTH_DEBUG_FIREBASE_ID)%'
$debugFirebaseEmail: '%env(string:AUTH_DEBUG_FIREBASE_EMAIL)%'
$debugFirebaseAdmin: '%env(bool:AUTH_DEBUG_FI REBASE_ADMIN)%'
|
こうすることで環境変数でいい感じにdevでダミーユーザーを使うことができます。
例:
1
2
3
4
|
AUTH_DEBUG_FIREBASE=false
AUTH_DEBUG_FIREBASE_ID="YclgOD4nYieqUf6AzPwv9gst8Gf1"
AUTH_DEBUG_FIREBASE_EMAIL="[email protected]"
AUTH_DEBUG_FIREBASE_ADMIN=false
|
この方法はSymfonyで開発時に使いたいコードを分離する と組み合わせることでプロダクションコードと綺麗に分離することができます。
最後に
昔に比べて認証まわりは非常に拡張しやすくなっています。印象としてはGuardでだいぶ拡張しやすく感じましたがそれ以上に拡張子やすさを感じました。
みなさんも年末年始ぜひSymfonyの認証周りで遊んでみてはいかがでしょうか?
明日は@77webさんの記事ですー!