okengine 0.17.2 → 0.18.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +5 -3
- package/README.md +14 -10
- package/manifest.v1.schema.json +61 -2
- package/package.json +19 -5
- package/site/content/docs/ai/index.mdx +1 -1
- package/site/content/docs/ai/mcp.mdx +10 -1
- package/site/content/docs/ai/skills.mdx +9 -9
- package/site/content/docs/elements/ai.mdx +1 -1
- package/site/content/docs/elements/clock.mdx +1 -1
- package/site/content/docs/elements/flow.mdx +25 -1
- package/site/content/docs/elements/gate.mdx +3 -2
- package/site/content/docs/elements/store.mdx +289 -341
- package/site/content/docs/elements/vault.mdx +5 -5
- package/site/content/docs/get-started/basic-usage.mdx +3 -10
- package/site/content/docs/get-started/index.mdx +1 -1
- package/site/content/docs/get-started/installation.mdx +2 -3
- package/site/content/docs/get-started/introduction.mdx +58 -121
- package/site/content/docs/get-started/meta.json +9 -1
- package/site/content/docs/get-started/project-structure.mdx +4 -11
- package/site/content/docs/get-started/testing.mdx +328 -0
- package/site/content/docs/get-started/why.mdx +93 -71
- package/site/content/docs/index.mdx +44 -11
- package/site/content/docs/meta.json +8 -5
- package/site/content/docs/plugins/apple.mdx +151 -0
- package/site/content/docs/plugins/discord.mdx +139 -0
- package/site/content/docs/plugins/facebook.mdx +134 -0
- package/site/content/docs/plugins/figma.mdx +138 -0
- package/site/content/docs/plugins/github.mdx +138 -0
- package/site/content/docs/plugins/google.mdx +153 -0
- package/site/content/docs/plugins/index.mdx +47 -1
- package/site/content/docs/plugins/meta.json +10 -0
- package/site/content/docs/plugins/microsoft.mdx +151 -0
- package/site/content/docs/plugins/oauth.mdx +188 -0
- package/site/content/docs/plugins/x.mdx +125 -0
- package/site/content/docs/providers/index.mdx +2 -0
- package/site/content/docs/recipes/index.mdx +2 -0
- package/site/content/docs/reference/cli.md +3 -2
- package/site/content/docs/reference/client.mdx +58 -1
- package/site/content/docs/reference/configuration.mdx +2 -4
- package/site/content/docs/reference/fx.mdx +3 -1
- package/site/content/docs/reference/index.mdx +0 -5
- package/site/content/docs/reference/meta.json +2 -2
- package/site/content/docs/reference/okid.mdx +137 -0
- package/src/auth/bindings.ts +1 -1
- package/src/auth/config.ts +9 -0
- package/src/auth/identity-sql.ts +314 -0
- package/src/auth/identity.ts +140 -2
- package/src/auth/index.ts +17 -1
- package/src/auth/method-context.ts +3 -0
- package/src/auth/oauth-as/cimd.ts +132 -0
- package/src/auth/oauth-as/crypto.test.ts +101 -0
- package/src/auth/oauth-as/crypto.ts +393 -0
- package/src/auth/oauth-as/errors.ts +68 -0
- package/src/auth/oauth-as/http.test.ts +419 -0
- package/src/auth/oauth-as/http.ts +842 -0
- package/src/auth/oauth-as/stores.ts +61 -0
- package/src/auth/oauth-as/tables.ts +142 -0
- package/src/auth/tables.ts +0 -11
- package/src/bench/README.md +83 -0
- package/src/bench/REPORT.md +176 -0
- package/src/bench/g01-rls-stamp.bench.ts +194 -0
- package/src/bench/g02-clock-per-tenant.bench.ts +158 -0
- package/src/bench/g03-signal-once.bench.ts +157 -0
- package/src/bench/g03-signal-reconnect.bench.ts +254 -0
- package/src/bench/g03-signal-sse-memory.bench.ts +191 -0
- package/src/bench/g04-auth-vault-hotpath.bench.ts +170 -0
- package/src/bench/g05-sustained-full.bench.ts +265 -0
- package/src/bench/g06-mixed-load.bench.ts +260 -0
- package/src/bench/g07-vault-crypto.bench.ts +100 -0
- package/src/bench/g07-vault-rotate-under-read.bench.ts +285 -0
- package/src/bench/g08-conn-oversubscribe.bench.ts +194 -0
- package/src/bench/g08-store-kv-durable.bench.ts +133 -0
- package/src/bench/g08-store-sql.bench.ts +178 -0
- package/src/bench/g09-journal-sustained.bench.ts +203 -0
- package/src/bench/g10-observability-contention.bench.ts +246 -0
- package/src/bench/g11-cold-start-cycle.bench.ts +164 -0
- package/src/bench/g13-elements.bench.ts +427 -0
- package/src/bench/g14-graceful-shutdown.bench.ts +244 -0
- package/src/bench/g15-postgres-degradation.bench.ts +264 -0
- package/src/bench/g16-live-query-fanout.bench.ts +206 -0
- package/src/bench/lib/event-loop-lag.ts +26 -0
- package/src/bench/lib/infra.ts +60 -0
- package/src/bench/lib/report.ts +52 -0
- package/src/bench/lib/rss-sampler.ts +61 -0
- package/src/bench/lib/signal-pg.ts +88 -0
- package/src/bench/load-app.ts +337 -0
- package/src/bench/load-child.ts +108 -0
- package/src/bench/smoke.bench.ts +43 -0
- package/src/cli/competitor-mention-removal.test.ts +28 -0
- package/src/cli/doctor-fd.ts +117 -0
- package/src/cli/doctor.test.ts +192 -0
- package/src/cli/doctor.ts +129 -1
- package/src/client/create.ts +95 -1
- package/src/client/index.ts +9 -2
- package/src/client/transport.ts +11 -4
- package/src/client/use-live-query.ts +154 -0
- package/src/client-react/index.ts +15 -1
- package/src/client-react/live-resource.ts +246 -0
- package/src/client-react/use-live-query.test.ts +475 -0
- package/src/client-react/use-live-query.ts +530 -0
- package/src/compiler/extract.test.ts +518 -0
- package/src/compiler/extract.ts +386 -19
- package/src/console/server/invoke-user-flow.ts +2 -1
- package/src/console/ui-next/dist/assets/{access-page-DnWbnGzq.js → access-page-De7Lc2JC.js} +1 -1
- package/src/console/ui-next/dist/assets/{flows-page-BiZ4-6yQ.js → flows-page-RGy7VEA_.js} +1 -1
- package/src/console/ui-next/dist/assets/{index-C8NRK2R-.js → index-_rgpdVzo.js} +3 -3
- package/src/console/ui-next/dist/assets/{observability-page-CrB6vd1T.js → observability-page-Ds6pcnh-.js} +1 -1
- package/src/console/ui-next/dist/assets/{store-page-CS5-aETQ.js → store-page-02xOiqIK.js} +3 -3
- package/src/console/ui-next/dist/assets/{units-page-CjtdlW8l.js → units-page-4rHOePuE.js} +1 -1
- package/src/console/ui-next/dist/assets/{vault-page-C6Xxm9SA.js → vault-page-DISPgxLM.js} +1 -1
- package/src/console/ui-next/dist/index.html +1 -1
- package/src/console/ui-next/src/features/store/lib/fields-from-table.ts +36 -2
- package/src/drivers/cdc-outbox.ts +389 -0
- package/src/drivers/memory.ts +20 -0
- package/src/drivers/oauth-apple.ts +156 -0
- package/src/drivers/oauth-discord.ts +79 -0
- package/src/drivers/oauth-facebook.ts +80 -0
- package/src/drivers/oauth-figma.ts +116 -0
- package/src/drivers/oauth-github.ts +92 -0
- package/src/drivers/oauth-google.ts +142 -0
- package/src/drivers/oauth-microsoft.ts +174 -0
- package/src/drivers/oauth-oidc.ts +293 -0
- package/src/drivers/oauth-shared.ts +326 -0
- package/src/drivers/oauth-types.ts +159 -0
- package/src/drivers/oauth-x.ts +77 -0
- package/src/drivers/oauth2-common.ts +95 -0
- package/src/drivers/oauth2-token.ts +61 -0
- package/src/drivers/pg-rls-row-passes.ts +251 -0
- package/src/drivers/pg-rls.ts +2 -0
- package/src/drivers/postgres.ts +45 -2
- package/src/drivers/signal-postgres.ts +2 -1
- package/src/elements/channel/runtime.ts +29 -2
- package/src/elements/channel.test.ts +52 -0
- package/src/elements/gate/boot.ts +29 -2
- package/src/elements/store/emit-drizzle.ts +147 -14
- package/src/elements/store/field-ddl.test.ts +118 -0
- package/src/elements/store/field-types.test.ts +455 -0
- package/src/elements/store/list-query.golden.json +777 -0
- package/src/elements/store/list-query.parity.test.ts +396 -0
- package/src/elements/store/list-query.ts +792 -0
- package/src/elements/store/live-default.test.ts +136 -0
- package/src/elements/store/live-http.test.ts +160 -0
- package/src/elements/store/live-isolation.test.ts +291 -0
- package/src/elements/store/live-query-runtime.test.ts +323 -0
- package/src/elements/store/live-query-runtime.ts +403 -0
- package/src/elements/store/live-query-server.test.ts +377 -0
- package/src/elements/store/live-query-server.ts +102 -0
- package/src/elements/store/live-query.ts +97 -0
- package/src/elements/store/resource.ts +189 -680
- package/src/elements/store/rls-row-passes-policies.parity.test.ts +665 -0
- package/src/elements/store/schema-decl.ts +539 -41
- package/src/elements/store/sql-rls-stamp.test.ts +27 -0
- package/src/elements/store/sql-session.ts +297 -35
- package/src/elements/store/table.ts +102 -21
- package/src/elements/store.test.ts +3 -1
- package/src/elements/store.ts +12 -1
- package/src/elements/vault/chaos-child.ts +74 -1
- package/src/elements/vault/chaos.test.ts +4 -2
- package/src/elements/vault/storage.ts +4 -2
- package/src/index.ts +5 -2
- package/src/kernel/app-auth.ts +1 -0
- package/src/kernel/app.ts +116 -2
- package/src/kernel/auth-sharing.test.ts +196 -0
- package/src/kernel/boot.test.ts +3 -3
- package/src/kernel/errors.ts +8 -0
- package/src/kernel/fx.test.ts +1 -0
- package/src/kernel/fx.ts +14 -2
- package/src/kernel/horizontal-child.ts +2 -1
- package/src/kernel/http-resource.ts +33 -7
- package/src/kernel/identity-host-persist.test.ts +119 -0
- package/src/kernel/instance-id.ts +4 -2
- package/src/kernel/journal.ts +2 -1
- package/src/kernel/mcp-tool.test.ts +95 -0
- package/src/kernel/on.ts +9 -0
- package/src/kernel/realtime-bind.ts +326 -0
- package/src/kernel/resource-live.ts +117 -0
- package/src/kernel/triggers.ts +86 -4
- package/src/manifest/diff.ts +37 -0
- package/src/manifest/types.ts +64 -2
- package/src/okid.bench.test.ts +64 -0
- package/src/okid.test.ts +338 -0
- package/src/okid.ts +245 -0
- package/src/plugins/anonymous.ts +19 -1
- package/src/plugins/auth/shared.ts +15 -0
- package/src/plugins/index.ts +2 -0
- package/src/plugins/magic-link.ts +10 -8
- package/src/plugins/mcp-oauth.ts +208 -0
- package/src/plugins/oauth/flow-store.ts +117 -0
- package/src/plugins/oauth/link.ts +69 -0
- package/src/plugins/oauth/shared.ts +108 -0
- package/src/plugins/oauth/token-vault.ts +100 -0
- package/src/plugins/oauth.security.test.ts +535 -0
- package/src/plugins/oauth.ts +532 -0
- package/src/plugins/otp.ts +48 -6
- package/src/plugins/passkey.ts +20 -1
- package/src/plugins/two-factor.ts +11 -0
- package/src/plugins/username.ts +40 -7
- package/src/release/build-lib.ts +7 -1
- package/src/release/measure.ts +1 -0
- package/src/release/official-plugins.ts +4 -1
- package/src/runs/collect.ts +2 -1
- package/src/runs/drivers/files.ts +2 -1
- package/src/test/create-test-app.ts +114 -5
- package/src/test/export-bundle.test.ts +33 -0
- package/src/test/live-signals.test.ts +83 -0
- package/src/test/tenant-isolation.test.ts +175 -0
- package/src/testing.ts +26 -0
- package/src/upgrade/codemods.ts +1 -1
- package/site/content/docs/reference/migrating-environments.mdx +0 -158
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Google"
|
|
3
|
+
description: "Official plugin — Google sign-in via OIDC discovery and JWKS-verified ID tokens."
|
|
4
|
+
icon: "Globe"
|
|
5
|
+
source: "docs/spec/unified-theory.md"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
Google is the reference OIDC provider for `oauth()`: discovery, ID-token
|
|
9
|
+
signature checks, and issuer pinning all work the textbook way. If you are
|
|
10
|
+
wiring your first social provider, start here.
|
|
11
|
+
|
|
12
|
+
<Callout title="The one rule">
|
|
13
|
+
Register `https://app.example.com/auth/oauth/callback/google` (your exact origin) as an Authorized
|
|
14
|
+
Redirect URI, then enable it with `providers.google.enabled`. The comparison is byte-exact.
|
|
15
|
+
</Callout>
|
|
16
|
+
|
|
17
|
+
## Quick start
|
|
18
|
+
|
|
19
|
+
<Steps>
|
|
20
|
+
|
|
21
|
+
<Step>
|
|
22
|
+
### Create OAuth credentials
|
|
23
|
+
|
|
24
|
+
In Google Cloud Console → APIs & Services → Credentials, create an **OAuth
|
|
25
|
+
client ID** of type _Web application_. Add your callback URI under Authorized
|
|
26
|
+
Redirect URIs.
|
|
27
|
+
|
|
28
|
+
</Step>
|
|
29
|
+
|
|
30
|
+
<Step>
|
|
31
|
+
### Plug it
|
|
32
|
+
|
|
33
|
+
```typescript title="src/app.ts"
|
|
34
|
+
import { oke } from "okengine";
|
|
35
|
+
import { oauth } from "okengine/plugins";
|
|
36
|
+
|
|
37
|
+
export const app = oke({
|
|
38
|
+
name: "shop",
|
|
39
|
+
env: "dev",
|
|
40
|
+
gate: { auth: {} },
|
|
41
|
+
}).plug(
|
|
42
|
+
oauth({
|
|
43
|
+
baseUrl: "https://app.example.com",
|
|
44
|
+
providers: {
|
|
45
|
+
google: { enabled: true },
|
|
46
|
+
},
|
|
47
|
+
}),
|
|
48
|
+
);
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
</Step>
|
|
52
|
+
|
|
53
|
+
<Step>
|
|
54
|
+
### Set the client secret
|
|
55
|
+
|
|
56
|
+
```text
|
|
57
|
+
# .env.local
|
|
58
|
+
OAUTH_GOOGLE_CLIENT_SECRET=GOCSPX-...
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Boot fails loudly if the secret is missing — the contract is declared by the
|
|
62
|
+
plugin itself.
|
|
63
|
+
|
|
64
|
+
</Step>
|
|
65
|
+
|
|
66
|
+
</Steps>
|
|
67
|
+
|
|
68
|
+
## How identity works
|
|
69
|
+
|
|
70
|
+
The driver discovers Google's endpoints from
|
|
71
|
+
`https://accounts.google.com/.well-known/openid-configuration` and caches them.
|
|
72
|
+
On callback it verifies the ID token end-to-end:
|
|
73
|
+
|
|
74
|
+
| Check | Rule |
|
|
75
|
+
| --------- | ------------------------------------------------------- |
|
|
76
|
+
| Signature | RS256 / ES256 against Google's published JWKS |
|
|
77
|
+
| Issuer | must equal `https://accounts.google.com` |
|
|
78
|
+
| Audience | must include your client id (`azp` when multi-audience) |
|
|
79
|
+
| Expiry | rejected when stale |
|
|
80
|
+
| Nonce | single-use, bound to the flow row |
|
|
81
|
+
|
|
82
|
+
Email trust follows the OIDC claim: `email_verified: true` marks the address
|
|
83
|
+
verified; anything else — including string `"false"` — stays unverified.
|
|
84
|
+
|
|
85
|
+
Default scopes: `openid`, `email`, `profile`.
|
|
86
|
+
|
|
87
|
+
## Options
|
|
88
|
+
|
|
89
|
+
| Option | Type | Default | Meaning |
|
|
90
|
+
| --------------------- | ---------- | ------------------- | -------------------------- |
|
|
91
|
+
| `enabled` | `boolean` | `false` | Turn the provider on |
|
|
92
|
+
| `clientId` | `string` | Vault/env\* | `\*OAUTH_GOOGLE_CLIENT_ID` |
|
|
93
|
+
| `redirectUri` | `string` | `{baseUrl}…/google` | Exact registered URI |
|
|
94
|
+
| `scopes` | `string[]` | driver defaults | Extra scopes |
|
|
95
|
+
| `storeProviderTokens` | `boolean` | `false` | Keep tokens in Vault |
|
|
96
|
+
|
|
97
|
+
## Surfaces
|
|
98
|
+
|
|
99
|
+
| Flow | Path |
|
|
100
|
+
| -------- | -------------------------------------- |
|
|
101
|
+
| Start | `POST /auth/oauth/google/start` |
|
|
102
|
+
| Callback | `GET+POST /auth/oauth/callback/google` |
|
|
103
|
+
| Link | `POST /auth/oauth/google/link` |
|
|
104
|
+
|
|
105
|
+
## Troubleshooting
|
|
106
|
+
|
|
107
|
+
<Accordions>
|
|
108
|
+
<Accordion title="redirect_uri_mismatch at Google">
|
|
109
|
+
|
|
110
|
+
The registered URI and the sent URI differ. Byte-exact means scheme, host,
|
|
111
|
+
port, path, no trailing slash drift. Copy the value from
|
|
112
|
+
`providers.google.redirectUri` or `baseUrl` synthesis.
|
|
113
|
+
|
|
114
|
+
</Accordion>
|
|
115
|
+
<Accordion title="invalid_client at token exchange">
|
|
116
|
+
|
|
117
|
+
`OAUTH_GOOGLE_CLIENT_SECRET` is missing or stale. Secrets resolve through the
|
|
118
|
+
Vault chain — update `.env.local` or your Vault driver, then restart.
|
|
119
|
+
|
|
120
|
+
</Accordion>
|
|
121
|
+
<Accordion title="aud_mismatch">
|
|
122
|
+
|
|
123
|
+
You are using credentials from a different Google project than the one that
|
|
124
|
+
issued the code, or swapped client ids between environments.
|
|
125
|
+
|
|
126
|
+
</Accordion>
|
|
127
|
+
</Accordions>
|
|
128
|
+
|
|
129
|
+
## Learn more
|
|
130
|
+
|
|
131
|
+
- [OAuth](/docs/plugins/oauth) — shared flows and security model
|
|
132
|
+
- [Vault](/docs/elements/vault) — where secrets live
|
|
133
|
+
- [Gate](/docs/elements/gate) — `gate.auth`
|
|
134
|
+
|
|
135
|
+
## Next
|
|
136
|
+
|
|
137
|
+
<Cards>
|
|
138
|
+
<Card
|
|
139
|
+
title="Apple"
|
|
140
|
+
description="form_post + ES256 client-secret JWT."
|
|
141
|
+
href="/docs/plugins/apple"
|
|
142
|
+
/>
|
|
143
|
+
<Card
|
|
144
|
+
title="Microsoft"
|
|
145
|
+
description="Entra tenants and issuer templates."
|
|
146
|
+
href="/docs/plugins/microsoft"
|
|
147
|
+
/>
|
|
148
|
+
<Card
|
|
149
|
+
title="GitHub"
|
|
150
|
+
description="OAuth2 with verified-email lookup."
|
|
151
|
+
href="/docs/plugins/github"
|
|
152
|
+
/>
|
|
153
|
+
</Cards>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: "Plugins"
|
|
3
|
-
description: "Official okengine/plugins extensions — authentication, security, operations, and performance."
|
|
3
|
+
description: "Official okengine/plugins extensions — authentication, OAuth, security, operations, and performance."
|
|
4
4
|
icon: "Puzzle"
|
|
5
5
|
source: "docs/spec/unified-theory.md"
|
|
6
6
|
---
|
|
@@ -22,6 +22,52 @@ First-party plugins you `.plug()` onto an app. Each page is one export from `oke
|
|
|
22
22
|
<Card title="Passkey" description="WebAuthn-shaped passkeys." href="/docs/plugins/passkey" />
|
|
23
23
|
</Cards>
|
|
24
24
|
|
|
25
|
+
## OAuth
|
|
26
|
+
|
|
27
|
+
<Cards>
|
|
28
|
+
<Card
|
|
29
|
+
title="OAuth"
|
|
30
|
+
description="Social sign-in — Authorization Code + PKCE for eight providers."
|
|
31
|
+
href="/docs/plugins/oauth"
|
|
32
|
+
/>
|
|
33
|
+
<Card
|
|
34
|
+
title="Apple"
|
|
35
|
+
description="Sign in with Apple — form_post + ES256 JWT."
|
|
36
|
+
href="/docs/plugins/apple"
|
|
37
|
+
/>
|
|
38
|
+
<Card
|
|
39
|
+
title="Discord"
|
|
40
|
+
description="Discord OAuth2 with nullable email."
|
|
41
|
+
href="/docs/plugins/discord"
|
|
42
|
+
/>
|
|
43
|
+
<Card
|
|
44
|
+
title="Facebook"
|
|
45
|
+
description="Facebook Login — never-verified emails."
|
|
46
|
+
href="/docs/plugins/facebook"
|
|
47
|
+
/>
|
|
48
|
+
<Card
|
|
49
|
+
title="Figma"
|
|
50
|
+
description="Figma OAuth2 with HTTP Basic token auth."
|
|
51
|
+
href="/docs/plugins/figma"
|
|
52
|
+
/>
|
|
53
|
+
<Card
|
|
54
|
+
title="GitHub"
|
|
55
|
+
description="GitHub OAuth2 with verified-email lookup."
|
|
56
|
+
href="/docs/plugins/github"
|
|
57
|
+
/>
|
|
58
|
+
<Card
|
|
59
|
+
title="Google"
|
|
60
|
+
description="Google OIDC — JWKS-verified ID tokens."
|
|
61
|
+
href="/docs/plugins/google"
|
|
62
|
+
/>
|
|
63
|
+
<Card
|
|
64
|
+
title="Microsoft"
|
|
65
|
+
description="Entra ID OIDC with tenant-aware issuer checks."
|
|
66
|
+
href="/docs/plugins/microsoft"
|
|
67
|
+
/>
|
|
68
|
+
<Card title="X" description="X OAuth2 public client — PKCE only." href="/docs/plugins/x" />
|
|
69
|
+
</Cards>
|
|
70
|
+
|
|
25
71
|
## Security
|
|
26
72
|
|
|
27
73
|
<Cards>
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Microsoft"
|
|
3
|
+
description: "Official plugin — Entra ID sign-in across personal, work, and school accounts with tenant-aware issuer checks."
|
|
4
|
+
icon: "LayoutGrid"
|
|
5
|
+
source: "docs/spec/unified-theory.md"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
Microsoft (Entra ID) is OIDC with a tenant twist: `common` discovery advertises
|
|
9
|
+
an issuer with the `{tenantid}` placeholder, so the value you pin at start is
|
|
10
|
+
a _template_ validated against the concrete token at callback.
|
|
11
|
+
|
|
12
|
+
<Callout title="The one rule">
|
|
13
|
+
Pick the audience first: `tenant: "common"` for everyone, `organizations` for work/school only,
|
|
14
|
+
`consumers` for personal accounts, or a tenant GUID to lock sign-in to one directory.
|
|
15
|
+
</Callout>
|
|
16
|
+
|
|
17
|
+
## Quick start
|
|
18
|
+
|
|
19
|
+
<Steps>
|
|
20
|
+
|
|
21
|
+
<Step>
|
|
22
|
+
### Register an app
|
|
23
|
+
|
|
24
|
+
In the Azure portal → Microsoft Entra ID → App registrations → New
|
|
25
|
+
registration. Choose _Accounts in any organizational directory and personal
|
|
26
|
+
Microsoft accounts_ for `common`. Add your redirect URI under **Web**.
|
|
27
|
+
|
|
28
|
+
</Step>
|
|
29
|
+
|
|
30
|
+
<Step>
|
|
31
|
+
### Plug it
|
|
32
|
+
|
|
33
|
+
```typescript title="src/app.ts"
|
|
34
|
+
import { oke } from "okengine";
|
|
35
|
+
import { oauth } from "okengine/plugins";
|
|
36
|
+
|
|
37
|
+
export const app = oke({
|
|
38
|
+
name: "shop",
|
|
39
|
+
env: "dev",
|
|
40
|
+
gate: { auth: {} },
|
|
41
|
+
}).plug(
|
|
42
|
+
oauth({
|
|
43
|
+
baseUrl: "https://app.example.com",
|
|
44
|
+
providers: {
|
|
45
|
+
microsoft: { enabled: true }, // tenant defaults to "common"
|
|
46
|
+
},
|
|
47
|
+
}),
|
|
48
|
+
);
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
</Step>
|
|
52
|
+
|
|
53
|
+
<Step>
|
|
54
|
+
### Set the client secret
|
|
55
|
+
|
|
56
|
+
```text
|
|
57
|
+
# .env.local
|
|
58
|
+
OAUTH_MICROSOFT_CLIENT_SECRET=...
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Certificates are not used — the driver authenticates with the shared secret
|
|
62
|
+
form field.
|
|
63
|
+
|
|
64
|
+
</Step>
|
|
65
|
+
|
|
66
|
+
</Steps>
|
|
67
|
+
|
|
68
|
+
## How identity works
|
|
69
|
+
|
|
70
|
+
| Aspect | Behavior |
|
|
71
|
+
| -------------- | ----------------------------------------------------------------------------------------------------- |
|
|
72
|
+
| Discovery | `{tenant}/oauth2/v2.0/.well-known/openid-configuration`, cached |
|
|
73
|
+
| Issuer pinning | flow stores `https://login.microsoftonline.com/{tenantid}/v2.0`; tokens must match it shape-for-shape |
|
|
74
|
+
| Tenant claim | the token's `tid` must be GUID-shaped and consistent with `iss` |
|
|
75
|
+
| Signature | RS256 against the tenant's published keys |
|
|
76
|
+
| Email trust | OIDC `email_verified` claim |
|
|
77
|
+
|
|
78
|
+
**Consequence:** an ID token minted by a _different_ tenant fails the issuer
|
|
79
|
+
template even though its signature is perfectly valid — the mix-up defense
|
|
80
|
+
survives multi-tenancy.
|
|
81
|
+
|
|
82
|
+
Default scopes: `openid`, `email`, `profile`.
|
|
83
|
+
|
|
84
|
+
## Options
|
|
85
|
+
|
|
86
|
+
| Option | Type | Default | Meaning |
|
|
87
|
+
| --------------------- | ---------- | ---------------------- | --------------------------------------- |
|
|
88
|
+
| `enabled` | `boolean` | `false` | Turn the provider on |
|
|
89
|
+
| `clientId` | `string` | Vault/env\* | `\*OAUTH_MICROSOFT_CLIENT_ID` |
|
|
90
|
+
| `tenant` | `string` | `"common"` | `organizations`, `consumers`, or a GUID |
|
|
91
|
+
| `redirectUri` | `string` | `{baseUrl}…/microsoft` | Exact registered URI |
|
|
92
|
+
| `scopes` | `string[]` | driver defaults | Extra scopes |
|
|
93
|
+
| `storeProviderTokens` | `boolean` | `false` | Keep tokens in Vault |
|
|
94
|
+
|
|
95
|
+
## Surfaces
|
|
96
|
+
|
|
97
|
+
| Flow | Path |
|
|
98
|
+
| -------- | ----------------------------------------- |
|
|
99
|
+
| Start | `POST /auth/oauth/microsoft/start` |
|
|
100
|
+
| Callback | `GET+POST /auth/oauth/callback/microsoft` |
|
|
101
|
+
| Link | `POST /auth/oauth/microsoft/link` |
|
|
102
|
+
|
|
103
|
+
## Troubleshooting
|
|
104
|
+
|
|
105
|
+
<Accordions>
|
|
106
|
+
<Accordion title="issuer_mismatch on every login">
|
|
107
|
+
|
|
108
|
+
Your app registration's _supported account types_ disagree with the configured
|
|
109
|
+
`tenant`. `common` requires multi-tenant + personal accounts; a single-tenant
|
|
110
|
+
GUID needs the matching registration.
|
|
111
|
+
|
|
112
|
+
</Accordion>
|
|
113
|
+
<Accordion title="AADSTS50011 (redirect mismatch)">
|
|
114
|
+
|
|
115
|
+
The reply URL registered in Azure does not byte-match the stored
|
|
116
|
+
`redirectUri`. Re-copy from your plugin config; trailing slashes count.
|
|
117
|
+
|
|
118
|
+
</Accordion>
|
|
119
|
+
<Accordion title="No email arrives">
|
|
120
|
+
|
|
121
|
+
Personal Microsoft accounts can omit email entirely. The flow signs the person
|
|
122
|
+
in without one — exactly like Discord phone-only accounts.
|
|
123
|
+
|
|
124
|
+
</Accordion>
|
|
125
|
+
</Accordions>
|
|
126
|
+
|
|
127
|
+
## Learn more
|
|
128
|
+
|
|
129
|
+
- [OAuth](/docs/plugins/oauth) — shared flows and security model
|
|
130
|
+
- [Google](/docs/plugins/google) — plain single-issuer OIDC
|
|
131
|
+
- [Vault](/docs/elements/vault) — where secrets live
|
|
132
|
+
|
|
133
|
+
## Next
|
|
134
|
+
|
|
135
|
+
<Cards>
|
|
136
|
+
<Card
|
|
137
|
+
title="Apple"
|
|
138
|
+
description="form_post + ES256 client-secret JWT."
|
|
139
|
+
href="/docs/plugins/apple"
|
|
140
|
+
/>
|
|
141
|
+
<Card
|
|
142
|
+
title="Discord"
|
|
143
|
+
description="Nullable emails, verified flag."
|
|
144
|
+
href="/docs/plugins/discord"
|
|
145
|
+
/>
|
|
146
|
+
<Card
|
|
147
|
+
title="GitHub"
|
|
148
|
+
description="OAuth2 with verified-email lookup."
|
|
149
|
+
href="/docs/plugins/github"
|
|
150
|
+
/>
|
|
151
|
+
</Cards>
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "OAuth"
|
|
3
|
+
description: "Official plugin — social sign-in with eight providers under /auth, Authorization Code + PKCE only."
|
|
4
|
+
icon: "FingerprintPattern"
|
|
5
|
+
source: "docs/spec/unified-theory.md"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
`oauth()` adds social login to Gate auth. Start at
|
|
9
|
+
`/auth/oauth/{provider}/start`, approve, then land on
|
|
10
|
+
`/auth/oauth/callback/{provider}` with a session — eight providers.
|
|
11
|
+
|
|
12
|
+
<Callout title="The one rule">
|
|
13
|
+
Enable `gate.auth`, then `.plug(oauth({ providers: { ... } }))`. Every
|
|
14
|
+
provider runs Authorization Code + PKCE with an exact registered redirect URI
|
|
15
|
+
— implicit and password grants do not exist here.
|
|
16
|
+
</Callout>
|
|
17
|
+
|
|
18
|
+
## Quick start
|
|
19
|
+
|
|
20
|
+
<Steps>
|
|
21
|
+
|
|
22
|
+
<Step>
|
|
23
|
+
### Plug it
|
|
24
|
+
|
|
25
|
+
```typescript title="src/app.ts"
|
|
26
|
+
import { oke } from "okengine";
|
|
27
|
+
import { oauth } from "okengine/plugins";
|
|
28
|
+
|
|
29
|
+
export const app = oke({
|
|
30
|
+
name: "shop",
|
|
31
|
+
env: "dev",
|
|
32
|
+
gate: { auth: {} },
|
|
33
|
+
}).plug(
|
|
34
|
+
oauth({
|
|
35
|
+
baseUrl: "https://app.example.com",
|
|
36
|
+
providers: {
|
|
37
|
+
google: { enabled: true },
|
|
38
|
+
github: { enabled: true },
|
|
39
|
+
},
|
|
40
|
+
}),
|
|
41
|
+
);
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`baseUrl` synthesizes `{baseUrl}/auth/oauth/callback/{provider}` — register
|
|
45
|
+
that exact string. Client secrets resolve from Vault
|
|
46
|
+
(`OAUTH_GOOGLE_CLIENT_SECRET`, `OAUTH_GITHUB_CLIENT_SECRET`, …).
|
|
47
|
+
|
|
48
|
+
</Step>
|
|
49
|
+
|
|
50
|
+
<Step>
|
|
51
|
+
### Start the flow
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
const { data } = await api.auth.oauthStart({ provider: "google" });
|
|
55
|
+
// redirect the browser to data.authorizationUrl
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The response carries `authorizationUrl`, `expiresInMs`, and the provider echo.
|
|
59
|
+
The flow row (state hash, PKCE verifier, nonce) lives for ten minutes.
|
|
60
|
+
|
|
61
|
+
</Step>
|
|
62
|
+
|
|
63
|
+
<Step>
|
|
64
|
+
### Callback issues a session
|
|
65
|
+
|
|
66
|
+
The provider redirects to your callback route; the flow verifies state,
|
|
67
|
+
exchanges the code, and signs the person in:
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
const body = {
|
|
71
|
+
accessToken: "...",
|
|
72
|
+
refreshToken: "...",
|
|
73
|
+
userId: "usr_...",
|
|
74
|
+
};
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
A brand-new visitor gets a fresh account. Someone whose email already belongs
|
|
78
|
+
to another credential is refused with `email_in_use`.
|
|
79
|
+
|
|
80
|
+
</Step>
|
|
81
|
+
|
|
82
|
+
</Steps>
|
|
83
|
+
|
|
84
|
+
## Providers
|
|
85
|
+
|
|
86
|
+
| Provider | Shape | Identity source | Email verified |
|
|
87
|
+
| --------- | ------ | ------------------------ | ------------------------------------ |
|
|
88
|
+
| Google | OIDC | JWKS-verified ID token | `email_verified` claim |
|
|
89
|
+
| Apple | OIDC | JWKS-verified ID token | strict parse — `"false"` stays false |
|
|
90
|
+
| Microsoft | OIDC | JWKS-verified ID token | `email_verified` claim |
|
|
91
|
+
| GitHub | OAuth2 | `/user` + `/user/emails` | primary `verified` flag |
|
|
92
|
+
| Discord | OAuth2 | `/users/@me` | `verified` flag |
|
|
93
|
+
| X | OAuth2 | `/2/users/me` | never — no trustworthy signal |
|
|
94
|
+
| Facebook | OAuth2 | Graph `/me` | never — no trustworthy signal |
|
|
95
|
+
| Figma | OAuth2 | `/v1/me` | never — no verification field |
|
|
96
|
+
|
|
97
|
+
Each provider has its own page under this category — endpoints, scopes, setup,
|
|
98
|
+
and trust rules.
|
|
99
|
+
|
|
100
|
+
## Security model
|
|
101
|
+
|
|
102
|
+
| Threat | Defense |
|
|
103
|
+
| ---------------------------------- | ----------------------------------------------------------------------- |
|
|
104
|
+
| Code interception | PKCE S256 on every provider |
|
|
105
|
+
| CSRF / forged callbacks | single-use `state`, SHA-256-hashed at rest |
|
|
106
|
+
| Mix-up across providers (RFC 9700) | per-provider callback routes + issuer pinning on every assertion |
|
|
107
|
+
| Unverified-email takeover | `linkOrProvision` refuses `email_in_use` without an authenticated owner |
|
|
108
|
+
| Redirect manipulation | byte-exact stored `redirect_uri` at token exchange |
|
|
109
|
+
|
|
110
|
+
**Consequence:** an attacker who completes a social login claiming your email
|
|
111
|
+
gains nothing — the account stays untouched unless they already own a session
|
|
112
|
+
for it.
|
|
113
|
+
|
|
114
|
+
## Options
|
|
115
|
+
|
|
116
|
+
| Option | Type | Default | Meaning |
|
|
117
|
+
| --------------- | ------------------- | -------- | ---------------------------------------- |
|
|
118
|
+
| `providers` | map | `{}` | Per-provider config, disabled by default |
|
|
119
|
+
| `baseUrl` | `string` | — | Origin for synthesized callback URIs |
|
|
120
|
+
| `secret` | `string` | active\* | HMAC secret (\*from `gate.auth`) |
|
|
121
|
+
| `sessions` | `SessionStore` | active\* | Session store |
|
|
122
|
+
| `identities` | `IdentityStore` | active\* | Shared user store |
|
|
123
|
+
| `verifications` | `VerificationStore` | new | Flow-state store |
|
|
124
|
+
| `fetch` | `typeof fetch` | global | Injectable transport (tests) |
|
|
125
|
+
|
|
126
|
+
Per-provider config: `enabled`, `clientId`, `redirectUri`, `scopes`,
|
|
127
|
+
`storeProviderTokens`, plus Microsoft `tenant` and Apple `teamId` / `keyId`.
|
|
128
|
+
|
|
129
|
+
## Surfaces
|
|
130
|
+
|
|
131
|
+
| Flow | Path | Gate |
|
|
132
|
+
| --------------------- | ------------------------------------------ | ------------------------ |
|
|
133
|
+
| `auth.oauthStart` | `POST /auth/oauth/{provider}/start` | `gate.public` + otp rate |
|
|
134
|
+
| `auth.oauthCallback` | `GET+POST /auth/oauth/callback/{provider}` | `gate.public` + otp rate |
|
|
135
|
+
| `auth.oauthLinkStart` | `POST /auth/oauth/{provider}/link` | session + bearer |
|
|
136
|
+
|
|
137
|
+
The GET + POST callback pair exists because Apple posts its response instead of
|
|
138
|
+
redirecting.
|
|
139
|
+
|
|
140
|
+
## Troubleshooting
|
|
141
|
+
|
|
142
|
+
<Accordions>
|
|
143
|
+
<Accordion title="Boot fails listing OAUTH_…_CLIENT_SECRET">
|
|
144
|
+
|
|
145
|
+
Every enabled provider needs its secret in Vault before boot. Seed
|
|
146
|
+
`OAUTH_{PROVIDER}_CLIENT_SECRET` (Apple uses `OAUTH_APPLE_PRIVATE_KEY`) through
|
|
147
|
+
your Vault driver or `.env.local`.
|
|
148
|
+
|
|
149
|
+
</Accordion>
|
|
150
|
+
<Accordion title="Callback returns invalid_state">
|
|
151
|
+
|
|
152
|
+
The state was consumed, expired (ten-minute TTL), or minted by a different
|
|
153
|
+
provider's start call. Restart from `/start`.
|
|
154
|
+
|
|
155
|
+
</Accordion>
|
|
156
|
+
<Accordion title="Callback returns issuer_mismatch">
|
|
157
|
+
|
|
158
|
+
The ID token's issuer does not match the provider that started the flow — the
|
|
159
|
+
signature was valid but the token came from elsewhere. This rejection is the
|
|
160
|
+
mix-up defense working; retry the real provider.
|
|
161
|
+
|
|
162
|
+
</Accordion>
|
|
163
|
+
<Accordion title="Callback returns email_in_use">
|
|
164
|
+
|
|
165
|
+
The provider returned an email already registered to another account without
|
|
166
|
+
proof you own that account. Sign in with the original method first, then link
|
|
167
|
+
via `POST /auth/oauth/{provider}/link`.
|
|
168
|
+
|
|
169
|
+
</Accordion>
|
|
170
|
+
</Accordions>
|
|
171
|
+
|
|
172
|
+
## Learn more
|
|
173
|
+
|
|
174
|
+
- [Gate](/docs/elements/gate) — `gate.auth`
|
|
175
|
+
- [Passkey](/docs/plugins/passkey) — phishing-resistant alternative
|
|
176
|
+
- [Client](/docs/reference/client) — calling `/auth` from the browser
|
|
177
|
+
|
|
178
|
+
## Next
|
|
179
|
+
|
|
180
|
+
<Cards>
|
|
181
|
+
<Card title="Google" description="OIDC reference provider." href="/docs/plugins/google" />
|
|
182
|
+
<Card
|
|
183
|
+
title="GitHub"
|
|
184
|
+
description="OAuth2 with verified-email lookup."
|
|
185
|
+
href="/docs/plugins/github"
|
|
186
|
+
/>
|
|
187
|
+
<Card title="Passkey" description="WebAuthn-shaped passkeys." href="/docs/plugins/passkey" />
|
|
188
|
+
</Cards>
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "X"
|
|
3
|
+
description: "Official plugin — X (Twitter) sign-in as a PKCE public client with confidential-client secrets left out."
|
|
4
|
+
icon: "X"
|
|
5
|
+
source: "docs/spec/unified-theory.md"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
X is the one provider where `oauth()` behaves as a **public client**: the
|
|
9
|
+
token exchange carries the code verifier but no secret. PKCE is the only
|
|
10
|
+
proof the callback is yours.
|
|
11
|
+
|
|
12
|
+
<Callout title="The one rule">
|
|
13
|
+
Enable OAuth 2.0 in the X Developer Portal, set up your exact callback URI, and request
|
|
14
|
+
`users.email` access if you need addresses. No `OAUTH_X_CLIENT_SECRET` exists — none is read.
|
|
15
|
+
</Callout>
|
|
16
|
+
|
|
17
|
+
## Quick start
|
|
18
|
+
|
|
19
|
+
<Steps>
|
|
20
|
+
|
|
21
|
+
<Step>
|
|
22
|
+
### Configure the app
|
|
23
|
+
|
|
24
|
+
X Developer Portal → Project → User authentication settings → **Set up**.
|
|
25
|
+
Choose _Web App_, enable OAuth 2.0, and add
|
|
26
|
+
`https://app.example.com/auth/oauth/callback/x` as a callback URI.
|
|
27
|
+
|
|
28
|
+
</Step>
|
|
29
|
+
|
|
30
|
+
<Step>
|
|
31
|
+
### Plug it
|
|
32
|
+
|
|
33
|
+
```typescript title="src/app.ts"
|
|
34
|
+
import { oke } from "okengine";
|
|
35
|
+
import { oauth } from "okengine/plugins";
|
|
36
|
+
|
|
37
|
+
export const app = oke({
|
|
38
|
+
name: "shop",
|
|
39
|
+
env: "dev",
|
|
40
|
+
gate: { auth: {} },
|
|
41
|
+
}).plug(
|
|
42
|
+
oauth({
|
|
43
|
+
baseUrl: "https://app.example.com",
|
|
44
|
+
providers: {
|
|
45
|
+
x: { enabled: true },
|
|
46
|
+
},
|
|
47
|
+
}),
|
|
48
|
+
);
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
</Step>
|
|
52
|
+
|
|
53
|
+
</Steps>
|
|
54
|
+
|
|
55
|
+
That is the whole setup — no Vault contract for this provider.
|
|
56
|
+
|
|
57
|
+
## How identity works
|
|
58
|
+
|
|
59
|
+
| Aspect | Behavior |
|
|
60
|
+
| -------------- | ------------------------------------------------------------------------------------------- |
|
|
61
|
+
| Token exchange | `POST https://api.x.com/2/oauth2/token` with the code verifier, no secret |
|
|
62
|
+
| Profile | `GET /2/users/me?user.fields=id,name,username,confirmed_email` |
|
|
63
|
+
| Subject | the string `id` inside `data` |
|
|
64
|
+
| Email trust | **always unverified** — `confirmed_email` gates API access, it does not attest verification |
|
|
65
|
+
|
|
66
|
+
**Consequence:** an X email never claims an existing account during sign-up.
|
|
67
|
+
It can attach to a fresh account or to an existing one you already control via
|
|
68
|
+
authenticated linking.
|
|
69
|
+
|
|
70
|
+
Default scopes: `users.read`, `tweet.read`.
|
|
71
|
+
|
|
72
|
+
## Options
|
|
73
|
+
|
|
74
|
+
| Option | Type | Default | Meaning |
|
|
75
|
+
| --------------------- | ---------- | --------------- | ---------------------------------- |
|
|
76
|
+
| `enabled` | `boolean` | `false` | Turn the provider on |
|
|
77
|
+
| `clientId` | `string` | Vault/env\* | `\*OAUTH_X_CLIENT_ID` |
|
|
78
|
+
| `redirectUri` | `string` | `{baseUrl}…/x` | Exact registered URI |
|
|
79
|
+
| `scopes` | `string[]` | driver defaults | Extra scopes (`offline.access`, …) |
|
|
80
|
+
| `storeProviderTokens` | `boolean` | `false` | Keep tokens in Vault |
|
|
81
|
+
|
|
82
|
+
## Surfaces
|
|
83
|
+
|
|
84
|
+
| Flow | Path |
|
|
85
|
+
| -------- | --------------------------------- |
|
|
86
|
+
| Start | `POST /auth/oauth/x/start` |
|
|
87
|
+
| Callback | `GET+POST /auth/oauth/callback/x` |
|
|
88
|
+
| Link | `POST /auth/oauth/x/link` |
|
|
89
|
+
|
|
90
|
+
## Troubleshooting
|
|
91
|
+
|
|
92
|
+
<Accordions>
|
|
93
|
+
<Accordion title="invalid_request on token exchange">
|
|
94
|
+
|
|
95
|
+
Callback URIs must match exactly and the code must be fresh. X codes are
|
|
96
|
+
single-use and short-lived; flow rows expire after ten minutes too.
|
|
97
|
+
|
|
98
|
+
</Accordion>
|
|
99
|
+
<Accordion title="No email ever arrives">
|
|
100
|
+
|
|
101
|
+
`confirmed_email` requires elevated access plus the `users.email` scope. Even
|
|
102
|
+
then the address stays unverified by design — see the trust table above.
|
|
103
|
+
|
|
104
|
+
</Accordion>
|
|
105
|
+
<Accordion title="403 forbidden">
|
|
106
|
+
|
|
107
|
+
Your app tier lacks user-context authentication, or the requested scope set
|
|
108
|
+
exceeds what the portal grants.
|
|
109
|
+
|
|
110
|
+
</Accordion>
|
|
111
|
+
</Accordions>
|
|
112
|
+
|
|
113
|
+
## Learn more
|
|
114
|
+
|
|
115
|
+
- [OAuth](/docs/plugins/oauth) — shared flows and security model
|
|
116
|
+
- [Facebook](/docs/plugins/facebook) — also never-verified emails
|
|
117
|
+
- [Vault](/docs/elements/vault) — optional token storage
|
|
118
|
+
|
|
119
|
+
## Next
|
|
120
|
+
|
|
121
|
+
<Cards>
|
|
122
|
+
<Card title="Facebook" description="Never-verified emails." href="/docs/plugins/facebook" />
|
|
123
|
+
<Card title="Figma" description="Basic-auth token endpoint." href="/docs/plugins/figma" />
|
|
124
|
+
<Card title="Discord" description="Nullable emails." href="/docs/plugins/discord" />
|
|
125
|
+
</Cards>
|