Google - v1.0.0
OAuth Flow
The package implements the OAuth 2.0 Authorization Code flow with PKCE end-to-end. Users visit a "Connect Google" link, get redirected to Google's consent screen, come back to a package-owned callback, and land in your app with a persisted GoogleConnection row.
This page walks through each leg. See the sub-pages for deeper coverage of specific stages.
Routes
The service provider mounts five routes under google.routes.prefix (default /google/auth):
| Route | Method | Name | Purpose |
|---|---|---|---|
/connect |
GET | google.auth.connect |
Redirect to Google's consent screen. |
/callback |
GET | google.auth.callback |
Handle Google's redirect back. |
/reauthorize |
GET | google.auth.reauthorize |
Incremental consent for scopes added since the initial connection. |
/disconnect |
POST | google.auth.disconnect |
Mark the current user's connection disconnected. |
/status |
GET | google.auth.status |
JSON status payload consumed by the React and Vue UIs. |
All five are configured with web middleware by default. All five expect an authenticated user (the controller abort( 401 )s on null). See Configuration → Routes for how to customize.
Set google.routes.enabled = false to skip the built-in routes entirely (useful if your app wants to bind the manager services but use its own controller / URL structure).
The flow, end to end
┌──────────────┐ ┌──────────────────┐ ┌─────────────┐
│ Your app │ │ Package routes │ │ Google │
└──────┬───────┘ └────────┬─────────┘ └──────┬──────┘
│ │ │
│ User clicks "Connect" │ │
│─────────────────────────>│ GET /connect │
│ │ │
│ │ Build authorize URL │
│ │ Store state + verifier │
│ │ in session │
│ │─────────────────────────>│
│ │ │ User approves scopes
│ │ │
│ │<─────────────────────────│ GET /callback?code=…&state=…
│ │ │
│ │ Verify state │
│ │ POST /token │
│ │─────────────────────────>│
│ │<─────────────────────────│ { access_token, refresh_token, ... }
│ │ │
│ │ Extract id_token claims │
│ │ Persist GoogleConnection│
│ │ │
│<─────────────────────────│ redirect_after_connect │
│ │ │
Connect
GoogleAuthController::connect() is the entry point. It resolves the authenticated user, calls OAuthManager::authorizationUrl( $userId ), and returns a redirect()->away() to the built URL.
authorizationUrl() builds the URL with:
client_id,redirect_urifrom the configured credential driver.scope= the full de-duplicated union of everything the scope registry returns.state= a random 40-char string, stored in the session.code_challenge/code_challenge_method=S256— PKCE. The verifier is stored in the session; the challenge is the SHA-256 hash.access_type=offline— required to receive a refresh token.prompt=consent— forces the consent screen every time, guaranteeing therefresh_tokenis included in the response.include_granted_scopes=true— Google merges any newly granted scopes with existing ones (relevant for incremental consent).
If the credential driver reports isConfigured() === false, this throws OAuthException("Google OAuth credentials are not configured.").
Details: OAuth → Connect.
Callback
Google redirects back to google.auth.callback with either ?code=…&state=… on success or ?error=… on failure.
GoogleAuthController::callback():
- If
?error=…is present, redirects togoogle.routes.redirect_after_errorwithgoogle.errorflashed to the session. - Otherwise, requires both
codeandstatein the query — missing either flashes an error and redirects. - Delegates to
OAuthManager::handleCallback( $code, $state ).
handleCallback():
- Pulls the stored
state,code_verifier, anduser_idfrom the session. Any missing value throws anOAuthException. - Compares the returned
stateagainst the stored one withhash_equals()to defeat timing attacks. Mismatch = "possible CSRF attempt". - POSTs to the token endpoint with
grant_type=authorization_code,code,code_verifier,client_id,client_secret,redirect_uri. - Decodes the returned
id_token's payload (base64url) to extractsub(Google user id) andemail. The JWT signature is not verified — the token arrived over TLS from Google's token endpoint, so the identity claims are trustworthy for persistence purposes only (not authorization). - Loads or creates a
GoogleConnectionfor the user, setsaccess_token,refresh_token,expires_at,scopes,status = 'connected', and saves.
Refresh token preservation: Google only returns a refresh_token on the first consent (and on subsequent consents when prompt=consent is used with a new grant). Incremental-consent regrants typically omit it. handleCallback() only overwrites the stored refresh token when the response contains one — otherwise the existing one is preserved.
Details: OAuth → Callback.
Reauthorize
When a new service package is installed after a user is already connected, the scope registry starts returning scopes the connection doesn't hold. GoogleAuthController::reauthorize():
- Requires an authenticated user.
- Builds a
ConnectionStatefor the user. If they aren't connected at all, redirects togoogle.auth.connect(so onboarding, gating, and analytics that hang off connect still fire). - Otherwise, calls
OAuthManager::reauthorizationUrl( $userId, $state->grantedScopes )and redirects to it.
reauthorizationUrl() computes the delta between what the connection holds and what the registry now requires, and asks Google for only the missing scopes with include_granted_scopes=true. Google merges the new grant with the existing one, so the user isn't reprompted for scopes they've already approved.
If nothing is missing (edge case — the caller shouldn't normally hit this route), the method falls back to requesting the full union to keep the URL valid.
Details: OAuth → Reauthorize.
Disconnect
POST /google/auth/disconnect calls GoogleConnection::markDisconnected( 'Disconnected by user.' ) on the current user's connection and redirects to redirect_after_connect with google.status = 'disconnected' flashed.
Local-only — the flow does not hit Google's revoke endpoint. If you need remote revocation, POST to https://oauth2.googleapis.com/revoke from your own code with the stored refresh token before you call markDisconnected().
Details: OAuth → Disconnect.
Exceptions
The OAuth manager throws two exception types:
ArtisanPackUI\Google\Exceptions\OAuthException— thrown byauthorizationUrl()when credentials are missing, and byhandleCallback()on state mismatch, missing PKCE verifier, or a failed code exchange.ArtisanPackUI\Google\Exceptions\TokenRefreshException— thrown by the token manager when a refresh fails.
The default controller catches OAuthException in callback() and flashes the message; other callers should handle it themselves.
Deeper topics
- Connect — building the authorize URL, PKCE, session state, error modes.
- Callback — code exchange, id_token decoding, refresh-token preservation.
- Reauthorize — incremental consent details and when to trigger it.
- Disconnect — local vs. remote revocation, restoring a disconnected connection.
Continue to Scopes →