Files
Arvin Xu c3a7642295 ✨ feat(oauth-apps): support web applications with redirect URIs (#19504)
* ✨ feat(oauth-apps): support web applications with redirect URIs

OAuth apps created from the settings page were device-flow only: no
redirect URIs, no response types, and no client secret. That covers CLIs
but leaves no way to register a server-side app that signs users in with
a LobeHub account through the authorization code flow.

Adds a `web` application type alongside the existing device type. A web
app gets `authorization_code` + `refresh_token` grants, a `code` response
type, the redirect URIs its owner registers, and a confidential client
secret issued once at creation with a rotate action afterwards.

The secret is encrypted at rest with `KeyVaultsGateKeeper`, mirroring how
API keys are stored, and decrypted in the OIDC adapter because the token
endpoint compares it in plaintext. A secret that fails to decrypt is
dropped rather than passed through, so the request fails as
`invalid_client` instead of making ciphertext the credential.

Redirect URIs are matched exactly: no wildcards, no fragments, no
embedded credentials, and https everywhere except loopback hosts. The
same rules run in the form and again in the tRPC input, since only the
server-side pass is binding.

No migration: `oidc_clients` already carries every column this needs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* 🐛 fix(oauth-apps): keep the adapter's secret round-trip symmetric

Two follow-ups from review.

`Client.upsert` still wrote the secret in plaintext while `find` now
expects ciphertext, so anything persisting a client through the adapter
would read back a credential it could not authenticate with. Encrypt on
the way in too, and leave the stored secret untouched when an update
payload omits it. The conflict branch also referenced a camelCase key
that never existed on the payload, so it silently wrote nothing.

An `update` carrying an empty redirect URI list passed validation and
stripped a web app of every callback, contradicting the create-time
requirement. Reject a list that normalizes to empty wherever one is
supplied.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* 💄 feat(oauth-apps): address acceptance review on create, detail and consent flows

Application type is now picked from two icon cards that explain each
kind, instead of a bare segmented toggle. Redirect URIs move out of the
create dialog and are configured on the app page afterwards; the type
stays at creation because it decides whether a client secret is issued
and cannot change once the app exists. A freshly created web app shows
that it has no redirect URIs yet and cannot sign anyone in until one is
added.

The detail page separates reading from editing. Basic info and redirect
URIs each render read-only with an Edit action, and open a form with
Save and Cancel only on request. Redirect URIs are edited as a list of
rows rather than a newline-separated textarea.

Authorization errors the provider cannot redirect back with, such as an
unregistered redirect URI, now land on a dedicated page with a plain
explanation per error code instead of a raw JSON dump. Only `error` and
`error_description` are forwarded.

The consent and sign-in confirmation cards center their title and
subtitle so the text lines up with the centered application logo.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* ♿ fix(oauth-apps): label redirect URI row removal and color the list error

The icon-only remove button on each redirect URI row had no accessible
name, so assistive tech announced it as an unlabeled button.

The "at least one redirect URI" message rendered in body color: antd's
ErrorList only picks up error styling inside a Form.Item with an error
status, and the list-level message sits outside one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 12:25:20 +08:00
..