feat: scope login attempts to specific subdomains if available - do not switch subdomains (#3741)
* make the user lookup in user creator sensitive to team * add team specific logic to oidc strat * factor out slugifyDomain * change type of req during auth to Koa.Context
This commit is contained in:
@@ -1,40 +1,42 @@
|
||||
import crypto from "crypto";
|
||||
import { addMinutes, subMinutes } from "date-fns";
|
||||
import type { Request } from "express";
|
||||
import fetch from "fetch-with-proxy";
|
||||
import type { Context } from "koa";
|
||||
import {
|
||||
StateStoreStoreCallback,
|
||||
StateStoreVerifyCallback,
|
||||
} from "passport-oauth2";
|
||||
import { getCookieDomain, parseDomain } from "@shared/utils/domains";
|
||||
import env from "@server/env";
|
||||
import { Team } from "@server/models";
|
||||
import { AuthRedirectError, OAuthStateMismatchError } from "../errors";
|
||||
|
||||
export class StateStore {
|
||||
key = "state";
|
||||
|
||||
store = (req: Request, callback: StateStoreStoreCallback) => {
|
||||
store = (ctx: Context, callback: StateStoreStoreCallback) => {
|
||||
// token is a short lived one-time pad to prevent replay attacks
|
||||
// appDomain is the domain the user originated from when attempting auth
|
||||
// we expect it to be a team subdomain, custom domain, or apex domain
|
||||
const token = crypto.randomBytes(8).toString("hex");
|
||||
const appDomain = parseDomain(req.hostname);
|
||||
const appDomain = parseDomain(ctx.hostname);
|
||||
const state = buildState(appDomain.host, token);
|
||||
|
||||
req.cookies.set(this.key, state, {
|
||||
ctx.cookies.set(this.key, state, {
|
||||
httpOnly: false,
|
||||
expires: addMinutes(new Date(), 10),
|
||||
domain: getCookieDomain(req.hostname),
|
||||
domain: getCookieDomain(ctx.hostname),
|
||||
});
|
||||
|
||||
callback(null, token);
|
||||
};
|
||||
|
||||
verify = (
|
||||
req: Request,
|
||||
ctx: Context,
|
||||
providedToken: string,
|
||||
callback: StateStoreVerifyCallback
|
||||
) => {
|
||||
const state = req.cookies.get(this.key);
|
||||
const state = ctx.cookies.get(this.key);
|
||||
|
||||
if (!state) {
|
||||
return callback(
|
||||
@@ -51,10 +53,10 @@ export class StateStore {
|
||||
// If there is an error during auth, the user will end up on the same domain
|
||||
// that they started from.
|
||||
const appDomain = parseDomain(host);
|
||||
if (appDomain.host !== parseDomain(req.hostname).host) {
|
||||
const reqProtocol = req.protocol;
|
||||
const requestHost = req.get("host");
|
||||
const requestPath = req.originalUrl;
|
||||
if (appDomain.host !== parseDomain(ctx.hostname).host) {
|
||||
const reqProtocol = ctx.protocol;
|
||||
const requestHost = ctx.get("host");
|
||||
const requestPath = ctx.originalUrl;
|
||||
const requestUrl = `${reqProtocol}://${requestHost}${requestPath}`;
|
||||
const url = new URL(requestUrl);
|
||||
|
||||
@@ -64,10 +66,10 @@ export class StateStore {
|
||||
}
|
||||
|
||||
// Destroy the one-time pad token and ensure it matches
|
||||
req.cookies.set(this.key, "", {
|
||||
ctx.cookies.set(this.key, "", {
|
||||
httpOnly: false,
|
||||
expires: subMinutes(new Date(), 1),
|
||||
domain: getCookieDomain(req.hostname),
|
||||
domain: getCookieDomain(ctx.hostname),
|
||||
});
|
||||
|
||||
if (!token || token !== providedToken) {
|
||||
@@ -98,3 +100,24 @@ export function parseState(state: string) {
|
||||
const [host, token] = state.split("|");
|
||||
return { host, token };
|
||||
}
|
||||
|
||||
export async function getTeamFromContext(ctx: Context) {
|
||||
// "domain" is the domain the user came from when attempting auth
|
||||
// we use it to infer the team they intend on signing into
|
||||
const state = ctx.cookies.get("state");
|
||||
const host = state ? parseState(state).host : ctx.hostname;
|
||||
const domain = parseDomain(host);
|
||||
|
||||
let team;
|
||||
if (env.DEPLOYMENT !== "hosted") {
|
||||
team = await Team.findOne();
|
||||
} else if (domain.custom) {
|
||||
team = await Team.findOne({ where: { domain: domain.host } });
|
||||
} else if (env.SUBDOMAINS_ENABLED && domain.teamSubdomain) {
|
||||
team = await Team.findOne({
|
||||
where: { subdomain: domain.teamSubdomain },
|
||||
});
|
||||
}
|
||||
|
||||
return team;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user