fix: Improve handling of suspended users signing in with email (#2012)
* chore: Separate signin/auth middleware fix: Email signin token parsed by JWT middleware fix: Email signin marked as active when logging in as suspended fix: Suspended email signin correctly redirected to login screen closes #1740 * refactor middleware -> lib * lint
This commit is contained in:
parent
1a889e9913
commit
190f0b6dc5
@ -4,10 +4,10 @@ import Router from "koa-router";
|
|||||||
import { find } from "lodash";
|
import { find } from "lodash";
|
||||||
import { AuthorizationError } from "../../errors";
|
import { AuthorizationError } from "../../errors";
|
||||||
import mailer from "../../mailer";
|
import mailer from "../../mailer";
|
||||||
import auth from "../../middlewares/authentication";
|
|
||||||
import methodOverride from "../../middlewares/methodOverride";
|
import methodOverride from "../../middlewares/methodOverride";
|
||||||
import validation from "../../middlewares/validation";
|
import validation from "../../middlewares/validation";
|
||||||
import { User, Team } from "../../models";
|
import { User, Team } from "../../models";
|
||||||
|
import { signIn } from "../../utils/authentication";
|
||||||
import { getUserForEmailSigninToken } from "../../utils/jwt";
|
import { getUserForEmailSigninToken } from "../../utils/jwt";
|
||||||
|
|
||||||
const router = new Router();
|
const router = new Router();
|
||||||
@ -84,25 +84,26 @@ router.post("email", async (ctx) => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
router.get("email.callback", auth({ required: false }), async (ctx) => {
|
router.get("email.callback", async (ctx) => {
|
||||||
const { token } = ctx.request.query;
|
const { token } = ctx.request.query;
|
||||||
|
|
||||||
ctx.assertPresent(token, "token is required");
|
ctx.assertPresent(token, "token is required");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const user = await getUserForEmailSigninToken(token);
|
const user = await getUserForEmailSigninToken(token);
|
||||||
|
if (!user.team.guestSignin) {
|
||||||
const team = await Team.findByPk(user.teamId);
|
return ctx.redirect("/?notice=auth-error");
|
||||||
if (!team.guestSignin) {
|
}
|
||||||
throw new AuthorizationError();
|
if (user.isSuspended) {
|
||||||
|
return ctx.redirect("/?notice=suspended");
|
||||||
}
|
}
|
||||||
|
|
||||||
await user.update({ lastActiveAt: new Date() });
|
await user.update({ lastActiveAt: new Date() });
|
||||||
|
|
||||||
// set cookies on response and redirect to team subdomain
|
// set cookies on response and redirect to team subdomain
|
||||||
ctx.signIn(user, team, "email", false);
|
signIn(ctx, user, user.team, "email", false);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
ctx.redirect(`${process.env.URL}?notice=expired-token`);
|
ctx.redirect(`/?notice=expired-token`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -9,7 +9,6 @@ import {
|
|||||||
GoogleWorkspaceRequiredError,
|
GoogleWorkspaceRequiredError,
|
||||||
GoogleWorkspaceInvalidError,
|
GoogleWorkspaceInvalidError,
|
||||||
} from "../../errors";
|
} from "../../errors";
|
||||||
import auth from "../../middlewares/authentication";
|
|
||||||
import passportMiddleware from "../../middlewares/passport";
|
import passportMiddleware from "../../middlewares/passport";
|
||||||
import { getAllowedDomains } from "../../utils/authentication";
|
import { getAllowedDomains } from "../../utils/authentication";
|
||||||
import { StateStore } from "../../utils/passport";
|
import { StateStore } from "../../utils/passport";
|
||||||
@ -90,11 +89,7 @@ if (GOOGLE_CLIENT_ID) {
|
|||||||
|
|
||||||
router.get("google", passport.authenticate(providerName));
|
router.get("google", passport.authenticate(providerName));
|
||||||
|
|
||||||
router.get(
|
router.get("google.callback", passportMiddleware(providerName));
|
||||||
"google.callback",
|
|
||||||
auth({ required: false }),
|
|
||||||
passportMiddleware(providerName)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
@ -76,11 +76,7 @@ if (SLACK_CLIENT_ID) {
|
|||||||
|
|
||||||
router.get("slack", passport.authenticate(providerName));
|
router.get("slack", passport.authenticate(providerName));
|
||||||
|
|
||||||
router.get(
|
router.get("slack.callback", passportMiddleware(providerName));
|
||||||
"slack.callback",
|
|
||||||
auth({ required: false }),
|
|
||||||
passportMiddleware(providerName)
|
|
||||||
);
|
|
||||||
|
|
||||||
router.get("slack.commands", auth({ required: false }), async (ctx) => {
|
router.get("slack.commands", auth({ required: false }), async (ctx) => {
|
||||||
const { code, state, error } = ctx.request.query;
|
const { code, state, error } = ctx.request.query;
|
||||||
|
@ -1,10 +1,7 @@
|
|||||||
// @flow
|
// @flow
|
||||||
import addMonths from "date-fns/add_months";
|
|
||||||
import JWT from "jsonwebtoken";
|
|
||||||
import { AuthenticationError, UserSuspendedError } from "../errors";
|
import { AuthenticationError, UserSuspendedError } from "../errors";
|
||||||
import { User, Event, Team, ApiKey } from "../models";
|
import { User, Team, ApiKey } from "../models";
|
||||||
import type { ContextWithState } from "../types";
|
import type { ContextWithState } from "../types";
|
||||||
import { getCookieDomain } from "../utils/domains";
|
|
||||||
import { getUserForJWT } from "../utils/jwt";
|
import { getUserForJWT } from "../utils/jwt";
|
||||||
|
|
||||||
export default function auth(options?: { required?: boolean } = {}) {
|
export default function auth(options?: { required?: boolean } = {}) {
|
||||||
@ -94,78 +91,6 @@ export default function auth(options?: { required?: boolean } = {}) {
|
|||||||
ctx.state.user = user;
|
ctx.state.user = user;
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.signIn = (user: User, team: Team, service, isFirstSignin = false) => {
|
|
||||||
if (user.isSuspended) {
|
|
||||||
return ctx.redirect("/?notice=suspended");
|
|
||||||
}
|
|
||||||
|
|
||||||
// update the database when the user last signed in
|
|
||||||
user.updateSignedIn(ctx.request.ip);
|
|
||||||
|
|
||||||
// don't await event creation for a faster sign-in
|
|
||||||
Event.create({
|
|
||||||
name: "users.signin",
|
|
||||||
actorId: user.id,
|
|
||||||
userId: user.id,
|
|
||||||
teamId: team.id,
|
|
||||||
data: {
|
|
||||||
name: user.name,
|
|
||||||
service,
|
|
||||||
},
|
|
||||||
ip: ctx.request.ip,
|
|
||||||
});
|
|
||||||
|
|
||||||
const domain = getCookieDomain(ctx.request.hostname);
|
|
||||||
const expires = addMonths(new Date(), 3);
|
|
||||||
|
|
||||||
// set a cookie for which service we last signed in with. This is
|
|
||||||
// only used to display a UI hint for the user for next time
|
|
||||||
ctx.cookies.set("lastSignedIn", service, {
|
|
||||||
httpOnly: false,
|
|
||||||
expires: new Date("2100"),
|
|
||||||
domain,
|
|
||||||
});
|
|
||||||
|
|
||||||
// set a transfer cookie for the access token itself and redirect
|
|
||||||
// to the teams subdomain if subdomains are enabled
|
|
||||||
if (process.env.SUBDOMAINS_ENABLED === "true" && team.subdomain) {
|
|
||||||
// get any existing sessions (teams signed in) and add this team
|
|
||||||
const existing = JSON.parse(
|
|
||||||
decodeURIComponent(ctx.cookies.get("sessions") || "") || "{}"
|
|
||||||
);
|
|
||||||
const sessions = encodeURIComponent(
|
|
||||||
JSON.stringify({
|
|
||||||
...existing,
|
|
||||||
[team.id]: {
|
|
||||||
name: team.name,
|
|
||||||
logoUrl: team.logoUrl,
|
|
||||||
url: team.url,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
ctx.cookies.set("sessions", sessions, {
|
|
||||||
httpOnly: false,
|
|
||||||
expires,
|
|
||||||
domain,
|
|
||||||
});
|
|
||||||
|
|
||||||
ctx.redirect(
|
|
||||||
`${team.url}/auth/redirect?token=${user.getTransferToken()}`
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
ctx.cookies.set("accessToken", user.getJwtToken(), {
|
|
||||||
httpOnly: false,
|
|
||||||
expires,
|
|
||||||
});
|
|
||||||
ctx.redirect(`${team.url}/home${isFirstSignin ? "?welcome" : ""}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return next();
|
return next();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Export JWT methods as a convenience
|
|
||||||
export const sign = JWT.sign;
|
|
||||||
export const verify = JWT.verify;
|
|
||||||
export const decode = JWT.decode;
|
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
// @flow
|
// @flow
|
||||||
import passport from "@outlinewiki/koa-passport";
|
import passport from "@outlinewiki/koa-passport";
|
||||||
|
import { type Context } from "koa";
|
||||||
import type { AccountProvisionerResult } from "../commands/accountProvisioner";
|
import type { AccountProvisionerResult } from "../commands/accountProvisioner";
|
||||||
import type { ContextWithAuthMiddleware } from "../types";
|
import { signIn } from "../utils/authentication";
|
||||||
|
|
||||||
export default function createMiddleware(providerName: string) {
|
export default function createMiddleware(providerName: string) {
|
||||||
return function passportMiddleware(ctx: ContextWithAuthMiddleware) {
|
return function passportMiddleware(ctx: Context) {
|
||||||
return passport.authorize(
|
return passport.authorize(
|
||||||
providerName,
|
providerName,
|
||||||
{ session: false },
|
{ session: false },
|
||||||
@ -27,7 +28,7 @@ export default function createMiddleware(providerName: string) {
|
|||||||
return ctx.redirect("/?notice=suspended");
|
return ctx.redirect("/?notice=suspended");
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.signIn(result.user, result.team, providerName, result.isNewUser);
|
signIn(ctx, result.user, result.team, providerName, result.isNewUser);
|
||||||
}
|
}
|
||||||
)(ctx);
|
)(ctx);
|
||||||
};
|
};
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
// @flow
|
// @flow
|
||||||
import { type Context } from "koa";
|
import { type Context } from "koa";
|
||||||
import { User, Team } from "./models";
|
import { User } from "./models";
|
||||||
|
|
||||||
export type ContextWithState = {|
|
export type ContextWithState = {|
|
||||||
...$Exact<Context>,
|
...$Exact<Context>,
|
||||||
@ -10,13 +10,3 @@ export type ContextWithState = {|
|
|||||||
authType: "app" | "api",
|
authType: "app" | "api",
|
||||||
},
|
},
|
||||||
|};
|
|};
|
||||||
|
|
||||||
export type ContextWithAuthMiddleware = {|
|
|
||||||
...$Exact<ContextWithState>,
|
|
||||||
signIn: (
|
|
||||||
user: User,
|
|
||||||
team: Team,
|
|
||||||
providerName: string,
|
|
||||||
isFirstSignin: boolean
|
|
||||||
) => void,
|
|
||||||
|};
|
|
||||||
|
@ -1,7 +1,82 @@
|
|||||||
// @flow
|
// @flow
|
||||||
|
import addMonths from "date-fns/add_months";
|
||||||
|
import { type Context } from "koa";
|
||||||
|
import { User, Event, Team } from "../models";
|
||||||
|
import { getCookieDomain } from "../utils/domains";
|
||||||
|
|
||||||
export function getAllowedDomains(): string[] {
|
export function getAllowedDomains(): string[] {
|
||||||
// GOOGLE_ALLOWED_DOMAINS included here for backwards compatability
|
// GOOGLE_ALLOWED_DOMAINS included here for backwards compatability
|
||||||
const env = process.env.ALLOWED_DOMAINS || process.env.GOOGLE_ALLOWED_DOMAINS;
|
const env = process.env.ALLOWED_DOMAINS || process.env.GOOGLE_ALLOWED_DOMAINS;
|
||||||
return env ? env.split(",") : [];
|
return env ? env.split(",") : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function signIn(
|
||||||
|
ctx: Context,
|
||||||
|
user: User,
|
||||||
|
team: Team,
|
||||||
|
service: string,
|
||||||
|
isFirstSignin: boolean = false
|
||||||
|
) {
|
||||||
|
if (user.isSuspended) {
|
||||||
|
return ctx.redirect("/?notice=suspended");
|
||||||
|
}
|
||||||
|
|
||||||
|
// update the database when the user last signed in
|
||||||
|
user.updateSignedIn(ctx.request.ip);
|
||||||
|
|
||||||
|
// don't await event creation for a faster sign-in
|
||||||
|
Event.create({
|
||||||
|
name: "users.signin",
|
||||||
|
actorId: user.id,
|
||||||
|
userId: user.id,
|
||||||
|
teamId: team.id,
|
||||||
|
data: {
|
||||||
|
name: user.name,
|
||||||
|
service,
|
||||||
|
},
|
||||||
|
ip: ctx.request.ip,
|
||||||
|
});
|
||||||
|
|
||||||
|
const domain = getCookieDomain(ctx.request.hostname);
|
||||||
|
const expires = addMonths(new Date(), 3);
|
||||||
|
|
||||||
|
// set a cookie for which service we last signed in with. This is
|
||||||
|
// only used to display a UI hint for the user for next time
|
||||||
|
ctx.cookies.set("lastSignedIn", service, {
|
||||||
|
httpOnly: false,
|
||||||
|
expires: new Date("2100"),
|
||||||
|
domain,
|
||||||
|
});
|
||||||
|
|
||||||
|
// set a transfer cookie for the access token itself and redirect
|
||||||
|
// to the teams subdomain if subdomains are enabled
|
||||||
|
if (process.env.SUBDOMAINS_ENABLED === "true" && team.subdomain) {
|
||||||
|
// get any existing sessions (teams signed in) and add this team
|
||||||
|
const existing = JSON.parse(
|
||||||
|
decodeURIComponent(ctx.cookies.get("sessions") || "") || "{}"
|
||||||
|
);
|
||||||
|
const sessions = encodeURIComponent(
|
||||||
|
JSON.stringify({
|
||||||
|
...existing,
|
||||||
|
[team.id]: {
|
||||||
|
name: team.name,
|
||||||
|
logoUrl: team.logoUrl,
|
||||||
|
url: team.url,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
ctx.cookies.set("sessions", sessions, {
|
||||||
|
httpOnly: false,
|
||||||
|
expires,
|
||||||
|
domain,
|
||||||
|
});
|
||||||
|
|
||||||
|
ctx.redirect(`${team.url}/auth/redirect?token=${user.getTransferToken()}`);
|
||||||
|
} else {
|
||||||
|
ctx.cookies.set("accessToken", user.getJwtToken(), {
|
||||||
|
httpOnly: false,
|
||||||
|
expires,
|
||||||
|
});
|
||||||
|
ctx.redirect(`${team.url}/home${isFirstSignin ? "?welcome" : ""}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -21,6 +21,10 @@ function getJWTPayload(token) {
|
|||||||
export async function getUserForJWT(token: string): Promise<User> {
|
export async function getUserForJWT(token: string): Promise<User> {
|
||||||
const payload = getJWTPayload(token);
|
const payload = getJWTPayload(token);
|
||||||
|
|
||||||
|
if (payload.type === "email-signin") {
|
||||||
|
throw new AuthenticationError("Invalid token");
|
||||||
|
}
|
||||||
|
|
||||||
// check the token is within it's expiration time
|
// check the token is within it's expiration time
|
||||||
if (payload.expiresAt) {
|
if (payload.expiresAt) {
|
||||||
if (new Date(payload.expiresAt) < new Date()) {
|
if (new Date(payload.expiresAt) < new Date()) {
|
||||||
|
Reference in New Issue
Block a user