Skip to content

Commit

Permalink
chore: Migrate authentication to new tables (#1929)
Browse files Browse the repository at this point in the history
This work provides a foundation for a more pluggable authentication system such as the one outlined in #1317.

closes #1317
  • Loading branch information
tommoor committed Mar 9, 2021
1 parent ab7b16b commit ed2a42a
Show file tree
Hide file tree
Showing 35 changed files with 1,275 additions and 292 deletions.
1 change: 0 additions & 1 deletion .gitignore
@@ -1,7 +1,6 @@
dist
build
node_modules/*
server/scripts
.env
.log
npm-debug.log
Expand Down
8 changes: 1 addition & 7 deletions app/models/Team.js
Expand Up @@ -6,8 +6,6 @@ class Team extends BaseModel {
id: string;
name: string;
avatarUrl: string;
slackConnected: boolean;
googleConnected: boolean;
sharing: boolean;
documentEmbeds: boolean;
guestSignin: boolean;
Expand All @@ -17,11 +15,7 @@ class Team extends BaseModel {

@computed
get signinMethods(): string {
if (this.slackConnected && this.googleConnected) {
return "Slack or Google";
}
if (this.slackConnected) return "Slack";
return "Google";
return "SSO";
}
}

Expand Down
3 changes: 1 addition & 2 deletions app/scenes/Settings/Notifications.js
Expand Up @@ -97,8 +97,7 @@ class Notifications extends React.Component<Props> {

<HelpText>
Manage when and where you receive email notifications from Outline.
Your email address can be updated in your{" "}
{team.slackConnected ? "Slack" : "Google"} account.
Your email address can be updated in your SSO provider.
</HelpText>

<Input
Expand Down
1 change: 1 addition & 0 deletions flow-typed/globals.js
@@ -1,5 +1,6 @@
// @flow
declare var process: {
exit: (code?: number) => void,
env: {
[string]: string,
},
Expand Down
2 changes: 1 addition & 1 deletion server/.jestconfig.json
@@ -1,5 +1,5 @@
{
"verbose": true,
"verbose": false,
"rootDir": "..",
"roots": [
"<rootDir>/server",
Expand Down
14 changes: 9 additions & 5 deletions server/api/auth.js
Expand Up @@ -37,10 +37,14 @@ services.push({
function filterServices(team) {
let output = services;

if (team && !team.googleId) {
const providerNames = team
? team.authenticationProviders.map((provider) => provider.name)
: [];

if (team && !providerNames.includes("google")) {
output = reject(output, (service) => service.id === "google");
}
if (team && !team.slackId) {
if (team && !providerNames.includes("slack")) {
output = reject(output, (service) => service.id === "slack");
}
if (!team || !team.guestSignin) {
Expand All @@ -55,7 +59,7 @@ router.post("auth.config", async (ctx) => {
// brand for the knowledge base and it's guest signin option is used for the
// root login page.
if (process.env.DEPLOYMENT !== "hosted") {
const teams = await Team.findAll();
const teams = await Team.scope("withAuthenticationProviders").findAll();

if (teams.length === 1) {
const team = teams[0];
Expand All @@ -70,7 +74,7 @@ router.post("auth.config", async (ctx) => {
}

if (isCustomDomain(ctx.request.hostname)) {
const team = await Team.findOne({
const team = await Team.scope("withAuthenticationProviders").findOne({
where: { domain: ctx.request.hostname },
});

Expand All @@ -95,7 +99,7 @@ router.post("auth.config", async (ctx) => {
) {
const domain = parseDomain(ctx.request.hostname);
const subdomain = domain ? domain.subdomain : undefined;
const team = await Team.findOne({
const team = await Team.scope("withAuthenticationProviders").findOne({
where: { subdomain },
});

Expand Down
64 changes: 53 additions & 11 deletions server/api/hooks.js
Expand Up @@ -3,6 +3,8 @@ import Router from "koa-router";
import { escapeRegExp } from "lodash";
import { AuthenticationError, InvalidRequestError } from "../errors";
import {
UserAuthentication,
AuthenticationProvider,
Authentication,
Document,
User,
Expand All @@ -25,7 +27,14 @@ router.post("hooks.unfurl", async (ctx) => {
}

const user = await User.findOne({
where: { service: "slack", serviceId: event.user },
include: [
{
where: { providerId: event.user },
model: UserAuthentication,
as: "authentications",
required: true,
},
],
});
if (!user) return;

Expand Down Expand Up @@ -70,11 +79,21 @@ router.post("hooks.interactive", async (ctx) => {
throw new AuthenticationError("Invalid verification token");
}

const team = await Team.findOne({
where: { slackId: data.team.id },
const authProvider = await AuthenticationProvider.findOne({
where: {
name: "slack",
providerId: data.team.id,
},
include: [
{
model: Team,
as: "team",
required: true,
},
],
});

if (!team) {
if (!authProvider) {
ctx.body = {
text:
"Sorry, we couldn’t find an integration for your team. Head to your Outline settings to set one up.",
Expand All @@ -84,6 +103,8 @@ router.post("hooks.interactive", async (ctx) => {
return;
}

const { team } = authProvider;

// we find the document based on the users teamId to ensure access
const document = await Document.findOne({
where: {
Expand Down Expand Up @@ -131,20 +152,41 @@ router.post("hooks.slack", async (ctx) => {
return;
}

let user;
let user, team;

// attempt to find the corresponding team for this request based on the team_id
let team = await Team.findOne({
where: { slackId: team_id },
team = await Team.findOne({
include: [
{
where: {
name: "slack",
providerId: team_id,
},
as: "authenticationProviders",
model: AuthenticationProvider,
required: true,
},
],
});

if (team) {
user = await User.findOne({
const authentication = await UserAuthentication.findOne({
where: {
teamId: team.id,
service: "slack",
serviceId: user_id,
providerId: user_id,
},
include: [
{
where: { teamId: team.id },
model: User,
as: "user",
required: true,
},
],
});

if (authentication) {
user = authentication.user;
}
} else {
// If we couldn't find a team it's still possible that the request is from
// a team that authenticated with a different service, but connected Slack
Expand Down
44 changes: 22 additions & 22 deletions server/api/hooks.test.js
Expand Up @@ -33,7 +33,7 @@ describe("#hooks.unfurl", () => {
event: {
type: "link_shared",
channel: "Cxxxxxx",
user: user.serviceId,
user: user.authentications[0].providerId,
message_ts: "123456789.9875",
links: [
{
Expand All @@ -56,8 +56,8 @@ describe("#hooks.slack", () => {
const res = await server.post("/api/hooks.slack", {
body: {
token: process.env.SLACK_VERIFICATION_TOKEN,
user_id: user.serviceId,
team_id: team.slackId,
user_id: user.authentications[0].providerId,
team_id: team.authenticationProviders[0].providerId,
text: "dsfkndfskndsfkn",
},
});
Expand All @@ -76,8 +76,8 @@ describe("#hooks.slack", () => {
const res = await server.post("/api/hooks.slack", {
body: {
token: process.env.SLACK_VERIFICATION_TOKEN,
user_id: user.serviceId,
team_id: team.slackId,
user_id: user.authentications[0].providerId,
team_id: team.authenticationProviders[0].providerId,
text: "contains",
},
});
Expand All @@ -98,8 +98,8 @@ describe("#hooks.slack", () => {
const res = await server.post("/api/hooks.slack", {
body: {
token: process.env.SLACK_VERIFICATION_TOKEN,
user_id: user.serviceId,
team_id: team.slackId,
user_id: user.authentications[0].providerId,
team_id: team.authenticationProviders[0].providerId,
text: "*contains",
},
});
Expand All @@ -118,8 +118,8 @@ describe("#hooks.slack", () => {
const res = await server.post("/api/hooks.slack", {
body: {
token: process.env.SLACK_VERIFICATION_TOKEN,
user_id: user.serviceId,
team_id: team.slackId,
user_id: user.authentications[0].providerId,
team_id: team.authenticationProviders[0].providerId,
text: "contains",
},
});
Expand All @@ -137,8 +137,8 @@ describe("#hooks.slack", () => {
await server.post("/api/hooks.slack", {
body: {
token: process.env.SLACK_VERIFICATION_TOKEN,
user_id: user.serviceId,
team_id: team.slackId,
user_id: user.authentications[0].providerId,
team_id: team.authenticationProviders[0].providerId,
text: "contains",
},
});
Expand All @@ -161,8 +161,8 @@ describe("#hooks.slack", () => {
const res = await server.post("/api/hooks.slack", {
body: {
token: process.env.SLACK_VERIFICATION_TOKEN,
user_id: user.serviceId,
team_id: team.slackId,
user_id: user.authentications[0].providerId,
team_id: team.authenticationProviders[0].providerId,
text: "help",
},
});
Expand All @@ -176,8 +176,8 @@ describe("#hooks.slack", () => {
const res = await server.post("/api/hooks.slack", {
body: {
token: process.env.SLACK_VERIFICATION_TOKEN,
user_id: user.serviceId,
team_id: team.slackId,
user_id: user.authentications[0].providerId,
team_id: team.authenticationProviders[0].providerId,
text: "",
},
});
Expand Down Expand Up @@ -206,7 +206,7 @@ describe("#hooks.slack", () => {
body: {
token: process.env.SLACK_VERIFICATION_TOKEN,
user_id: "unknown-slack-user-id",
team_id: team.slackId,
team_id: team.authenticationProviders[0].providerId,
text: "contains",
},
});
Expand Down Expand Up @@ -260,8 +260,8 @@ describe("#hooks.slack", () => {
const res = await server.post("/api/hooks.slack", {
body: {
token: "wrong-verification-token",
team_id: team.slackId,
user_id: user.serviceId,
user_id: user.authentications[0].providerId,
team_id: team.authenticationProviders[0].providerId,
text: "Welcome",
},
});
Expand All @@ -280,8 +280,8 @@ describe("#hooks.interactive", () => {

const payload = JSON.stringify({
token: process.env.SLACK_VERIFICATION_TOKEN,
user: { id: user.serviceId },
team: { id: team.slackId },
user: { id: user.authentications[0].providerId },
team: { id: team.authenticationProviders[0].providerId },
callback_id: document.id,
});
const res = await server.post("/api/hooks.interactive", {
Expand All @@ -305,7 +305,7 @@ describe("#hooks.interactive", () => {
const payload = JSON.stringify({
token: process.env.SLACK_VERIFICATION_TOKEN,
user: { id: "unknown-slack-user-id" },
team: { id: team.slackId },
team: { id: team.authenticationProviders[0].providerId },
callback_id: document.id,
});
const res = await server.post("/api/hooks.interactive", {
Expand All @@ -322,7 +322,7 @@ describe("#hooks.interactive", () => {
const { user } = await seed();
const payload = JSON.stringify({
token: "wrong-verification-token",
user: { id: user.serviceId, name: user.name },
user: { id: user.authentications[0].providerId, name: user.name },
callback_id: "doesnt-matter",
});
const res = await server.post("/api/hooks.interactive", {
Expand Down
23 changes: 12 additions & 11 deletions server/auth/email.js
@@ -1,6 +1,7 @@
// @flow
import subMinutes from "date-fns/sub_minutes";
import Router from "koa-router";
import { find } from "lodash";
import { AuthorizationError } from "../errors";
import mailer from "../mailer";
import auth from "../middlewares/authentication";
Expand All @@ -19,23 +20,27 @@ router.post("email", async (ctx) => {

ctx.assertEmail(email, "email is required");

const user = await User.findOne({
const user = await User.scope("withAuthentications").findOne({
where: { email: email.toLowerCase() },
});

if (user) {
const team = await Team.findByPk(user.teamId);
const team = await Team.scope("withAuthenticationProviders").findByPk(
user.teamId
);
if (!team) {
ctx.redirect(`/?notice=auth-error`);
return;
}

// If the user matches an email address associated with an SSO
// signin then just forward them directly to that service's
// login page
if (user.service && user.service !== "email") {
// provider then just forward them directly to that sign-in page
if (user.authentications.length) {
const authProvider = find(team.authenticationProviders, {
id: user.authentications[0].authenticationProviderId,
});
ctx.body = {
redirect: `${team.url}/auth/${user.service}`,
redirect: `${team.url}/auth/${authProvider.name}`,
};
return;
}
Expand Down Expand Up @@ -87,11 +92,7 @@ router.get("email.callback", auth({ required: false }), async (ctx) => {
throw new AuthorizationError();
}

if (!user.service) {
user.service = "email";
user.lastActiveAt = new Date();
await user.save();
}
await user.update({ lastActiveAt: new Date() });

// set cookies on response and redirect to team subdomain
ctx.signIn(user, team, "email", false);
Expand Down

0 comments on commit ed2a42a

Please sign in to comment.