zuplo 6.68.17 → 6.68.24

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.
@@ -4,13 +4,6 @@ title: Role Permissions
4
4
 
5
5
  <EnterpriseFeature name="Role Based Access Control" />
6
6
 
7
- :::info{title="Beta"}
8
-
9
- The specific permissions of each role are currently in beta and may change
10
- without notice.
11
-
12
- :::
13
-
14
7
  Accounts in Zuplo can have multiple members with different roles. Each account
15
8
  member can be a role that defines the permissions they have in the account.
16
9
 
@@ -0,0 +1,82 @@
1
+ ---
2
+ title: Certificate Pinning
3
+ sidebar_label: Certificate Pinning
4
+ ---
5
+
6
+ Certificate pinning is a security technique where a client validates a server's
7
+ TLS certificate against a known copy (or public key hash) stored locally in the
8
+ client application. While this can mitigate certain classes of man-in-the-middle
9
+ attacks, it's generally not recommended for modern APIs and is
10
+ [especially problematic](https://scotthelme.co.uk/why-we-need-to-do-more-to-reduce-certificate-lifetimes/)
11
+ for services that use short-lived, automatically rotated certificates.
12
+
13
+ :::warning
14
+
15
+ Zuplo strongly discourages certificate pinning for APIs running on Zuplo-managed
16
+ custom domains. Certificates are short-lived and rotate automatically on a
17
+ schedule outside of your control, which can break pinned clients without
18
+ warning.
19
+
20
+ :::
21
+
22
+ ## Why pinning is discouraged on Zuplo
23
+
24
+ By default, Zuplo manages SSL certificates for your custom domain through
25
+ Cloudflare. These certificates are issued by either Google Trust Services or
26
+ Let's Encrypt and have the following properties:
27
+
28
+ - Certificates are issued for **90 days**.
29
+ - Certificates are automatically renewed approximately **30 days before
30
+ expiry**.
31
+ - Rotation is **not guaranteed to follow a strict 90-day cadence**. We may
32
+ rotate certificates earlier for security, operational, or infrastructure
33
+ reasons.
34
+ - Rotation happens without advance notification to the gateway owner.
35
+
36
+ Because rotation is automatic and the exact schedule isn't under your control,
37
+ any client that pins a specific certificate or public key can stop working at
38
+ any time. For most production APIs, this risk far outweighs the marginal
39
+ security benefit pinning provides.
40
+
41
+ ## Recommended alternatives
42
+
43
+ If you or your clients are concerned about man-in-the-middle attacks or
44
+ unauthorized certificate issuance, use these alternatives instead of pinning:
45
+
46
+ - **[HTTP Strict Transport Security (HSTS)](https://https.cio.gov/hsts/)** to
47
+ force HTTPS and prevent protocol downgrade attacks.
48
+ - **[CAA DNS records](./custom-domains.mdx#caa-records)** to restrict which
49
+ certificate authorities can issue certificates for your domain.
50
+
51
+ ## If a client insists on pinning
52
+
53
+ Pinning is strongly discouraged, but if a client application insists on it, they
54
+ can self-serve. The public portion of the certificate is returned on every TLS
55
+ handshake, so anyone connecting to your domain can retrieve it using standard
56
+ tools like `openssl` or `curl`. Zuplo doesn't need to send the certificate and
57
+ has no record of who has downloaded it.
58
+
59
+ If a client goes down this path, they should be aware that:
60
+
61
+ - Certificates rotate automatically and can change at any time.
62
+ - Pinning the Subject Public Key Info (SPKI) hash is more resilient than pinning
63
+ the full certificate, but still not guaranteed to survive rotation.
64
+ - The client is responsible for monitoring the certificate and updating their
65
+ pins before the next rotation breaks their application.
66
+
67
+ ## Using your own long-lived SSL certificate
68
+
69
+ If you truly need full control over certificate rotation, the only supported
70
+ option is to supply your own SSL certificate for your domain and have Zuplo
71
+ install it. Contact [support@zuplo.com](mailto:support@zuplo.com) to arrange
72
+ this.
73
+
74
+ :::caution
75
+
76
+ Using a custom, long-lived SSL certificate shifts all renewal responsibility to
77
+ you. Expired certificates are a common cause of production outages. Before going
78
+ down this path, verify that you have an established process for tracking
79
+ expiration, renewing certificates ahead of time, and delivering the updated
80
+ certificate to Zuplo.
81
+
82
+ :::
@@ -166,10 +166,9 @@ Certificates are issued for 90 days and are automatically renewed approximately
166
166
 
167
167
  Certificate pinning isn't recommended for Zuplo APIs as the certificates are
168
168
  issued for short periods of time and renewed automatically. If you or your end
169
- clients require certificate pinning, it's recommended you use a custom,
170
- long-lived SSL certificate. (Although this is
171
- [not recommended](https://scotthelme.co.uk/why-we-need-to-do-more-to-reduce-certificate-lifetimes/)
172
- for most use cases.)
169
+ clients require certificate pinning, see the dedicated
170
+ [Certificate Pinning](./certificate-pinning.mdx) page for the trade-offs,
171
+ alternatives, and options for retrieving or managing your own certificate.
173
172
 
174
173
  :::
175
174
 
@@ -1,5 +1,5 @@
1
1
  ---
2
- title: Splunk Plugin (Beta)
2
+ title: Splunk Plugin
3
3
  sidebar_label: Splunk Logging
4
4
  ---
5
5
 
package/docs/cli/list.mdx CHANGED
@@ -23,6 +23,22 @@ sidebar_label: list
23
23
  "deprecated": false,
24
24
  "hidden": false
25
25
  },
26
+ {
27
+ "name": "output",
28
+ "type": "string",
29
+ "description": "Output format",
30
+ "default": "default",
31
+ "required": false,
32
+ "deprecated": false,
33
+ "hidden": false,
34
+ "alias": [
35
+ "o"
36
+ ],
37
+ "choices": [
38
+ "default",
39
+ "json"
40
+ ]
41
+ },
26
42
  {
27
43
  "name": "self-hosted-endpoint",
28
44
  "type": "string",
@@ -30,6 +46,18 @@ sidebar_label: list
30
46
  "required": false,
31
47
  "deprecated": false,
32
48
  "hidden": false
49
+ },
50
+ {
51
+ "name": "show-details",
52
+ "type": "boolean",
53
+ "description": "Include deployment metadata in the output",
54
+ "default": false,
55
+ "required": false,
56
+ "deprecated": false,
57
+ "hidden": false,
58
+ "alias": [
59
+ "d"
60
+ ]
33
61
  }
34
62
  ]}
35
63
  examples={[
@@ -37,6 +65,14 @@ sidebar_label: list
37
65
  "$0 list",
38
66
  "List all deployed environments for your project"
39
67
  ],
68
+ [
69
+ "$0 list --show-details",
70
+ "List all deployed environments with project and deployment names"
71
+ ],
72
+ [
73
+ "$0 list --output json",
74
+ "List deployed environments as JSON"
75
+ ],
40
76
  [
41
77
  "$0 list --account my-account --project my-project",
42
78
  "Explicitly specify the account and project"
@@ -0,0 +1,294 @@
1
+ ---
2
+ title: API Errors
3
+ ---
4
+
5
+ Well-designed API errors are as important as the successful responses your API
6
+ returns. A good error response tells the caller what went wrong, whether the
7
+ problem is on their side or yours, and what they can do about it. Zuplo
8
+ encourages every API to return standard, actionable error messages so that
9
+ developers integrating with your API spend less time guessing and more time
10
+ building.
11
+
12
+ This page explains the error format Zuplo uses by default, how it shows up in
13
+ the gateway, and how to customize the shape of error responses when your API has
14
+ its own conventions.
15
+
16
+ ## Why standard errors matter
17
+
18
+ When every endpoint invents its own error shape, client code becomes brittle.
19
+ Developers have to special-case each response, parse ad-hoc fields, and guess at
20
+ whether a failure is retryable. Standardizing errors across your API produces
21
+ three concrete benefits:
22
+
23
+ - **Faster integration** -- consumers write one error handler that works
24
+ everywhere.
25
+ - **Better observability** -- logs, dashboards, and tools can parse errors
26
+ consistently.
27
+ - **Clearer contracts** -- your OpenAPI document can describe errors using the
28
+ same schema for every operation.
29
+
30
+ A good error response is short, machine-readable, and specific. It identifies
31
+ the kind of problem, says what happened in human terms, and includes enough
32
+ context (a request ID, a field name, a retry hint) that the caller can take the
33
+ next step without opening a support ticket.
34
+
35
+ ## The Problem Details format
36
+
37
+ Zuplo defaults to the [Problem Details for HTTP APIs](https://httpproblems.com/)
38
+ format defined by [RFC 7807](https://datatracker.ietf.org/doc/html/rfc7807).
39
+ Problem Details is a small, widely adopted JSON schema for representing errors
40
+ from HTTP APIs. Responses use the `application/problem+json` content type and
41
+ follow a consistent shape.
42
+
43
+ A typical Problem Details response from Zuplo looks like this:
44
+
45
+ ```json
46
+ {
47
+ "type": "https://httpproblems.com/http-status/401",
48
+ "title": "Unauthorized",
49
+ "status": 401,
50
+ "instance": "/v1/widgets",
51
+ "trace": {
52
+ "timestamp": "2026-04-19T17:13:31.352Z",
53
+ "requestId": "28f2d802-8e27-49c8-970d-39d90ef0ac61",
54
+ "buildId": "eb9ef87d-b55d-446e-9fdd-13c209c01b95"
55
+ }
56
+ }
57
+ ```
58
+
59
+ The standard fields are:
60
+
61
+ - **`type`** -- a URI that identifies the kind of problem. Every occurrence of
62
+ the same problem should share the same `type`.
63
+ - **`title`** -- a short, human-readable summary that should stay consistent for
64
+ a given `type`.
65
+ - **`status`** -- the HTTP status code, duplicated in the body so clients that
66
+ log only the payload still see it.
67
+ - **`detail`** -- a human-readable explanation of this particular occurrence.
68
+ This is the field that varies from request to request.
69
+ - **`instance`** -- a URI or path that identifies the specific request that
70
+ produced the error.
71
+
72
+ Problem Details also allows **extensions** -- arbitrary additional fields that
73
+ carry problem-specific data. Zuplo uses extensions to include a `trace` object
74
+ containing the request ID, build ID, and timestamp on every error, which makes
75
+ support requests easy to correlate with logs.
76
+
77
+ :::tip
78
+
79
+ Keep `title` stable for a given error type and put request-specific information
80
+ in `detail` or in extensions. Clients match on `type` and `title`; humans read
81
+ `detail`.
82
+
83
+ :::
84
+
85
+ ## How Zuplo uses Problem Details
86
+
87
+ Zuplo's built-in policies, handlers, and system errors all return Problem
88
+ Details responses out of the box. When an inbound policy rejects a request --
89
+ for example, the
90
+ [API Key Authentication policy](../policies/api-key-inbound.mdx) when a key is
91
+ missing, or the [Rate Limiting policy](../policies/rate-limit-inbound.mdx) when
92
+ a caller exceeds their quota -- the response body is a Problem Details object
93
+ with a `type`, `title`, `status`, and `trace`. The same is true for system
94
+ responses like unmatched routes and unsupported HTTP methods.
95
+
96
+ This means that consumers of a Zuplo-fronted API get a consistent error contract
97
+ for free across gateway errors, even before you write any custom code.
98
+
99
+ ### Returning Problem Details from custom code
100
+
101
+ When you write a [custom handler](../handlers/custom-handler.mdx) or a
102
+ [custom policy](../articles/policies.mdx), return Problem Details responses
103
+ using the `HttpProblems` helper from `@zuplo/runtime`. The helper has a method
104
+ for every HTTP status code and automatically fills in `type`, `title`, `status`,
105
+ `instance`, and `trace`.
106
+
107
+ ```ts
108
+ import { HttpProblems, ZuploContext, ZuploRequest } from "@zuplo/runtime";
109
+
110
+ export default async function (request: ZuploRequest, context: ZuploContext) {
111
+ if (!request.user) {
112
+ return HttpProblems.unauthorized(request, context);
113
+ }
114
+
115
+ return request;
116
+ }
117
+ ```
118
+
119
+ For the full list of methods and options, see the
120
+ [HttpProblems helper reference](../programmable-api/http-problems.mdx).
121
+
122
+ ### Adding context with `detail` and extensions
123
+
124
+ Override the default fields when you have something more useful to say. Use
125
+ `detail` for a human-readable explanation of the specific failure, and use
126
+ extension members for structured data that clients can act on.
127
+
128
+ ```ts
129
+ return HttpProblems.badRequest(request, context, {
130
+ title: "Invalid value for query parameter 'take'",
131
+ detail:
132
+ "The take parameter must be a number less than 100. The provided value was 'hello'.",
133
+ extensions: {
134
+ parameter: "take",
135
+ providedValue: "hello",
136
+ },
137
+ });
138
+ ```
139
+
140
+ The `title` stays consistent for every instance of this error, while `detail`
141
+ and the `parameter` extension tell the caller exactly what to fix.
142
+
143
+ ### Throwing runtime errors
144
+
145
+ If your code throws rather than returning a response, use `RuntimeError` and
146
+ `ConfigurationError` to attach structured context that the gateway can surface.
147
+ Thrown errors are converted to Problem Details responses automatically, and any
148
+ `extensionMembers` you attach flow through to the response body.
149
+
150
+ ```ts
151
+ import { RuntimeError } from "@zuplo/runtime";
152
+
153
+ throw new RuntimeError({
154
+ message: "Upstream database timed out",
155
+ extensionMembers: {
156
+ service: "orders-db",
157
+ timeoutMs: 5000,
158
+ },
159
+ });
160
+ ```
161
+
162
+ See [Runtime Errors](../programmable-api/runtime-errors.mdx) for details on both
163
+ error classes and patterns for mapping them to problem responses.
164
+
165
+ ## Customizing the error response format
166
+
167
+ Problem Details is the default, but it isn't the only option. If your API
168
+ already has an established error schema, or if you want to wrap every error in a
169
+ custom envelope, Zuplo provides two levels of customization.
170
+
171
+ ### Per-response overrides
172
+
173
+ The simplest customization is to override fields on individual responses.
174
+ `HttpProblems` lets you change `title`, `detail`, `type`, `instance`, and add
175
+ arbitrary extensions without giving up the standard format.
176
+
177
+ ```ts
178
+ return HttpProblems.tooManyRequests(
179
+ request,
180
+ context,
181
+ {
182
+ type: "https://errors.example.com/rate-limit-exceeded",
183
+ detail: "You've exceeded the 1000 requests per hour plan limit.",
184
+ extensions: {
185
+ plan: "free",
186
+ upgradeUrl: "https://example.com/upgrade",
187
+ },
188
+ },
189
+ {
190
+ "Retry-After": "3600",
191
+ },
192
+ );
193
+ ```
194
+
195
+ This approach keeps the Problem Details shape while letting you customize types,
196
+ link to documentation, and include plan-specific or caller-specific metadata.
197
+
198
+ ### Formatting every error with `ProblemResponseFormatter`
199
+
200
+ For more control, use the
201
+ [`ProblemResponseFormatter`](../programmable-api/problem-response-formatter.mdx)
202
+ to build problem responses directly. This is useful when you want to compute the
203
+ problem body yourself -- for example, mapping an upstream error payload into
204
+ your own error taxonomy.
205
+
206
+ ```ts
207
+ import { ProblemResponseFormatter } from "@zuplo/runtime";
208
+
209
+ const problemDetails = {
210
+ type: "https://errors.example.com/validation-failed",
211
+ title: "Validation Failed",
212
+ status: 400,
213
+ detail: "The request body contains invalid fields.",
214
+ instance: request.url,
215
+ extensions: {
216
+ code: "VAL_001",
217
+ fields: ["email", "phone"],
218
+ },
219
+ };
220
+
221
+ return ProblemResponseFormatter.format(problemDetails, request, context);
222
+ ```
223
+
224
+ ### Replacing the error format globally
225
+
226
+ To change the shape of every error response the gateway returns -- including
227
+ errors raised by built-in policies -- register a global error handler in your
228
+ [runtime extensions](../programmable-api/runtime-extensions.mdx). The handler
229
+ runs for any unhandled error in the pipeline and returns the response of your
230
+ choice.
231
+
232
+ ```ts
233
+ import { RuntimeExtensions } from "@zuplo/runtime";
234
+
235
+ export function runtimeInit(runtime: RuntimeExtensions) {
236
+ runtime.addErrorHandler(async (error, request, context) => {
237
+ return new Response(
238
+ JSON.stringify({
239
+ error: {
240
+ code: "internal_error",
241
+ message: error.message,
242
+ requestId: context.requestId,
243
+ },
244
+ }),
245
+ {
246
+ status: 500,
247
+ headers: { "content-type": "application/json" },
248
+ },
249
+ );
250
+ });
251
+ }
252
+ ```
253
+
254
+ Global error handlers let you keep Problem Details internally while projecting a
255
+ different schema to your callers, or replace the format entirely if your API has
256
+ an established error contract.
257
+
258
+ :::caution
259
+
260
+ Replacing the default format means you also take responsibility for including
261
+ trace information, preserving status codes, and documenting the new schema in
262
+ your OpenAPI document. Most APIs are best served by customizing Problem Details
263
+ fields rather than replacing the format.
264
+
265
+ :::
266
+
267
+ ### Customizing specific system errors
268
+
269
+ Some gateway behaviors have dedicated extension points for error customization.
270
+ For example, you can replace the default 404 response by registering a
271
+ [not-found handler](../programmable-api/not-found-handler.mdx), which is useful
272
+ for serving custom error pages or matching your API's error schema on unmatched
273
+ routes.
274
+
275
+ ## Choosing an approach
276
+
277
+ | Scenario | Approach |
278
+ | ---------------------------------------------------- | -------------------------------------------------------------- |
279
+ | Return a one-off error from a handler or policy | `HttpProblems` with `detail` and extensions |
280
+ | Build problem responses from external error payloads | `ProblemResponseFormatter.format()` |
281
+ | Attach context to thrown errors | `RuntimeError` with `extensionMembers` |
282
+ | Replace the error schema for every response | Global error handler via `runtime.addErrorHandler` |
283
+ | Customize only the 404 response | [Not-found handler](../programmable-api/not-found-handler.mdx) |
284
+
285
+ ## Related resources
286
+
287
+ - [HttpProblems helper](../programmable-api/http-problems.mdx)
288
+ - [ProblemResponseFormatter](../programmable-api/problem-response-formatter.mdx)
289
+ - [Runtime Errors](../programmable-api/runtime-errors.mdx)
290
+ - [Not-found Handler](../programmable-api/not-found-handler.mdx)
291
+ - [Runtime Extensions](../programmable-api/runtime-extensions.mdx)
292
+ - [Custom Handlers](../handlers/custom-handler.mdx)
293
+ - [Policies](../articles/policies.mdx)
294
+ - [RFC 7807: Problem Details for HTTP APIs](https://datatracker.ietf.org/doc/html/rfc7807)
@@ -34,7 +34,10 @@ If you don't have an Auth0 account, you can sign up for a
34
34
  - Production: `https://your-site.com/oauth/callback`
35
35
  - Preview (wildcard): `https://*.your-domain.com/oauth/callback`
36
36
  - Local Development: `http://localhost:3000/oauth/callback`
37
- - **Allowed Logout URLs**: Same as callback URLs above
37
+ - **Allowed Logout URLs**:
38
+ - Production: `https://your-site.com/oauth/logout-callback`
39
+ - Preview (wildcard): `https://*.your-domain.com/oauth/logout-callback`
40
+ - Local Development: `http://localhost:3000/oauth/logout-callback`
38
41
 
39
42
  - **Allowed Web Origins**:
40
43
  - Production: `https://your-site.com`
@@ -112,8 +115,8 @@ To enable logout for your Auth0 application:
112
115
 
113
116
  1. Ensure your **Allowed Logout URLs** are configured in Auth0 (see
114
117
  [Configure Auth0 Application](#setup-steps) above)
115
- 2. The logout URL should match your callback URL pattern (e.g., `https://your-site.com/` for
116
- production)
118
+ 2. The logout URL must use the `/oauth/logout-callback` path (e.g.,
119
+ `https://your-site.com/oauth/logout-callback` for production)
117
120
 
118
121
  For older tenants, you may need to enable **RP-Initiated Logout** in your tenant settings. See the
119
122
  [Auth0 logout documentation](https://auth0.com/docs/authenticate/login/logout/log-users-out-of-auth0)
@@ -0,0 +1,110 @@
1
+ ---
2
+ title: OpenID Connect (OIDC)
3
+ sidebar_label: OpenID Connect
4
+ description:
5
+ Configure any OpenID Connect compliant identity provider (Okta, Keycloak, Authentik, etc.) as the
6
+ authentication provider for Zudoku.
7
+ ---
8
+
9
+ Dev Portal supports any identity provider that implements the
10
+ [OpenID Connect](https://openid.net/specs/openid-connect-core-1_0.html) protocol via the generic
11
+ `openid` provider type. This includes Okta, Keycloak, Authentik, Ory, ZITADEL, AWS Cognito, Google
12
+ Identity, and most enterprise IdPs.
13
+
14
+ ## Configuration
15
+
16
+ Add the `authentication` property to your [Dev Portal configuration](./overview.md):
17
+
18
+ ```typescript title="zudoku.config.ts"
19
+ {
20
+ // ...
21
+ authentication: {
22
+ type: "openid",
23
+ clientId: "<your-client-id>",
24
+ issuer: "<the-issuer-url>",
25
+ scopes: ["openid", "profile", "email"], // Optional
26
+ },
27
+ // ...
28
+ }
29
+ ```
30
+
31
+ | Option | Required | Description |
32
+ | ---------- | -------- | -------------------------------------------------------------------------------------------- |
33
+ | `clientId` | Yes | The OAuth client ID issued by your provider. |
34
+ | `issuer` | Yes | The issuer URL. Dev Portal discovers endpoints from `<issuer>/.well-known/openid-configuration`. |
35
+ | `scopes` | No | Scopes to request. Defaults to `["openid", "profile", "email"]`. |
36
+
37
+ ## Provider Setup
38
+
39
+ Register Dev Portal as a public SPA / single page application client in your identity provider and set:
40
+
41
+ - Callback / Redirect URI to `https://your-site.com/oauth/callback`
42
+ - For local development, add `http://localhost:3000/oauth/callback`
43
+ - If your provider supports wildcards, add `https://*.your-domain.com/oauth/callback` for preview
44
+ environments
45
+ - Add your site origin to the list of allowed CORS origins
46
+ - Enable the `Authorization Code` grant with PKCE and the `Refresh Token` grant
47
+
48
+ ### Okta
49
+
50
+ 1. In the Okta admin console go to **Applications** → **Applications** → **Create App Integration**.
51
+ 2. Select **OIDC - OpenID Connect** and **Single Page Application**.
52
+ 3. Set **Sign-in redirect URIs** to `https://your-site.com/oauth/callback` (add
53
+ `http://localhost:3000/oauth/callback` for local development).
54
+ 4. Under **Assignments**, assign the users or groups that should have access.
55
+ 5. After creating the app, copy the **Client ID**. Your issuer is your Okta domain, for example
56
+ `https://your-tenant.okta.com` or a custom authorization server like
57
+ `https://your-tenant.okta.com/oauth2/default`.
58
+ 6. Under **Security** → **API** → **Trusted Origins**, add your site origin for both CORS and
59
+ Redirect.
60
+
61
+ ```typescript title="zudoku.config.ts"
62
+ {
63
+ authentication: {
64
+ type: "openid",
65
+ clientId: "<your-okta-client-id>",
66
+ issuer: "https://your-tenant.okta.com/oauth2/default",
67
+ scopes: ["openid", "profile", "email"],
68
+ },
69
+ }
70
+ ```
71
+
72
+ ### Keycloak
73
+
74
+ Use the realm issuer URL:
75
+
76
+ ```typescript title="zudoku.config.ts"
77
+ {
78
+ authentication: {
79
+ type: "openid",
80
+ clientId: "zudoku",
81
+ issuer: "https://keycloak.example.com/realms/<your-realm>",
82
+ },
83
+ }
84
+ ```
85
+
86
+ In the realm, create a client with **Client type** `OpenID Connect`, **Access type** `public`, and
87
+ enable **Standard Flow** (Authorization Code).
88
+
89
+ ## Verifying the Issuer
90
+
91
+ You can confirm your issuer URL is correct by opening `<issuer>/.well-known/openid-configuration` in
92
+ a browser. It should return a JSON document listing `authorization_endpoint`, `token_endpoint`,
93
+ `userinfo_endpoint`, and `jwks_uri`.
94
+
95
+ ## User Profile
96
+
97
+ After sign-in Dev Portal calls the provider's
98
+ [UserInfo endpoint](https://openid.net/specs/openid-connect-core-1_0.html#UserInfo) and reads
99
+ `name`, `email`, `picture`, and `email_verified` from the response. Map these claims in your
100
+ provider if they are not emitted by default.
101
+
102
+ ## Troubleshooting
103
+
104
+ - **Discovery fails**: verify `<issuer>/.well-known/openid-configuration` resolves and matches the
105
+ `issuer` value in the document.
106
+ - **CORS errors on token / userinfo**: add your site origin to the provider's allowed origins.
107
+ - **Redirect URI mismatch**: the URI registered with the provider must match the Dev Portal origin
108
+ exactly, including protocol and port.
109
+ - **Missing profile fields**: ensure `profile` and `email` scopes are granted and that the provider
110
+ includes `name`, `email`, and `picture` claims in the UserInfo response.
@@ -15,8 +15,8 @@ authentication provider you use.
15
15
 
16
16
  ## Authentication Providers
17
17
 
18
- Dev Portal supports Clerk, Auth0, Supabase, Firebase, Azure B2C, and any OpenID provider that supports
19
- the OpenID Connect protocol (including PingFederate).
18
+ Dev Portal supports Clerk, Auth0, Supabase, Firebase, Azure B2C, and any OpenID Connect provider
19
+ (including Okta, Keycloak, Authentik, and PingFederate).
20
20
 
21
21
  Not seeing your authentication provider? [Let us know](https://github.com/zuplo/zudoku/issues)
22
22
 
@@ -96,6 +96,9 @@ When configuring your OpenID provider, you will need to set the following:
96
96
  By default, the scopes "openid", "profile", and "email" are requested. You can customize these by
97
97
  providing your own array of scopes.
98
98
 
99
+ For provider-specific guides (Okta, Keycloak, etc.), see the
100
+ [OpenID Connect setup page](./authentication-openid.md).
101
+
99
102
  ### Firebase
100
103
 
101
104
  For Firebase authentication, you will need your Firebase project configuration. You can find this in