Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ members = [
"crates/locality-protocol",
"crates/locality-platform",
"crates/locality-store",
"crates/locality-auth-core",
"crates/locality-notion",
"crates/locality-google-docs",
"crates/locality-google-calendar",
Expand All @@ -34,6 +35,7 @@ locality-engine = { path = "crates/locality-engine" }
locality-protocol = { path = "crates/locality-protocol" }
locality-platform = { path = "crates/locality-platform" }
locality-store = { path = "crates/locality-store" }
locality-auth-core = { path = "crates/locality-auth-core" }
locality-notion = { path = "crates/locality-notion" }
locality-google-docs = { path = "crates/locality-google-docs" }
locality-google-calendar = { path = "crates/locality-google-calendar" }
Expand Down
7 changes: 3 additions & 4 deletions apps/oauth-service/.dev.vars.example
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
# Local development only. Never commit .dev.vars.
LOCALITY_BROKER_PUBLIC_BASE_URL="https://oauth.locality.example"
LOCALITY_BROKER_SESSION_SECRET=replace-with-at-least-32-random-bytes
LOCALITY_REFRESH_HANDLE_KEY=replace-with-at-least-32-random-bytes
LOCALITY_NOTION_CLIENT_ID=notion-oauth-client-id
LOCALITY_NOTION_CLIENT_SECRET=notion-oauth-client-secret
LOCALITY_GOOGLE_DOCS_CLIENT_ID=google-docs-oauth-client-id
LOCALITY_GOOGLE_DOCS_CLIENT_SECRET=google-docs-oauth-client-secret
LOCALITY_GMAIL_CLIENT_ID=gmail-oauth-client-id
LOCALITY_GMAIL_CLIENT_SECRET=gmail-oauth-client-secret
LOCALITY_GOOGLE_CLIENT_ID=google-oauth-client-id
LOCALITY_GOOGLE_CLIENT_SECRET=google-oauth-client-secret
LOCALITY_TOKEN_MODE=handle
LOCALITY_NOTION_REDIRECT_URIS=http://localhost:8757/oauth/notion/callback,http://127.0.0.1:8757/oauth/notion/callback
LOCALITY_GOOGLE_DOCS_REDIRECT_URIS=http://localhost:8757/oauth/google-docs/callback,http://127.0.0.1:8757/oauth/google-docs/callback
Expand Down
59 changes: 39 additions & 20 deletions apps/oauth-service/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,29 @@ only performs the confidential token exchange and refresh calls.
## Flow

```text
loc CLI -> broker /start
loc CLI <- authorization_url, state, signed session
loc CLI -> browser -> provider OAuth consent
provider -> localhost callback on the user's machine
loc CLI -> broker /exchange with code, state, session, redirect_uri
broker -> provider token endpoint with client_secret
loc CLI -> broker /start with localhost client redirect_uri
broker -> loc CLI with authorization_url, session, state, provider_redirect_uri
browser -> provider consent using provider_redirect_uri=https://<broker>/v1/oauth/<connector>/callback
provider -> broker HTTPS callback with code/state
broker -> localhost client redirect_uri with code/state
loc CLI -> broker /exchange with code, state, session, and localhost client redirect_uri
broker -> provider token endpoint with provider_redirect_uri and client_secret
broker -> loc CLI with access token and refresh handle
```

Refresh is similarly narrow:

```text
later:
loc CLI -> broker /refresh with refresh_token_handle
broker -> provider token endpoint with client_secret
broker -> loc CLI with new access token and new refresh handle
```

Provider OAuth apps should register only the broker HTTPS callback URLs, such as
`https://oauth.locality.example/v1/oauth/notion/callback`. The localhost URL is
only the desktop completion URL used after the broker receives and verifies the
provider callback.

The current stateless broker uses the signed session token as the OAuth `state`,
so `session` and `state` match in `/start` and `/exchange` payloads.

The broker does not persist page content or tokens. In `handle` mode, it returns
an encrypted opaque refresh handle instead of the raw provider refresh token.

Expand All @@ -51,8 +57,9 @@ Response:
"client_id": "public-client-id",
"authorization_url": "https://api.notion.com/v1/oauth/authorize?...",
"redirect_uri": "http://localhost:8757/oauth/notion/callback",
"provider_redirect_uri": "https://oauth.locality.example/v1/oauth/notion/callback",
"session": "signed-session",
"state": "opaque-state",
"state": "signed-session",
"expires_in": 600
}
```
Expand All @@ -64,7 +71,7 @@ Request:
```json
{
"session": "signed-session",
"state": "opaque-state",
"state": "signed-session",
"code": "provider-authorization-code",
"redirect_uri": "http://localhost:8757/oauth/notion/callback"
}
Expand Down Expand Up @@ -101,8 +108,9 @@ Response:
"client_id": "public-client-id",
"authorization_url": "https://accounts.google.com/o/oauth2/v2/auth?...",
"redirect_uri": "http://localhost:8757/oauth/google-docs/callback",
"provider_redirect_uri": "https://oauth.locality.example/v1/oauth/google-docs/callback",
"session": "signed-session",
"state": "opaque-state",
"state": "signed-session",
"expires_in": 600
}
```
Expand All @@ -114,7 +122,7 @@ Request:
```json
{
"session": "signed-session",
"state": "opaque-state",
"state": "signed-session",
"code": "provider-authorization-code",
"redirect_uri": "http://localhost:8757/oauth/google-docs/callback"
}
Expand Down Expand Up @@ -157,8 +165,9 @@ Response:
"client_id": "public-client-id",
"authorization_url": "https://accounts.google.com/o/oauth2/v2/auth?...",
"redirect_uri": "http://localhost:8757/oauth/google-calendar/callback",
"provider_redirect_uri": "https://oauth.locality.example/v1/oauth/google-calendar/callback",
"session": "signed-session",
"state": "opaque-state",
"state": "signed-session",
"expires_in": 600
}
```
Expand All @@ -170,7 +179,7 @@ Request:
```json
{
"session": "signed-session",
"state": "opaque-state",
"state": "signed-session",
"code": "provider-authorization-code",
"redirect_uri": "http://localhost:8757/oauth/google-calendar/callback"
}
Expand Down Expand Up @@ -209,8 +218,9 @@ Response:
"client_id": "public-client-id",
"authorization_url": "https://accounts.google.com/o/oauth2/v2/auth?...",
"redirect_uri": "http://localhost:8757/oauth/gmail/callback",
"provider_redirect_uri": "https://oauth.locality.example/v1/oauth/gmail/callback",
"session": "signed-session",
"state": "opaque-state",
"state": "signed-session",
"expires_in": 600
}
```
Expand All @@ -222,7 +232,7 @@ Request:
```json
{
"session": "signed-session",
"state": "opaque-state",
"state": "signed-session",
"code": "provider-authorization-code",
"redirect_uri": "http://localhost:8757/oauth/gmail/callback"
}
Expand Down Expand Up @@ -260,8 +270,9 @@ Response:
"client_id": "public-client-id",
"authorization_url": "https://slack.com/oauth/v2/authorize?...",
"redirect_uri": "http://localhost:8757/oauth/slack/callback",
"provider_redirect_uri": "https://oauth.locality.example/v1/oauth/slack/callback",
"session": "signed-session",
"state": "opaque-state",
"state": "signed-session",
"expires_in": 600
}
```
Expand All @@ -273,7 +284,7 @@ Request:
```json
{
"session": "signed-session",
"state": "opaque-state",
"state": "signed-session",
"code": "provider-authorization-code",
"redirect_uri": "http://localhost:8757/oauth/slack/callback"
}
Expand Down Expand Up @@ -307,6 +318,14 @@ Run checks:
npm run check
```

## Required Configuration

- `LOCALITY_BROKER_PUBLIC_BASE_URL`: HTTPS public origin for the broker, for
example `https://oauth.locality.example`. The broker uses this value to build
provider callback URLs returned as `provider_redirect_uri`. `/start` endpoints
fail with `broker_config_error` until it is configured. See
[`docs/deployment.md`](docs/deployment.md) for Cloudflare Workers setup.

## Required Secrets

- `LOCALITY_BROKER_SESSION_SECRET`: signs short-lived OAuth sessions.
Expand Down
61 changes: 42 additions & 19 deletions apps/oauth-service/docs/deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,47 +18,69 @@ wrangler secret put LOCALITY_GOOGLE_CLIENT_ID
wrangler secret put LOCALITY_GOOGLE_CLIENT_SECRET
wrangler secret put LOCALITY_SLACK_CLIENT_ID
wrangler secret put LOCALITY_SLACK_CLIENT_SECRET
wrangler deploy
```

Configure the Notion OAuth integration with the exact localhost callback used by
Locality:
Before deploying the double-redirect broker, configure the required public base
URL:

- `LOCALITY_BROKER_PUBLIC_BASE_URL`: HTTPS public origin for the broker, for
example `https://oauth.locality.example`. Provider OAuth apps must register
callback URLs under this origin:
- `/v1/oauth/notion/callback`
- `/v1/oauth/google-docs/callback`
- `/v1/oauth/google-calendar/callback`
- `/v1/oauth/gmail/callback`
- `/v1/oauth/slack/callback`

Configure it as a non-secret Worker variable, either in the Cloudflare dashboard
or in `wrangler.toml`:

```toml
[vars]
LOCALITY_BROKER_PUBLIC_BASE_URL = "https://oauth.locality.example"
```

Broker `/start` endpoints fail with `broker_config_error` until
`LOCALITY_BROKER_PUBLIC_BASE_URL` is configured.

Configure the Notion OAuth integration with the broker HTTPS callback:

```text
http://localhost:8757/oauth/notion/callback
http://127.0.0.1:8757/oauth/notion/callback
https://oauth.locality.example/v1/oauth/notion/callback
```

Configure one Google OAuth client with the exact localhost callbacks used by
Configure one Google OAuth client with the broker HTTPS callbacks used by
Locality for Google Docs, Google Calendar, and Gmail:

```text
http://localhost:8757/oauth/google-docs/callback
http://127.0.0.1:8757/oauth/google-docs/callback
http://localhost:8757/oauth/google-calendar/callback
http://127.0.0.1:8757/oauth/google-calendar/callback
http://localhost:8757/oauth/gmail/callback
http://127.0.0.1:8757/oauth/gmail/callback
https://oauth.locality.example/v1/oauth/google-docs/callback
https://oauth.locality.example/v1/oauth/google-calendar/callback
https://oauth.locality.example/v1/oauth/gmail/callback
```

Configure the Slack OAuth app with the exact localhost callbacks used by
Locality:
Configure the Slack OAuth app with the broker HTTPS callback:

```text
http://localhost:8757/oauth/slack/callback
http://127.0.0.1:8757/oauth/slack/callback
https://oauth.locality.example/v1/oauth/slack/callback
```

After the secrets, `LOCALITY_BROKER_PUBLIC_BASE_URL`, and provider callbacks are
configured, deploy:

```sh
wrangler deploy
```

Use a stable production URL such as:

```text
https://auth.locality.dev
https://oauth.locality.example
```

The Locality client should have:

```text
LOCALITY_AUTH_BROKER_URL=https://auth.locality.dev
LOCALITY_AUTH_BROKER_URL=https://oauth.locality.example
LOCALITY_NOTION_OAUTH_CLIENT_ID=<public client id>
```

Expand All @@ -68,7 +90,8 @@ The client ID may also be fetched from `/v1/oauth/notion/start`,
binary is fine because it is not confidential. The three Google start endpoints
return the same shared Google OAuth client ID.

Optional broker environment overrides for connector local testing:
Optional broker environment overrides for connector local completion URI
allowlist testing:

```text
LOCALITY_GOOGLE_CALENDAR_REDIRECT_URIS=http://localhost:8757/oauth/google-calendar/callback,http://127.0.0.1:8757/oauth/google-calendar/callback
Expand Down
23 changes: 18 additions & 5 deletions apps/oauth-service/docs/security.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ The broker supports two refresh modes:
- OAuth sessions are short-lived HMAC-signed payloads.
- Session verification checks state, connector, redirect URI, expiry, and payload
shape before exchanging a code.
- Notion, Google Docs, and Gmail redirect URIs are restricted to configured
loopback callback URLs.
- Brokered OAuth connector redirect URIs are restricted to configured loopback
callback URLs.
- Production handle mode keeps provider refresh tokens inside encrypted opaque
handles before returning them to local clients.
- Upstream OAuth error bodies are not returned to callers.
Expand All @@ -56,6 +56,19 @@ Deployment controls to add before public launch:

## Redirects

The broker accepts only configured loopback redirect URIs for Notion, Google
Docs, and Gmail. The Locality CLI should use stable localhost callbacks so each
provider integration can keep a small static redirect allowlist.
The broker accepts only configured loopback redirect URIs for brokered OAuth
connectors, including Notion, Google Docs, Google Calendar, Gmail, and Slack.
The Locality CLI should use stable localhost callbacks so each provider
integration can keep a small static redirect allowlist.

## Provider Callback Boundary

Production brokered OAuth uses a double redirect. Provider applications redirect
to the broker over HTTPS. The broker verifies the signed OAuth state, checks that
the session is for the callback connector, and then redirects back to the
validated loopback client completion URI. The provider never sees
`http://localhost` as its registered redirect URI.

The localhost completion URI remains restricted to `localhost` and `127.0.0.1`
allowlists. The broker callback response sets `Cache-Control: no-store` and
`Referrer-Policy: no-referrer`.
Loading