| Current Path : /var/www/html/ains/docs/ |
| 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`.