Google OAuth
Connect Google Business Profile accounts using OAuth 2.0
Google OAuth enables users to authorize Synoveo to access their Google Business Profile. This is used for dashboard login and connecting GBP accounts.
Overview
The OAuth flow grants Synoveo permission to:
- Read your Google Business Profile data
- Update profile information, hours, and attributes
- Create and manage posts
- Access reviews and insights
- Sync data between your source and GBP
Scope requested: https://www.googleapis.com/auth/business.manage
OAuth Flow
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ User │ │ Synoveo │ │ Synoveo │ │ Google │
│ Browser │ │Dashboard │ │ API │ │ OAuth │
└────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │ │
│ Click Login │ │ │
│───────────────>│ │ │
│ │ │ │
│ │ GET OAuth URL │ │
│ │───────────────>│ │
│ │ │ │
│ │ { authUrl } │ │
│ │<───────────────│ │
│ │ │ │
│ Redirect to Google │ │
│<───────────────│ │ │
│ │ │ │
│ Authorize │ │ │
│───────────────────────────────────────────────>│
│ │ │ │
│ Redirect with code │ │
│<───────────────────────────────────────────────│
│ │ │ │
│ POST callback │ │ │
│───────────────>│ │ │
│ │ │ │
│ │ Exchange code │ │
│ │───────────────>│ │
│ │ │ Get tokens │
│ │ │───────────────>│
│ │ │ │
│ │ │ { tokens } │
│ │ │<───────────────│
│ │ │ │
│ │ { jwt, user } │ │
│ │<───────────────│ │
│ │ │ │
│ Set cookie, redirect │ │
│<───────────────│ │ │
│ │ │ │Step 1: Generate OAuth URL
Request an OAuth URL to redirect the user to Google.
POST /api/v1/auth/google-oauth-url HTTP/1.1
Host: api.synoveo.com
Content-Type: application/json
{
"return_to": "https://app.synoveo.com/dashboard",
"force_consent": false
}Request Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
return_to | string | No | URL to redirect after successful auth |
force_consent | boolean | No | Force consent screen for new refresh token |
Response:
{
"status": "ok",
"data": {
"authUrl": "https://accounts.google.com/o/oauth2/v2/auth?client_id=...&redirect_uri=...&scope=...&state=...",
"projectId": "synoveo-prod",
"mode": "production"
}
}OAuth URL Parameters
The generated URL includes:
| Parameter | Value | Description |
|---|---|---|
client_id | Synoveo's Google Client ID | Identifies the application |
redirect_uri | Registered callback URL | Where Google redirects after auth |
scope | openid email profile https://www.googleapis.com/auth/business.manage | Requested permissions |
response_type | code | Authorization code flow |
access_type | offline | Request refresh token |
prompt | select_account or select_account consent | Account picker / consent screen |
include_granted_scopes | true | Incremental authorization |
state | JWT token | CSRF protection, contains return_to |
Step 2: User Authorization
Redirect the user to the authUrl. Google will:
- Show account picker (select Google account)
- Show consent screen (if
force_consent: trueor first authorization) - Redirect to callback URL with authorization code
Step 3: Exchange Authorization Code
After Google redirects back, exchange the code for tokens.
POST /api/v1/auth/google/callback HTTP/1.1
Host: api.synoveo.com
Content-Type: application/json
{
"code": "4/0Ab32j90xYz...",
"state": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}Request Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
code | string | Yes | Authorization code from Google |
state | string | Yes | State token from OAuth URL |
Response:
{
"status": "ok",
"data": {
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"expires_in": 604800,
"user": {
"id": 570,
"email": "user@gmail.com",
"name": "John Doe",
"picture": "https://lh3.googleusercontent.com/a/...",
"plan": "pro",
"email_verified": true
},
"is_new_user": false,
"return_to": "https://app.synoveo.com/dashboard"
}
}Response Fields:
| Field | Type | Description |
|---|---|---|
access_token | string | JWT token for API requests |
expires_in | number | Token TTL in seconds |
user | object | User profile information |
is_new_user | boolean | True if account was just created |
return_to | string | Original redirect URL |
Force Consent
Use force_consent: true to:
- Get a new refresh token
- Fix "token expired" or "invalid grant" errors
- Reconnect after revoking access in Google settings
POST /api/v1/auth/google-oauth-url HTTP/1.1
Host: api.synoveo.com
Content-Type: application/json
{
"force_consent": true
}When force_consent is true:
- Google shows the full consent screen
- A new refresh token is issued
- Previous refresh token is invalidated
Token Storage
Google OAuth tokens are stored encrypted:
| Field | Description |
|---|---|
access_token_enc | AES-256-GCM encrypted access token |
access_token_iv | Encryption initialization vector |
access_token_tag | GCM authentication tag |
refresh_token_enc | AES-256-GCM encrypted refresh token |
refresh_token_iv | Encryption initialization vector |
refresh_token_tag | GCM authentication tag |
expires_at | Access token expiration timestamp |
Synoveo automatically:
- Refreshes access tokens before expiration
- Preserves refresh tokens across OAuth flows
- Sends alerts before refresh token expiration (~6 months)
New User Flow
When a new user signs in via Google OAuth:
- Account Created - User record created with
plan: "lite" - Trial Started - 14-day trial with Pro features (if enabled)
- Email Sent - Welcome email with trial information
- Locations Synced - Google Business locations imported
{
"status": "ok",
"data": {
"is_new_user": true,
"user": {
"id": 571,
"email": "newuser@gmail.com",
"plan": "lite",
"trial_status": "trialing",
"trial_ends_at": "2025-02-01T00:00:00Z"
}
}
}Location Sync
After successful OAuth, Synoveo automatically:
- Fetches all Google Business accounts
- Imports locations within plan limits
- Stores verification status
- Enables sync for connected locations
Account Hierarchy
{
"accounts": [
{
"name": "accounts/123456789",
"accountName": "My Business Group",
"type": "LOCATION_GROUP",
"locations": [
{
"name": "locations/987654321",
"title": "Downtown Store",
"address": "123 Main St, City, State 12345",
"verificationState": "VERIFIED"
}
]
}
]
}Disconnecting
Via Synoveo
DELETE /api/v1/auth/google-connection HTTP/1.1
Host: api.synoveo.com
Authorization: Bearer <user_jwt_token>This removes:
- Stored Google tokens
- Connected locations
- Sync settings
Via Google
Users can also revoke access in Google:
- Go to Google Account Security
- Click Third-party apps with account access
- Find Synoveo and click Remove Access
After revoking in Google, use force_consent: true to reconnect.
Error Handling
Authorization Errors
| Error | Description | Solution |
|---|---|---|
access_denied | User denied consent | Retry OAuth flow |
invalid_grant | Code expired or already used | Request new OAuth URL |
invalid_request | Missing or invalid parameters | Check request format |
Token Errors
{
"status": "error",
"error": {
"code": "GOOGLE_AUTH_ERROR",
"message": "Failed to exchange authorization code",
"details": {
"google_error": "invalid_grant",
"google_description": "Token has been expired or revoked"
}
}
}State Token Errors
{
"status": "error",
"error": {
"code": "AUTH_INVALID_TOKEN",
"message": "Invalid or expired state token"
}
}State tokens expire after 15 minutes. If expired, restart the OAuth flow.
Refresh Token Lifecycle
Google refresh tokens:
- Valid for approximately 6 months
- Can be revoked by user in Google settings
- May be invalidated if user changes password
- New token issued with
force_consent: true
Expiration Alerts
Synoveo sends notifications before refresh token expiration:
- 7 days before: Email warning
- 3 days before: Email + dashboard alert
- Expired: Prompts reconnection in dashboard
Security
State Token (CSRF Protection)
The state parameter is a JWT containing:
{
"return_to": "https://app.synoveo.com/dashboard",
"ts": 1703030400000
}- Signed with HS256 using
JWT_SECRET - Expires in 15 minutes
- Prevents CSRF attacks
Redirect URL Validation
Only whitelisted origins are allowed:
https://app.synoveo.comhttps://synoveo.com- Development:
http://localhost:3000
Invalid redirect URLs are rejected with an error.
Dashboard Integration
The Synoveo dashboard handles OAuth automatically:
// React component example
function LoginButton() {
const handleLogin = async () => {
// 1. Get OAuth URL
const response = await fetch('/api/v1/auth/google-oauth-url', {
method: 'POST',
body: JSON.stringify({ return_to: window.location.href })
})
const { authUrl } = await response.json()
// 2. Redirect to Google
window.location.href = authUrl
}
return <button onClick={handleLogin}>Sign in with Google</button>
}
// Callback page handles code exchange automaticallyScopes Reference
| Scope | Description |
|---|---|
openid | OpenID Connect authentication |
email | User's email address |
profile | User's name and picture |
https://www.googleapis.com/auth/business.manage | Full GBP access |
The business.manage scope grants:
- Read/write access to business information
- Post creation and management
- Review access
- Media management
- Insights and analytics