带有Symfony API平台的Firebase Authenticator JWT



我有一个Flutter客户端,它使用firebase创建用户帐户。用户可以发布到达由Symfony 6和API平台构建的web管理面板的票证。

所以我需要2个验证器:

  • 1个原始Symfony Authenticator,供管理员使用表单进行连接并管理票证
  • 1身份验证器JWT,它将检查Firebase凭据,返回JWT,然后允许发布。所以我保护我的API路由

我陷入了最后一点。我正在使用Symfony的Firebase Bundle SDK。我的Firebase用户恢复得很好。我编写了FirebaseUserProviderFirebaseAuthenticator

当然,FirebaseUser实体与条令ORM没有任何联系。

我想我离得不远,但我被卡住了">401:无效凭据";我通过Postman提交的所有登录信息。如果没有Doctrine对SQL数据库的干预,我能做到这一点吗?我希望我不必";将firebase用户复制到我的sql数据库中;。

这是我的重要档案。

security:
enable_authenticator_manager: true
# https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords
password_hashers:
SymfonyComponentSecurityCoreUserPasswordAuthenticatedUserInterface: 'auto'
# https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
providers:
firebase_user_provider:
id: AppSecurityFirebaseUserProvider
app_user_provider:
entity:
class: AppEntityUser
property: email
jwt:
lexik_jwt: ~
firewalls:
dev:
pattern: (_(profiler|wdt)|css|images|js)/
security: false
# APILogin
login:
pattern: ^/api/login$
stateless: true
custom_authenticators:
- AppSecurityFirebaseAuthenticator
provider: firebase_user_provider
json_login:
check_path: /api/login
success_handler: lexik_jwt_authentication.handler.authentication_success
failure_handler: lexik_jwt_authentication.handler.authentication_failure
api:
pattern: ^/api
stateless: true
provider: jwt
jwt: ~
# AppFormLogin
main:
lazy: true
stateless: true
provider: app_user_provider
custom_authenticator: AppSecurityLoginFormAuthenticator
logout:
path: app_logout
# activate different ways to authenticate
# https://symfony.com/doc/current/security.html#the-firewall
# https://symfony.com/doc/current/security/impersonating_user.html
# switch_user: true
# 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: ^/api/login, roles: PUBLIC_ACCESS }
#        - { path: ^/api,       roles: IS_AUTHENTICATED_FULLY }
class FirebaseAuthenticator extends AbstractAuthenticator
{
private FirebaseUserProvider $firebaseUserProvider;
public function __construct(FirebaseUserProvider $firebaseUserProvider)
{
$this->firebaseUserProvider = $firebaseUserProvider;
}
/**
* Called on every request to decide if this authenticator should be
* used for the request. Returning `false` will cause this authenticator
* to be skipped.
*/
public function supports(Request $request): ?bool
{
return $request->isMethod('POST');
}
/**
* @throws JsonException
*/
public function authenticate(Request $request): Passport
{
$credentials = [
'username' => json_decode($request->getContent(), false, 512, JSON_THROW_ON_ERROR)->username,
'password' => json_decode($request->getContent(), false, 512, JSON_THROW_ON_ERROR)->password
];
return new SelfValidatingPassport(new UserBadge($credentials['username']));
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
return null;
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
{
$data = [
'message' => strtr($exception->getMessageKey(), $exception->getMessageData())
];
return new JsonResponse($data, Response::HTTP_UNAUTHORIZED);
}
}
class FirebaseUserProvider implements UserProviderInterface, PasswordUpgraderInterface
{
private Auth $auth;
/**
* @param Auth $auth
*/
public function __construct(Auth $auth)
{
$this->auth = $auth;
}
/**
* Symfony calls this method if you use features like switch_user
* or remember_me. If you're not using these features, you do not
* need to implement this method.
*
* @param string $identifier
* @return UserInterface
* @throws AuthException
* @throws FirebaseException
*/
public function loadUserByIdentifier(string $identifier): UserInterface
{
$user = $this->auth->getUserByEmail($identifier);
return new FirebaseUser(
$user->uid ?? '',
$user->email ?? '',
$user->passwordHash ?? '',
$user->displayName ?? ''
);
}
/**
* Refreshes the user after being reloaded from the session.
*
* When a user is logged in, at the beginning of each request, the
* User object is loaded from the session and then this method is
* called. Your job is to make sure the user's data is still fresh by,
* for example, re-querying for fresh User data.
*
* If your firewall is "stateless: true" (for a pure API), this
* method is not called.
*
* @param UserInterface $user
* @return UserInterface
*/
public function refreshUser(UserInterface $user): UserInterface
{
if (!$user instanceof FirebaseUser) {
throw new UnsupportedUserException(sprintf('Invalid user class "%s".', get_class($user)));
}
// Return a User object after making sure its data is "fresh".
// Or throw a UserNotFoundException if the user no longer exists.
throw new RuntimeException('TODO: fill in refreshUser() inside '.__FILE__);
}
/**
* Tells Symfony to use this provider for this User class.
*/
public function supportsClass(string $class): bool
{
return FirebaseUser::class === $class || is_subclass_of($class, FirebaseUser::class);
}
/**
* Upgrades the hashed password of a user, typically for using a better hash algorithm.
*/
public function upgradePassword(PasswordAuthenticatedUserInterface $user, string $newHashedPassword): void
{
if (!$user instanceof FirebaseUser) {
throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', get_class($user)));
}
$user->setPasswordHash($newHashedPassword);
// TODO: when hashed passwords are in use, this method should:
// 1. persist the new password in the user storage
// 2. update the $user object with $user->setPassword($newHashedPassword);
}
}
class FirebaseUser implements UserInterface, PasswordAuthenticatedUserInterface
{
#[ApiProperty(
identifier: true
)]
private ?string $uid;
#[ApiProperty(
description: 'Email de l'utilisateur provenant de Firebase'
)]
private ?string $email;
#[ApiProperty(
description: 'Mot de passe de l'utilisateur Firebase'
)]
private ?string $passwordHash;
#[ApiProperty(
description: 'Nom de l'utilisateur provenant de Firebase'
)]
private ?string $displayName;
/**
* @param string|null $uid
* @param string|null $email
* @param string|null $passwordHash
* @param string|null $displayName
*/
public function __construct(?string $uid, ?string $email, ?string $passwordHash, ?string $displayName)
{
$this->uid = $uid;
$this->email = $email;
$this->passwordHash = $passwordHash;
$this->displayName = $displayName;
}
/**
* @return string|null
*/
public function getUid(): ?string
{
return $this->uid;
}
/**
* @param string|null $uid
*/
public function setUid(?string $uid): void
{
$this->uid = $uid;
}
/**
* @return string|null
*/
public function getEmail(): ?string
{
return $this->email;
}
/**
* @param string|null $email
*/
public function setEmail(?string $email): void
{
$this->email = $email;
}
/**
* @return string|null
*/
public function getPasswordHash(): ?string
{
return $this->passwordHash;
}
/**
* @param string|null $passwordHash
*/
public function setPasswordHash(?string $passwordHash): void
{
$this->passwordHash = $passwordHash;
}
/**
* @return string|null
*/
public function getDisplayName(): ?string
{
return $this->displayName;
}
/**
* @param string|null $displayName
*/
public function setDisplayName(?string $displayName): void
{
$this->displayName = $displayName;
}
/**
* @return string[]
*/
public function getRoles(): array
{
return ['ROLE_USER'];
}
/**
* @see UserInterface
*/
public function eraseCredentials(): void
{
// If you store any temporary, sensitive data on the user, clear it here
// $this->plainPassword = null;
}
/**
* A visual identifier that represents this user.
* @see UserInterface
*/
public function getUserIdentifier(): string
{
return (string) $this->email;
}
public function getPassword(): ?string
{
// passwordHash provide firebase
return $this->passwordHash;
}
}

几天后我就回来了。虽然知道如何通过JWT中的API平台与来自Firebase的用户进行身份验证会很有趣,但我在这里分享我的想法,因为我已经改变了我的情况。

事实上,我分析得还不够。虽然我喜欢ApiPlatform,但我意识到我必须删除它,并使我的Symfony应用程序成为连接到Firebase SDK的非常经典的BackOffice。因此,我有2个应用程序(Symfony/Flutter(链接到1个后端(Firebase、Auth+CloudFirestore(,符合逻辑。

因此,不需要使用额外的API进行额外的身份验证,这只会使事情变得更重,并使我的目标复杂化。有时候把事情做好是件好事。

总是在没有原则的情况下对验证器抱有幻想。给你力量。

最新更新