BluAuth
Docs
Sign in
User FAQ
  • Reset my password
  • I can't sign in
  • Didn't get reset email
  • Account linking
  • Session expiry
  • Two-factor auth
Admin Guides
Theme Studio
  • Overview
  • Layouts
  • Styling tokens
  • Concept copy
  • Assets & backgrounds
  • Advanced CSS
Admin Shell
  • Users
  • Providers
  • Clients
  • Invitations
Integrations
  • OIDC flow
  • Legacy OAuth flow
  • Provider token brokering
  • Email triggers
  • Webhook events
  • Session contract
Reference
  • API
  • Error codes
  • Event shapes
  • Design tokens
Runbooks
  • Deployment
  • Local operations

Provider Token Brokering

Some downstream apps need more than the user's identity — they need to call the upstream provider's APIs on the user's behalf. Example: media-conductor reading a user's Google Drive, or a workflow tool posting to their Microsoft Teams.

BluAuth brokers those upstream tokens so downstream apps never implement upstream OAuth themselves. The user consents once, at BluAuth. BluAuth holds the refresh token, refreshes upstream tokens on demand, and hands downstream apps a short-lived access token for each request.

This page covers how to ask for a brokered token, how BluAuth decides whether to grant it, and the error semantics a client must handle.

Why brokering, not redirection

The architectural mandate is that no downstream Blu app implements standalone OAuth against a third-party IdP. Every third-party identity relationship is owned by BluAuth. The benefits:

  • Single consent surface. The user consents to "media-conductor via BluAuth" once, not per-app.
  • Centralized refresh. Refresh tokens live in one encrypted store and rotate correctly even for Microsoft's rotating-refresh-token model.
  • Revocation in one place. Admin-level revocation from /admin/users/:id invalidates everything downstream.
  • No secret distribution. Downstream apps never see upstream client secrets or refresh tokens.

The principle

  • BluAuth owns the upstream OAuth relationship. The user consents during BluAuth sign-in, and BluAuth stores the upstream refresh token (KMS-encrypted, at rest in the accounts table).
  • Downstream apps call BluAuth, not the upstream provider, when they need a fresh upstream access token.
  • Refresh is transparent. BluAuth refreshes upstream tokens on demand; the downstream app receives only the short-lived access token.
  • Tokens are per-client. Each OAuth client must be explicitly allowed to broker tokens for a given provider (via oauth_clients.allowed_provider_tokens).

Consent and scope allowlisting

Two allowlists apply per request:

  1. Client-level: Admins configure each OAuth client's allowedProviderTokens — an array of provider slugs (e.g. ["google", "microsoft"]). A request for google tokens from a client that doesn't list google returns unauthorized_client.
  2. User-level: Each (user, client, provider) tuple has a grant record in provider_token_grants capturing the exact scopes the user consented to. Downstream apps can only request scopes that are a subset of the granted scopes.

When a user signs in via BluAuth with additional_scopes=<upstream-scopes> passed through the authorize flow, the upstream provider's consent screen lists those scopes. On success, the grant record is written or updated. Subsequent token requests for that client check the grant.

Endpoint

POST https://auth.example.com/api/provider-tokens/<provider>
Authorization: Bearer <bluauth-access-token>
Content-Type: application/json

{
  "requiredScopes": ["https://www.googleapis.com/auth/drive.readonly"]
}

Where <provider> is the provider slug — one of google, microsoft, github, or a custom OIDC provider slug configured in /admin/providers.

Authentication is the BluAuth access token (JWT, ES256) issued to your client via the OIDC flow. The token must belong to the same user whose upstream tokens you want.

Source: server/api/provider-tokens/[provider].post.ts.

Request parameters

FieldRequiredNotes
requiredScopesNoArray of upstream scope strings. When omitted, BluAuth returns whatever scopes were last granted for this user/client/provider. When provided, BluAuth verifies every entry is present in the stored grant before returning the token.

Scopes use the upstream provider's syntax:

  • Google — full URLs like https://www.googleapis.com/auth/drive.readonly, https://www.googleapis.com/auth/calendar.events.
  • Microsoft — delegated permission names like Mail.Read, Files.Read.All.
  • GitHub — OAuth scopes like repo, read:org.

BluAuth does not translate or normalize scope strings; pass them exactly as the upstream documents them.

Response

{
    "success": true,
    "data": {
        "accessToken": "ya29.a0AQ...",
        "expiresIn": 3240,
        "provider": "google",
        "scopes": ["https://www.googleapis.com/auth/drive.readonly", "openid", "email"],
        "clientMetadata": {
            "clientId": "533765293212-abc123.apps.googleusercontent.com",
            "appId": "533765293212"
        }
    }
}
  • accessToken — the upstream provider's access token. Use it as a Bearer token against Google/Microsoft/GitHub APIs directly.
  • expiresIn — seconds until this specific token expires. Never assume the 3600-second default; the value returned here accounts for cache age and refresh recency.
  • scopes — every scope currently associated with the stored token. A superset of requiredScopes.
  • clientMetadata — public identifiers that downstream widgets (notably the Google Drive Picker web component) need alongside the access token:
    • clientMetadata.clientId — the OAuth 2.0 client ID BluAuth uses for this provider. Publicly visible on any consent screen; safe to expose to the browser.
    • clientMetadata.appId — present only for google providers. Google Cloud project number derived from the client ID prefix. The Picker widget requires it alongside the OAuth token.
    • Downstream apps must NOT hold their own copies of these identifiers. They correspond to BluAuth's Google Cloud project, not the consuming app's. Re-read them on every broker call.

Security headers on the response are forced to Cache-Control: no-store, private — do not log this body, and do not store the access token beyond request scope.

Encryption and storage

  • Upstream access tokens are KMS-encrypted at rest in accounts.access_token and decrypted on demand.
  • Upstream refresh tokens are KMS-encrypted in accounts.refresh_token and never leave BluAuth. They are not exposed to any endpoint.
  • Decryption uses the same AWS KMS key as other BluAuth secrets (client secrets, webhook secrets).
  • A per-(user, provider) lock (provider-refresh:<userId>:<providerSlug>) serializes refresh attempts so concurrent requests don't waste upstream rate-limit quota or lose a rotated refresh token.

Refresh strategy

When a request arrives:

  1. BluAuth looks up the user's accounts row for the provider.
  2. If the stored access token is valid for at least 5 more minutes, it's decrypted and returned directly.
  3. Otherwise, BluAuth acquires a refresh lock, re-checks freshness (another thread may have refreshed), and exchanges the stored refresh token at the upstream's token endpoint.
  4. For Microsoft, which rotates refresh tokens on every exchange, BluAuth persists the new refresh token atomically alongside the new access token.
  5. The new access token is KMS-encrypted, stored, and returned.

If the upstream rejects the refresh token (user revoked at the provider, admin revoked, scope change required, etc.), BluAuth returns upstream_reauth_required. Your app should walk the user through BluAuth sign-in again with the scopes they need. See error handling below.

Example: Google Drive on behalf of the user

const bluAuthAccessToken = req.session.bluauthAccessToken;

const res = await fetch('https://auth.example.com/api/provider-tokens/google', {
    method: 'POST',
    headers: {
        Authorization: `Bearer ${bluAuthAccessToken}`,
        'Content-Type': 'application/json'
    },
    body: JSON.stringify({
        requiredScopes: ['https://www.googleapis.com/auth/drive.readonly']
    })
});

if (!res.ok) {
    const err = await res.json();
    if (err.error?.code === 'upstream_reauth_required') {
        return redirectToReauth(err.error.provider, err.error.requiredScopes);
    }
    throw new Error(`provider-token failed: ${err.error?.code}`);
}

const { data } = await res.json();

const drive = await fetch('https://www.googleapis.com/drive/v3/files?pageSize=10', {
    headers: { Authorization: `Bearer ${data.accessToken}` }
});

Error handling

All errors use the BluAuth standard error envelope:

{
    "success": false,
    "error": {
        "code": "upstream_reauth_required",
        "message": "Refresh token has been revoked",
        "status": 403
    }
}
Error codeHTTPMeaningRequired action
invalid_token401BluAuth access token is missing, malformed, or expired.Re-authenticate the user via OIDC.
unauthorized_client403The calling client is not allowed to broker tokens for this provider.Ask an admin to add the provider slug to the client's allowedProviderTokens.
no_linked_account404The user has never linked this provider.Redirect the user through BluAuth sign-in with the provider.
insufficient_scope403The grant does not include every requiredScope. The error payload includes grantedScopes and requiredScopes.Redirect user through BluAuth sign-in with the additional scopes.
upstream_reauth_required403The refresh token is revoked / invalid. BluAuth has cleared the grant.Redirect user through BluAuth sign-in.
upstream_provider_error502Upstream provider returned an error during refresh.Log the providerError detail; retry with exponential backoff. Check provider status page before escalating.
validation_error400requiredScopes was not an array of strings.Fix the request.

Re-authentication flow

When a downstream app receives upstream_reauth_required or insufficient_scope, drive the user back through the OIDC authorize flow with the upstream scopes inlined. BluAuth propagates additional_scopes to the upstream provider's consent screen:

GET https://auth.example.com/api/oidc/authorize
  ?client_id=<your-client>
  &redirect_uri=<your-callback>
  &response_type=code
  &scope=openid%20profile%20email
  &state=<opaque>
  &nonce=<opaque>
  &code_challenge=<...>
  &code_challenge_method=S256
  &additional_scopes=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fdrive.readonly

additional_scopes are upstream scopes. BluAuth validates they're in the provider's admin-approved list and then forwards them upstream. On successful consent, the grant record is updated and subsequent /api/provider-tokens/... calls succeed.

What BluAuth does NOT do

  • Does not expose refresh tokens. Ever. Not via any endpoint, not to any client.
  • Does not proxy API calls. Downstream apps still talk directly to Google/Microsoft/etc. with the brokered access token. BluAuth is a token source, not a reverse proxy.
  • Does not support arbitrary providers. Only providers explicitly configured in /admin/providers with refresh-token issuance enabled and allowedProviderTokens populated on the client.
  • Does not support providers that don't return refresh tokens. GitHub's OAuth Apps, for example, issue long-lived access tokens without refresh; those can be brokered but cannot be refreshed — once they expire, the user re-authenticates.
  • Does not validate upstream scopes. BluAuth trusts the upstream provider to reject malformed scopes at consent time.

Limitations

  • Not all providers support offline access. Google requires access_type=offline&prompt=consent to issue a refresh token; the BluAuth Google provider strategy sets these automatically. Microsoft requires the offline_access scope. For custom OIDC providers, the admin must configure scopes and prompts correctly.
  • Scope additions require consent. A scope that wasn't in the original consent won't retroactively appear in the grant — the user must re-authenticate.
  • Token revocation is not instantaneous. If the user revokes access at the upstream provider directly (e.g. via Google's third-party apps page), BluAuth discovers the revocation on the next refresh attempt and returns upstream_reauth_required. In-flight access tokens remain valid upstream until they expire.

On this page

  • Why brokering, not redirection
  • The principle
  • Consent and scope allowlisting
  • Endpoint
  • Request parameters
  • Response
  • Encryption and storage
  • Refresh strategy
  • Example: Google Drive on behalf of the user
  • Error handling
  • Re-authentication flow
  • What BluAuth does NOT do
  • Limitations
DocsPrivacyTerms
© 2026 Blu Digital Group