iT邦幫忙

2023 iThome 鐵人賽

DAY 10
1

當我們在開發產品的時候,使用者驗證是不能跳過的重要一環,選用好的供應商可以省去很多的維運的成本,讓你的晚上睡的更安穩

因為我們起手式使用了 GCP,所以不免俗就要拿 GCP 的服務跟其他供應商去比較。所以下面就會針對 GCP Identity Platform 跟 Auth0 兩者中去選擇一個來作為我們的認證服務的供應商。

Auth0 是什麼?

Auth0
Auth0 是在社群上能見度很高的身分認證管理服務平台,提供各種語言的 SDK 與相關功能,可以達到一站式管理從會員認證、權限管理,甚至是可以做到複雜的 pipeline 整合。不只是支援各家身分認證供應商的整合,也支援很多最新型的認證方法:像是 passwordless 、 MFA 與 Single Sign On ......等等。

GCP Identity Platform vs Auth0 比較

GCP Identity Platform
GCP Identity Platform 提供類似的功能,但是都主要針對認證的服務,其他的延伸功能沒有支援到 auth0 這麼全面。但是也支援有名的身分認證供應商,跟基本的 email 與 MFA 認證方式。

終極二選一,開始動手

雖然 auth0 提供了近乎連新手都可以順利串接的教學文章,但是因為目前針對 Octane 的相容支援還在路上,所以這次的實作就會以 GCP Identity 為例。

但是如果有機會可以使用 auth0 ,基本上就是閉著眼睛選也要使用的好選擇,各式各樣與支援不同語言的 SDK 與文件,對開發者非常友善。

無論選擇哪個平台,將身份驗證外包給第三方的供應商都有一些好處:

  1. 不需自己維護使用者系統
  2. 可以專注在核心業務功能上
  3. 更安全可靠的驗證系統
  4. 半夜不需要 on call

Do it!

1. 安裝 firebase SDK (GCP Identity Platform 是原本的 Firebase Auth)

composer require "kreait/firebase-php:^7.0" 

2. 建立 Guard

<?php

namespace App\Guard;

use App\Models\User;
use Exception;
use Illuminate\Http\Request;
use Kreait\Firebase\JWT\Error\IdTokenVerificationFailed;
use Kreait\Firebase\JWT\IdTokenVerifier;
use Kreait\Firebase\Contract\Auth;

class FirebaseGuard
{
    public function __construct(
        protected IdTokenVerifier $verifier,
		protected Auth $auth,
    )
    {}

    /**
     * Get User by request claims.
     *
     * @throws Exception
     */
    public function user(Request $request): mixed
    {
        $token = $request->bearerToken();

        if (empty($token)) {
            return null;
        }

        try {
            $firebaseToken = $this->verifier->verifyIdToken($token);

            /* @var User $user */
            $user = app(config('auth.providers.users.model'));

            return $user
                ->setFirebaseAuthenticationToken($token)
                ->resolveByPayload($firebaseToken->payload());
        } catch (Exception $e) {
            if ($e instanceof IdTokenVerificationFailed) {
                return null;
            }

            if (config('app.debug')) {
                throw $e;
            }

            return null;
        }
    }
}

3. 新增 FirebaseAuthenticable 並且新增至 User model

FirebaseAuthenticable.php

<?php

namespace App\Models\Trait;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Arr;
use Kreait\Firebase\Contract\Auth;
use Kreait\Firebase\Exception\AuthException;
use Kreait\Firebase\Exception\FirebaseException;

trait FirebaseAuthenticable
{
    protected array $claims;

    protected ?string $firebaseAuthenticationToken;

    /**
     * @throws FirebaseException
     * @throws AuthException
     */
    public function resolveByPayload(array $payload): object
    {
        $id = data_get($payload, 'sub');
        return $this->updateOrCreateUser($id, $this->transformPayload($payload));
    }

    /**
     * Update or create user.
     *
     * @throws AuthException
     * @throws FirebaseException
     */
    public function updateOrCreateUser(string $id, array $attributes): object
    {
        if ($user = self::query()->find($id)) {
            $user->fill($attributes);

            if ($user->isDirty()) {
                $user->save();
            }

            return $user;
        }

        /* @var Auth $firebaseAuth*/
        $firebaseAuth = app(Auth::class);
        $userInfo = $firebaseAuth->getUser($id);

        $user = $this->fill(array_merge($attributes, [
            'name' => $userInfo->displayName,
            'info' => Arr::first($userInfo->providerData),
        ]));

        $user->id = $id;
        $user->save();

        return $user;
    }

    protected function transformPayload(array $payload): array
    {
        $attributes = [];

        if (!is_null(data_get($payload, 'firebase.identities.name'))) {
            $attributes['name'] = (string) data_get($payload, 'firebase.identities.name');
        }

        return $attributes;
    }

    public function setFirebaseAuthenticationToken(string $token): self
    {
        $this->firebaseAuthenticationToken = $token;

        return $this;
    }

    public function getFirebaseAuthenticationToken(): string
    {
        return $this->firebaseAuthenticationToken;
    }

    public function getAuthIdentifierName(): string
    {
        return 'id';
    }

    public function getAuthIdentifier(): mixed
    {
        return $this->id;
    }

    public function getAuthPassword(): string
    {
        throw new \RuntimeException('No password support for Firebase Users');
    }

    public function getRememberToken(): string
    {
        throw new \RuntimeException('No remember token support for Firebase Users');
    }

    /**
     * Set the token value for the "remember me" session.
     *
     * @param string $value
     *
     * @return void
     */
    public function setRememberToken($value): void
    {
        throw new \RuntimeException('No remember token support for Firebase User');
    }

    public function getRememberTokenName(): string
    {
        throw new \RuntimeException('No remember token support for Firebase User');
    }
}

User.php

<?php
// ...
class User extends Authenticatable
{
    use HasFactory;
    use Notifiable;
    use FirebaseAuthenticable;
    // ...
}

4. 註冊 firebase 的 provider 至 AuthProvider

AuthServiceProvider.php

<?php
    // ...
public function boot()
{
    $this->registerPolicies();

    Auth::viaRequest('firebase', function ($request) {
        return app(FirebaseGuard::class)->user($request);
    });
}

public function register(): void
{
    $this->app->singleton(IdTokenVerifier::class, function ($app) {
        $projectId = config('firebase.project_id', env('GOOGLE_CLOUD_PROJECT'));

        if (empty($projectId)) {
            throw new \RuntimeException('Missing GOOGLE_CLOUD_PROJECT env variable.');
        }

        return IdTokenVerifier::createWithProjectId($projectId);
    });
}

5. 更改 config/auth.php 的 設定

<?php
return [
    // ...
    'guards' => [
        'api' => [
            'driver' => 'firebase',
            'provider' => 'users',
            'hash' => false,
        ],
    ],
    // ...
]

6. 收工!

Reference


上一篇
#8 你其實不需要在 Laravel 使用 Repository Pattern (2/2)
下一篇
#10 快速建立可靠的定時爬蟲 (1/2)
系列文
Laravel 擴展宇宙:從 1 到 100 十倍速打造產品獨角獸30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言