Skip to main content
CMDashCybersecurity Maturity Dashboard
← Back to Google Workspace

Coming soon

Google Workspace — Integration Setup

Prerequisites

  • A Google Workspace account (any edition — the Admin SDK is available on all of them)
  • A user with the Super Admin role, or a delegated admin role that includes the Users → Read privilege (and Reports if you want the OAuth app inventory)
  • A Google Cloud project you can create an OAuth client in
  • CMDash Essentials tier or above

Step 1: Enable the Admin SDK APIs

  1. Open the Google Cloud Console and select (or create) the project you want to own this integration.
  2. Go to APIs & Services → Library.
  3. Enable Admin SDK API. This covers both the Directory API and the Reports API — there is no separate "Reports API" to enable.

Which organisation a connection reads

Google's authorize URL is not tenant-scoped the way Microsoft's is: one OAuth client can serve any Workspace organisation, and the token CMDash receives acts for the administrator who consents. So what pins a deployment to one organisation is only the Google Cloud OAuth consent screen's user type:

  • Internal — only accounts in the Workspace organisation that owns the Cloud project can consent. Right for a self-hosted install connecting its own organisation.
  • External — any Workspace organisation's administrator can consent. What CMDash SaaS uses: one CMDash-owned client serves every customer.

CMDash adds no hd (hosted-domain) or other domain restriction to the authorize request, so an External client is never limited to one domain by CMDash.

After the code exchange, CMDash reads which Workspace customer id the token belongs to (one user record, through the Directory API it already has access to) and records it on the connection; the integration's page shows it. A reconnect of that connection only accepts consent from the same customer account; to connect a different organisation, Disconnect first (the disconnect is audited and releases the record). The stored token carries the same customer id, and CMDash hands it to nothing — sync, health check, remediation — unless the connection still records that account. A token that cannot read its own customer id (for example a non-admin's) is refused rather than connected.

What happens to a refused grant:

  • Google says the account may not read its own directory (not an administrator, or the directory permission was left unticked on the consent screen): CMDash revokes the grant at Google straight away — such a grant can power no CMDash connection.
  • The consent came from a different Workspace than the connection is bound to: the grant is not revoked. Google revokes an account's whole grant to an app, not one token ("Revocation removes all OAuth 2.0 scopes previously granted to a project", Google), so revoking would also disconnect whatever CMDash connection that same administrator powers — typically that Workspace's own connection in another CMDash workspace. The token is discarded, never stored or logged; that Workspace's administrators can remove CMDash under Security → API controls if the consent was a mistake.
  • Anything else reading the directory — an outage, a Google quota or rate limit (the Directory API reports its per-user rate limit and quotas as HTTP 403 too, Google), or the Admin SDK API not being enabled on the project: nothing is revoked; start the connect again.

The consent is also bound to the browser that clicked Connect, by a short-lived Secure cookie: finish within 10 minutes, in the same browser. A consent link opened by anyone else is refused. The code Google returns is additionally bound to that handshake by PKCE (S256): even a code copied from a log line or an address bar cannot be redeemed without the one-time verifier, which only existed inside the handshake. Because the cookie is Secure, CMDash must be served over HTTPS (browsers make an exception only for http://localhost) with its API on the app's own host; a self-hosted install on plain HTTP is refused when Connect is clicked, with the reason. (Google has the same rule for the callback URL itself: "Redirect URIs must use the HTTPS scheme, not plain HTTP. Localhost URIs (including localhost IP address URIs) are exempt from this rule." — Google.)

Connecting on CMDash SaaS

Available once CMDash's Google app is published and verified by Google (see For the CMDash operator). Until then Connect answers that Google Workspace OAuth is not configured on this deployment.

  1. In CMDash: Integrations → Google Workspace → Connect.
  2. Sign in as a Super Admin (or a delegated admin with Users → Read, and Reports for the OAuth app inventory) and allow the two read-only scopes below.
  3. You are returned to CMDash; the connection records your Workspace customer id and runs its first health check.

If your organisation restricts third-party apps, a Super Admin must first allow CMDash: Security → Access and data control → API controls → Manage Third-Party App Access, add CMDash's client, and set it to Trusted or to Specific Google data limited to the two scopes below (Google: control which apps access Workspace data).

To revoke CMDash's access: Security → Access and data control → API controls → Manage Third-Party App Access, then block or remove CMDash.

Step 2 (self-hosted): Create the OAuth client

CMDash uses a dedicated OAuth client for Google Workspace. Do not reuse the client behind "Sign in with Google" — that client is consented to by every ordinary end user and must never carry admin.directory.* scopes.

  1. APIs & Services → OAuth consent screen — configure it as an Internal app if this project lives in the same Workspace organisation you are connecting. (An install that serves several organisations needs an External app — see For the CMDash operator.)
  2. APIs & Services → Credentials → Create Credentials → OAuth client ID → Web application.
  3. Add CMDash's OAuth redirect URI: <APP_URL>/api/integrations/oauth/callback (or INTEGRATION_OAUTH_REDIRECT_URI, if you set one). It must match exactly.
  4. Copy the Client ID and Client secret.
  5. Set them on the CMDash API as GOOGLE_WORKSPACE_CLIENT_ID and GOOGLE_WORKSPACE_CLIENT_SECRET, and restart.

Scopes requested

CMDash requests exactly two scopes, both read-only:

ScopeWhy
https://www.googleapis.com/auth/admin.directory.user.readonlyRead the user directory: 2-Step Verification enrolment, super/delegated admin flags, suspended and archived state, last login
https://www.googleapis.com/auth/admin.reports.audit.readonlyRead the token audit log — which third-party OAuth apps your users have authorised

CMDash never requests a write scope against your directory. If your consent screen offers to grant more, do not.

Step 3 (optional, self-hosted): Domain-wide delegation

If you would rather CMDash acted through a service account than an interactive admin's grant (CMDash SaaS does not use this):

  1. Create a service account in the same project and enable Domain-wide delegation on it.
  2. In the Workspace Admin Console: Security → Access and data control → API controls → Domain-wide delegation → Add new.
  3. Enter the service account's Client ID and the two scopes above, comma-separated.
  4. The service account must impersonate a real admin user when it mints tokens — Google's Directory API has no "act as the organisation" identity.

Step 4: Connect in CMDash

  1. Navigate to Integrations in your CMDash dashboard.
  2. Find Google Workspace and click Connect.
  3. Optionally set Customer ID — leave it blank to use Google's my_customer alias, which resolves to the account of the admin whose token CMDash holds. Set it explicitly only if you administer more than one Workspace customer from the same identity (a reseller). The customer id CMDash records on the connection is the one this setting reads.
  4. Complete the Google consent flow.
  5. Click Save & Test Connection.

What Syncs

Data TypeGoogle APIRequired scopeBehaviour on failure
Identity / 2SV coverage, admin counts, dormancyGET /admin/directory/v1/users?customer=…admin.directory.user.readonlyFails the sync — this is the primary dataset
Third-party OAuth app inventoryGET /admin/reports/v1/activity/users/all/applications/tokenadmin.reports.audit.readonlyDegrades silently — the SaaS-inventory signal is skipped for that cycle, identity data still lands

How the numbers are computed

  • MFA coverage counts users with isEnrolledIn2Sv. Suspended and archived accounts are excluded from both the numerator and the denominator. An account that cannot sign in cannot enrol a factor, so counting it would make good offboarding hygiene look like a control failure.
  • Privileged accounts are users with isAdmin (super admin) or isDelegatedAdmin (role-scoped admin).
  • Dormant accounts are sign-in-capable accounts whose lastLoginTime is older than 45 days, or that have never signed in (Google reports the Unix epoch, not an absent field, for those). 45 days is the same threshold the Okta and Entra ID integrations use, so dormancy is comparable across identity sources.
  • Third-party OAuth apps are derived from the last 180 days of token audit events, collapsed to one row per OAuth client. An app whose most recent event is a revoke is dropped — it is no longer authorised.

Troubleshooting

  • 401 Unauthorized: The refresh token was revoked, or the admin whose grant CMDash holds was suspended or lost their admin role.
  • Connect refused — different account (directory_mismatch): the consent came from a different Workspace customer than the one this connection is bound to. Sign in to the right organisation, or Disconnect first to change organisation.
  • Connect refused — account not verified (directory_unverified): the token could not read its own customer id — usually the consenting user is not an admin, or the Admin SDK API is not enabled on the project.
  • Connect refused — connect cookie missing (binding_cookie_missing): the browser that came back did not carry the cookie CMDash set when Connect was clicked — a different browser, or one that could not keep it (CMDash on plain HTTP, or its API on a different site than the app). Finish in the browser you started in; if it keeps happening, serve CMDash over HTTPS with the API under the app's own /api path.
  • Connect refused — session mismatch (session_mismatch): the consent finished in a browser holding a different connect cookie than the one that clicked Connect. Start again from the browser you will finish in.
  • Connect refused — superseded (superseded): a newer Connect (or a Disconnect) for Google Workspace happened while you were consenting, so this attempt changed nothing.
  • 403 Forbidden on the directory read: The Admin SDK API is not enabled on the project, or the consenting user is not an admin. A non-admin's token authenticates fine and then 403s here.
  • "SaaS app inventory is empty" but identity data is fine: the admin.reports.audit.readonly scope was not granted. This is by design — CMDash does not fail the whole sync over the optional dataset. Re-consent with both scopes to enable it.
  • MFA coverage looks higher than expected: Suspended and archived accounts are excluded by design (see above). The suspended/archived counts are carried in the observation drill-down so you can see the denominator.
  • Privileged-account governance shows no data: CMDash deliberately emits nothing rather than a perfect score when it reads zero admins — every Workspace account has at least one super admin, so a zero read means the directory read was scoped wrong, not that the domain is admin-free.
  • A very large directory shows truncated in the drill-down: the sync stops after 20 pages (10,000 users). The ratios are still computed, but they are computed over a prefix, and CMDash says so rather than presenting them as whole-domain figures.

Rate Limits & Sync Behavior

SettingValue
Sync frequencyEvery 4 hours by default (syncInterval is configurable per connection)
PaginationDirectory: pageToken, 500 users per page, 20 pages max. Reports: pageToken, 1,000 activities per page, 10 pages max
Rate limit handlingThis adapter does not route through CMDash's shared token-bucket limiter — a Google 429 fails the sync attempt rather than being paced mid-run
Failed sync retryJob queue retries 3 times with exponential backoff (starting at 30s) — see docs/developers/dead-letter.md

Credential Rotation

Google refresh tokens do not expire on a schedule for an Internal app or a published External app, but they stop working when the grant is revoked (Security → API controls → App access control), when the granting admin's account is suspended, or when the token goes unused for six months (Google: refresh token expiration). An External app still in Testing is issued refresh tokens that expire after 7 days.

To rotate the client secret (self-hosted; on SaaS CMDash rotates its own):

  1. In Google Cloud Console: Credentials → your OAuth client → Add secret, then remove the old one after CMDash is updated.
  2. Update GOOGLE_WORKSPACE_CLIENT_SECRET on the CMDash API and restart.
  3. Re-run Save & Test Connection in CMDash.

Known Limitations

  • No group or org-unit breakdown. Coverage is computed domain-wide; there is no per-OU or per-group slice yet.
  • Context-Aware Access and 2SV enforcement policy are not read. CMDash reports 2SV enrolment (isEnrolledIn2Sv), which is the fact that matters for coverage, not whether policy compels it.
  • Device data is not read. Google Endpoint Management devices are not part of this integration.

For the CMDash operator: publishing the Google app

This is the one-time setup that makes Google Workspace connections available on CMDash SaaS (a CMDash operator step). It uses the existing variables GOOGLE_WORKSPACE_CLIENT_ID and GOOGLE_WORKSPACE_CLIENT_SECRET; no code change is needed.

  1. In a CMDash-owned Google Cloud project: APIs & Services → Library → enable Admin SDK API.
  2. OAuth consent screen: user type External. Set the app name, support e-mail and logo, homepage, privacy-policy and terms URLs on a CMDash domain, and add that domain under Authorized domains (it must be verified in Google Search Console).
  3. Data access (scopes): add exactly https://www.googleapis.com/auth/admin.directory.user.readonly and https://www.googleapis.com/auth/admin.reports.audit.readonly, nothing else. The console shows each scope's classification; plan for Google's sensitive-scope verification, and restricted-scope verification with a security assessment if the console marks either scope restricted.
  4. Credentials → Create Credentials → OAuth client ID → Web application. Authorized redirect URI: https://app.cmdash.net/api/integrations/oauth/callback (confirm against APP_URL on the hosts). Put the client id and secret into GOOGLE_WORKSPACE_CLIENT_ID / GOOGLE_WORKSPACE_CLIENT_SECRET on every API host, identically.
  5. Publish to Production and submit for verification. Google states that "sensitive scopes require review by Google before any Google Account can grant access", and that the review "typically takes 3-5 business days" (Google: sensitive scope verification). Brand verification and follow-up questions add to that: plan for weeks. Until verification completes, consenting admins see Google's unverified-app screen, and the number of accounts that can grant access is capped.
  6. Do not connect customers while the app is in Testing: refresh tokens issued then expire after 7 days, which would stop every such customer's sync a week later.
  7. Connect a test Workspace organisation end to end, then a second unrelated one, and confirm each connection records its own customer id.

Ready to see your posture?

Start a 30-day free trial of any paid plan — a card is required, and it converts unless you cancel — or sign up for Free with no card at all.