Google sign-in for the Grav 2 Google Drive plugin family. You connect a Google account once, as an OAuth user account through your own Google Cloud "Web application" client (most people) or as a service account, and every compatible Drive plugin (one built on Google Drive Auth) can use it. It also gives those plugins a thin Drive v3 client and handles the OAuth callback. Its settings page in Admin2 (Plugins → Google Drive Auth) manages the accounts and carries the guided setup and full guides. Its only front-end route is that callback, and it has no Composer dependencies.
Plugins that use it: gdrive-images (photo galleries) and gdrive-backup (backups to Drive).
Requires PHP 8.3+ and Grav 2.0.23+, plus the api and admin2 plugins for the settings page (declared as dependencies, so GPM installs them). CI checks every change against Grav 2.0.23 with api 1.0.41 and Grav 2.2.4 with api 1.0.44.
Any of the usual three ways:
- GPM:
bin/gpm install gdrive-auth. Plugins built on it (such asgdrive-imagesandgdrive-backup) list it as a dependency, so installing one of them installs this too. - Admin2: Plugins → Add, search for Google Drive Auth.
- Manual: download the zip from the
releases page,
unzip it into
user/plugins/, and rename the folder togdrive-auth, so thatuser/plugins/gdrive-auth/gdrive-auth.phpexists.
No Composer step: no PHP libraries beyond the curl and openssl extensions, which Grav already requires.
Open Plugins → Google Drive Auth in Admin2 and read Start here (OAuth or service account?), then go to the Guided setup tab: answer a few questions (which Google account, personal or Workspace, and so on) and it shows only the steps you need, with the console links opening your Cloud project. Most people want OAuth with their own Google account. The other tabs have the full guides, with this site's redirect URI, service-account emails and needed scopes filled in, and an Accounts tab to upload credentials, Test and Connect. The same guides, readable here:
- Start here: OAuth or service account?
- OAuth guide
- Service account guide
- Troubleshooting, one entry per error code
The redirect URI the guides show is built from system.custom_base_url when that
is set, and from the current request's scheme and host otherwise. On a site
behind a proxy or CDN, set custom_base_url so the address Google sees is stable
and starts with https://.
Managing accounts needs the Manage Google Drive accounts permission
(api.gdrive.manage); API super users have it. The settings page is served by the
api plugin and rendered by admin2 (both declared dependencies); the library
classes themselves need neither.
The plugin is Google Drive Auth (gdrive-auth). Accounts are declared in user/config/plugins/gdrive-auth.yaml:
accounts:
site: { type: service_account }
personal: { type: oauth }Their credentials live in user/data/gdrive/auth/ under fixed names, mode 0600:
<name>.sa.json (service-account key), <name>.client.json (OAuth client),
<name>.token.json (refresh token, granted scopes, email), plus
<name>.test.json (the last Test result, no secrets). Secrets never go
in config. Make sure your web server refuses user/data/.
Everything below is stable under semver. Anything not listed is internal.
Dependent plugins declare { name: gdrive-auth, version: '>=1.0.0' } and should
check class_exists(\Grav\Plugin\Gdrive\Drive::class) before use.
Gdrive::drive(string $account, array $scopes): Drive
Gdrive::accounts(): Accounts
Gdrive::scopes(): array // list of {plugin, account, scopes} from onGdriveScopes
Gdrive::redirectUri(): string // rootUrl(true) . '/gdrive-oauth/callback'
Gdrive::setHttp(?callable $http): void // test seam, see belowReturns Drive's raw JSON arrays; you choose fields. supportsAllDrives=true
is sent on every call and includeItemsFromAllDrives=true on every /files
listing. No caching.
const SCOPE_FILE, SCOPE_READONLY, SCOPE_FULL, FOLDER, API, UPLOAD_API
request(string $method, string $path, array $query = [], ?array $json = null): array
paginate(string $path, array $query): \Generator
children(string $folderId, array $mimes, string $fields): array
findByAppProperty(string $parentId, string $key, string $value, string $fields): array
ensureFolder(string $name, string $parentId = 'root'): string
download(string $id, string $dest): void
upload(string $localPath, string $parentId, string $name, array $appProperties = [], string $fields = 'id,name,md5Checksum,size'): array
trash(string $id): void
about(string $fields = 'user(emailAddress,displayName),storageQuota'): arrayupload() opens a resumable session and streams the whole file in one PUT;
on failure it starts again from zero.
Extends \RuntimeException. Every failure is one of these.
int $status: the HTTP status (0 when there was none).string $reason: Google's error reason (storageQuotaExceeded,notFound,invalid_grant…) or one of ours:scope_not_granted,not_connected,bad_credential,bad_state,unknown_account,transport,io.anchor(): string: the Troubleshooting anchor for the reason (storageQuotaExceeded→storage-quota-exceeded).
Gdrive::drive() throws scope_not_granted (reconnect to grant it) when an
OAuth account lacks a scope, on the first call that needs a token. A granted
drive covers drive.file and drive.readonly; any other scope only covers
itself.
Declare what your plugin needs, so the settings page can request it on Connect
and show "who uses what". The event is Event(['declarations' => [...]]); its
array access returns copies, so append by reassigning:
public function onGdriveScopes(Event $e): void
{
$e['declarations'] = [...$e['declarations'], [
'plugin' => 'gdrive-images',
'account' => (string) $this->config->get('plugins.gdrive-images.account', 'site'),
'scopes' => [\Grav\Plugin\Gdrive\Drive::SCOPE_READONLY],
]];
}Static, safe to call from a blueprint: each falls back to sensible text if Grav
isn't fully booted, and never emits <script or <div id= (Admin2 hides such
display fields).
Setup::accountOptions(): array // name => "name (Service account|OAuth, email)"
Setup::consumerNotice(string $plugin): string // markdown: declared account + scopes, granted?, link to the settings page
Setup::guide(string $name): string // docs/setup/<name>.md with {{redirect_uri}}, {{sa_emails}}, {{scopes}}, {{site}} filled
Setup::checklist(): string // markdown: per-account status, each ✘ linked to Troubleshooting
Setup::whoUsesWhat(): string // markdown table: plugin · account · scopes · ready?
In a dependent plugin's blueprint:
account:
type: select
label: Google Drive account
default: site
data-options@: '\Grav\Plugin\Gdrive\Setup::accountOptions'
drive_notice:
type: display
markdown: true
data-content@: ['\Grav\Plugin\Gdrive\Setup::consumerNotice', 'gdrive-images']Registered through the api plugin's onApiRegisterRoutes, under /api/v1.
All need api.gdrive.manage. Success is {"data": …}; errors are
application/problem+json with code (the DriveException reason) and
anchor (its Troubleshooting entry). No response ever carries a secret.
| Method and path | Does |
|---|---|
GET /gdrive/accounts |
{accounts, redirect_uri, wanted}: each account's status() plus declared (per plugin), missing (declared scopes an OAuth account hasn't granted) and test (the last Test result); wanted lists declared account names that don't exist yet |
POST /gdrive/accounts |
Body {name, type: service_account|oauth, json} (json is the file's text, ≤ 64 KB). Validates and stores the credential, records the account in user/config/plugins/gdrive-auth.yaml. 201 with the list |
DELETE /gdrive/accounts/{name} |
Revokes (OAuth), deletes the files, drops it from config. The list |
POST /gdrive/accounts/{name}/test |
A real about.get with the declared scopes; stored in user/data/gdrive/auth/<name>.test.json. {result, …list} |
POST /gdrive/accounts/{name}/connect |
{url}: Google's consent URL for the declared ∪ granted scopes (OAuth only) |
GET /gdrive/guide?kind=&method=&shared_drive=&admin=&project= |
{html, method, tags, account}: the Guided setup steps and the suggested account name. kind is gmail (personal) or workspace; method oauth (default) or sa; shared_drive yes|no|unsure; admin yes|no; project a Cloud project ID. Anything else is a 422 |
Every HTTP call goes through one callable:
callable(string $method, string $url, array $opts): array{0: int, 1: string, 2: array<string, string>}
// returns [status, body, lowercase response headers]
// $opts: headers (string[]), body (string), infile (resource) + infile_size (int), sink (resource),
// timeout (int seconds; Http::curl only ever shortens its defaults with it)Gdrive::setHttp($fake) makes every client built afterwards use $fake
instead of curl (null restores curl), so a dependent plugin's smoke test can
stub Drive. Drive, ServiceAccount, OAuthUser and Accounts also take it
as a constructor argument, and Gdrive::accounts()->withHttp($http)
returns a copy of the registry whose credentials (token refreshes included)
use $http, e.g. a short-timeout, no-retry transport for a settings-page check.
php tests/smoke.php
phpstan analyse --memory-limit=1G # needs a Grav install at .gravtest/grav-admin, with the api plugin in its user/plugins/api
MIT