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:
Nan Yu
2022-07-19 06:50:55 -07:00
committed by GitHub
parent 4ee3929e9d
commit c3f5563e7f
12 changed files with 148 additions and 64 deletions

View File

@@ -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;
}