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
3 changes: 3 additions & 0 deletions src/betterAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,9 @@ export const getBetterAuthOptions = (pool: Pool): BetterAuthOptions => {
basePath: '/auth',
secret: process.env.BETTER_AUTH_SECRET ?? '',
trustedOrigins,
onAPIError: {
errorURL: `${process.env.COMMENTS_PREFIX}/callback`,
},
secondaryStorage: {
get: (key) => singleRedisClient.get(`ba:${key}`),
set: (key, value, ttl) =>
Expand Down
63 changes: 63 additions & 0 deletions src/routes/betterAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,57 @@ export const logoutBetterAuth = async (
}
};

const isOAuthCallbackPath = (url: string): boolean =>
/\/auth\/callback\//.test(url);

const rewriteOAuthErrorRedirect = (
request: FastifyRequest,
response: Response,
): string | undefined => {
if (!isOAuthCallbackPath(request.url)) {
return undefined;
}

if (response.status < 300 || response.status >= 400) {
return undefined;
}

const location = response.headers.get('location');
if (!location) {
return undefined;
}

const webappCallback = `${process.env.COMMENTS_PREFIX}/callback`;
if (!webappCallback) {
return undefined;
}

if (location.startsWith(webappCallback)) {
return undefined;
}

let redirectUrl: URL;
try {
redirectUrl = new URL(location, `${request.protocol}://${request.host}`);
} catch {
return undefined;
}

const hasError =
redirectUrl.searchParams.has('error') ||
redirectUrl.searchParams.get('state') === 'state_not_found';
if (!hasError) {
return undefined;
}

const callbackUrl = new URL(webappCallback);
redirectUrl.searchParams.forEach((value, key) => {
callbackUrl.searchParams.set(key, value);
});

return callbackUrl.toString();
};

const betterAuthRoute = async (fastify: FastifyInstance): Promise<void> => {
// Apple sends OAuth callbacks as application/x-www-form-urlencoded POSTs.
// Fastify does not parse this content type by default, so collect the raw
Expand All @@ -142,6 +193,18 @@ const betterAuthRoute = async (fastify: FastifyInstance): Promise<void> => {
reply,
body,
});

const rewrittenUrl = rewriteOAuthErrorRedirect(request, response);
if (rewrittenUrl) {
request.log.warn(
{ originalLocation: response.headers.get('location') },
'OAuth callback error redirect rewritten to webapp',
);
reply.header('location', rewrittenUrl);
reply.status(302);
return reply.send();
}

return sendBetterAuthResponse(reply, response);
} catch (error) {
return sendBetterAuthError(
Expand Down
26 changes: 23 additions & 3 deletions src/routes/redirects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,9 +171,29 @@ const redirectToProfileImage = async (
export default async function (fastify: FastifyInstance): Promise<void> {
const con = await createOrGetConnection();

fastify.get('/', (req, res) =>
res.redirect('https://r.daily.dev/api-redirect'),
);
fastify.get('/', (req, res) => {
// Some OAuth providers (e.g. GitHub) redirect cancellations to the
// API root instead of the BetterAuth callback URL. Forward the error
// params to the webapp callback so the popup can close cleanly.
const { error, state } = req.query as Record<string, string>;
if (error && state) {
const webappOrigin = process.env.COMMENTS_PREFIX;
if (webappOrigin) {
req.log.warn(
{ error, state },
'OAuth error redirected to API root, forwarding to webapp callback',
);
const callbackUrl = new URL(`${webappOrigin}/callback`);
const params = new URLSearchParams(req.url.split('?')[1] ?? '');
params.forEach((value, key) => {
callbackUrl.searchParams.set(key, value);
});
return res.redirect(callbackUrl.toString());
}
}

return res.redirect('https://r.daily.dev/api-redirect');
});
fastify.get('/landing', (req, res) => res.redirect('https://daily.dev'));
fastify.get('/tos', (req, res) => res.redirect('https://daily.dev/tos'));
fastify.get('/privacy', (req, res) =>
Expand Down
Loading