mirror of
https://github.com/roleypoly/roleypoly.git
synced 2025-04-25 03:49:11 +00:00
chore: temporarily loosen CORS, add OAuth state info for backend bouncing
This commit is contained in:
parent
ed82a67594
commit
6edfe7455f
8 changed files with 72 additions and 9 deletions
1
packages/api/bindings.d.ts
vendored
1
packages/api/bindings.d.ts
vendored
|
@ -6,6 +6,7 @@ declare global {
|
||||||
const UI_PUBLIC_URI: string;
|
const UI_PUBLIC_URI: string;
|
||||||
const API_PUBLIC_URI: string;
|
const API_PUBLIC_URI: string;
|
||||||
const ROOT_USERS: string;
|
const ROOT_USERS: string;
|
||||||
|
const ALLOWED_CALLBACK_HOSTS: string;
|
||||||
|
|
||||||
const KV_SESSIONS: KVNamespace;
|
const KV_SESSIONS: KVNamespace;
|
||||||
const KV_GUILDS: KVNamespace;
|
const KV_GUILDS: KVNamespace;
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import KSUID from 'ksuid';
|
import { StateSession } from '@roleypoly/types';
|
||||||
|
import { getQuery, isAllowedCallbackHost, setupStateSession } from '../utils/api-tools';
|
||||||
import { Bounce } from '../utils/bounce';
|
import { Bounce } from '../utils/bounce';
|
||||||
import { apiPublicURI, botClientID } from '../utils/config';
|
import { apiPublicURI, botClientID } from '../utils/config';
|
||||||
|
|
||||||
|
@ -16,9 +17,17 @@ const buildURL = (params: URLParams) =>
|
||||||
)}&state=${params.state}`;
|
)}&state=${params.state}`;
|
||||||
|
|
||||||
export const LoginBounce = async (request: Request): Promise<Response> => {
|
export const LoginBounce = async (request: Request): Promise<Response> => {
|
||||||
const state = await KSUID.random();
|
const stateSessionData: StateSession = {};
|
||||||
|
|
||||||
|
const { cbh: callbackHost } = getQuery(request);
|
||||||
|
if (callbackHost && isAllowedCallbackHost(callbackHost)) {
|
||||||
|
stateSessionData.callbackHost = callbackHost;
|
||||||
|
}
|
||||||
|
|
||||||
|
const state = await setupStateSession(stateSessionData);
|
||||||
|
|
||||||
const redirectURI = `${apiPublicURI}/login-callback`;
|
const redirectURI = `${apiPublicURI}/login-callback`;
|
||||||
const clientID = botClientID;
|
const clientID = botClientID;
|
||||||
|
|
||||||
return Bounce(buildURL({ state: state.string, redirectURI, clientID }));
|
return Bounce(buildURL({ state, redirectURI, clientID }));
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,9 +1,17 @@
|
||||||
import { AuthTokenResponse, DiscordUser, GuildSlug, SessionData } from '@roleypoly/types';
|
import {
|
||||||
|
AuthTokenResponse,
|
||||||
|
DiscordUser,
|
||||||
|
GuildSlug,
|
||||||
|
SessionData,
|
||||||
|
StateSession,
|
||||||
|
} from '@roleypoly/types';
|
||||||
import KSUID from 'ksuid';
|
import KSUID from 'ksuid';
|
||||||
import {
|
import {
|
||||||
AuthType,
|
AuthType,
|
||||||
discordFetch,
|
discordFetch,
|
||||||
formData,
|
formData,
|
||||||
|
getStateSession,
|
||||||
|
isAllowedCallbackHost,
|
||||||
parsePermissions,
|
parsePermissions,
|
||||||
resolveFailures,
|
resolveFailures,
|
||||||
userAgent,
|
userAgent,
|
||||||
|
@ -21,8 +29,9 @@ const AuthErrorResponse = (extra?: string) =>
|
||||||
export const LoginCallback = resolveFailures(
|
export const LoginCallback = resolveFailures(
|
||||||
AuthErrorResponse,
|
AuthErrorResponse,
|
||||||
async (request: Request): Promise<Response> => {
|
async (request: Request): Promise<Response> => {
|
||||||
const query = new URL(request.url).searchParams;
|
let bounceBaseUrl = uiPublicURI;
|
||||||
|
|
||||||
|
const query = new URL(request.url).searchParams;
|
||||||
const stateValue = query.get('state');
|
const stateValue = query.get('state');
|
||||||
|
|
||||||
if (stateValue === null) {
|
if (stateValue === null) {
|
||||||
|
@ -37,6 +46,14 @@ export const LoginCallback = resolveFailures(
|
||||||
if (currentTime > stateExpiry) {
|
if (currentTime > stateExpiry) {
|
||||||
return AuthErrorResponse('state expired');
|
return AuthErrorResponse('state expired');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const stateSession = await getStateSession<StateSession>(state.string);
|
||||||
|
if (
|
||||||
|
stateSession?.callbackHost &&
|
||||||
|
isAllowedCallbackHost(stateSession.callbackHost)
|
||||||
|
) {
|
||||||
|
bounceBaseUrl = stateSession.callbackHost;
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return AuthErrorResponse('state invalid');
|
return AuthErrorResponse('state invalid');
|
||||||
}
|
}
|
||||||
|
@ -90,7 +107,7 @@ export const LoginCallback = resolveFailures(
|
||||||
await Sessions.put(sessionID.string, sessionData, 60 * 60 * 6);
|
await Sessions.put(sessionID.string, sessionData, 60 * 60 * 6);
|
||||||
|
|
||||||
return Bounce(
|
return Bounce(
|
||||||
uiPublicURI + '/machinery/new-session?session_id=' + sessionID.string
|
bounceBaseUrl + '/machinery/new-session?session_id=' + sessionID.string
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
|
@ -3,8 +3,9 @@ import {
|
||||||
permissions as Permissions,
|
permissions as Permissions,
|
||||||
} from '@roleypoly/misc-utils/hasPermission';
|
} from '@roleypoly/misc-utils/hasPermission';
|
||||||
import { SessionData, UserGuildPermissions } from '@roleypoly/types';
|
import { SessionData, UserGuildPermissions } from '@roleypoly/types';
|
||||||
|
import KSUID from 'ksuid';
|
||||||
import { Handler } from '../router';
|
import { Handler } from '../router';
|
||||||
import { rootUsers, uiPublicURI } from './config';
|
import { allowedCallbackHosts, apiPublicURI, rootUsers } from './config';
|
||||||
import { Sessions, WrappedKVNamespace } from './kv';
|
import { Sessions, WrappedKVNamespace } from './kv';
|
||||||
|
|
||||||
export const formData = (obj: Record<string, any>): string => {
|
export const formData = (obj: Record<string, any>): string => {
|
||||||
|
@ -17,7 +18,7 @@ export const addCORS = (init: ResponseInit = {}) => ({
|
||||||
...init,
|
...init,
|
||||||
headers: {
|
headers: {
|
||||||
...(init.headers || {}),
|
...(init.headers || {}),
|
||||||
'access-control-allow-origin': uiPublicURI,
|
'access-control-allow-origin': '*',
|
||||||
'access-control-allow-methods': '*',
|
'access-control-allow-methods': '*',
|
||||||
'access-control-allow-headers': '*',
|
'access-control-allow-headers': '*',
|
||||||
},
|
},
|
||||||
|
@ -159,6 +160,20 @@ export const withSession = (
|
||||||
return await wrappedHandler(session)(request);
|
return await wrappedHandler(session)(request);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const setupStateSession = async <T>(data: T): Promise<string> => {
|
||||||
|
const stateID = (await KSUID.random()).string;
|
||||||
|
|
||||||
|
await Sessions.put(`state_${stateID}`, { data }, 60 * 5);
|
||||||
|
|
||||||
|
return stateID;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getStateSession = async <T>(stateID: string): Promise<T | undefined> => {
|
||||||
|
const stateSession = await Sessions.get<{ data: T }>(`state_${stateID}`);
|
||||||
|
|
||||||
|
return stateSession?.data;
|
||||||
|
};
|
||||||
|
|
||||||
export const isRoot = (userID: string): boolean => rootUsers.includes(userID);
|
export const isRoot = (userID: string): boolean => rootUsers.includes(userID);
|
||||||
|
|
||||||
export const onlyRootUsers = (handler: Handler): Handler =>
|
export const onlyRootUsers = (handler: Handler): Handler =>
|
||||||
|
@ -176,3 +191,17 @@ export const onlyRootUsers = (handler: Handler): Handler =>
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const getQuery = (request: Request): { [x: string]: string } => {
|
||||||
|
const output: { [x: string]: string } = {};
|
||||||
|
|
||||||
|
for (let [key, value] of new URL(request.url).searchParams.entries()) {
|
||||||
|
output[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isAllowedCallbackHost = (host: string): boolean => {
|
||||||
|
return host === apiPublicURI || allowedCallbackHosts.includes(host);
|
||||||
|
};
|
||||||
|
|
|
@ -11,3 +11,4 @@ export const botToken = env('BOT_TOKEN');
|
||||||
export const uiPublicURI = safeURI(env('UI_PUBLIC_URI'));
|
export const uiPublicURI = safeURI(env('UI_PUBLIC_URI'));
|
||||||
export const apiPublicURI = safeURI(env('API_PUBLIC_URI'));
|
export const apiPublicURI = safeURI(env('API_PUBLIC_URI'));
|
||||||
export const rootUsers = list(env('ROOT_USERS'));
|
export const rootUsers = list(env('ROOT_USERS'));
|
||||||
|
export const allowedCallbackHosts = list(env('ALLOWED_CALLBACK_HOSTS'));
|
||||||
|
|
|
@ -10,6 +10,7 @@ module.exports = {
|
||||||
'UI_PUBLIC_URI',
|
'UI_PUBLIC_URI',
|
||||||
'API_PUBLIC_URI',
|
'API_PUBLIC_URI',
|
||||||
'ROOT_USERS',
|
'ROOT_USERS',
|
||||||
|
'ALLOWED_CALLBACK_HOSTS',
|
||||||
]),
|
]),
|
||||||
kv: ['KV_SESSIONS', 'KV_GUILDS', 'KV_GUILD_DATA'],
|
kv: ['KV_SESSIONS', 'KV_GUILDS', 'KV_GUILD_DATA'],
|
||||||
};
|
};
|
||||||
|
|
|
@ -16,3 +16,7 @@ export type SessionData = {
|
||||||
user: DiscordUser;
|
user: DiscordUser;
|
||||||
guilds: GuildSlug[];
|
guilds: GuildSlug[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type StateSession = {
|
||||||
|
callbackHost?: string;
|
||||||
|
};
|
||||||
|
|
|
@ -11,9 +11,10 @@ const Login = () => {
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const url = new URL(window.location.href);
|
const url = new URL(window.location.href);
|
||||||
|
const callbackHost = `${url.protocol}://${url.host}`;
|
||||||
const redirectServerID = url.searchParams.get('r');
|
const redirectServerID = url.searchParams.get('r');
|
||||||
if (!redirectServerID) {
|
if (!redirectServerID) {
|
||||||
window.location.href = `${apiUrl}/login-bounce`;
|
window.location.href = `${apiUrl}/login-bounce?cbh=${callbackHost}`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue