Skip to content
Merged
2 changes: 1 addition & 1 deletion .changeset/24773-oauth-application-type.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
---
'@objectstack/plugin-auth': patch
---
Expand All @@ -9,5 +9,5 @@
`POST /api/v1/auth/sys-oauth-application/register` (the Setup → OAuth Applications form) forwarded the form's Application Type as `type`. The provider's `/oauth2/create-client` body declares `application_type` (`web` | `native`) and no `type`, so the value was dropped and every client was judged and stored as `web`. A `native` client therefore could not register a loopback redirect URI such as `http://localhost:4001/callback`, and its `sys_oauth_application.type` read `web`.

- **Now.** `web` and `native` are forwarded as `application_type`, and the provider's own rule decides. A `native` client may use http only on exactly `localhost`, `127.0.0.1` or `[::1]` (any port), and is stored as `native`. A `web` client still needs an https URI on a non-loopback host.
- **Unchanged, with a warning.** Any other type, including the form's `user-agent-based` and `public`, has no `application_type` in the provider. It keeps today's outcome: registered and judged as `web`, with a client secret. The server now logs one warning per such registration, naming the value. To use a loopback redirect URI, register the client as `native`.
- **Refused.** Any other type, including the form's `user-agent-based` and `public`, has no `application_type` in the provider. The register route refuses it with `400 INVALID_REQUEST` and registers nothing (this release's breaking entry for the route). To use a loopback redirect URI, register the client as `native`.
- **Unchanged.** No type at all is `web`. Refusals keep the codes and statuses they had: `400 OAUTH_REGISTER_FAILED`, with `error.details.upstreamError` `invalid_redirect_uri` when the redirect-URI rule refuses.
6 changes: 3 additions & 3 deletions .changeset/24773-oauth-form-types.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
---
'@objectstack/platform-objects': patch
---

fix(platform-objects): the OAuth application register form offers only Web and Native, and its Redirect URLs help states the rule for each (#24773)

Clause-②: no (a form's offering: the register route still accepts every type value it accepted, and nothing on the server reads the action's param options, so no accept set or served shape moves)
Clause-②: no (a form's offering: nothing on the server reads the action's param options, so this change moves no accept set or served shape; the register route's refusal of the other types is its own breaking entry in this release)

Setup › OAuth Applications › Register offered four application types: Web, Native, User-agent based and Public. The OAuth provider judges only `web` and `native`, so the register route registers the other two as `web`, with a client secret. The form now offers Web and Native, and still defaults to Web.
Setup › OAuth Applications › Register offered four application types: Web, Native, User-agent based and Public. The OAuth provider judges only `web` and `native`, and the register route refuses any other type with `400 INVALID_REQUEST`. The form now offers Web and Native, and still defaults to Web.

- **Redirect URLs help.** It said "Must use https:// in production", which did not tell an admin that a local `http://localhost` callback needs the Native type. It now states the rule for each type: a Web application needs `https://` on a host other than localhost or another loopback address; a Native application may also use `http://` on exactly `localhost`, `127.0.0.1` or `[::1]`, on any port.
- **Application Type help.** New: choose Native for a desktop or mobile app, or for an app in local development that redirects to `http://localhost`.
- **Unchanged.** The register route accepts the same values as before, including `user-agent-based` and `public`. The stored `sys_oauth_application.type` column still lists all four values, so a row written under an earlier provider line keeps its value. The `public` column is unchanged.
- **Unchanged.** The stored `sys_oauth_application.type` column still lists all four values, so a row written under an earlier provider line keeps its value. The `public` column is unchanged.

The zh-CN, ja-JP and es-ES translations carry the new help texts.
21 changes: 21 additions & 0 deletions .changeset/24773-oauth-type-refusal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
'@objectstack/plugin-auth': minor
'@objectstack/platform-objects': patch
---

fix(plugin-auth)!: `POST /api/v1/auth/sys-oauth-application/register` refuses an application type the OAuth provider does not judge with `400 INVALID_REQUEST`, where it registered it as a `web` client

Clause-②: no (narrowing) — the register door accepted any `type` value and registered every one other than `web` / `native` as a `web` client with a secret; it now refuses each of those values with 400, so it accepts exactly `web`, `native` and no type.

<!-- adr-0087: not-required (no-migration-prescription) No metadata moves: no spec key, authorable spelling, object definition or stored shape is added, removed, renamed or re-shaped, and no exported declaration is removed or narrowed, so there is nothing for `objectstack migrate meta` to rewrite. What changes is the answer one runtime REST door gives a caller whose request body names an application type the OAuth provider has no counterpart for; the remedy is a different value in that request, never a rewrite of a package source or a stored row. The Console form that posts to this door already offers only Web and Native, and `sys_oauth_application` rows are not touched. The other categories are closed on facts: the package publishes (not unpublished); no ADR-0087 id is named or touched (not registered or already-registered); and nothing exported is removed or narrowed (not runtime-interface-only, type-surface-only or runtime-export-retired). -->

**BREAKING** (a REST door's accept set narrows), shipped as `minor` under the launch-window convention for breaking changes.

The OAuth provider (`@better-auth/oauth-provider`) judges two application types, `web` and `native`, and this door forwards the type the Setup → OAuth Applications form chose as the provider's `application_type`. Any other value has no provider counterpart. The door used to register such a request as a confidential `web` client, with a client secret and the web redirect-URI rule, which is a different kind of client from the one the request asked for. It now refuses the request instead.

- **FROM:** a request whose `type` is present and is neither `web` nor `native` (`user-agent-based`, `public`, any other string including the empty string, or a value that is not a string) answered `200` and stored a `web` client, or was refused by the web rule (`OAUTH_REGISTER_FAILED`) for a loopback redirect URI.
- **TO:** the same request answers `400` with `error.code: 'INVALID_REQUEST'`. The message names the value (its first 64 characters) and the two accepted types, and says to choose `native` for an `http://localhost`, `http://127.0.0.1` or `http://[::1]` redirect URI. The provider is not called, nothing is stored, and no warning is logged.
- **Accepted:** `web` and `native` are forwarded as the provider's `application_type` (this release's Application Type entry), and the provider's own redirect-URI rule decides.
- **Unchanged:** A request with no `type`, or with `type: null`, registers a `web` client, the provider's default. The other refusals keep their codes (their statuses are this release's refusal-status entry). Clients already registered are not touched.
- **`@objectstack/platform-objects`:** only the code comment on the `create_oauth_application` action changes, to state this refusal; it ships in the package's JavaScript. The form offers only Web and Native (this release's register-form entry).
- **The fix for an integrator:** send `type: 'web'` for a client whose redirect URIs are https on a host that is not a loopback address, or `type: 'native'` for a client that also redirects to `http://localhost`, `http://127.0.0.1` or `http://[::1]`. Omitting `type` registers `web`.
Original file line number Diff line number Diff line change
Expand Up @@ -151,14 +151,14 @@ export const SysOauthApplication = ObjectSchema.create({
// provider judges (`@better-auth/oauth-provider` 1.7.3 declares
// `application_type` as `web` | `native`), and the redirect-URL help
// states that provider's redirect-URI rule per type, which the register
// route forwards the type to. `user-agent-based` and `public` are no
// longer offered: the provider has no such application type, so the
// route registers either as `web`. The route itself still accepts both
// (refusing them narrows the door, which waits for a major version), and
// nothing on the server reads these options: the generic action door
// refuses a `type: 'api'` action before its param contract runs, so the
// route is the only judge. The stored `type` column below is a separate
// surface and is not narrowed here: that waits for the same major version.
// route forwards the type to. The route accepts only those two: in 18.x it
// refuses any other present type (`user-agent-based`, `public`, ...) with
// 400 before the provider runs; no type (absent or null) registers `web`.
// Nothing on the server reads these options: the generic action door
// refuses a `type: 'api'` action before its param contract runs. The
// stored `type` column below is not narrowed: 1.7.3 writes only `web` or
// `native` (null for a client-metadata-document client), an earlier
// provider line could write `user-agent-based`, and no in-tree reader branches on it.
params: [
{ name: 'name', label: 'Application Name', type: 'text', required: true },
{
Expand Down
41 changes: 25 additions & 16 deletions packages/plugins/plugin-auth/src/auth-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3647,26 +3647,35 @@ export class AuthPlugin implements Plugin {
// non-loopback host, `native` may use http only on exactly `localhost`,
// `127.0.0.1` or `[::1]` — and nothing here re-judges a URI.
//
// Any other value (the form's `user-agent-based` and `public`, which
// have no `application_type`, or anything else) keeps the outcome it
// always had: registered and judged as `web`. Refusing it would narrow
// what this door accepts, and such a narrowing ships its loud half
// first (the standing post-GA rule, #6015 `6292005854`): one warning
// per registration, naming the value. No type at all is the
// provider's own default, `web`, and says nothing.
// The door accepts exactly those two types. Any other value present in
// the body — the form's former `user-agent-based` and `public`, which
// have no `application_type`, an empty string, a non-string, anything
// else — is refused here with the route's own 400 `INVALID_REQUEST`,
// before the provider is called, so nothing is stored. It is never
// registered as `web` and never mapped to another type: that silent
// substitution issued a confidential web client, with a secret, for a
// request that asked for something else. The refusal ships in 18.x as
// a breaking minor (the standing post-GA rule as amended, #6015
// `6303865025`). No type at all — absent, or JSON `null` — is the
// provider's own default, `web`, and is unchanged.
let applicationType: 'web' | 'native' = 'web';
if (type === 'web' || type === 'native') {
applicationType = type;
} else if (type !== undefined && type !== null) {
const requestedType = typeof type === 'string' ? type.slice(0, 64) : `(${typeof type})`;
ctx.logger.warn(
`[AuthPlugin] sys-oauth-application/register: application type ${JSON.stringify(requestedType)} is not ` +
"one the OAuth provider judges (it knows 'web' and 'native'), so this client is registered and judged " +
"as 'web': https redirect URIs on non-loopback hosts only, and a client secret is issued. Register it " +
"as 'web', or as 'native' to use an http://localhost, http://127.0.0.1 or http://[::1] redirect URI. " +
'A future major version refuses this value.',
{ requestedType, registeredAs: 'web' },
);
const shown =
typeof type === 'string'
? JSON.stringify(type.length > 64 ? `${type.slice(0, 64)}…` : type)
: `(${typeof type})`;
return c.json({
success: false,
error: {
code: 'INVALID_REQUEST',
message:
`type ${shown} is not an application type the OAuth provider judges: send 'web' or 'native', ` +
"or no type for 'web'. Choose 'native' for an http://localhost, http://127.0.0.1 or http://[::1] " +
"redirect URI; 'web' needs https on a host that is not a loopback address.",
},
}, 400);
}

const authApi: any = await this.authManager!.getApi();
Expand Down
Loading
Loading