zuplo 6.73.26 → 6.73.28
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/docs/articles/workload-identity-inbound.mdx +359 -0
- package/docs/build-with-ai.mdx +11 -2
- package/docs/cli/create-zuplo-api.mdx +227 -13
- package/docs/cli/overview.mdx +3 -1
- package/docs/concepts/upstream-credentials.mdx +193 -0
- package/docs/policies/_index.md +1 -0
- package/docs/policies/upstream-oauth-client-credentials-inbound/doc.md +124 -0
- package/docs/policies/upstream-oauth-client-credentials-inbound/intro.md +18 -0
- package/docs/policies/upstream-oauth-client-credentials-inbound/schema.json +126 -0
- package/package.json +4 -4
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Authenticate a cloud workload identity
|
|
3
|
+
sidebar_label: Cloud Workload Identity
|
|
4
|
+
description:
|
|
5
|
+
Verify the OIDC identity token a GCP, AWS, or Azure workload mints from its
|
|
6
|
+
own ambient credentials, then pin the route to the workloads you allow.
|
|
7
|
+
tags:
|
|
8
|
+
- authentication
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
A workload running in a cloud already has an identity: a Google service account,
|
|
12
|
+
a Kubernetes service account on EKS or AKS. On most of those platforms it can
|
|
13
|
+
get a short-lived OIDC identity token for that identity — minted on demand by
|
|
14
|
+
the metadata server on GCP, projected into the pod on EKS and AKS. The caller
|
|
15
|
+
then authenticates to your gateway without anyone creating, distributing, or
|
|
16
|
+
rotating a new secret.
|
|
17
|
+
|
|
18
|
+
Zuplo has no dedicated GCP or Azure inbound JWT policy, and no inbound AWS SigV4
|
|
19
|
+
verification policy. The path that works is the generic
|
|
20
|
+
[Open ID JWT Auth](../policies/open-id-jwt-auth-inbound.mdx) policy pointed at
|
|
21
|
+
the cloud's OIDC issuer, followed by
|
|
22
|
+
[Require User Claims](../policies/require-user-claims-inbound.mdx) to decide
|
|
23
|
+
which of that issuer's identities may call the route.
|
|
24
|
+
|
|
25
|
+
## How it works
|
|
26
|
+
|
|
27
|
+
1. The caller asks its platform for an identity token for a fixed audience
|
|
28
|
+
string.
|
|
29
|
+
2. The JWT policy validates the signature against the issuer's JWKS, checks the
|
|
30
|
+
`iss` and `aud` claims, and populates `request.user`.
|
|
31
|
+
3. A claims policy allows only the identities on your allowlist and returns
|
|
32
|
+
`403` to everything else.
|
|
33
|
+
4. Later policies (rate limits, quotas, logging) group on the identity that step
|
|
34
|
+
2 established.
|
|
35
|
+
|
|
36
|
+
Step 3 isn't optional. Every Google customer's workloads share the issuer
|
|
37
|
+
`https://accounts.google.com`, so on GCP the issuer check alone proves only that
|
|
38
|
+
_someone's_ Google service account made the call. The audience check and the
|
|
39
|
+
claims allowlist are what narrow that to your workloads. A cluster issuer on EKS
|
|
40
|
+
or AKS is already yours, but the claims rule is still what separates one
|
|
41
|
+
namespace or service account from every other one on the cluster.
|
|
42
|
+
|
|
43
|
+
## Prerequisites
|
|
44
|
+
|
|
45
|
+
- A Zuplo project with at least one route.
|
|
46
|
+
- A caller running on a platform that issues OIDC identity tokens for its own
|
|
47
|
+
workload identity: Compute Engine, GKE, Cloud Run, or Cloud Build on GCP; EKS
|
|
48
|
+
with IAM Roles for Service Accounts or EKS Pod Identity on AWS; AKS workload
|
|
49
|
+
identity on Azure.
|
|
50
|
+
- Access to set an [environment variable](./environment-variables.mdx) on the
|
|
51
|
+
project.
|
|
52
|
+
|
|
53
|
+
## 1/ Settle on a fixed audience
|
|
54
|
+
|
|
55
|
+
The gateway's job is to require one exact `aud` value. Settle on that value
|
|
56
|
+
first, and store it as an environment variable assigned to every environment:
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
GATEWAY_AUDIENCE=https://api.example.com/agents
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Changing an environment variable requires a new deployment, so treat this value
|
|
63
|
+
as stable configuration rather than something to tune per branch.
|
|
64
|
+
|
|
65
|
+
How much freedom you have in choosing it belongs to the caller's platform, not
|
|
66
|
+
to the gateway. On GCP the caller passes any audience it likes as a query
|
|
67
|
+
parameter, per call. On EKS and AKS the audience is fixed on the projected-token
|
|
68
|
+
volume, so it is chosen once at deploy time — on AKS it defaults to
|
|
69
|
+
`api://AzureADTokenExchange`. On a token issued by Microsoft Entra ID it has to
|
|
70
|
+
be a registered Application ID URI, in the form `api://<app-client-id>`. Where
|
|
71
|
+
the platform constrains the value, the gateway's `audience` has to match what
|
|
72
|
+
the platform already issues rather than a value you invent.
|
|
73
|
+
|
|
74
|
+
:::caution{title="Keep the audience decoupled from the deployment URL"}
|
|
75
|
+
|
|
76
|
+
Where you do get to choose, reusing the deployment URL as the audience looks
|
|
77
|
+
tidy until the first preview build. Every branch deployment gets its own
|
|
78
|
+
hostname, so every branch would need its own audience, its own caller-side
|
|
79
|
+
configuration, and a redeploy to change. One constant that never varies across
|
|
80
|
+
environments keeps a single caller configuration working everywhere.
|
|
81
|
+
|
|
82
|
+
:::
|
|
83
|
+
|
|
84
|
+
## 2/ Mint a token in the caller
|
|
85
|
+
|
|
86
|
+
On GCP, a workload reads an identity token straight from the metadata server,
|
|
87
|
+
with no SDK and no `gcloud` install required:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
AUDIENCE="https://api.example.com/agents"
|
|
91
|
+
|
|
92
|
+
TOKEN=$(curl -s -H "Metadata-Flavor: Google" \
|
|
93
|
+
"http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?audience=${AUDIENCE}")
|
|
94
|
+
|
|
95
|
+
curl -H "Authorization: Bearer ${TOKEN}" \
|
|
96
|
+
https://my-api-main-abc123.zuplo.app/agents/ping
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
The decoded payload of a service account identity token looks like this:
|
|
100
|
+
|
|
101
|
+
```json
|
|
102
|
+
{
|
|
103
|
+
"aud": "https://api.example.com/agents",
|
|
104
|
+
"azp": "112010400000000710080",
|
|
105
|
+
"email": "batch-runner@my-project.iam.gserviceaccount.com",
|
|
106
|
+
"email_verified": true,
|
|
107
|
+
"exp": 1745365618,
|
|
108
|
+
"iat": 1745362018,
|
|
109
|
+
"iss": "https://accounts.google.com",
|
|
110
|
+
"sub": "112010400000000710080"
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Two claims carry the caller's identity, and they are not interchangeable. `sub`
|
|
115
|
+
is the service account's immutable numeric unique ID. `email` is its address,
|
|
116
|
+
which is what a reviewer recognizes when reading an allowlist six months later.
|
|
117
|
+
Pick whichever your review process prefers, or match on either with an `or`
|
|
118
|
+
rule.
|
|
119
|
+
|
|
120
|
+
:::tip{title="Getting a token on a workstation"}
|
|
121
|
+
|
|
122
|
+
For local development,
|
|
123
|
+
`gcloud auth print-identity-token --audiences="$AUDIENCE"` prints the same kind
|
|
124
|
+
of token. Pass `--audiences` explicitly: without it you get a token with an
|
|
125
|
+
audience the gateway is not configured to accept, and a `401` that looks like a
|
|
126
|
+
signature problem. Add `--include-email` when printing a token for an
|
|
127
|
+
impersonated service account, otherwise the token carries no `email` claim and
|
|
128
|
+
an email allowlist rejects it.
|
|
129
|
+
|
|
130
|
+
:::
|
|
131
|
+
|
|
132
|
+
### Other clouds
|
|
133
|
+
|
|
134
|
+
| Platform | Where the caller gets the token | `issuer` | `jwkUrl` |
|
|
135
|
+
| ------------------------------------------------------------------ | ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | -------------------------------------------------------------- |
|
|
136
|
+
| GCP service accounts (Compute Engine, GKE, Cloud Run, Cloud Build) | Metadata server identity endpoint, with the audience as a query parameter | `https://accounts.google.com` | `https://www.googleapis.com/oauth2/v3/certs` |
|
|
137
|
+
| AWS EKS (IAM Roles for Service Accounts, EKS Pod Identity) | Projected service account token, with the audience set on the volume | The cluster's OIDC issuer URL | The `jwks_uri` from the issuer's OpenID configuration document |
|
|
138
|
+
| AKS workload identity | Projected service account token, with the audience set on the volume | The cluster's OIDC issuer URL, from `az aks show --query oidcIssuerProfile.issuerUrl` | The `jwks_uri` from the issuer's OpenID configuration document |
|
|
139
|
+
|
|
140
|
+
On both EKS and AKS the token the pod receives is the cluster's own projected
|
|
141
|
+
service account token, so the subject is the Kubernetes service account —
|
|
142
|
+
`system:serviceaccount:<namespace>:<name>` — and the issuer is the cluster, not
|
|
143
|
+
the cloud's identity provider. A token issued by Microsoft Entra ID is a
|
|
144
|
+
different thing: getting one means the workload first exchanges its projected
|
|
145
|
+
token for an Entra token, its `aud` has to be a registered Application ID URI,
|
|
146
|
+
and its `sub` is an object-ID GUID rather than anything human-readable.
|
|
147
|
+
Verifying the cluster-issued token directly is the shorter path, and it is the
|
|
148
|
+
one this guide follows.
|
|
149
|
+
|
|
150
|
+
For any issuer other than Google, read `jwks_uri` out of the issuer's
|
|
151
|
+
`/.well-known/openid-configuration` document rather than guessing the path.
|
|
152
|
+
|
|
153
|
+
## 3/ Verify the token at the gateway
|
|
154
|
+
|
|
155
|
+
```json title="config/policies.json"
|
|
156
|
+
{
|
|
157
|
+
"name": "workload-identity-auth",
|
|
158
|
+
"policyType": "open-id-jwt-auth-inbound",
|
|
159
|
+
"handler": {
|
|
160
|
+
"export": "OpenIdJwtInboundPolicy",
|
|
161
|
+
"module": "$import(@zuplo/runtime)",
|
|
162
|
+
"options": {
|
|
163
|
+
"issuer": "https://accounts.google.com",
|
|
164
|
+
"audience": "$env(GATEWAY_AUDIENCE)",
|
|
165
|
+
"jwkUrl": "https://www.googleapis.com/oauth2/v3/certs"
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Set `audience`. The option is optional in the schema and load-bearing in
|
|
172
|
+
practice: without it, every service account identity token Google issues — for
|
|
173
|
+
any project, aimed at any service — satisfies the policy.
|
|
174
|
+
|
|
175
|
+
Leave `allowUnauthenticatedRequests` unset. It defaults to `false`, which
|
|
176
|
+
returns `401` for a missing, expired, or wrong-audience token. Setting it to
|
|
177
|
+
`true` lets that request continue down the pipeline with no `request.user`
|
|
178
|
+
attached, and everything that depends on an identity then degrades quietly
|
|
179
|
+
rather than failing: `rateLimitBy: "user"` falls back to a single shared
|
|
180
|
+
`user-anonymous` bucket, so every unauthenticated caller competes for one
|
|
181
|
+
counter and any one of them can exhaust it for all the others. The route is then
|
|
182
|
+
only as closed as the next policy or the handler makes it. Set it to `true` only
|
|
183
|
+
in a deliberate fallback arrangement, where a second authentication policy runs
|
|
184
|
+
after this one. See [Multiple Auth Policies](./multiple-auth-policies.mdx) for
|
|
185
|
+
that pattern.
|
|
186
|
+
|
|
187
|
+
## 4/ Pin which workloads may call the route
|
|
188
|
+
|
|
189
|
+
The JWT policy proves the caller holds a token from the issuer. The claims
|
|
190
|
+
policy decides which callers that covers. Match on a claim the token actually
|
|
191
|
+
carries: `email` exists on GCP service account identity tokens, and a projected
|
|
192
|
+
Kubernetes service account token has no `email` claim at all.
|
|
193
|
+
|
|
194
|
+
```json title="config/policies.json"
|
|
195
|
+
{
|
|
196
|
+
"name": "allow-known-workloads",
|
|
197
|
+
"policyType": "require-user-claims-inbound",
|
|
198
|
+
"handler": {
|
|
199
|
+
"export": "RequireUserClaimsInboundPolicy",
|
|
200
|
+
"module": "$import(@zuplo/runtime)",
|
|
201
|
+
"options": {
|
|
202
|
+
"rule": {
|
|
203
|
+
"claim": "email",
|
|
204
|
+
"in": [
|
|
205
|
+
"batch-runner@my-project.iam.gserviceaccount.com",
|
|
206
|
+
"report-agent@my-project.iam.gserviceaccount.com"
|
|
207
|
+
]
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
This policy runs after an authentication policy, never instead of one. With no
|
|
215
|
+
authenticated user on the request it returns `401`; when the rule evaluates to
|
|
216
|
+
false it returns `403` without echoing claim values, and writes the failing
|
|
217
|
+
checks to the request log instead.
|
|
218
|
+
|
|
219
|
+
On EKS and AKS the subject is the Kubernetes service account,
|
|
220
|
+
`system:serviceaccount:<namespace>:<name>`, so a namespace prefix pins every
|
|
221
|
+
workload in one namespace at once:
|
|
222
|
+
|
|
223
|
+
```json
|
|
224
|
+
{
|
|
225
|
+
"rule": {
|
|
226
|
+
"claim": "sub",
|
|
227
|
+
"startsWith": "system:serviceaccount:agents:"
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
`startsWith` also covers AWS STS-issued tokens, whose subject is an assumed-role
|
|
233
|
+
ARN such as `arn:aws:sts::123456789012:assumed-role/my-role/<session>` — a
|
|
234
|
+
prefix match is the only workable rule there, because the trailing session name
|
|
235
|
+
changes on every call. That is a different token from the projected service
|
|
236
|
+
account token an EKS pod holds, so check which one your caller actually sends
|
|
237
|
+
before writing the rule.
|
|
238
|
+
|
|
239
|
+
For namespaced claims, array-valued group claims, and `and`/`or` nesting, see
|
|
240
|
+
the [Require User Claims](../policies/require-user-claims-inbound.mdx) policy
|
|
241
|
+
reference.
|
|
242
|
+
|
|
243
|
+
## 5/ Rate limit per identity
|
|
244
|
+
|
|
245
|
+
```json title="config/policies.json"
|
|
246
|
+
{
|
|
247
|
+
"name": "rate-limit-per-workload",
|
|
248
|
+
"policyType": "rate-limit-inbound",
|
|
249
|
+
"handler": {
|
|
250
|
+
"export": "RateLimitInboundPolicy",
|
|
251
|
+
"module": "$import(@zuplo/runtime)",
|
|
252
|
+
"options": {
|
|
253
|
+
"rateLimitBy": "user",
|
|
254
|
+
"requestsAllowed": 600,
|
|
255
|
+
"timeWindowMinutes": 1
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
`rateLimitBy: "user"` groups on `request.user.sub`, which the authentication
|
|
262
|
+
policy earlier in the pipeline populates, so nothing here is specific to JWTs.
|
|
263
|
+
For Google tokens that subject is the numeric unique ID; set
|
|
264
|
+
`subPropertyName: "email"` on the JWT policy if you would rather the subject,
|
|
265
|
+
and therefore the rate limit bucket, carry the readable service account address.
|
|
266
|
+
|
|
267
|
+
[Rate Limiting](../policies/rate-limit-inbound.mdx) is available on every plan.
|
|
268
|
+
[Complex Rate Limiting](../policies/complex-rate-limit-inbound.mdx), which
|
|
269
|
+
counts several resources per request, is an Enterprise policy.
|
|
270
|
+
|
|
271
|
+
## 6/ Attach the policies to the route
|
|
272
|
+
|
|
273
|
+
Nothing above takes effect until the three policies are listed on the route, in
|
|
274
|
+
pipeline order — authenticate, then authorize, then meter:
|
|
275
|
+
|
|
276
|
+
```json title="config/routes.oas.json (excerpt)"
|
|
277
|
+
{
|
|
278
|
+
"x-zuplo-route": {
|
|
279
|
+
"policies": {
|
|
280
|
+
"inbound": [
|
|
281
|
+
"workload-identity-auth",
|
|
282
|
+
"allow-known-workloads",
|
|
283
|
+
"rate-limit-per-workload"
|
|
284
|
+
]
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
:::note
|
|
291
|
+
|
|
292
|
+
Per-request structured logging works on every plan through `context.log.info`
|
|
293
|
+
and `context.log.setLogProperties` — see [Logging](./logging.mdx) — so the
|
|
294
|
+
allowed identity can appear on the log line for the call it authorized. The
|
|
295
|
+
[Audit Log](../policies/audit-log-inbound.mdx) policy, which emits a CloudEvents
|
|
296
|
+
audit trail, is an Enterprise policy on top of that.
|
|
297
|
+
|
|
298
|
+
:::
|
|
299
|
+
|
|
300
|
+
## 7/ Verify it worked
|
|
301
|
+
|
|
302
|
+
Run all three cases against the deployed route:
|
|
303
|
+
|
|
304
|
+
```bash
|
|
305
|
+
# 1. A token for the configured audience, from an allowlisted identity: 200.
|
|
306
|
+
curl -o /dev/null -w "%{http_code}\n" \
|
|
307
|
+
-H "Authorization: Bearer ${TOKEN}" \
|
|
308
|
+
https://my-api-main-abc123.zuplo.app/agents/ping
|
|
309
|
+
|
|
310
|
+
# 2. A token minted for a different audience: 401.
|
|
311
|
+
OTHER=$(curl -s -H "Metadata-Flavor: Google" \
|
|
312
|
+
"http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?audience=https://api.example.com/other")
|
|
313
|
+
curl -o /dev/null -w "%{http_code}\n" \
|
|
314
|
+
-H "Authorization: Bearer ${OTHER}" \
|
|
315
|
+
https://my-api-main-abc123.zuplo.app/agents/ping
|
|
316
|
+
|
|
317
|
+
# 3. No token at all: 401.
|
|
318
|
+
curl -o /dev/null -w "%{http_code}\n" \
|
|
319
|
+
https://my-api-main-abc123.zuplo.app/agents/ping
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
A token from a workload that is not on the allowlist returns `403` rather than
|
|
323
|
+
`401` — the signature and audience were fine, the identity was not. That
|
|
324
|
+
distinction is the fastest way to tell a caller-configuration problem from an
|
|
325
|
+
authorization problem.
|
|
326
|
+
|
|
327
|
+
## Limitations
|
|
328
|
+
|
|
329
|
+
- **No inbound AWS SigV4 verification policy.** Verifying a SigV4-signed request
|
|
330
|
+
means writing the verification in
|
|
331
|
+
[Custom Code](../policies/custom-code-inbound.mdx). This matters on platforms
|
|
332
|
+
where the ambient AWS credential is a signature rather than an OIDC token — a
|
|
333
|
+
plain EC2 instance or a Lambda function, for example. The outbound direction
|
|
334
|
+
has policies for it:
|
|
335
|
+
[AWS service auth](../policies/upstream-aws-service-auth-inbound.mdx) and
|
|
336
|
+
[AWS federated auth](../policies/upstream-aws-federated-auth-inbound.mdx) are
|
|
337
|
+
both Enterprise features and both in beta, and they are free to try in
|
|
338
|
+
development. Neither sets an `Authorization` header on its own — SigV4 signs
|
|
339
|
+
the exact final request, which is only known inside the handler, so the
|
|
340
|
+
policies resolve the credentials and the `awsLambdaHandler` or your own code
|
|
341
|
+
signs the request with `AwsClient.fromContext(context)`.
|
|
342
|
+
- **No dedicated GCP or Azure inbound JWT policy.** Zuplo ships
|
|
343
|
+
provider-specific inbound JWT policies for identity providers — Auth0, Okta,
|
|
344
|
+
Clerk, Firebase, Supabase, Cognito user pools — but none for cloud workload
|
|
345
|
+
identity. The generic OIDC policy covers it, which means you supply `issuer`
|
|
346
|
+
and `jwkUrl` yourself instead of picking a provider from a list.
|
|
347
|
+
- **Token lifetime belongs to the platform.** Mint a token per call or hold it
|
|
348
|
+
only in memory; never write one into configuration or a stored secret.
|
|
349
|
+
|
|
350
|
+
## Additional resources
|
|
351
|
+
|
|
352
|
+
- [Authentication](../concepts/authentication.mdx) — every inbound
|
|
353
|
+
authentication method Zuplo supports and how `request.user` is populated.
|
|
354
|
+
- [Rate limiting](../rate-limiting/getting-started.mdx) — the full set of
|
|
355
|
+
identification strategies, including dynamic per-caller limits.
|
|
356
|
+
- [Request user](../programmable-api/request-user.mdx) — reading the
|
|
357
|
+
authenticated identity and its claims in a handler.
|
|
358
|
+
- [Securing your backend](./securing-your-backend.mdx) — the other half of the
|
|
359
|
+
request: how the gateway authenticates itself to your origin.
|
package/docs/build-with-ai.mdx
CHANGED
|
@@ -18,12 +18,21 @@ The `zuplo` npm package ships the full documentation at
|
|
|
18
18
|
your project. An `AGENTS.md` file at the repo root tells agents to read those
|
|
19
19
|
docs before writing any code — no network calls required.
|
|
20
20
|
|
|
21
|
-
New projects
|
|
21
|
+
New projects get the bundled docs with the `zuplo` package, and
|
|
22
|
+
[`create-zuplo-api`](./cli/create-zuplo-api.mdx) asks which coding agents to
|
|
23
|
+
configure. Pick **OpenAI Codex** for an `AGENTS.md`, or name the agents up
|
|
24
|
+
front:
|
|
22
25
|
|
|
23
26
|
```bash
|
|
24
|
-
npx create-zuplo-api@latest
|
|
27
|
+
npx create-zuplo-api@latest --agents codex
|
|
25
28
|
```
|
|
26
29
|
|
|
30
|
+
Each agent gets the instruction file it reads natively — `AGENTS.md` for Codex,
|
|
31
|
+
`CLAUDE.md` for Claude Code, `.cursorrules` for Cursor — plus a shared
|
|
32
|
+
`.mcp.json`. See
|
|
33
|
+
[Configuring AI coding agents](./cli/create-zuplo-api.mdx#configuring-ai-coding-agents)
|
|
34
|
+
for the full list.
|
|
35
|
+
|
|
27
36
|
For existing projects on `zuplo` 0.66.0 or later, drop in the default
|
|
28
37
|
`AGENTS.md`:
|
|
29
38
|
|
|
@@ -16,8 +16,11 @@ npx create-zuplo-api@latest
|
|
|
16
16
|
`create-zuplo-api` comes with the following options:
|
|
17
17
|
|
|
18
18
|
- `-v, --version` - Output the current version of create-zuplo-api
|
|
19
|
-
- `--
|
|
20
|
-
|
|
19
|
+
- `--linter <linter>` - The linter to configure (`eslint`, `biome`, `oxlint`, or
|
|
20
|
+
`none`). See
|
|
21
|
+
[Choosing a linter and formatter](#choosing-a-linter-and-formatter)
|
|
22
|
+
- `--formatter <formatter>` - The formatter to configure (`prettier`, `biome`,
|
|
23
|
+
`oxfmt`, or `none`)
|
|
21
24
|
- `--empty` - Initialize an empty project
|
|
22
25
|
- `--use-npm` - Explicitly tell the CLI to bootstrap the application using npm
|
|
23
26
|
- `--use-pnpm` - Explicitly tell the CLI to bootstrap the application using pnpm
|
|
@@ -28,7 +31,19 @@ npx create-zuplo-api@latest
|
|
|
28
31
|
- `--git` - Whether or not to initialize the project as a git repository
|
|
29
32
|
- `--version-check` - Whether or not to check for an outdated version
|
|
30
33
|
- `--install` - Whether or not to install packages
|
|
34
|
+
- `--verbose` - Enable verbose logging
|
|
31
35
|
- `--yes` - Use saved preferences or defaults for unprovided options
|
|
36
|
+
- `--server-project` - Create a matching project on portal.zuplo.com and link
|
|
37
|
+
this working copy to it, skipping the interactive prompt. See
|
|
38
|
+
[Creating a matching portal project](#creating-a-matching-portal-project)
|
|
39
|
+
- `--account <account-name>` - The Zuplo account that will host the server
|
|
40
|
+
project. Implies `--server-project`
|
|
41
|
+
- `--agents <agents>` - Comma-separated list of AI coding agents to configure
|
|
42
|
+
(`claude`, `copilot`, `cursor`, `windsurf`, `codex`), or `none` to skip agent
|
|
43
|
+
setup. See [Configuring AI coding agents](#configuring-ai-coding-agents)
|
|
44
|
+
- `--template <template-name>` - A built-in template to bootstrap the API with
|
|
45
|
+
(`default`, `default-empty`, or `ai-gateway-v2`). Can't be combined with
|
|
46
|
+
`--example` or `--empty`. See [Choosing a template](#choosing-a-template)
|
|
32
47
|
- `-e, --example <example-name|github-url>` - An example to bootstrap the API
|
|
33
48
|
with. You can use an example name from the official Zuplo repository or a
|
|
34
49
|
public GitHub URL. The URL can use any branch and/or subdirectory
|
|
@@ -38,27 +53,222 @@ npx create-zuplo-api@latest
|
|
|
38
53
|
the example separately: `--example-path foo/bar`
|
|
39
54
|
- `-h, --help` - Display the help message
|
|
40
55
|
|
|
41
|
-
###
|
|
56
|
+
### Opting out with `--no-` flags
|
|
57
|
+
|
|
58
|
+
Each boolean option also accepts a `--no-` form that turns the feature off:
|
|
59
|
+
|
|
60
|
+
| Opt-out flag | Effect |
|
|
61
|
+
| --------------------- | --------------------------------------------------- |
|
|
62
|
+
| `--no-git` | Don't initialize the project as a git repository |
|
|
63
|
+
| `--no-install` | Don't install packages |
|
|
64
|
+
| `--no-version-check` | Don't check for an outdated version |
|
|
65
|
+
| `--no-server-project` | Don't create a matching project on portal.zuplo.com |
|
|
66
|
+
|
|
67
|
+
These flags matter most in CI and scripted use. `--yes` accepts whatever is
|
|
68
|
+
saved in your preferences for any option you didn't pass, so a value you chose
|
|
69
|
+
once on a workstation can carry into a later run. A `--no-` flag, or an explicit
|
|
70
|
+
value such as `--linter none`, ignores saved preferences and suppresses the
|
|
71
|
+
prompt outright, which makes the result the same on every machine:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
npx create-zuplo-api@latest my-api --yes --linter none --formatter none --no-install --no-git
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
If you pass both spellings of the same option, the affirmative flag wins — so
|
|
78
|
+
`--git --no-git` initializes a git repository. `--no-server-project` is the
|
|
79
|
+
exception: it always wins, even when combined with `--server-project` or
|
|
80
|
+
`--account`.
|
|
81
|
+
|
|
82
|
+
### Choosing a template
|
|
83
|
+
|
|
84
|
+
`--template` selects which built-in template the CLI scaffolds from. Without the
|
|
85
|
+
flag you get the `default` template.
|
|
86
|
+
|
|
87
|
+
| Value | What it scaffolds |
|
|
88
|
+
| --------------- | --------------------------------------------------------------------------------------------- |
|
|
89
|
+
| `default` | Sample `/todos` routes, a `hello-world` module, a dev portal in `docs/`, and VS Code settings |
|
|
90
|
+
| `default-empty` | The same project with no routes defined and no VS Code settings |
|
|
91
|
+
| `ai-gateway-v2` | An [AI Gateway](../ai-gateway/introduction.mdx) project, without a dev portal |
|
|
92
|
+
|
|
93
|
+
`--empty` is shorthand for `--template default-empty`.
|
|
94
|
+
|
|
95
|
+
The `ai-gateway-v2` template scaffolds `config/ai.oas.json`, which routes
|
|
96
|
+
`/:app_id/v1/*` to the AI Gateway handler, and `config/policies.json`, which
|
|
97
|
+
declares the policies an application's policy chain can select. It ships no
|
|
98
|
+
`docs/` directory, so the CLI skips the
|
|
99
|
+
[dev portal](../dev-portal/introduction.mdx) workspace:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
npx create-zuplo-api@latest my-gateway --template ai-gateway-v2
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
`--template` names a built-in template, which is why it can't be combined with
|
|
106
|
+
`--example` (a project pulled from GitHub) or with `--empty` (use
|
|
107
|
+
`--template default-empty`). Either combination stops the CLI before it writes
|
|
108
|
+
any files, as does a template name that isn't in the table above.
|
|
109
|
+
|
|
110
|
+
### Choosing a linter and formatter
|
|
111
|
+
|
|
112
|
+
`--linter` and `--formatter` pick the tools the CLI sets up. Without either flag
|
|
113
|
+
the CLI asks, with ESLint and Prettier preselected.
|
|
114
|
+
|
|
115
|
+
| `--linter` | Config file | Dev dependencies |
|
|
116
|
+
| ---------- | ------------------ | ------------------------------------------- |
|
|
117
|
+
| `eslint` | `eslint.config.js` | `eslint`, `@eslint/js`, `typescript-eslint` |
|
|
118
|
+
| `biome` | `biome.json` | `@biomejs/biome` |
|
|
119
|
+
| `oxlint` | `.oxlintrc.json` | `oxlint` |
|
|
120
|
+
| `none` | None | None |
|
|
121
|
+
|
|
122
|
+
| `--formatter` | Config file | Dev dependencies |
|
|
123
|
+
| ----------------- | ------------------ | ---------------- |
|
|
124
|
+
| `prettier` | `.prettierrc.json` | `prettier` |
|
|
125
|
+
| `biome` | `biome.json` | `@biomejs/biome` |
|
|
126
|
+
| `oxfmt` (in beta) | `.oxfmtrc.json` | `oxfmt` |
|
|
127
|
+
| `none` | None | None |
|
|
128
|
+
|
|
129
|
+
A linter adds `lint` and `lint:fix` scripts to `package.json`, and a formatter
|
|
130
|
+
adds `format` and `format:check`. The dev portal workspace in `docs/` gets the
|
|
131
|
+
same `lint` script. The CLI also writes a `.vscode/extensions.json` that
|
|
132
|
+
recommends the editor extensions for the tools you picked.
|
|
133
|
+
|
|
134
|
+
Biome fills both roles from one dependency and a single `biome.json`:
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
npx create-zuplo-api@latest my-api --linter biome --formatter biome
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Picking Biome as the linter preselects it as the formatter in the interactive
|
|
141
|
+
prompt. Pairing ESLint with Prettier also installs `eslint-config-prettier`, so
|
|
142
|
+
the linter doesn't fight the formatter.
|
|
143
|
+
|
|
144
|
+
An unrecognized value is an error and prints the valid values. With `--yes` or in
|
|
145
|
+
CI, the CLI uses your saved preferences, or ESLint and Prettier if you have none.
|
|
146
|
+
|
|
147
|
+
### Configuring AI coding agents
|
|
148
|
+
|
|
149
|
+
Pass `--agents` to write instruction files and MCP configuration for the coding
|
|
150
|
+
agents you use. Each selected agent also gets the
|
|
151
|
+
[Zuplo agent skills](../build-with-ai.mdx#agent-skills), so it works from
|
|
152
|
+
accurate Zuplo documentation instead of training data. Claude Code enables them
|
|
153
|
+
through the `zuplo/tools` plugin marketplace in `.claude/settings.json`. For
|
|
154
|
+
every other agent, the CLI downloads the skill files into that agent's skills
|
|
155
|
+
directory, such as `.cursor/skills/`.
|
|
156
|
+
|
|
157
|
+
| Value | Agent | Files written |
|
|
158
|
+
| ---------- | -------------- | ------------------------------------------------- |
|
|
159
|
+
| `claude` | Claude Code | `CLAUDE.md`, `.claude/settings.json`, `.mcp.json` |
|
|
160
|
+
| `copilot` | GitHub Copilot | `.github/copilot-instructions.md`, `.mcp.json` |
|
|
161
|
+
| `cursor` | Cursor | `.cursorrules`, `.mcp.json` |
|
|
162
|
+
| `windsurf` | Windsurf | `.windsurfrules`, `.mcp.json` |
|
|
163
|
+
| `codex` | OpenAI Codex | `AGENTS.md`, `.mcp.json` |
|
|
164
|
+
| `none` | — | Nothing — skips agent setup |
|
|
165
|
+
|
|
166
|
+
Combine values with commas:
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
npx create-zuplo-api@latest my-api --agents claude,cursor
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
The CLI validates the list before scaffolding anything:
|
|
173
|
+
|
|
174
|
+
- An unrecognized agent name is an error. `--agents zed` fails and prints the
|
|
175
|
+
valid values.
|
|
176
|
+
- `none` can't be combined with a real agent. Use `--agents none` to skip agent
|
|
177
|
+
setup, or list only the agents you want.
|
|
178
|
+
|
|
179
|
+
## Creating a matching portal project
|
|
180
|
+
|
|
181
|
+
By default, the CLI asks whether to create a matching project on
|
|
182
|
+
portal.zuplo.com and link your new working copy to it. Answering yes runs
|
|
183
|
+
`zuplo project create` and `zuplo link`, which creates the project in your Zuplo
|
|
184
|
+
account and writes `ZUPLO_ACCOUNT_NAME` and `ZUPLO_PROJECT_NAME` to a
|
|
185
|
+
`.env.zuplo` file in the project directory. The CLI then prints the portal URL
|
|
186
|
+
for the new project.
|
|
187
|
+
|
|
188
|
+
Pass `--server-project` to opt in without the prompt, or `--account <name>` to
|
|
189
|
+
also choose which account hosts the project:
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
npx create-zuplo-api@latest my-api --account my-account
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Creating the project requires authentication. Run
|
|
196
|
+
[`zuplo login`](./authentication.mdx) first, or let `zuplo project create` walk
|
|
197
|
+
you through signing in. When you have access to exactly one account, the CLI
|
|
198
|
+
picks it automatically; with more than one and no `--account`, it asks which
|
|
199
|
+
account should host the project.
|
|
200
|
+
|
|
201
|
+
To keep everything local, pass `--no-server-project`:
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
npx create-zuplo-api@latest my-api --no-server-project
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
:::note
|
|
208
|
+
|
|
209
|
+
`--yes` and CI environments skip this step unless you pass `--server-project` or
|
|
210
|
+
`--account`. Unlike the other prompts, the answer isn't saved to your
|
|
211
|
+
preferences, so a yes on your workstation never becomes a yes in CI.
|
|
212
|
+
|
|
213
|
+
:::
|
|
214
|
+
|
|
215
|
+
In non-interactive runs the CLI can't ask which account to use. Unless your
|
|
216
|
+
credentials resolve to exactly one account, pair `--server-project` with
|
|
217
|
+
`--account`. Otherwise the CLI prints a note, leaves your local files in place,
|
|
218
|
+
and skips the portal project:
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
npx create-zuplo-api@latest my-api --yes --account my-account
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
If project creation or linking fails, the scaffolded files stay on disk and the
|
|
225
|
+
CLI tells you how to finish up — run `zuplo project create --name my-api` and
|
|
226
|
+
`zuplo link` from inside the project directory.
|
|
227
|
+
|
|
228
|
+
## Examples
|
|
42
229
|
|
|
43
230
|
The following examples show different ways to use `create-zuplo-api`:
|
|
44
231
|
|
|
45
|
-
|
|
232
|
+
### With Default Template
|
|
46
233
|
|
|
47
234
|
```bash
|
|
48
|
-
npx create-zuplo-api@latest
|
|
49
|
-
|
|
50
|
-
|
|
235
|
+
npx create-zuplo-api@latest
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
The CLI asks the following questions:
|
|
239
|
+
|
|
240
|
+
```text
|
|
241
|
+
? What is your project named? › my-api
|
|
242
|
+
? Create a matching project on portal.zuplo.com? › No / Yes
|
|
243
|
+
? Which linter would you like to use? › - Use arrow-keys. Return to submit.
|
|
244
|
+
❯ ESLint
|
|
245
|
+
Biome
|
|
246
|
+
Oxlint
|
|
247
|
+
None
|
|
248
|
+
? Which formatter would you like to use? › - Use arrow-keys. Return to submit.
|
|
249
|
+
❯ Prettier
|
|
250
|
+
Biome
|
|
251
|
+
Oxfmt
|
|
252
|
+
None
|
|
253
|
+
? Which AI coding agents would you like to configure? › - Space to select. Return to submit
|
|
254
|
+
◯ Claude Code
|
|
255
|
+
◯ GitHub Copilot
|
|
256
|
+
◯ Cursor
|
|
257
|
+
◯ Windsurf
|
|
258
|
+
◯ OpenAI Codex
|
|
259
|
+
◯ None
|
|
51
260
|
```
|
|
52
261
|
|
|
53
|
-
|
|
262
|
+
Pass the directory as an argument to skip the first question, then start the
|
|
263
|
+
development server:
|
|
54
264
|
|
|
55
265
|
```bash
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
266
|
+
npx create-zuplo-api@latest my-api
|
|
267
|
+
cd my-api
|
|
268
|
+
npm run dev
|
|
59
269
|
```
|
|
60
270
|
|
|
61
|
-
|
|
271
|
+
### With an Official Example from GitHub
|
|
62
272
|
|
|
63
273
|
To create a new Zuplo API using an official example from the Zuplo GitHub
|
|
64
274
|
repository, you specify the example name using the `--example` option.
|
|
@@ -70,7 +280,11 @@ npx create-zuplo-api@latest my-api --example my-example
|
|
|
70
280
|
You can find the list of available examples in the
|
|
71
281
|
[Zuplo examples repository](https://github.com/zuplo/zuplo/tree/main/examples).
|
|
72
282
|
|
|
73
|
-
|
|
283
|
+
Examples ship their own linting, formatting, and agent configuration, so the
|
|
284
|
+
CLI skips those questions. It still asks about creating a matching project on
|
|
285
|
+
portal.zuplo.com, because that choice is independent of the template.
|
|
286
|
+
|
|
287
|
+
### With any Public GitHub Repository
|
|
74
288
|
|
|
75
289
|
To create a new Zuplo API using any public GitHub repository, you can specify
|
|
76
290
|
the repository URL using the `--example` option.
|
package/docs/cli/overview.mdx
CHANGED
|
@@ -75,4 +75,6 @@ For a complete list of commands and flags, run `zuplo --help` or see
|
|
|
75
75
|
[Global Options](./global-options.mdx).
|
|
76
76
|
|
|
77
77
|
To scaffold a new project without installing the CLI, see
|
|
78
|
-
[create-zuplo-api](./create-zuplo-api.mdx).
|
|
78
|
+
[create-zuplo-api](./create-zuplo-api.mdx). It can also run `project create` and
|
|
79
|
+
`link` for you, so a new working copy comes out already connected to a project on
|
|
80
|
+
portal.zuplo.com.
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Upstream Credentials
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Every request that passes through Zuplo involves two separate authentication
|
|
6
|
+
decisions. The caller authenticates to the gateway, and the gateway
|
|
7
|
+
authenticates to whatever it calls next. The two rarely use the same credential,
|
|
8
|
+
and on most routes they don't even use the same scheme: a client might present a
|
|
9
|
+
Zuplo API key while the upstream expects an AWS SigV4 signature, a Google ID
|
|
10
|
+
token, or a vendor API key in a query string.
|
|
11
|
+
|
|
12
|
+
[Authentication](./authentication.mdx) covers the first decision. This page
|
|
13
|
+
covers the second — which credential the gateway attaches on the way out, which
|
|
14
|
+
policy attaches it, and where the credential itself lives.
|
|
15
|
+
|
|
16
|
+
## Two directions, one request
|
|
17
|
+
|
|
18
|
+
An upstream credential is attached in the middle of the request pipeline, after
|
|
19
|
+
the caller's identity is known and before the request leaves for the origin.
|
|
20
|
+
|
|
21
|
+
<Diagram height="h-96">
|
|
22
|
+
<DiagramNode id="caller">Caller</DiagramNode>
|
|
23
|
+
<DiagramGroup id="zuplo" label="Zuplo API Gateway">
|
|
24
|
+
<DiagramNode id="verify" variant="zuplo">
|
|
25
|
+
Verify caller credential
|
|
26
|
+
</DiagramNode>
|
|
27
|
+
<DiagramNode id="enforce" variant="zuplo">
|
|
28
|
+
Authorize, rate limit, log
|
|
29
|
+
</DiagramNode>
|
|
30
|
+
<DiagramNode id="attach" variant="zuplo">
|
|
31
|
+
Attach upstream credential
|
|
32
|
+
</DiagramNode>
|
|
33
|
+
</DiagramGroup>
|
|
34
|
+
<DiagramNode id="upstream">Upstream</DiagramNode>
|
|
35
|
+
<DiagramEdge from="caller" to="zuplo" />
|
|
36
|
+
<DiagramEdge from="verify" to="enforce" fromSide="bottom" toSide="top" />
|
|
37
|
+
<DiagramEdge from="enforce" to="attach" fromSide="bottom" toSide="top" />
|
|
38
|
+
<DiagramEdge from="zuplo" to="upstream" />
|
|
39
|
+
</Diagram>
|
|
40
|
+
|
|
41
|
+
The ordering is what makes the pattern useful. By the time the credential is
|
|
42
|
+
attached, `request.user` is populated, so the same request can be authorized
|
|
43
|
+
with [Require User Claims](../policies/require-user-claims-inbound.mdx), rate
|
|
44
|
+
limited per identity, and logged with the caller's subject — and the credential
|
|
45
|
+
the upstream receives is one the caller never held.
|
|
46
|
+
|
|
47
|
+
That changes what a leaked caller credential is worth. A revoked gateway API key
|
|
48
|
+
takes its holder's access with it, and the upstream secret exists in one place
|
|
49
|
+
instead of one place per caller. It does not change what a caller with
|
|
50
|
+
legitimate access can do. Deciding whether a call _should_ happen is
|
|
51
|
+
authorization, rate limiting, and logging — separate policies, on the same
|
|
52
|
+
request.
|
|
53
|
+
|
|
54
|
+
## Why every credential policy ends in `-inbound`
|
|
55
|
+
|
|
56
|
+
Zuplo's pipeline has exactly two policy stages.
|
|
57
|
+
[Inbound policies](../articles/policies.mdx) run before the handler sends the
|
|
58
|
+
request upstream. Outbound policies run on the response, on its way back to the
|
|
59
|
+
caller.
|
|
60
|
+
|
|
61
|
+
Attaching an upstream credential happens before the request leaves, so every
|
|
62
|
+
credential-injection policy is an inbound policy and carries the `-inbound`
|
|
63
|
+
suffix — including the ones whose entire purpose is the outbound call, like
|
|
64
|
+
`upstream-gcp-service-auth-inbound`. The suffix names the pipeline stage, not
|
|
65
|
+
the direction of the credential. It is not a typo.
|
|
66
|
+
|
|
67
|
+
The `-outbound` policies work on responses. That is where
|
|
68
|
+
[Secret Masking](../policies/secret-masking-outbound.mdx) belongs: it redacts
|
|
69
|
+
credentials that leaked into a response body before the caller — or an LLM
|
|
70
|
+
reading the response — ever sees them.
|
|
71
|
+
|
|
72
|
+
## Choosing a policy
|
|
73
|
+
|
|
74
|
+
| The upstream expects | Policy | Availability | Pick it when |
|
|
75
|
+
| -------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
76
|
+
| A static key or token in one header | [`set-upstream-api-key-inbound`](../policies/set-upstream-api-key-inbound.mdx) | All plans | One secret, one header. The common case, and the default header is `Authorization`. |
|
|
77
|
+
| Several headers, or a value composed from more than one variable | [`set-headers-inbound`](../policies/set-headers-inbound.mdx) | All plans | The upstream wants a non-standard header name, two headers, or a key plus a tenant identifier. |
|
|
78
|
+
| A GCP ID token or a scoped Google access token, from a stored SA key | [`upstream-gcp-service-auth-inbound`](../policies/upstream-gcp-service-auth-inbound.mdx) | Enterprise | Cloud Run, Cloud Functions, or GKE behind IAP (`audience`), or a Google API that needs OAuth `scopes`. |
|
|
79
|
+
| A GCP ID token, with no service-account key stored anywhere | [`upstream-gcp-federated-auth-inbound`](../policies/upstream-gcp-federated-auth-inbound.mdx) | Enterprise | The same IAM-protected services, using Workload Identity Federation instead of a stored key. Audience-bound ID tokens only. |
|
|
80
|
+
| A self-signed JWT accepted by Cloud Endpoints or ESPv2 | [`upstream-gcp-jwt-inbound`](../policies/upstream-gcp-jwt-inbound.mdx) | Enterprise | Rarely. The policy's own page steers you to GCP service auth in most cases. |
|
|
81
|
+
| An access token from any OAuth 2.0 client-credentials endpoint | `upstream-oauth-client-credentials-inbound` | All plans | An internal authorization server, or any SaaS vendor that issues client-credentials tokens. Set `tokenUrl`, `clientId`, `clientSecret`, and `scope` or `audience` if the provider needs them. |
|
|
82
|
+
| An Entra ID (Azure AD) access token from the client-credentials flow | [`upstream-azure-ad-service-auth-inbound`](../policies/upstream-azure-ad-service-auth-inbound.mdx) | Enterprise | Azure App Service, Azure Functions, or any Entra-protected resource. It builds the Entra token URL for you from a tenant id. |
|
|
83
|
+
| A SigV4-signed request, credentials from a stored IAM key pair | [`upstream-aws-service-auth-inbound`](../policies/upstream-aws-service-auth-inbound.mdx) | Enterprise · Beta | Lambda or another AWS service, optionally through an STS AssumeRole. Read [AWS is different](#aws-is-different) first. |
|
|
84
|
+
| A SigV4-signed request, with no AWS keys stored anywhere | [`upstream-aws-federated-auth-inbound`](../policies/upstream-aws-federated-auth-inbound.mdx) | Enterprise · Beta | The same, using `AssumeRoleWithWebIdentity` instead of a stored key pair. |
|
|
85
|
+
| A Firebase admin token | [`upstream-firebase-admin-auth-inbound`](../policies/upstream-firebase-admin-auth-inbound.mdx) | All plans | Firestore or another Firebase service called with service-account permissions. |
|
|
86
|
+
| A Firebase token scoped to one end user | [`upstream-firebase-user-auth-inbound`](../policies/upstream-firebase-user-auth-inbound.mdx) | All plans | Firebase should apply that user's own permissions. `userId` can be read from `request.user`. |
|
|
87
|
+
| A JWT proving the call came from your gateway | [`upstream-zuplo-jwt-auth-inbound`](../policies/upstream-zuplo-jwt-auth-inbound.mdx) | Enterprise | Your own upstream, with no third-party identity provider in the picture. Claims can be copied from the caller. |
|
|
88
|
+
| Anything else | [`custom-code-inbound`](../policies/custom-code-inbound.mdx) | All plans | Query-string keys, HMAC signatures, or picking the credential from the caller's identity. |
|
|
89
|
+
|
|
90
|
+
:::info{title="Enterprise policies are free to try in development"}
|
|
91
|
+
|
|
92
|
+
The GCP, Azure AD, AWS, and Zuplo-JWT upstream policies above are Enterprise
|
|
93
|
+
features for production traffic, and free to use on any plan for development.
|
|
94
|
+
The two AWS policies are also in beta and may change in non-backward-compatible
|
|
95
|
+
ways. The same applies to two policies this pattern often pairs with:
|
|
96
|
+
[`audit-log-inbound`](../policies/audit-log-inbound.mdx) and
|
|
97
|
+
[`complex-rate-limit-inbound`](../policies/complex-rate-limit-inbound.mdx) are
|
|
98
|
+
Enterprise, while [`rate-limit-inbound`](../policies/rate-limit-inbound.mdx) and
|
|
99
|
+
[`quota-inbound`](../policies/quota-inbound.mdx) are not.
|
|
100
|
+
|
|
101
|
+
:::
|
|
102
|
+
|
|
103
|
+
Two policies in the catalog look like they belong in the table and don't.
|
|
104
|
+
`set-headers-outbound` sets headers on the **response**, so it can't
|
|
105
|
+
authenticate an upstream call. `custom-code-outbound` runs after the response
|
|
106
|
+
arrives — useful for reshaping a body, not for signing a request.
|
|
107
|
+
|
|
108
|
+
### AWS is different
|
|
109
|
+
|
|
110
|
+
The two AWS policies do not set an `Authorization` header. SigV4 signs the exact
|
|
111
|
+
final request — method, path, query, headers, body hash — so a header computed
|
|
112
|
+
earlier in the pipeline would be wrong by the time the request left.
|
|
113
|
+
|
|
114
|
+
Instead, both policies resolve AWS credentials and register them on the request
|
|
115
|
+
context. Something later in the pipeline signs with them:
|
|
116
|
+
|
|
117
|
+
- The [AWS Lambda handler](../handlers/aws-lambda.mdx), which does it for you.
|
|
118
|
+
- Your own code, via `AwsClient.fromContext(context)` in a
|
|
119
|
+
[custom code policy](../policies/custom-code-inbound.mdx) or
|
|
120
|
+
[custom handler](../handlers/custom-handler.mdx).
|
|
121
|
+
|
|
122
|
+
If a route uses one of these policies and the upstream returns 403, check that
|
|
123
|
+
something is actually consuming the resolved credentials. The policy alone does
|
|
124
|
+
not change the outgoing request.
|
|
125
|
+
|
|
126
|
+
### MCP routes
|
|
127
|
+
|
|
128
|
+
MCP routes have their own credential broker.
|
|
129
|
+
[`mcp-token-exchange-inbound`](../policies/mcp-token-exchange-inbound.mdx)
|
|
130
|
+
resolves gateway-managed upstream credentials in three modes: `user-oauth`
|
|
131
|
+
(per-user OAuth federation, tokens stored encrypted at rest and keyed to the
|
|
132
|
+
user's subject), `shared-oauth` (one gateway-wide grant an administrator
|
|
133
|
+
connects once), and `id-jag` (Cross-App Access token exchange, built on an
|
|
134
|
+
active IETF draft rather than a finalized RFC). Per-user upstream API keys are
|
|
135
|
+
not part of this set.
|
|
136
|
+
|
|
137
|
+
See
|
|
138
|
+
[Per-user OAuth to upstream MCP servers](../mcp-gateway/auth/upstream-oauth.mdx)
|
|
139
|
+
for how the outbound surface works, and
|
|
140
|
+
[Connect an upstream that uses an API key](../mcp-gateway/how-to/connect-upstream-api-key.mdx)
|
|
141
|
+
for upstreams with no OAuth at all.
|
|
142
|
+
|
|
143
|
+
## Where the credential lives
|
|
144
|
+
|
|
145
|
+
Upstream secrets belong in
|
|
146
|
+
[environment variables](../articles/environment-variables.mdx), referenced from
|
|
147
|
+
policy configuration with `$env(VAR_NAME)` and from code with
|
|
148
|
+
`environment.VAR_NAME`. Values are encrypted at rest; values marked as secrets
|
|
149
|
+
are write-only, so they can't be read back after they're set.
|
|
150
|
+
|
|
151
|
+
Two behaviors surprise people:
|
|
152
|
+
|
|
153
|
+
- **A changed value needs a new deployment.** Variables are applied to an
|
|
154
|
+
environment at deploy time. Updating a credential without redeploying leaves
|
|
155
|
+
the old value in service.
|
|
156
|
+
- **The CLI and the Developer API set variables per branch.**
|
|
157
|
+
[`zuplo variable create`](../cli/variable-create.mdx) and
|
|
158
|
+
[`zuplo variable update`](../cli/variable-update.mdx) both take a `--branch`
|
|
159
|
+
flag and create a record for that branch alone.
|
|
160
|
+
|
|
161
|
+
:::caution{title="Set shared credentials in the Portal, not the CLI"}
|
|
162
|
+
|
|
163
|
+
There is no all-environments assignment in the CLI or the Developer API. Only
|
|
164
|
+
the
|
|
165
|
+
[environment-variable editor](../articles/environment-variables.mdx#environment-variable-editor)
|
|
166
|
+
in the Zuplo Portal can apply one variable to several environments at once —
|
|
167
|
+
open **Settings → Environment Variables** in your project and select every
|
|
168
|
+
environment the credential belongs to.
|
|
169
|
+
|
|
170
|
+
A credential seeded per branch looks correct in the environment where it was
|
|
171
|
+
created and is simply absent everywhere else. Nothing fails at build or deploy
|
|
172
|
+
time; the first sign of trouble is a 401 or 403 from the upstream on live
|
|
173
|
+
traffic, which can go unnoticed for days on a low-volume route.
|
|
174
|
+
|
|
175
|
+
:::
|
|
176
|
+
|
|
177
|
+
The strongest option is to store nothing. Both federated policies
|
|
178
|
+
([GCP](../policies/upstream-gcp-federated-auth-inbound.mdx),
|
|
179
|
+
[AWS](../policies/upstream-aws-federated-auth-inbound.mdx)) exchange the
|
|
180
|
+
gateway's own OIDC identity for short-lived cloud credentials, so there is no
|
|
181
|
+
long-lived key to leak, rotate, or seed into an environment.
|
|
182
|
+
|
|
183
|
+
## Related
|
|
184
|
+
|
|
185
|
+
- [Securing your backend](../articles/securing-your-backend.mdx) — the six ways
|
|
186
|
+
to make sure only your gateway can reach your origin.
|
|
187
|
+
- [Secure a GCP backend with Zuplo upstream auth](../articles/gke-with-upstream-auth-policy.mdx)
|
|
188
|
+
— an end-to-end walkthrough with GKE and Identity-Aware Proxy.
|
|
189
|
+
- [Authentication](./authentication.mdx) — the inbound half of the request.
|
|
190
|
+
- [Environment variables](../articles/environment-variables.mdx) — syntax,
|
|
191
|
+
environments, and deployment behavior.
|
|
192
|
+
- [Policies](../articles/policies.mdx) — how the inbound and outbound stages fit
|
|
193
|
+
together.
|
package/docs/policies/_index.md
CHANGED
|
@@ -101,6 +101,7 @@
|
|
|
101
101
|
| upstream-gcp-federated-auth-inbound | Upstream GCP Federated Auth | Authenticates with GCP resources or Google services using Workload Identity Federation allowing secure access to these resources without requiring the use of a service account private key. | api-gateway |
|
|
102
102
|
| upstream-gcp-jwt-inbound | Upstream GCP Self-Signed JWT | Creates a self-signed JWT token (generated using a Google Service Account JSON) and attaches it to the outgoing request. Useful when calling GCP services like Cloud Endpoints / ESPv2 | api-gateway |
|
|
103
103
|
| upstream-gcp-service-auth-inbound | Upstream GCP Service Auth | Creates an ID Token from Google's OAuth service and attaches it to the outgoing request. Useful when calling GCP services or Google APIs that are secured with GCP IAM. | api-gateway |
|
|
104
|
+
| upstream-oauth-client-credentials-inbound | Upstream OAuth 2.0 Client Credentials Auth | Fetches an access token from any OAuth 2.0 token endpoint using the client credentials grant and adds it to a header (`Authorization` by default) on the upstream request. Tokens are cached until shortly before they expire. | api-gateway |
|
|
104
105
|
| upstream-zuplo-jwt-auth-inbound | Upstream Zuplo JWT | Generates a Zuplo JWT token and attaches it to the outgoing request. This policy creates a self-signed JWT using the Zuplo JWT plugin and adds it to the specified header for upstream authentication. | api-gateway |
|
|
105
106
|
| web-bot-auth-inbound | Web Bot Auth | Authenticate bots using web-bot-auth HTTP Message Signatures. | api-gateway |
|
|
106
107
|
| xml-to-json-outbound | XML to JSON | Parses XML and converts it to JSON. | api-gateway |
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
This policy authenticates your Zuplo gateway to OAuth 2.0-protected backend
|
|
2
|
+
services by automatically adding an access token to the `Authorization` header
|
|
3
|
+
(or a custom header) of upstream requests. It uses the OAuth 2.0 client
|
|
4
|
+
credentials grant against any token endpoint you configure, so it works with
|
|
5
|
+
Auth0, Okta, Keycloak, Microsoft Entra ID, and any other standards-compliant
|
|
6
|
+
identity provider.
|
|
7
|
+
|
|
8
|
+
### How It Works
|
|
9
|
+
|
|
10
|
+
The policy performs the following operations:
|
|
11
|
+
|
|
12
|
+
1. Requests an access token from the configured token endpoint using the client
|
|
13
|
+
credentials grant
|
|
14
|
+
2. Caches the token for subsequent requests until it nears expiration
|
|
15
|
+
3. Adds the token to the configured header (default `Authorization`) using the
|
|
16
|
+
`token_type` returned by the token endpoint, falling back to the configured
|
|
17
|
+
`headerScheme` (default `Bearer`)
|
|
18
|
+
4. Automatically handles token renewal when needed
|
|
19
|
+
|
|
20
|
+
Tokens are cached for `expires_in - expirationOffsetSeconds` seconds. If the
|
|
21
|
+
token endpoint doesn't return an `expires_in` value, the policy caches the token
|
|
22
|
+
conservatively as if it expired after 10 minutes (5 minutes with the default
|
|
23
|
+
`expirationOffsetSeconds` of 300) rather than caching it indefinitely.
|
|
24
|
+
|
|
25
|
+
### Policy Configuration
|
|
26
|
+
|
|
27
|
+
Configure the policy with your identity provider's token endpoint and client
|
|
28
|
+
credentials:
|
|
29
|
+
|
|
30
|
+
```json
|
|
31
|
+
{
|
|
32
|
+
"name": "upstream-oauth-client-credentials",
|
|
33
|
+
"export": "UpstreamOAuthClientCredentialsInboundPolicy",
|
|
34
|
+
"module": "$import(@zuplo/runtime)",
|
|
35
|
+
"options": {
|
|
36
|
+
"tokenUrl": "https://your-tenant.us.auth0.com/oauth/token",
|
|
37
|
+
"clientId": "$env(OAUTH_CLIENT_ID)",
|
|
38
|
+
"clientSecret": "$env(OAUTH_CLIENT_SECRET)",
|
|
39
|
+
"audience": "https://api.example.com",
|
|
40
|
+
"scope": "read:orders write:orders"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Sending Client Credentials
|
|
46
|
+
|
|
47
|
+
By default (`credentialsIn: "body"`), the client credentials are sent as
|
|
48
|
+
`client_id` and `client_secret` form parameters in the token request body. While
|
|
49
|
+
[RFC 6749 section 2.3.1](https://datatracker.ietf.org/doc/html/rfc6749#section-2.3.1)
|
|
50
|
+
recommends HTTP Basic authentication, `body` is the default because it's the
|
|
51
|
+
method most widely accepted across identity providers (including Auth0, Okta,
|
|
52
|
+
Entra ID, and Keycloak) and avoids credential-encoding differences between
|
|
53
|
+
providers' Basic authentication implementations.
|
|
54
|
+
|
|
55
|
+
Set `credentialsIn` to `header` to send the credentials with HTTP Basic
|
|
56
|
+
authentication instead:
|
|
57
|
+
|
|
58
|
+
```json
|
|
59
|
+
{
|
|
60
|
+
"options": {
|
|
61
|
+
"tokenUrl": "https://idp.example.com/oauth2/token",
|
|
62
|
+
"clientId": "$env(OAUTH_CLIENT_ID)",
|
|
63
|
+
"clientSecret": "$env(OAUTH_CLIENT_SECRET)",
|
|
64
|
+
"credentialsIn": "header"
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Provider-Specific Parameters
|
|
70
|
+
|
|
71
|
+
Some identity providers require extra form parameters on the token request. Use
|
|
72
|
+
`additionalParameters` to send them, for example the `resource` parameter:
|
|
73
|
+
|
|
74
|
+
```json
|
|
75
|
+
{
|
|
76
|
+
"options": {
|
|
77
|
+
"tokenUrl": "https://idp.example.com/oauth2/token",
|
|
78
|
+
"clientId": "$env(OAUTH_CLIENT_ID)",
|
|
79
|
+
"clientSecret": "$env(OAUTH_CLIENT_SECRET)",
|
|
80
|
+
"additionalParameters": {
|
|
81
|
+
"resource": "https://api.example.com"
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
The `grant_type` parameter is always `client_credentials` and the dedicated
|
|
88
|
+
`clientId`, `clientSecret`, `scope`, and `audience` options always take
|
|
89
|
+
precedence over entries in `additionalParameters`.
|
|
90
|
+
|
|
91
|
+
### Usage Example
|
|
92
|
+
|
|
93
|
+
Apply the policy to routes that need to call your OAuth-protected backend:
|
|
94
|
+
|
|
95
|
+
```json
|
|
96
|
+
{
|
|
97
|
+
"paths": {
|
|
98
|
+
"/api/orders": {
|
|
99
|
+
"get": {
|
|
100
|
+
"x-zuplo-route": {
|
|
101
|
+
"policies": {
|
|
102
|
+
"inbound": ["jwt-auth", "upstream-oauth-client-credentials"]
|
|
103
|
+
},
|
|
104
|
+
"handler": {
|
|
105
|
+
"export": "forwardToOrigin",
|
|
106
|
+
"module": "$import(@zuplo/runtime)",
|
|
107
|
+
"options": {
|
|
108
|
+
"baseUrl": "https://api.internal.example.com"
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Security Considerations
|
|
119
|
+
|
|
120
|
+
- Store the client secret as an environment variable using `$env(VARIABLE_NAME)`
|
|
121
|
+
syntax
|
|
122
|
+
- Grant the OAuth client the minimum scopes required to access your backend
|
|
123
|
+
services
|
|
124
|
+
- Regularly rotate your client secrets according to your security policies
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
Secure your origin server with OAuth 2.0 authentication by automatically adding
|
|
2
|
+
an `Authorization` header to upstream requests. This policy enables your Zuplo
|
|
3
|
+
gateway to authenticate with any OAuth 2.0 identity provider that supports the
|
|
4
|
+
client credentials grant — such as Auth0, Okta, Keycloak, Microsoft Entra ID, or
|
|
5
|
+
your own authorization server.
|
|
6
|
+
|
|
7
|
+
With this policy, you'll benefit from:
|
|
8
|
+
|
|
9
|
+
- **Enhanced Backend Security**: Restrict access to your origin servers to only
|
|
10
|
+
your Zuplo gateway
|
|
11
|
+
- **Simplified Authentication**: Delegate authentication and authorization to
|
|
12
|
+
your gateway without backend code changes
|
|
13
|
+
- **Automatic Token Management**: Handle token acquisition, caching, and renewal
|
|
14
|
+
automatically
|
|
15
|
+
- **Provider Flexibility**: Works with any standards-compliant OAuth 2.0 token
|
|
16
|
+
endpoint, no provider-specific policy required
|
|
17
|
+
- **Credential Security**: Store sensitive client credentials securely in your
|
|
18
|
+
Zuplo environment
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft-07/schema",
|
|
3
|
+
"$id": "https://cdn.zuplo.com/policies/runtime/schemas/upstream-oauth-client-credentials-inbound.json",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"title": "Upstream OAuth 2.0 Client Credentials Auth",
|
|
6
|
+
"isDeprecated": false,
|
|
7
|
+
"isPaidAddOn": false,
|
|
8
|
+
"isEnterprise": false,
|
|
9
|
+
"isInternal": false,
|
|
10
|
+
"isBeta": false,
|
|
11
|
+
"isHidden": false,
|
|
12
|
+
"requiresAI": false,
|
|
13
|
+
"products": ["api-gateway"],
|
|
14
|
+
"description": "Fetches an access token from any OAuth 2.0 token endpoint using the client credentials grant and adds it to a header (`Authorization` by default) on the upstream request. Tokens are cached until shortly before they expire.",
|
|
15
|
+
"deprecatedMessage": "",
|
|
16
|
+
"required": ["handler"],
|
|
17
|
+
"properties": {
|
|
18
|
+
"handler": {
|
|
19
|
+
"type": "object",
|
|
20
|
+
"default": {},
|
|
21
|
+
"required": ["export", "module", "options"],
|
|
22
|
+
"properties": {
|
|
23
|
+
"export": {
|
|
24
|
+
"const": "UpstreamOAuthClientCredentialsInboundPolicy",
|
|
25
|
+
"description": "The name of the exported type"
|
|
26
|
+
},
|
|
27
|
+
"module": {
|
|
28
|
+
"const": "$import(@zuplo/runtime)",
|
|
29
|
+
"description": "The module containing the policy"
|
|
30
|
+
},
|
|
31
|
+
"options": {
|
|
32
|
+
"title": "UpstreamOAuthClientCredentialsInboundPolicyOptions",
|
|
33
|
+
"type": "object",
|
|
34
|
+
"description": "The options for this policy.",
|
|
35
|
+
"additionalProperties": false,
|
|
36
|
+
"required": ["tokenUrl", "clientId", "clientSecret"],
|
|
37
|
+
"properties": {
|
|
38
|
+
"tokenUrl": {
|
|
39
|
+
"type": "string",
|
|
40
|
+
"examples": ["https://your-tenant.us.auth0.com/oauth/token"],
|
|
41
|
+
"description": "The URL of the OAuth 2.0 token endpoint that issues the access token."
|
|
42
|
+
},
|
|
43
|
+
"clientId": {
|
|
44
|
+
"type": "string",
|
|
45
|
+
"examples": ["my-client-id"],
|
|
46
|
+
"description": "The client ID used to authenticate with the token endpoint."
|
|
47
|
+
},
|
|
48
|
+
"clientSecret": {
|
|
49
|
+
"type": "string",
|
|
50
|
+
"examples": ["$env(OAUTH_CLIENT_SECRET)"],
|
|
51
|
+
"description": "The client secret used to authenticate with the token endpoint."
|
|
52
|
+
},
|
|
53
|
+
"scope": {
|
|
54
|
+
"type": "string",
|
|
55
|
+
"examples": ["read:orders write:orders"],
|
|
56
|
+
"description": "Space-delimited list of scopes to request. When not set, the `scope` parameter is omitted from the token request."
|
|
57
|
+
},
|
|
58
|
+
"audience": {
|
|
59
|
+
"type": "string",
|
|
60
|
+
"examples": ["https://api.example.com"],
|
|
61
|
+
"description": "The value of the `audience` form parameter sent to the token endpoint. Required by some identity providers such as Auth0. When not set, the parameter is omitted from the token request."
|
|
62
|
+
},
|
|
63
|
+
"credentialsIn": {
|
|
64
|
+
"type": "string",
|
|
65
|
+
"title": "OAuthClientCredentialsLocation",
|
|
66
|
+
"default": "body",
|
|
67
|
+
"enum": ["body", "header"],
|
|
68
|
+
"x-advanced": true,
|
|
69
|
+
"description": "Where the client credentials are sent on the token request. `body` sends `client_id` and `client_secret` as form parameters. `header` sends them with HTTP Basic authentication as described in RFC 6749 section 2.3.1."
|
|
70
|
+
},
|
|
71
|
+
"additionalParameters": {
|
|
72
|
+
"type": "object",
|
|
73
|
+
"additionalProperties": {
|
|
74
|
+
"type": "string"
|
|
75
|
+
},
|
|
76
|
+
"x-advanced": true,
|
|
77
|
+
"description": "Additional form parameters to include in the token request, for example `resource` for identity providers that require it."
|
|
78
|
+
},
|
|
79
|
+
"headerName": {
|
|
80
|
+
"type": "string",
|
|
81
|
+
"default": "Authorization",
|
|
82
|
+
"x-advanced": true,
|
|
83
|
+
"description": "The name of the header on the upstream request that the access token is set on."
|
|
84
|
+
},
|
|
85
|
+
"headerScheme": {
|
|
86
|
+
"type": "string",
|
|
87
|
+
"default": "Bearer",
|
|
88
|
+
"x-advanced": true,
|
|
89
|
+
"description": "The scheme that prefixes the access token in the header. When the token response includes a `token_type`, that value is used instead."
|
|
90
|
+
},
|
|
91
|
+
"tokenRetries": {
|
|
92
|
+
"type": "number",
|
|
93
|
+
"default": 3,
|
|
94
|
+
"x-advanced": true,
|
|
95
|
+
"description": "The number of times to retry fetching the token in the event of a failure."
|
|
96
|
+
},
|
|
97
|
+
"expirationOffsetSeconds": {
|
|
98
|
+
"type": "number",
|
|
99
|
+
"default": 300,
|
|
100
|
+
"x-advanced": true,
|
|
101
|
+
"description": "The number of seconds less than the token expiration to cache the token."
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
"examples": [
|
|
107
|
+
{
|
|
108
|
+
"export": "UpstreamOAuthClientCredentialsInboundPolicy",
|
|
109
|
+
"module": "$import(@zuplo/runtime)",
|
|
110
|
+
"options": {
|
|
111
|
+
"audience": "https://api.example.com",
|
|
112
|
+
"clientId": "my-client-id",
|
|
113
|
+
"clientSecret": "$env(OAUTH_CLIENT_SECRET)",
|
|
114
|
+
"credentialsIn": "body",
|
|
115
|
+
"expirationOffsetSeconds": 300,
|
|
116
|
+
"headerName": "Authorization",
|
|
117
|
+
"headerScheme": "Bearer",
|
|
118
|
+
"scope": "read:orders write:orders",
|
|
119
|
+
"tokenRetries": 3,
|
|
120
|
+
"tokenUrl": "https://your-tenant.us.auth0.com/oauth/token"
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
]
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zuplo",
|
|
3
|
-
"version": "6.73.
|
|
3
|
+
"version": "6.73.28",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "The programmable API Gateway",
|
|
6
6
|
"author": "Zuplo, Inc.",
|
|
@@ -19,9 +19,9 @@
|
|
|
19
19
|
"zuplo": "zuplo.js"
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@zuplo/cli": "6.73.
|
|
23
|
-
"@zuplo/core": "6.73.
|
|
24
|
-
"@zuplo/runtime": "6.73.
|
|
22
|
+
"@zuplo/cli": "6.73.28",
|
|
23
|
+
"@zuplo/core": "6.73.28",
|
|
24
|
+
"@zuplo/runtime": "6.73.28",
|
|
25
25
|
"@zuplo/test": "1.4.0"
|
|
26
26
|
}
|
|
27
27
|
}
|