update login flow to prevent session leakage

This commit is contained in:
41666 2022-01-31 23:32:41 -05:00
parent be826b613e
commit 1cb04c8b5a
4 changed files with 41 additions and 30 deletions

View file

@ -9,10 +9,7 @@ import { seeOther } from '@roleypoly/api/src/utils/response';
import { AuthTokenResponse, StateSession } from '@roleypoly/types'; import { AuthTokenResponse, StateSession } from '@roleypoly/types';
const authFailure = (uiPublicURI: string, extra?: string) => const authFailure = (uiPublicURI: string, extra?: string) =>
seeOther( seeOther(uiPublicURI + `/error/authFailure${extra ? `?extra=${extra}` : ''}`);
uiPublicURI +
`/machinery/error?error_code=authFailure${extra ? `&extra=${extra}` : ''}`
);
export const authCallback: RoleypolyHandler = async ( export const authCallback: RoleypolyHandler = async (
request: Request, request: Request,
@ -72,5 +69,5 @@ export const authCallback: RoleypolyHandler = async (
return authFailure(config.uiPublicURI, 'session setup failure'); return authFailure(config.uiPublicURI, 'session setup failure');
} }
return seeOther(bounceBaseUrl + 'machinery/new-session/' + session.sessionID); return seeOther(bounceBaseUrl + 'machinery/new-session/#/' + session.sessionID);
}; };

View file

@ -44,10 +44,7 @@ export const AppRouter = () => {
<RouteWrapper component={ErrorPage} path="/error" /> <RouteWrapper component={ErrorPage} path="/error" />
<RouteWrapper component={ErrorPage} path="/error/:identity" /> <RouteWrapper component={ErrorPage} path="/error/:identity" />
<RouteWrapper <RouteWrapper component={MachineryNewSession} path="/machinery/new-session" />
component={MachineryNewSession}
path="/machinery/new-session/:sessionID"
/>
<RouteWrapper component={MachineryLogout} path="/machinery/logout" /> <RouteWrapper component={MachineryLogout} path="/machinery/logout" />
<RouteWrapper component={MachineryBotJoin} path="/machinery/bot-join" /> <RouteWrapper component={MachineryBotJoin} path="/machinery/bot-join" />
<RouteWrapper component={MachineryBotJoin} path="/machinery/bot-join/:serverID" /> <RouteWrapper component={MachineryBotJoin} path="/machinery/bot-join/:serverID" />

View file

@ -1,4 +1,6 @@
import { redirectTo } from '@reach/router'; import { useLocation, useNavigate } from '@reach/router';
import { palette } from '@roleypoly/design-system/atoms/colors';
import { Link } from '@roleypoly/design-system/atoms/typography';
import { AuthLogin } from '@roleypoly/design-system/templates/auth-login'; import { AuthLogin } from '@roleypoly/design-system/templates/auth-login';
import { GenericLoadingTemplate } from '@roleypoly/design-system/templates/generic-loading'; import { GenericLoadingTemplate } from '@roleypoly/design-system/templates/generic-loading';
import { GuildSlug } from '@roleypoly/types'; import { GuildSlug } from '@roleypoly/types';
@ -12,21 +14,23 @@ const Login = (props: { path: string }) => {
const { apiUrl } = useApiContext(); const { apiUrl } = useApiContext();
const { isAuthenticated } = useSessionContext(); const { isAuthenticated } = useSessionContext();
const { getGuildSlug } = useGuildContext(); const { getGuildSlug } = useGuildContext();
const navigate = useNavigate();
const location = useLocation();
// If ?r is in query, then let's render the slug page // If ?r is in query, then let's render the slug page
// If not, redirect. // If not, redirect.
const [guildSlug, setGuildSlug] = React.useState<GuildSlug | null>(null); const [guildSlug, setGuildSlug] = React.useState<GuildSlug | null>(null);
const [oauthLink, setOauthLink] = React.useState(`${apiUrl}/auth/bounce`); const [oauthLink, setOauthLink] = React.useState(`${apiUrl}/auth/bounce`);
React.useEffect(() => { React.useEffect(() => {
const url = new URL(window.location.href); const url = new URL(location.href);
const callbackHost = new URL('/', url); const callbackHost = new URL('/', url);
const redirectServerID = url.searchParams.get('r'); const redirectServerID = url.searchParams.get('r');
const redirectUrl = `${apiUrl}/auth/bounce?cbh=${callbackHost.href}`; const redirectUrl = `${apiUrl}/auth/bounce?cbh=${callbackHost.href}`;
if (!redirectServerID) { if (!redirectServerID) {
if (isAuthenticated) { if (isAuthenticated) {
redirectTo('/servers'); navigate('/servers');
} }
window.location.href = redirectUrl; navigate(redirectUrl);
return; return;
} }
@ -43,12 +47,21 @@ const Login = (props: { path: string }) => {
fetchGuildSlug(redirectServerID); fetchGuildSlug(redirectServerID);
if (isAuthenticated) { if (isAuthenticated) {
redirectTo(`/s/${redirectServerID}`); navigate(`/s/${redirectServerID}`);
} }
}, [apiUrl, getGuildSlug, isAuthenticated]); }, [apiUrl, getGuildSlug, isAuthenticated, location, navigate]);
if (guildSlug === null) { if (guildSlug === null) {
return <GenericLoadingTemplate>Sending you to Discord...</GenericLoadingTemplate>; return (
<GenericLoadingTemplate>
<div style={{ textAlign: 'center' }}>
<div>Sending you to Discord...</div>
<Link style={{ color: palette.taupe400 }} href={oauthLink}>
If you aren't redirected soon, click here.
</Link>
</div>
</GenericLoadingTemplate>
);
} }
return ( return (

View file

@ -1,3 +1,4 @@
import { useLocation, useNavigate } from '@reach/router';
import { palette } from '@roleypoly/design-system/atoms/colors'; import { palette } from '@roleypoly/design-system/atoms/colors';
import { Link } from '@roleypoly/design-system/atoms/typography'; import { Link } from '@roleypoly/design-system/atoms/typography';
import { GenericLoadingTemplate } from '@roleypoly/design-system/templates/generic-loading'; import { GenericLoadingTemplate } from '@roleypoly/design-system/templates/generic-loading';
@ -6,8 +7,10 @@ import { useSessionContext } from '../../contexts/session/SessionContext';
import { Title } from '../../utils/metaTitle'; import { Title } from '../../utils/metaTitle';
const NewSession = (props: { sessionID: string }) => { const NewSession = (props: { sessionID: string }) => {
const { setupSession, isAuthenticated } = useSessionContext(); const { setupSession, sessionID } = useSessionContext();
const [postauthUrl, setPostauthUrl] = React.useState('/servers'); const [postauthUrl, setPostauthUrl] = React.useState('/servers');
const navigate = useNavigate();
const location = useLocation();
React.useEffect(() => { React.useEffect(() => {
const storedPostauthUrl = localStorage.getItem('rp_postauth_redirect'); const storedPostauthUrl = localStorage.getItem('rp_postauth_redirect');
@ -18,20 +21,21 @@ const NewSession = (props: { sessionID: string }) => {
}, [setPostauthUrl]); }, [setPostauthUrl]);
React.useEffect(() => { React.useEffect(() => {
if (isAuthenticated) { if (!sessionID) {
setTimeout(() => { const sessionToken = location.hash.substring(2);
window.history.replaceState(null, '', '/'); if (!sessionToken) {
window.location.href = postauthUrl; console.error({ sessionToken });
}, 0); navigate('/error/400?extra=missing-hash');
} return;
}, [postauthUrl, isAuthenticated]); }
React.useCallback( setupSession(sessionToken);
(sessionID) => { }
setupSession(sessionID);
}, if (sessionID) {
[setupSession] navigate(postauthUrl);
)(props.sessionID); }
}, [sessionID, location, postauthUrl, setupSession, navigate]);
return ( return (
<GenericLoadingTemplate> <GenericLoadingTemplate>