Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
(#58)
- Additional unit coverage for registration UIA helpers and signup pages
(`matrixRegistrationUia.spec.ts`, page specs)
- Matrix delegated OIDC sign-up (MSC2965 / MAS): OpenID discovery, PKCE,
dynamic client registration, token exchange, `/auth/matrix-oidc/callback`,
and Matrix JS SDK session wiring (`matrixOidcNative.ts`, `useMatrixClient`
with persisted OIDC client and token-endpoint metadata for refresh)
- Sign-up entry point for delegated browser OAuth; runtime
`nuxt.public.siteUrl` / `matrixOidcClientId`; EN/DE i18n for callback and
error paths; README notes on HTTPS redirect requirements for matrix.org

### Changed

Expand Down
23 changes: 19 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,22 @@ Signup uses Matrix **User-Interactive Authentication** against
`/register`: first request may return **401** with `session`, `flows`,
and `params`; the client keeps the session and completes stages in order.

**Supported stages today**
**Delegated auth (MAS / OAuth — e.g. matrix.org)**

If `/.well-known/matrix/client` includes `org.matrix.msc2965.authentication`,
the homeserver typically **blocks legacy `POST /register`** for web clients but
delegates new-account creation (and related flows) to a Matrix Authentication
Service (OAuth/OIDC).

- Set **`NUXT_PUBLIC_SITE_URL`** to your app's public **`https://` origin**
**without path** (Matrix dynamic client registration rejects `http://localhost`
redirects; use an HTTPS tunnel / preview URL for local OAuth).
- Optional **`NUXT_PUBLIC_MATRIX_OIDC_CLIENT_ID`** — static OAuth client id if you
register one yourself.
- Redirect/callback **`/auth/matrix-oidc/callback`** completes the PKCE exchange,
restores the Matrix JS SDK session (`matrixOidcNative.ts`, `useMatrixClient`).

**Supported stages today (legacy `/register` UIA)**

| Stage | Notes |
| --- | --- |
Expand All @@ -147,9 +162,9 @@ and `params`; the client keeps the session and completes stages in order.

**Explicit non-support**

- **`m.login.sso`** – SSO/OIDC signup is not implemented in-app. Users see a
message to complete registration via a Matrix web client (e.g. Element),
then sign in here.
- **`m.login.sso`** as an in-flow stage **after `/register` already returned UIA**
(`session` + flows) — not wired in-app; where MSC2965 is advertised, prefer
the **delegated OAuth** button on the **sign-up** page instead.
- **`m.login.msisdn`** – Phone/SMS registration is not implemented; users get
a clear “not supported” message instead of failing silently.

Expand Down
33 changes: 32 additions & 1 deletion app/components/auth/SignupRecaptchaStep.vue
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,28 @@ const {
const v3Running = ref(false)
const v3Error = ref('')

const widgetAllowed = computed(() => canLoadGoogleRecaptcha())
const needsExplicitGoogleTransferAck = computed(() => !iubendaConfigured())
const acknowledgesGoogleTransfer = ref(false)

const widgetAllowed = computed(() => {
if (!canLoadGoogleRecaptcha()) {
return false
}
if (needsExplicitGoogleTransferAck.value && !acknowledgesGoogleTransfer.value) {
return false
}
return true
})

const policyHref = computed(() => privacyPolicyUrl())

function onConsentContinue(): void {
if (
needsExplicitGoogleTransferAck.value &&
!acknowledgesGoogleTransfer.value
) {
return
}
grantLocalRecaptchaConsent()
}

Expand Down Expand Up @@ -95,10 +112,24 @@ function onRecaptchaV2Verified(response: unknown): void {
</UButton>
</div>

<label
v-if="needsExplicitGoogleTransferAck && !canLoadGoogleRecaptcha()"
class="flex cursor-pointer items-start gap-3 text-sm text-gray-300"
>
<input
v-model="acknowledgesGoogleTransfer"
type="checkbox"
class="mt-0.5"
>
<span>{{ translateText('auth.signUpRecaptchaTransferAck') }}</span>
</label>

<div v-if="!widgetAllowed">
<UButton
type="button"
class="w-full justify-center"
:disabled="needsExplicitGoogleTransferAck &&
!acknowledgesGoogleTransfer"
@click="onConsentContinue"
>
{{ translateText('auth.signUpAgreeLoadRecaptcha') }}
Expand Down
44 changes: 44 additions & 0 deletions app/composables/matrix/matrixClientShared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,33 @@ export function readMatrixErrorCode(error: unknown): string {
)
}

/**
* Narrow {@link HOMESERVER_CONNECTION_HINT_ERROR} mapping to cases where fetch
* failed before a usable Matrix JSON body (no {@link readMatrixErrorCode},
* no HTTP status). Signup PR #60 added broad browser-message matching; without
* this, legitimate API failures can be mislabeled as “homeserver unreachable”.
*/
export function isTransportFailureWithoutMatrixBody(
error: unknown
): boolean {
if (!isLikelyBrowserNetworkOrCorsError(error)) {
return false
}
if (readMatrixErrorCode(error)) {
return false
}
if (!error || typeof error !== 'object') {
return true
}
const shaped = error as MatrixApiErrorShape
const httpStatus = shaped.httpStatus ?? shaped.statusCode
return !(
typeof httpStatus === 'number' &&
Number.isFinite(httpStatus) &&
httpStatus > 0
)
}

export function readMatrixErrorMessage(error: unknown): string {
if (error instanceof Error && error.message.trim()) {
return error.message
Expand All @@ -144,6 +171,23 @@ export function readMatrixErrorMessage(error: unknown): string {
)
}

/**
* Homeserver refuses open registration via POST /register (Synapse/matrix.org).
* Separate from flows Decentra can complete via UIA when a session exists.
*/
export function isPublicRegisterEndpointDisabled(error: unknown): boolean {
const raw = readMatrixErrorMessage(error)
const normalized = raw.toLowerCase()
return (
normalized.includes('registration has been disabled') ||
normalized.includes('registration is disabled') ||
(
normalized.includes('application_service') &&
normalized.includes('registrations are allowed')
)
)
}

export function isSignupUnsupported(error: unknown): boolean {
const matrixErrorCode = readMatrixErrorCode(error)
if (
Expand Down
Loading
Loading