fix: use HTML+JS redirect for native app OAuth callbacks

Android Chrome Custom Tabs block 302 redirects to custom URI schemes
(fedilab://, moshidon-android-auth://) for security. The server sends
the redirect correctly but the WebView silently ignores it — "nothing
happens" when the user taps Authorize.

Fix: detect non-HTTP redirect URIs and render an HTML page with both
a JavaScript window.location redirect and a meta refresh fallback.
Client-side navigation to custom schemes is allowed by WebViews.

HTTP(S) redirect URIs (Phanpy, Elk) still use standard 302.
This commit is contained in:
Ricardo
2026-03-21 09:42:31 +01:00
parent 41c43be4cb
commit 2a4ac75c77
2 changed files with 35 additions and 3 deletions

View File

@@ -310,7 +310,7 @@ router.post("/oauth/authorize", async (req, res, next) => {
"error_description",
"The resource owner denied the request",
);
return res.redirect(url.toString());
return redirectToUri(res, redirect_uri, url.toString());
}
return res.status(403).json({
error: "access_denied",
@@ -362,7 +362,7 @@ router.post("/oauth/authorize", async (req, res, next) => {
// Redirect with code
const url = new URL(redirect_uri);
url.searchParams.set("code", code);
res.redirect(url.toString());
redirectToUri(res, redirect_uri, url.toString());
} catch (error) {
next(error);
}
@@ -600,4 +600,36 @@ function extractClientCredentials(req) {
};
}
/**
* Redirect to a URI, handling custom schemes for native apps.
*
* HTTP(S) redirect URIs use a standard 302 redirect (web clients).
* Custom scheme URIs (fedilab://, moshidon-android-auth://) use an
* HTML page with JavaScript + meta refresh. Android Chrome Custom Tabs
* block 302 redirects to non-HTTP schemes but allow client-side navigation.
*
* @param {object} res - Express response
* @param {string} originalUri - The registered redirect_uri (to detect scheme)
* @param {string} fullUrl - The complete redirect URL with query params
*/
function redirectToUri(res, originalUri, fullUrl) {
if (originalUri.startsWith("http://") || originalUri.startsWith("https://")) {
return res.redirect(fullUrl);
}
// Native app — HTML page with JS redirect + meta refresh fallback
res.type("html").send(`<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="refresh" content="0;url=${fullUrl}">
<title>Redirecting…</title>
</head>
<body>
<p>Redirecting to application…</p>
<script>window.location.href = ${JSON.stringify(fullUrl)};</script>
</body>
</html>`);
}
export default router;