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,7 +1,7 @@
|
||||
import passport from "@outlinewiki/koa-passport";
|
||||
import { Strategy as AzureStrategy } from "@outlinewiki/passport-azure-ad-oauth2";
|
||||
import jwt from "jsonwebtoken";
|
||||
import { Request } from "koa";
|
||||
import type { Context } from "koa";
|
||||
import Router from "koa-router";
|
||||
import { Profile } from "passport";
|
||||
import accountProvisioner, {
|
||||
@@ -11,7 +11,11 @@ import env from "@server/env";
|
||||
import { MicrosoftGraphError } from "@server/errors";
|
||||
import passportMiddleware from "@server/middlewares/passport";
|
||||
import { User } from "@server/models";
|
||||
import { StateStore, request } from "@server/utils/passport";
|
||||
import {
|
||||
StateStore,
|
||||
request,
|
||||
getTeamFromContext,
|
||||
} from "@server/utils/passport";
|
||||
|
||||
const router = new Router();
|
||||
const providerName = "azure";
|
||||
@@ -36,7 +40,7 @@ if (env.AZURE_CLIENT_ID && env.AZURE_CLIENT_SECRET) {
|
||||
scope: scopes,
|
||||
},
|
||||
async function (
|
||||
req: Request,
|
||||
ctx: Context,
|
||||
accessToken: string,
|
||||
refreshToken: string,
|
||||
params: { expires_in: number; id_token: string },
|
||||
@@ -88,12 +92,15 @@ if (env.AZURE_CLIENT_ID && env.AZURE_CLIENT_SECRET) {
|
||||
);
|
||||
}
|
||||
|
||||
const team = await getTeamFromContext(ctx);
|
||||
|
||||
const domain = email.split("@")[1];
|
||||
const subdomain = domain.split(".")[0];
|
||||
const teamName = organization.displayName;
|
||||
const result = await accountProvisioner({
|
||||
ip: req.ip,
|
||||
ip: ctx.ip,
|
||||
team: {
|
||||
id: team?.id,
|
||||
name: teamName,
|
||||
domain,
|
||||
subdomain,
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import passport from "@outlinewiki/koa-passport";
|
||||
import type { Request } from "express";
|
||||
import type { Context } from "koa";
|
||||
import Router from "koa-router";
|
||||
import { capitalize } from "lodash";
|
||||
import { Profile } from "passport";
|
||||
import { Strategy as GoogleStrategy } from "passport-google-oauth2";
|
||||
import { parseDomain } from "@shared/utils/domains";
|
||||
import { slugifyDomain } from "@shared/utils/domains";
|
||||
import accountProvisioner, {
|
||||
AccountProvisionerResult,
|
||||
} from "@server/commands/accountProvisioner";
|
||||
@@ -15,8 +15,8 @@ import {
|
||||
TeamDomainRequiredError,
|
||||
} from "@server/errors";
|
||||
import passportMiddleware from "@server/middlewares/passport";
|
||||
import { Team, User } from "@server/models";
|
||||
import { StateStore, parseState } from "@server/utils/passport";
|
||||
import { User } from "@server/models";
|
||||
import { StateStore, getTeamFromContext } from "@server/utils/passport";
|
||||
|
||||
const router = new Router();
|
||||
const providerName = "google";
|
||||
@@ -51,7 +51,7 @@ if (env.GOOGLE_CLIENT_ID && env.GOOGLE_CLIENT_SECRET) {
|
||||
scope: scopes,
|
||||
},
|
||||
async function (
|
||||
req: Request,
|
||||
ctx: Context,
|
||||
accessToken: string,
|
||||
refreshToken: string,
|
||||
params: { expires_in: number },
|
||||
@@ -63,27 +63,32 @@ if (env.GOOGLE_CLIENT_ID && env.GOOGLE_CLIENT_SECRET) {
|
||||
) => void
|
||||
) {
|
||||
try {
|
||||
const state = req.cookies.get("state");
|
||||
const host = state ? parseState(state).host : req.hostname;
|
||||
// appDomain is the domain the user originated from when attempting auth
|
||||
const appDomain = parseDomain(host);
|
||||
|
||||
let result;
|
||||
|
||||
// "domain" is the Google Workspaces domain
|
||||
const domain = profile._json.hd;
|
||||
const team = await getTeamFromContext(ctx);
|
||||
|
||||
// Existence of domain means this is a Google Workspaces account
|
||||
// so we'll attempt to provision an account (team and user)
|
||||
if (domain) {
|
||||
const subdomain = domain.split(".")[0];
|
||||
// remove the TLD and form a subdomain from the remaining
|
||||
// subdomains of the form "foo.bar.com" are allowed as primary Google Workspaces domains
|
||||
// see https://support.google.com/nonprofits/thread/19685140/using-a-subdomain-as-a-primary-domain
|
||||
const subdomain = slugifyDomain(domain);
|
||||
const teamName = capitalize(subdomain);
|
||||
|
||||
// Request a larger size profile picture than the default by tweaking
|
||||
// the query parameter.
|
||||
const avatarUrl = profile.picture.replace("=s96-c", "=s128-c");
|
||||
|
||||
// if a team can be inferred, we assume the user is only interested in signing into
|
||||
// that team in particular; otherwise, we will do a best effort at finding their account
|
||||
// or provisioning a new one (within AccountProvisioner)
|
||||
result = await accountProvisioner({
|
||||
ip: req.ip,
|
||||
ip: ctx.ip,
|
||||
team: {
|
||||
id: team?.id,
|
||||
name: teamName,
|
||||
domain,
|
||||
subdomain,
|
||||
@@ -107,19 +112,7 @@ if (env.GOOGLE_CLIENT_ID && env.GOOGLE_CLIENT_SECRET) {
|
||||
});
|
||||
} else {
|
||||
// No domain means it's a personal Gmail account
|
||||
// We only allow sign-in to existing user accounts
|
||||
|
||||
let team;
|
||||
if (appDomain.custom) {
|
||||
team = await Team.findOne({ where: { domain: appDomain.host } });
|
||||
} else if (env.SUBDOMAINS_ENABLED && appDomain.teamSubdomain) {
|
||||
team = await Team.findOne({
|
||||
where: { subdomain: appDomain.teamSubdomain },
|
||||
});
|
||||
} else if (env.DEPLOYMENT !== "hosted") {
|
||||
team = await Team.findOne();
|
||||
}
|
||||
|
||||
// We only allow sign-in to existing user accounts with these
|
||||
if (!team) {
|
||||
// No team usually means this is the apex domain
|
||||
// Throw different errors depending on whether we think the user is
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import passport from "@outlinewiki/koa-passport";
|
||||
import { Request } from "koa";
|
||||
import type { Context } from "koa";
|
||||
import Router from "koa-router";
|
||||
import { get } from "lodash";
|
||||
import { Strategy } from "passport-oauth2";
|
||||
import { slugifyDomain } from "@shared/utils/domains";
|
||||
import accountProvisioner, {
|
||||
AccountProvisionerResult,
|
||||
} from "@server/commands/accountProvisioner";
|
||||
@@ -13,7 +14,11 @@ import {
|
||||
} from "@server/errors";
|
||||
import passportMiddleware from "@server/middlewares/passport";
|
||||
import { User } from "@server/models";
|
||||
import { StateStore, request } from "@server/utils/passport";
|
||||
import {
|
||||
StateStore,
|
||||
request,
|
||||
getTeamFromContext,
|
||||
} from "@server/utils/passport";
|
||||
|
||||
const router = new Router();
|
||||
const providerName = "oidc";
|
||||
@@ -60,7 +65,7 @@ if (env.OIDC_CLIENT_ID && env.OIDC_CLIENT_SECRET) {
|
||||
// Any claim supplied in response to the userinfo request will be
|
||||
// available on the `profile` parameter
|
||||
async function (
|
||||
req: Request,
|
||||
ctx: Context,
|
||||
accessToken: string,
|
||||
refreshToken: string,
|
||||
params: { expires_in: number },
|
||||
@@ -77,6 +82,7 @@ if (env.OIDC_CLIENT_ID && env.OIDC_CLIENT_SECRET) {
|
||||
`An email field was not returned in the profile parameter, but is required.`
|
||||
);
|
||||
}
|
||||
const team = await getTeamFromContext(ctx);
|
||||
|
||||
const parts = profile.email.toLowerCase().split("@");
|
||||
const domain = parts.length && parts[1];
|
||||
@@ -85,10 +91,13 @@ if (env.OIDC_CLIENT_ID && env.OIDC_CLIENT_SECRET) {
|
||||
throw OIDCMalformedUserInfoError();
|
||||
}
|
||||
|
||||
const subdomain = domain.split(".")[0];
|
||||
// remove the TLD and form a subdomain from the remaining
|
||||
const subdomain = slugifyDomain(domain);
|
||||
|
||||
const result = await accountProvisioner({
|
||||
ip: req.ip,
|
||||
ip: ctx.ip,
|
||||
team: {
|
||||
id: team?.id,
|
||||
// https://github.com/outline/outline/pull/2388#discussion_r681120223
|
||||
name: "Wiki",
|
||||
domain,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import passport from "@outlinewiki/koa-passport";
|
||||
import { Request } from "koa";
|
||||
import type { Context } from "koa";
|
||||
import Router from "koa-router";
|
||||
import { Profile } from "passport";
|
||||
import { Strategy as SlackStrategy } from "passport-slack-oauth2";
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
Team,
|
||||
User,
|
||||
} from "@server/models";
|
||||
import { StateStore } from "@server/utils/passport";
|
||||
import { getTeamFromContext, StateStore } from "@server/utils/passport";
|
||||
import * as Slack from "@server/utils/slack";
|
||||
import { assertPresent, assertUuid } from "@server/validation";
|
||||
|
||||
@@ -63,7 +63,7 @@ if (env.SLACK_CLIENT_ID && env.SLACK_CLIENT_SECRET) {
|
||||
scope: scopes,
|
||||
},
|
||||
async function (
|
||||
req: Request,
|
||||
ctx: Context,
|
||||
accessToken: string,
|
||||
refreshToken: string,
|
||||
params: { expires_in: number },
|
||||
@@ -75,9 +75,11 @@ if (env.SLACK_CLIENT_ID && env.SLACK_CLIENT_SECRET) {
|
||||
) => void
|
||||
) {
|
||||
try {
|
||||
const team = await getTeamFromContext(ctx);
|
||||
const result = await accountProvisioner({
|
||||
ip: req.ip,
|
||||
ip: ctx.ip,
|
||||
team: {
|
||||
id: team?.id,
|
||||
name: profile.team.name,
|
||||
subdomain: profile.team.domain,
|
||||
avatarUrl: profile.team.image_230,
|
||||
|
||||
Reference in New Issue
Block a user