Your IP : 216.73.216.79


Current Path : /var/www/html/ains/docs/
Upload File :
Current File : /var/www/html/ains/docs/google-oauth-implementation.md

# Replace Firebase Google Login with Native Google OAuth

Goal: remove Firebase from the login path and use server-side Google OAuth
(`yiisoft/yii2-authclient`). This removes Firebase's Spark DAU caps
(3,000 DAU/day) and Blaze MAU pricing entirely.

## Why
The old flow routed every login through Firebase (client-side `firebase-auth.js`
+ FirebaseUI + a Firebase ID token), so it was bound by Firebase plan limits
and the project's monthly no-cost quota. Native server-side Google OAuth has no
per-user / DAU pricing. The only remaining Google "limit" is the OAuth
consent-screen verification threshold (100 test users until verified) — not a
billing limit. With the app's non-sensitive scopes (`userinfo.profile` +
`userinfo.email`) it can be published to Production without verification.

## Architecture notes (this app)
- Yii2 advanced template. Frontend module: `frontend/modules/loginprovider/`.
- The student login page is **DB-driven**, not a normal view:
  - `frontend_page` row `page_id = 120` ("Student | Home", slug `home`, default).
  - `frontend_content` row `content_id = 251` ("AINS | STUDENT | SLIDER") embeds
    the Google login link
    (`'   . '<a class="btn btn-google btn-lg w-100" href="/loginprovider/google-auth/auth">Masuk dengan Google</a>' . '`).
    It previously embedded
    `\frontend\modules\loginprovider\widgets\FirebaseAuthButton::widget()`.
  - Content is compiled into
    `frontend/runtime/frontendpage/{page}_{last_update}-content-251.php` by
    `common/components/PortalDBContent.php`.
  - So changing the login button means editing the DB content row, then the
    runtime file regenerates (PortalDBContent compiles lazily per `last_update`).
- Users are keyed by `username = email` (`FirebaseAuthController.php:91`), so a
  user created via Firebase with the same Google email is matched seamlessly.
  No DB schema change needed (`provider`, `access_token`, `login_token` columns
  already exist on `frontend_user`).
- DB stores content with real CRLF newlines (mysql batch mode only *displays*
  them as `\n`).
- PHP 8.4.18, Composer available at `/usr/local/bin/composer`.

## Google Cloud Console setup (manual, by admin)
Project: `hepili` (same project as the Firebase app).

1. **APIs & Services -> OAuth consent screen** (External):
   - App name, support email, app logo.
   - Authorized domain(s) = the deployed host, e.g. `moe-dl.edu.my` / your host.
2. **Credentials -> Create credentials -> OAuth client ID -> Web application**:
   - Authorized JavaScript origins: `https://<host>`
   - Authorized redirect URIs: `https://<host>/loginprovider/google-auth/callback`
   - (Add `http://localhost:<port>` + redirect for local dev.)
   - **Every host used for testing must be added** — a host that is not
     registered returns `Error 400` "Access blocked: This app's request is
     invalid" (`redirect_uri_mismatch`).
3. Copy `client_id` / `client_secret` into the git-ignored
   `frontend/config/params-local.php` (see below).
4. Verification / user cap:
   - The app uses only **non-sensitive** scopes
     (`userinfo.profile` + `userinfo.email`), so it can be published to
     **Production** without Google app verification — no 100-user cap applies.
   - If the org runs Google Workspace (`moe-dl.edu.my`), making the consent app
     **Internal** removes caps entirely for org users.
   - Only add External verification if sensitive/restricted scopes are ever added.

## Code changes

### 1. Composer
```
composer require yiisoft/yii2-authclient:^2.2
```

### 2. Config (`frontend/config/main.php`)
Add under `components`:

```php
'authClientCollection' => [
    'class' => 'yii\authclient\Collection',
    'clients' => [
        'google' => [
            'class' => 'yii\authclient\clients\Google',
            'clientId' => $params['googleOauth']['clientId'] ?? '',
            'clientSecret' => $params['googleOauth']['clientSecret'] ?? '',
        ],
    ],
],
```

**Important:** read creds from the local `$params` variable (top of `main.php`),
NOT `Yii::$app->params` — the config file is evaluated before the app exists, so
`Yii::$app->params` is `null` and silently resolves to `''` (empty client_id).

The `loginprovider` module also declares a `googleOauth` public property
(`frontend/modules/loginprovider/Module.php`) wired from the same `$params`, and
`GoogleAuthController::getClient()` overrides the client's credentials from it.

Credentials live in the git-ignored file `frontend/config/params-local.php`:

```php
return [
    'googleOauth' => [
        'clientId' => '...',
        'clientSecret' => '...',
    ],
];
```

### 3. New controller
`frontend/modules/loginprovider/controllers/GoogleAuthController.php`
- `actionAuth`: set the return URL to `/loginprovider/google-auth/callback`,
  build the Google auth URL (state stored in session by the client) and redirect.
- `actionCallback`: set the same return URL, exchange code (`fetchAccessToken`
  verifies `state` server-side), fetch user attributes, then
  find-or-create `common\models\FrontendUser` (port logic from
  `FirebaseAuthController::actionSaveProvider`, lines 91-119), log in with
  `Yii::$app->user->login(...)`, redirect to module `redirect.success`
  on success / `redirect.failed` on failure.
- User mapping:
  - `username` = `email`
  - `fullname` = `name`
  - `email` = `email`
  - `img_url` = `picture`
  - `provider` = `google.com`
  - `status` = 1, `role` = `murid`, `last_login` = time(), `login_token` = UUID
- Routes: `/loginprovider/google-auth/auth` and
  `/loginprovider/google-auth/callback` (pretty URLs enabled in
  `frontend/config/main.php`).

### 4. Swap the login button (DB)
Edit `frontend_content.content_details` for `content_id = 251`:

Replace
`'   . \frontend\modules\loginprovider\widgets\FirebaseAuthButton::widget() . '`
with
`'   . '<a class="btn btn-google btn-lg w-100" href="/loginprovider/google-auth/auth">Masuk dengan Google</a>' . '`

Then bump `frontend_page.last_update` so `PortalDBContent` regenerates
`runtime/frontendpage/...-content-251.php`.

Also audit `frontend_content` rows `177` ("Firebase Ui") and `257`
("AINS | FIREBASE UI") — standalone Firebase UI blocks. Done:
- Row `257` was orphaned (not assigned to any page) — left intact.
- Row `177` WAS assigned to pages `36` ("Login Page") and `91`
  ("Login Page - Student"); its `frontend_content_assign` rows were removed
  and the runtime files cleared so both login pages no longer render Firebase UI.

### 5. Cleanup / rollback
- Firebase module files (`FirebaseAuthController.php`, `FirebaseAuthButton.php`,
  `views/firebase-auth/index.php`) are left intact as rollback; only the DB
  button and the page no longer reference them.
- Rollback = revert `frontend_content` row 251 + remove the config + revert
  controller.

## Security notes
- The old `actionSaveProvider` trusted client-posted JSON. Native OAuth verifies
  the token server-side, so `actionSaveProvider` can be retired.
- OAuth `state` must be verified in `actionCallback` (store in session at
  `actionAuth`, compare + clear at `actionCallback`).

## Testing checklist
1. Add a test user in Google OAuth consent screen (Testing mode).
2. Local/staging: add the exact redirect URI
   `https://<host>/loginprovider/google-auth/callback` (and the host as an
   authorized origin). A missing host returns `Error 400` "Access blocked:
   This app's request is invalid" (`redirect_uri_mismatch`).
3. Clear `frontend/runtime/frontendpage/120_*` so the login page regenerates.
4. Login -> Google account -> confirm `frontend_user` row created/updated and
   the user lands on `redirect.success`.
5. Confirm a user created by the old Firebase flow still logs in (same email).
6. Failure path: deny access at Google -> redirected to `redirect.failed`.