import { createSignal, Show } from "solid-js";
import { useNavigate } from "@solidjs/router";
import { OAuth2, createClient } from "matrix-js-sdk";
import { adoptMatrixClient, setActiveAccountId, upsertSession } from "../../matrix";
// The stored context produced during login (shape matches OAuth2's Context)
interface StoredContext {
clientId: string;
deviceId?: string;
codeVerifier?: string;
redirectUri: string;
}
interface MappedClient {
homeserverUrl: string;
context: StoredContext;
}
function parseCallbackParams(): URLSearchParams {
// response_mode is "query", but handle "fragment" too since the spec says
// the IdP may answer in either fragment or query depending on the request.
const query = new URLSearchParams(window.location.search);
const hashValue = window.location.hash.slice(1);
if (!query.get("state") && hashValue.includes("=")) {
return new URLSearchParams(hashValue);
}
return query;
}
function mapClient(state: string): MappedClient | null {
const stored = sessionStorage.getItem(`asterisk.oauth.${state}`);
if (!stored) return null;
sessionStorage.removeItem(`asterisk.oauth.${state}`);
return JSON.parse(stored) as MappedClient;
}
export default function AuthCallback() {
const navigate = useNavigate();
const [error, setError] = createSignal(null);
const [errorUri, setErrorUri] = createSignal(null);
// Component body runs once on mount in Solid v2 — no onMount needed.
void (async () => {
const params = parseCallbackParams();
// Error case: the authorisation failed, params carry error info.
const errorParam = params.get("error");
if (errorParam) {
setError(
params.get("error_description")
? `${errorParam}: ${params.get("error_description")}`
: errorParam,
);
setErrorUri(params.get("error_uri"));
return;
}
const code = params.get("code");
const state = params.get("state");
if (!code || !state) {
setError("Missing code or state in callback URL.");
return;
}
const mapped = mapClient(state);
if (!mapped) {
setError("No matching authorisation request found for this state.");
return;
}
const { homeserverUrl, context } = mapped;
const oauth = new OAuth2(
await createClient({ baseUrl: homeserverUrl }).getAuthMetadata(),
context,
);
try {
// Exchange the code for tokens at the token endpoint.
const token = await oauth.completeAuthorizationCodeGrant(code);
const clientOpts = {
baseUrl: homeserverUrl,
deviceId: context.deviceId,
accessToken: token.access_token,
refreshToken: token.refresh_token,
};
// whoami validates the token and resolves the user id so that the
// resulting client's getUserId() is non-null for adoption.
const who = await createClient(clientOpts).whoami();
const session = adoptMatrixClient(
createClient({ ...clientOpts, userId: who.user_id }),
);
// clientId is needed for refresh-token grants; it lives only in the
// SDK's OAuth context, so we have to patch it into the session here.
upsertSession({ ...session, clientId: context.clientId });
setActiveAccountId(session.id);
navigate("/");
} catch (e) {
setError(e instanceof Error ? e.message : String(e));
}
})();
return (
Completing sign in…
}
>
{(getError) => (
)}
);
}