Skip to content
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ coverage
*.local
.env
.env_*
# local env variants like .env.localsetup.env / .env.testsandbox.env (contain credentials)
.env.*
!.env.example
ai_multi_logon_env
env_ai

Expand Down
125 changes: 125 additions & 0 deletions docs/GOOGLE_OIDC_ONBOARDING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# Onboarding Google OIDC login (API Explorer II — frontend / BFF side)

How to add **"Sign in with Google"** to API Explorer II. This is the
client/BFF half of the work; the OBP-API backend must be configured to accept
the resulting tokens — see the companion runbook
`GOOGLE_OIDC_ONBOARDING.md` in the OBP-API repo.

API Explorer II acts as an OIDC **relying party (RP)** running a server-side
(BFF) authorization-code + PKCE flow. It never exposes the client secret to the
browser. Google is just one more provider in the existing multi-provider
framework (`server/services/OAuth2ProviderFactory.ts`,
`OAuth2ProviderManager.ts`).

---

## 1. Prerequisites

- An OBP-API instance that advertises `google` at
`GET /obp/v5.1.0/well-known` and is configured to validate Google id_tokens
(`oauth2.oidc_provider` includes `google`, `oauth2.jwk_set.url` includes
Google's JWKS, and `oauth2.google.allowed_audiences` lists this app's client
ID). See the OBP-API runbook.
- A Google Cloud project with the OAuth consent screen configured.

## 2. Create Google OAuth credentials

In [Google Cloud Console](https://console.cloud.google.com/) →
**APIs & Services → Credentials → Create credentials → OAuth client ID**:

- Application type: **Web application**
- **Authorized redirect URI**: the BFF callback, exactly matching
`VITE_OAUTH2_REDIRECT_URL`, e.g.
- local: `http://localhost:5173/api/oauth2/callback`
- prod: `https://<your-host>/api/oauth2/callback`

Note the generated **Client ID** (`...apps.googleusercontent.com`) and
**Client secret**.

> The Client ID you register here is the value the OBP-API operator must add to
> `oauth2.google.allowed_audiences`. Keep it consistent across environments, or
> register one client per environment and list them all on the backend.

## 3. Configure environment variables

The factory registers a `google` strategy **iff** `VITE_GOOGLE_CLIENT_ID` is
set (`server/services/OAuth2ProviderFactory.ts`). Add to your `.env`:

```bash
# Google OIDC
VITE_GOOGLE_CLIENT_ID=xxxxxxxx.apps.googleusercontent.com
VITE_GOOGLE_CLIENT_SECRET=your-google-client-secret

# Shared by all providers — must match the Google Authorized redirect URI exactly
VITE_OAUTH2_REDIRECT_URL=http://localhost:5173/api/oauth2/callback

# OBP-API host the BFF fetches /well-known from
VITE_OBP_API_HOST=http://localhost:8080
```

No code change is needed — `google` is already a recognized strategy. Scopes
are `openid profile email`.

## 4. How the flow works (what the code already does)

1. **Discovery** — on startup `OAuth2ProviderManager` calls OBP
`/obp/v5.1.0/well-known`, gets the list of providers, and for each one it
has a local strategy for, fetches that provider's
`.well-known/openid-configuration`
(`https://accounts.google.com/.well-known/openid-configuration` for Google).
Providers advertised by OBP but **not** configured locally are skipped (not
retried — config is read once at startup).
2. **Connect** — `GET /api/oauth2/connect?provider=google` generates PKCE +
`state`, stores them in the session, and redirects to Google's
`authorization_endpoint` with `scope=openid profile email`,
`code_challenge_method=S256`.
3. **Callback** — `GET /api/oauth2/callback` validates `state`, exchanges the
code (+ PKCE verifier) for tokens, and fetches userinfo using the **access
token**.
4. **Token selection for OBP** — this is the Google-specific bit
(`server/routes/oauth2.ts`). Google's access token is **opaque**
(`ya29...`), not a JWT, but OBP-API requires a JWT Bearer. So the BFF stores
the **`id_token`** in `session.clientConfig.oauth2.accessToken` when the
access token is not a 3-part JWT. Providers that issue JWT access tokens
(obp-oidc, Keycloak) are sent unchanged.

```ts
const isJwt = (token?: string) => !!token && token.split('.').length === 3
const obpAccessToken = isJwt(tokens.accessToken) ? tokens.accessToken : tokens.idToken
```

## 5. Verify

1. Restart the Explorer server; the logs should show
`OK Google strategy loaded` and `OAuth2ProviderManager: OK google initialized`.
2. `curl http://localhost:5173/api/oauth2/providers` → `google` listed as
`healthy`.
3. In the UI, click **Sign in with Google**, complete consent, and confirm you
land back authenticated and OBP calls succeed (no `OBP-20214` / `OBP-20217` /
`OBP-20218`).

## 6. Troubleshooting

| Symptom | Likely cause |
|---|---|
| `google` not offered in UI | OBP `/well-known` doesn't list it, or `VITE_GOOGLE_CLIENT_ID` unset locally (provider skipped at startup) |
| `redirect_uri_mismatch` from Google | `VITE_OAUTH2_REDIRECT_URL` ≠ the Authorized redirect URI in Google Console |
| OBP `OBP-20214` (token not recognised) | BFF sent the opaque access token instead of the id_token — check the `isJwt` branch; or OBP's `oauth2.jwk_set.url` lacks Google's JWKS |
| OBP `OBP-20217` (audience not allowed) | This app's Client ID isn't in `oauth2.google.allowed_audiences` on OBP |
| OBP `OBP-20218` (provider not enabled) | `google` missing from `oauth2.oidc_provider` on OBP |

## 7. Known limitation / follow-up

The Google `id_token` stored for OBP calls is **short-lived (~1h)** and is
captured once at callback. `OAuth2ClientWithConfig.refreshTokens()` exists but
is **not** wired into the OBP request path, so Google-backed OBP calls will
start failing after the id_token expires even while the session looks valid.
Refreshing the id_token (using the stored refresh token) before it expires is
the main outstanding hardening item. Adding `nonce` to the auth request and
validating it on the id_token is a further hardening step.

---

**Related:** `../README.md` (OAuth2 multi-provider setup),
`server/routes/oauth2.ts`, `server/services/OAuth2ProviderFactory.ts`,
OBP-API `GOOGLE_OIDC_ONBOARDING.md` and `OAUTH2_IDENTITY_PROVIDERS.md`.
2 changes: 1 addition & 1 deletion src/components/ChatWidgetOld.vue
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@
const route = useRoute();
const loginUrl = computed(() => {
const currentPath = route.path;
const queryString = new URLSearchParams(route.query as Record<string, string>).toString();
const queryString = new URLSearchParams(route.query).toString();
const fullPath = queryString ? `${currentPath}?${queryString}` : currentPath;
return `/api/oauth2/connect?redirect=${encodeURIComponent(fullPath)}`;
});
Expand Down