Disable OAuth in iframe (#701)

<!--

Make sure you've read the CONTRIBUTING.md guidelines:
https://github.com/stack-auth/stack-auth/blob/dev/CONTRIBUTING.md

-->

<!-- ELLIPSIS_HIDDEN -->

----

> [!IMPORTANT]
> Disable OAuth in iframes using `useInIframe` hook and update
`SimpleTooltip` to support conditional disabling.
> 
>   - **Behavior**:
> - Disable OAuth buttons in iframes by using `useInIframe` hook in
`oauth-button.tsx`.
> - Show tooltip "OAuth is disabled in iFrame" when OAuth buttons are
disabled.
>   - **Components**:
> - Add `useInIframe` hook in `use-in-iframe.tsx` to detect iframe
context.
> - Remove `IframePreventer` component and its usage in
`stack-handler.tsx`.
> - Update `SimpleTooltip` in `simple-tooltip.tsx` to accept `disabled`
prop to conditionally disable tooltips.
>   - **Misc**:
>     - Remove unused `useTranslation` import in `auth-page.tsx`.
> 
> <sup>This description was created by </sup>[<img alt="Ellipsis"
src="https://img.shields.io/badge/Ellipsis-blue?color=175173">](https://www.ellipsis.dev?ref=stack-auth%2Fstack-auth&utm_source=github&utm_medium=referral)<sup>
for 35793e8055. You can
[customize](https://app.ellipsis.dev/stack-auth/settings/summaries) this
summary. It will automatically update as commits are pushed.</sup>

<!-- ELLIPSIS_HIDDEN -->

---------

Co-authored-by: Konsti Wohlwend <n2d4xc@gmail.com>
This commit is contained in:
Zai Shi 2025-06-06 04:36:46 +02:00 committed by GitHub
parent 2d42f44483
commit e6bf381e2f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 34 additions and 39 deletions

View File

@ -7,6 +7,7 @@ export function SimpleTooltip(props: {
type?: 'info' | 'warning',
inline?: boolean,
className?: string,
disabled?: boolean,
}) {
const iconClassName = cn("w-4 h-4 text-zinc-500", props.inline && "inline");
const icon = props.type === 'warning' ?
@ -21,7 +22,7 @@ export function SimpleTooltip(props: {
return (
<TooltipProvider>
<Tooltip>
<Tooltip open={props.disabled ? false : undefined}>
<TooltipTrigger asChild>
{props.inline ? (
<span className={cn(props.className)}>

View File

@ -42,8 +42,6 @@ export function AuthPage(props: Props) {
}
function Fallback(props: Props) {
const { t } = useTranslation();
return (
<MaybeFullPage fullPage={!!props.fullPage}>
<div className='stack-scope flex flex-col items-stretch' style={{ maxWidth: '380px', flexBasis: '380px', padding: props.fullPage ? '1rem' : 0 }}>

View File

@ -4,7 +4,6 @@ import { getRelativePart } from "@stackframe/stack-shared/dist/utils/urls";
import { RedirectType, notFound, redirect } from 'next/navigation'; // THIS_LINE_PLATFORM next
import { useMemo } from 'react';
import { SignIn, SignUp, StackServerApp } from "..";
import { IframePreventer } from "../components/iframe-preventer";
import { MessageCard } from "../components/message-cards/message-card";
import { HandlerUrls, StackClientApp } from "../lib/stack-app";
import { AccountSettings } from "./account-settings";
@ -249,9 +248,7 @@ async function NextStackHandler<HasTokenStore extends boolean>(props: BaseHandle
{next15DeprecationWarning}. This warning will not be shown in production.
</span>
)}
<IframePreventer>
{result}
</IframePreventer>
{result}
</>;
}
@ -317,11 +314,7 @@ function ReactStackHandler<HasTokenStore extends boolean>(props: BaseHandlerProp
return null;
}
return (
<IframePreventer>
{result}
</IframePreventer>
);
return result;
}
// END_PLATFORM

View File

@ -1,10 +1,11 @@
'use client';
import { BrandIcons, Button } from '@stackframe/stack-ui';
import { BrandIcons, Button, SimpleTooltip } from '@stackframe/stack-ui';
import Color from 'color';
import { useEffect, useId, useState } from 'react';
import { useStackApp } from '..';
import { useTranslation } from '../lib/translations';
import { useInIframe } from './use-in-iframe';
const iconSize = 22;
@ -27,6 +28,7 @@ export function OAuthButton({
const { t } = useTranslation();
const stackApp = useStackApp();
const styleId = useId().replaceAll(':', '-');
const isIframe = useInIframe();
const [lastUsed, setLastUsed] = useState<string | null>(null);
useEffect(() => {
@ -167,28 +169,35 @@ export function OAuthButton({
return (
<>
<style>{styleSheet}</style>
<Button
onClick={async () => {
localStorage.setItem('_STACK_AUTH.lastUsed', provider);
await stackApp.signInWithOAuth(provider);
}}
className={`stack-oauth-button-${styleId} stack-scope relative`}
<SimpleTooltip
disabled={!isIframe}
tooltip={isIframe ? "This auth provider is not supported in an iframe for security reasons." : undefined}
className='stack-scope w-full inline-flex'
>
{!isMock && lastUsed === provider && (
<span className="absolute -top-2 -right-2 bg-blue-500 text-white text-xs px-2 py-1 rounded-md">
<Button
onClick={async () => {
localStorage.setItem('_STACK_AUTH.lastUsed', provider);
await stackApp.signInWithOAuth(provider);
}}
className={`stack-oauth-button-${styleId} stack-scope relative w-full`}
disabled={isIframe}
>
{!isMock && lastUsed === provider && (
<span className="absolute -top-2 -right-2 bg-blue-500 text-white text-xs px-2 py-1 rounded-md">
last
</span>
)}
<div className='flex items-center w-full gap-4'>
{style.icon}
<span className='flex-1'>
{type === 'sign-up' ?
</span>
)}
<div className='flex items-center w-full gap-4'>
{style.icon}
<span className='flex-1'>
{type === 'sign-up' ?
t('Sign up with {provider}', { provider: style.name }) :
t('Sign in with {provider}', { provider: style.name })
}
</span>
</div>
</Button>
}
</span>
</div>
</Button>
</SimpleTooltip>
</>
);
}

View File

@ -1,9 +1,7 @@
'use client';
import { useEffect, useState } from "react";
export function IframePreventer({ children }: {
children: React.ReactNode,
}) {
export function useInIframe() {
const [isIframe, setIsIframe] = useState(false);
useEffect(() => {
if (window.self !== window.top) {
@ -11,9 +9,5 @@ export function IframePreventer({ children }: {
}
}, []);
if (isIframe) {
return <div>Stack Auth components may not run in an {'<'}iframe{'>'}.</div>;
}
return children;
return isIframe;
}