OAuth2 DB State (oauth2-db-state)
OAuth2 DB State manages OAuth tokens for database connections in Looker (such as BigQuery, Snowflake External OAuth, or Trino Okta) when using custom OAuth setups that Looker does not natively support. It handles user authentication, stores encrypted tokens in a SQLite database, and syncs active access tokens to Looker using create_oauth_application_user_state. A background worker periodically scans stored tokens, refreshes expiring ones with the identity provider, and removes records if a refresh token is revoked.
System Architecture
oauth2-db-state consists of four core subsystems:
Core Subsystems
HTTP Server (internal/server)
Handles the OAuth authorization flow (/login), provider callback (/oauth/callback), health checks (/healthz), and status dashboard (/). It resolves the user's Looker User ID by querying the Looker API (SearchUsers) using the email address returned from the OAuth provider's userinfo endpoint.
Encryption & Storage (internal/db)
Stores tokens in a local SQLite database, encrypting access and refresh tokens at rest using ENCRYPTION_KEY.
Looker API Integration (internal/looker)
Wraps the official Looker SDK to manage Looker API session tokens and user state synchronization. It pushes active access tokens into Looker using CreateOauthApplicationUserState. Refresh tokens are kept in SQLite and omitted from Looker API requests so Looker does not attempt to refresh tokens directly.
Background Maintenance Worker (internal/worker)
Runs a periodic loop (configured via REFRESH_INTERVAL_SECONDS, default 60s) scanning stored tokens in SQLite. It automatically refreshes tokens expiring within the threshold window (REFRESH_WINDOW_SECONDS, default 10m) with the OAuth provider, and purges token records from SQLite if a refresh token is revoked or rejected by the provider.
Looker User Resolution by Email
When a user completes the OAuth flow, the server requests their email from the provider's userinfo endpoint (https://openidconnect.googleapis.com/v1/userinfo) and looks up their corresponding Looker User ID using sdk.SearchUsers.
The email address returned by the identity provider must match the user's registered email address in Looker.
Step-up Auth (Group-Based Scope Upgrades)
Looker's native database OAuth integrations apply a single fixed set of OAuth scopes across an entire instance (for example, native BigQuery OAuth only requests https://www.googleapis.com/auth/bigquery.readonly). If specific users require write access, elevated data insertion permissions, or broader database roles, Looker cannot natively request elevated scopes for just a subset of users.
oauth2-db-state supports Step-up Auth (Incremental Authorization) to request elevated OAuth scopes exclusively for members of specified Looker Groups.
Configuration
Define OAUTH_STEPUP_SCOPES in .env as comma-separated GROUP_ID:SCOPE pairs:
# Baseline scopes for all users
OAUTH_SCOPES="https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/bigquery.readonly"
# Elevated scopes triggered for specific Looker Group members (e.g. BigQuery write access for Group 123)
OAUTH_STEPUP_SCOPES="123:https://www.googleapis.com/auth/bigquery.insertData,456:https://www.googleapis.com/auth/bigquery"
Authorization Flow
- User authenticates with standard baseline scopes (
OAUTH_SCOPES). oauth2-db-stateresolves the user email and queries Looker (sdk.User(userID, "group_ids")) to check group membership.- If the user belongs to a group defined in
OAUTH_STEPUP_SCOPES(such as Group 123) and their token lacks the required scope, the server redirects them back to the OAuth provider to grant the elevated scopes (includinginclude_granted_scopes=truefor Google providers). - Upon consent, the server saves the upgraded token in SQLite and syncs it to Looker via
CreateOAuthApplicationUserState.
Sequence Diagram
Codebase Layout
oauth2-db-state/
├── cmd/oauth2-db-state/ # CLI entrypoint & subcommand handlers
├── internal/
│ ├── config/ # Configuration loader & .env parser
│ ├── db/ # SQLite schema, encryption & CRUD operations
│ ├── looker/ # Looker SDK integration wrapper
│ ├── oauth/ # OAuth 2.0 authorization code exchange & userinfo
│ ├── server/ # HTTP server, handlers & HTML templates
│ └── worker/ # Background token refresh worker
CLI Commands
First-Time Setup
./oauth2-db-state setup
Prompts interactively for OAuth client credentials and Looker instance settings, generates an ENCRYPTION_KEY in .env, discovers OIDC endpoints, and initializes oauth2_state.db.
Start Server and Background Worker
./oauth2-db-state serve
Manual Token Refresh
# Refresh all tokens expiring within threshold
./oauth2-db-state refresh
# Force refresh ALL stored user tokens regardless of expiration
./oauth2-db-state refresh -force
# Force refresh a specific Looker user ID
./oauth2-db-state refresh <looker_user_id>
# or
./oauth2-db-state refresh -user <looker_user_id>
Expire User Token (For Testing Background Worker)
./oauth2-db-state expire-token <looker_user_id>
Sets the user's token expires_at timestamp in SQLite and Looker to 5 minutes in the past while preserving valid token credentials, allowing you to test background worker auto-refresh without waiting.
Invalidate User Token
./oauth2-db-state invalidate-token <looker_user_id>
Sets the user's access_token to "__invalidated__" and expiration timestamp to 1 in SQLite, then syncs "__invalidated__" to Looker via create_oauth_application_user_state.
Test Looker API Connection
./oauth2-db-state test-looker
List OAuth Connections and App IDs
./oauth2-db-state connections
Queries Looker for database connections configured for OAuth and lists registered External OAuth Applications:
Found 1 OAuth Database Connection(s):
• Connection Name: bfw-oauth
Dialect: bigquery_standard_sql
LOOKER_OAUTH_APP_ID: 3
Found 1 Registered External OAuth Application(s):
• LOOKER_OAUTH_APP_ID: 3
App Name: lkr-dev-testing
Dialect: bigquery_standard_sql
Client ID: 599613958943-ftrl8f9d4qoochj7i06a0neipdrsg62t.apps.googleusercontent.com
Environment Variables (.env)
| Variable | Description | Default |
|---|---|---|
ENCRYPTION_KEY | Secret key for token encryption | Required |
HOST | Bind address for HTTP server (0.0.0.0 for containers) | 0.0.0.0 |
PORT | HTTP server port | 8080 |
DB_PATH | Path to SQLite database file | oauth2_state.db |
OAUTH_CLIENT_ID | OAuth provider client ID | "" |
OAUTH_CLIENT_SECRET | OAuth provider client secret | "" |
OAUTH_ISSUER_URL | OAuth provider issuer / base URL | "" |
OAUTH_AUTH_URL | OAuth authorization endpoint URL | "" |
OAUTH_TOKEN_URL | OAuth token endpoint URL | "" |
OAUTH_USERINFO_URL | OAuth userinfo endpoint URL | "" |
OAUTH_REDIRECT_URI | OAuth callback redirect URL | "" |
OAUTH_SCOPES | Space-separated OAuth scope string | "" |
LOOKER_BASE_URL | Looker instance base URL | https://instance.cloud.looker.com |
LOOKER_CLIENT_ID | Looker API 4.0 client ID | "" |
LOOKER_CLIENT_SECRET | Looker API 4.0 client secret | "" |
LOOKER_OAUTH_APP_ID | External OAuth application ID in Looker | "1" |
REFRESH_INTERVAL_SECONDS | Background worker check interval in seconds | 60 |
REFRESH_WINDOW_SECONDS | Token refresh threshold window in seconds | 600 (10 minutes) |
BigQuery & Google OAuth Scopes
When configuring OAUTH_SCOPES for BigQuery OAuth integration with Looker, include the following space-separated scopes:
- Include
https://www.googleapis.com/auth/bigqueryorhttps://www.googleapis.com/auth/cloud-platformto grant access for Looker to execute BigQuery queries on the user's behalf. - Include
https://www.googleapis.com/auth/userinfo.emailto allowoauth2-db-stateto fetch the user's email address from the Google userinfo endpoint and resolve their Looker User ID automatically. - Include
https://www.googleapis.com/auth/userinfo.profileto grant access to basic Google profile details.
Example .env configuration:
OAUTH_SCOPES="https://www.googleapis.com/auth/bigquery https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile"
Refresh Token Handling Across OAuth Providers
Provider Differences
- Google OAuth 2.0 refresh tokens do not expire by default unless revoked by the user, inactive for 6 months, or if the limit of 100 active refresh tokens per user/app is reached. Google requires
access_type=offlineandprompt=consentin the auth URL to issue a refresh token during code exchange. User resolution useshttps://openidconnect.googleapis.com/v1/userinfowith scopehttps://www.googleapis.com/auth/userinfo.email. - Okta, Auth0, and Azure AD (Entra ID) configure refresh token lifetimes in the provider admin console. When providers enforce Refresh Token Rotation (RTR),
oauth2-db-stateupdates SQLite with the newly returned refresh token during each refresh cycle. - Snowflake External OAuth relies on external identity providers (such as Okta, Azure AD, or PingFederate).
oauth2-db-stateexecutes the authorization code grant with the IdP and stores encrypted tokens for Looker synchronization. - Trino Okta and Starburst integrate via Okta or OIDC discovery endpoints and handle token rotation according to Okta security policies.
Refresh Token Lifetimes and Worker Configuration
- Access token expiration (
expires_in) is returned directly by the OAuth provider (typically 3600 seconds). - Refresh token lifetimes are set inside the OAuth provider's admin console, not in API request parameters.
- Background refresh timing is controlled by
REFRESH_INTERVAL_SECONDS(database scan frequency) andREFRESH_WINDOW_SECONDS(lead time before expiration to initiate refresh).
Quick Start
Build and Run Tests
go test -v ./...
CGO_ENABLED=0 go build -o oauth2-db-state ./cmd/oauth2-db-state
Local Development & Hot Reloading
An .air.toml file is configured for hot reloading when .go, .html, or .css files change:
go run github.com/air-verse/air@latest
Public Tunneling for OAuth Callbacks (cloudflared)
Development testing with HTTPS callbacks can use Cloudflare Tunnel:
# Start dev server and Cloudflare Tunnel concurrently:
make dev-tunnel
# Or run just the tunnel alongside an existing server:
./tunnel.sh
Copy the generated https://<subdomain>.trycloudflare.com URL and set OAUTH_REDIRECT_URI in .env to https://<subdomain>.trycloudflare.com/oauth/callback.
Local Setup Steps
-
Run initial setup and endpoint discovery:
./oauth2-db-state setup --discover https://accounts.google.com -
Start dev server and public tunnel if testing remote callbacks:
make dev-tunnel -
Add the redirect URI (
http://127.0.0.1:8080/oauth/callbackor your tunnel URL) to authorized redirect URIs in your OAuth provider console, and updateOAUTH_CLIENT_ID,OAUTH_CLIENT_SECRET, andOAUTH_REDIRECT_URIin.env. -
Start the server:
./oauth2-db-state serveAccess the web interface at
http://127.0.0.1:8080/and view active sessions athttp://127.0.0.1:8080/status.
Known Issues
Looker /account Page Buttons
On Looker's /account page, the standard Reauthorize and Logout buttons for external database OAuth connections do not display and will show as Login instead. This is a known Looker bug.
Initial Database OAuth Login Link
If a new Looker user attempts to use a database with OAuth without using this application first, they may see a Login button that links to Looker's default database authorization URL rather than this hosted application. Users should authenticate through this hosted application first to set up their OAuth state.
Deployment
Resource Requirements & Free Tier Sizing
oauth2-db-state is a lightweight application using a local SQLite database. It has minimal resource overhead and can comfortably run on cloud provider free-tier VMs or small container instances:
- CPU: 0.25 – 1 vCPU
- RAM: 256 MB – 512 MB (1 GB recommended for large user counts with concurrent background refresh jobs)
- Disk: < 1 GB (SQLite database size scales with the number of active users, ~1–2 KB per token record)
Free Tier Compatibility:
- Google Cloud Platform (GCP): Fully compatible with GCP Compute Engine
e2-microAlways Free instance (1 vCPU, 1 GB RAM). - Amazon Web Services (AWS): Compatible with AWS EC2
t2.micro/t3.microFree Tier instances. - Google Cloud Run: Fits within Cloud Run free tier allowances (2 million requests/month, 360,000 GB-seconds memory).
SQLite Storage Requirement in Containers
When deploying to serverless platforms (such as Google Cloud Run or AWS Fargate), mount a persistent volume (e.g. Cloud Storage FUSE, Filestore/NFS, or EFS) for the database directory (DB_PATH). Ephemeral container storage will erase SQLite token records across restarts or scaling events.
Google Cloud Run
Build and deploy using Cloud Build and Cloud Run with a GCS bucket or Filestore volume mount:
gcloud builds submit --config=cloudbuild.yaml
gcloud run deploy oauth2-db-state \
--image us-central1-docker.pkg.dev/lkr-dev-production/oauth2-db-state/server:latest \
--region us-central1 \
--platform managed \
--port 8080 \
--add-volume name=sqlite-storage,type=cloud-storage,bucket=YOUR_GCS_BUCKET \
--add-volume-mount volume=sqlite-storage,mount-path=/app/data \
--set-env-vars DB_PATH=/app/data/oauth2_state.db,HOST=0.0.0.0
Docker Deployment
Public Container Image (lkr-dev-production)
docker pull us-central1-docker.pkg.dev/lkr-dev-production/oauth2-db-state/server:latest
docker run -d \
-p 8080:8080 \
-v $(pwd)/data:/app/data \
-e DB_PATH=/app/data/oauth2_state.db \
-e HOST=0.0.0.0 \
--name oauth2-db-state \
us-central1-docker.pkg.dev/lkr-dev-production/oauth2-db-state/server:latest
Local Container Build
docker build -t oauth2-db-state .
docker run -d \
-p 8080:8080 \
-v $(pwd)/data:/app/data \
-e DB_PATH=/app/data/oauth2_state.db \
-e HOST=0.0.0.0 \
--name oauth2-db-state \
oauth2-db-state
Linux VM Deployment (systemd)
-
Clone the repository, build the binary, and generate the initial
.envfile:git clone https://github.com/lkrdev/oauth2-db-state.git
cd oauth2-db-state
go build -o oauth2-db-state ./cmd/oauth2-db-state
./oauth2-db-state setup -
Copy the binary and
.envfile to/opt/oauth2-db-state:sudo mkdir -p /opt/oauth2-db-state/data
sudo cp oauth2-db-state /opt/oauth2-db-state/
sudo cp .env /opt/oauth2-db-state/.env
sudo chmod 600 /opt/oauth2-db-state/.env[!TIP] For production deployments, store secrets in Google Secret Manager, AWS Secrets Manager, HashiCorp Vault, or systemd credential storage instead of a flat
.envfile. -
Create systemd unit file
/etc/systemd/system/oauth2-db-state.service:[Unit]
Description=OAuth2 DB State Service for Looker
After=network.target
[Service]
Type=simple
User=root
WorkingDirectory=/opt/oauth2-db-state
ExecStart=/opt/oauth2-db-state/oauth2-db-state serve
EnvironmentFile=/opt/oauth2-db-state/.env
Restart=always
RestartSec=5s
[Install]
WantedBy=multi-user.target -
Enable and start the service:
sudo systemctl daemon-reload
sudo systemctl enable oauth2-db-state
sudo systemctl start oauth2-db-state -
Check service status and inspect live structured logs:
sudo systemctl status oauth2-db-state
sudo journalctl -u oauth2-db-state -f