Skip to content

Commit ed2a42a

Browse files
authored
chore: Migrate authentication to new tables (outline#1929)
This work provides a foundation for a more pluggable authentication system such as the one outlined in outline#1317. closes outline#1317
1 parent ab7b16b commit ed2a42a

35 files changed

+1275
-292
lines changed

.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
dist
22
build
33
node_modules/*
4-
server/scripts
54
.env
65
.log
76
npm-debug.log

app/models/Team.js

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@ class Team extends BaseModel {
66
id: string;
77
name: string;
88
avatarUrl: string;
9-
slackConnected: boolean;
10-
googleConnected: boolean;
119
sharing: boolean;
1210
documentEmbeds: boolean;
1311
guestSignin: boolean;
@@ -17,11 +15,7 @@ class Team extends BaseModel {
1715

1816
@computed
1917
get signinMethods(): string {
20-
if (this.slackConnected && this.googleConnected) {
21-
return "Slack or Google";
22-
}
23-
if (this.slackConnected) return "Slack";
24-
return "Google";
18+
return "SSO";
2519
}
2620
}
2721

app/scenes/Settings/Notifications.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,7 @@ class Notifications extends React.Component<Props> {
9797

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

104103
<Input

flow-typed/globals.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// @flow
22
declare var process: {
3+
exit: (code?: number) => void,
34
env: {
45
[string]: string,
56
},

server/.jestconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"verbose": true,
2+
"verbose": false,
33
"rootDir": "..",
44
"roots": [
55
"<rootDir>/server",

server/api/auth.js

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,14 @@ services.push({
3737
function filterServices(team) {
3838
let output = services;
3939

40-
if (team && !team.googleId) {
40+
const providerNames = team
41+
? team.authenticationProviders.map((provider) => provider.name)
42+
: [];
43+
44+
if (team && !providerNames.includes("google")) {
4145
output = reject(output, (service) => service.id === "google");
4246
}
43-
if (team && !team.slackId) {
47+
if (team && !providerNames.includes("slack")) {
4448
output = reject(output, (service) => service.id === "slack");
4549
}
4650
if (!team || !team.guestSignin) {
@@ -55,7 +59,7 @@ router.post("auth.config", async (ctx) => {
5559
// brand for the knowledge base and it's guest signin option is used for the
5660
// root login page.
5761
if (process.env.DEPLOYMENT !== "hosted") {
58-
const teams = await Team.findAll();
62+
const teams = await Team.scope("withAuthenticationProviders").findAll();
5963

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

7276
if (isCustomDomain(ctx.request.hostname)) {
73-
const team = await Team.findOne({
77+
const team = await Team.scope("withAuthenticationProviders").findOne({
7478
where: { domain: ctx.request.hostname },
7579
});
7680

@@ -95,7 +99,7 @@ router.post("auth.config", async (ctx) => {
9599
) {
96100
const domain = parseDomain(ctx.request.hostname);
97101
const subdomain = domain ? domain.subdomain : undefined;
98-
const team = await Team.findOne({
102+
const team = await Team.scope("withAuthenticationProviders").findOne({
99103
where: { subdomain },
100104
});
101105

server/api/hooks.js

Lines changed: 53 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import Router from "koa-router";
33
import { escapeRegExp } from "lodash";
44
import { AuthenticationError, InvalidRequestError } from "../errors";
55
import {
6+
UserAuthentication,
7+
AuthenticationProvider,
68
Authentication,
79
Document,
810
User,
@@ -25,7 +27,14 @@ router.post("hooks.unfurl", async (ctx) => {
2527
}
2628

2729
const user = await User.findOne({
28-
where: { service: "slack", serviceId: event.user },
30+
include: [
31+
{
32+
where: { providerId: event.user },
33+
model: UserAuthentication,
34+
as: "authentications",
35+
required: true,
36+
},
37+
],
2938
});
3039
if (!user) return;
3140

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

73-
const team = await Team.findOne({
74-
where: { slackId: data.team.id },
82+
const authProvider = await AuthenticationProvider.findOne({
83+
where: {
84+
name: "slack",
85+
providerId: data.team.id,
86+
},
87+
include: [
88+
{
89+
model: Team,
90+
as: "team",
91+
required: true,
92+
},
93+
],
7594
});
7695

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

106+
const { team } = authProvider;
107+
87108
// we find the document based on the users teamId to ensure access
88109
const document = await Document.findOne({
89110
where: {
@@ -131,20 +152,41 @@ router.post("hooks.slack", async (ctx) => {
131152
return;
132153
}
133154

134-
let user;
155+
let user, team;
135156

136157
// attempt to find the corresponding team for this request based on the team_id
137-
let team = await Team.findOne({
138-
where: { slackId: team_id },
158+
team = await Team.findOne({
159+
include: [
160+
{
161+
where: {
162+
name: "slack",
163+
providerId: team_id,
164+
},
165+
as: "authenticationProviders",
166+
model: AuthenticationProvider,
167+
required: true,
168+
},
169+
],
139170
});
171+
140172
if (team) {
141-
user = await User.findOne({
173+
const authentication = await UserAuthentication.findOne({
142174
where: {
143-
teamId: team.id,
144-
service: "slack",
145-
serviceId: user_id,
175+
providerId: user_id,
146176
},
177+
include: [
178+
{
179+
where: { teamId: team.id },
180+
model: User,
181+
as: "user",
182+
required: true,
183+
},
184+
],
147185
});
186+
187+
if (authentication) {
188+
user = authentication.user;
189+
}
148190
} else {
149191
// If we couldn't find a team it's still possible that the request is from
150192
// a team that authenticated with a different service, but connected Slack

server/api/hooks.test.js

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ describe("#hooks.unfurl", () => {
3333
event: {
3434
type: "link_shared",
3535
channel: "Cxxxxxx",
36-
user: user.serviceId,
36+
user: user.authentications[0].providerId,
3737
message_ts: "123456789.9875",
3838
links: [
3939
{
@@ -56,8 +56,8 @@ describe("#hooks.slack", () => {
5656
const res = await server.post("/api/hooks.slack", {
5757
body: {
5858
token: process.env.SLACK_VERIFICATION_TOKEN,
59-
user_id: user.serviceId,
60-
team_id: team.slackId,
59+
user_id: user.authentications[0].providerId,
60+
team_id: team.authenticationProviders[0].providerId,
6161
text: "dsfkndfskndsfkn",
6262
},
6363
});
@@ -76,8 +76,8 @@ describe("#hooks.slack", () => {
7676
const res = await server.post("/api/hooks.slack", {
7777
body: {
7878
token: process.env.SLACK_VERIFICATION_TOKEN,
79-
user_id: user.serviceId,
80-
team_id: team.slackId,
79+
user_id: user.authentications[0].providerId,
80+
team_id: team.authenticationProviders[0].providerId,
8181
text: "contains",
8282
},
8383
});
@@ -98,8 +98,8 @@ describe("#hooks.slack", () => {
9898
const res = await server.post("/api/hooks.slack", {
9999
body: {
100100
token: process.env.SLACK_VERIFICATION_TOKEN,
101-
user_id: user.serviceId,
102-
team_id: team.slackId,
101+
user_id: user.authentications[0].providerId,
102+
team_id: team.authenticationProviders[0].providerId,
103103
text: "*contains",
104104
},
105105
});
@@ -118,8 +118,8 @@ describe("#hooks.slack", () => {
118118
const res = await server.post("/api/hooks.slack", {
119119
body: {
120120
token: process.env.SLACK_VERIFICATION_TOKEN,
121-
user_id: user.serviceId,
122-
team_id: team.slackId,
121+
user_id: user.authentications[0].providerId,
122+
team_id: team.authenticationProviders[0].providerId,
123123
text: "contains",
124124
},
125125
});
@@ -137,8 +137,8 @@ describe("#hooks.slack", () => {
137137
await server.post("/api/hooks.slack", {
138138
body: {
139139
token: process.env.SLACK_VERIFICATION_TOKEN,
140-
user_id: user.serviceId,
141-
team_id: team.slackId,
140+
user_id: user.authentications[0].providerId,
141+
team_id: team.authenticationProviders[0].providerId,
142142
text: "contains",
143143
},
144144
});
@@ -161,8 +161,8 @@ describe("#hooks.slack", () => {
161161
const res = await server.post("/api/hooks.slack", {
162162
body: {
163163
token: process.env.SLACK_VERIFICATION_TOKEN,
164-
user_id: user.serviceId,
165-
team_id: team.slackId,
164+
user_id: user.authentications[0].providerId,
165+
team_id: team.authenticationProviders[0].providerId,
166166
text: "help",
167167
},
168168
});
@@ -176,8 +176,8 @@ describe("#hooks.slack", () => {
176176
const res = await server.post("/api/hooks.slack", {
177177
body: {
178178
token: process.env.SLACK_VERIFICATION_TOKEN,
179-
user_id: user.serviceId,
180-
team_id: team.slackId,
179+
user_id: user.authentications[0].providerId,
180+
team_id: team.authenticationProviders[0].providerId,
181181
text: "",
182182
},
183183
});
@@ -206,7 +206,7 @@ describe("#hooks.slack", () => {
206206
body: {
207207
token: process.env.SLACK_VERIFICATION_TOKEN,
208208
user_id: "unknown-slack-user-id",
209-
team_id: team.slackId,
209+
team_id: team.authenticationProviders[0].providerId,
210210
text: "contains",
211211
},
212212
});
@@ -260,8 +260,8 @@ describe("#hooks.slack", () => {
260260
const res = await server.post("/api/hooks.slack", {
261261
body: {
262262
token: "wrong-verification-token",
263-
team_id: team.slackId,
264-
user_id: user.serviceId,
263+
user_id: user.authentications[0].providerId,
264+
team_id: team.authenticationProviders[0].providerId,
265265
text: "Welcome",
266266
},
267267
});
@@ -280,8 +280,8 @@ describe("#hooks.interactive", () => {
280280

281281
const payload = JSON.stringify({
282282
token: process.env.SLACK_VERIFICATION_TOKEN,
283-
user: { id: user.serviceId },
284-
team: { id: team.slackId },
283+
user: { id: user.authentications[0].providerId },
284+
team: { id: team.authenticationProviders[0].providerId },
285285
callback_id: document.id,
286286
});
287287
const res = await server.post("/api/hooks.interactive", {
@@ -305,7 +305,7 @@ describe("#hooks.interactive", () => {
305305
const payload = JSON.stringify({
306306
token: process.env.SLACK_VERIFICATION_TOKEN,
307307
user: { id: "unknown-slack-user-id" },
308-
team: { id: team.slackId },
308+
team: { id: team.authenticationProviders[0].providerId },
309309
callback_id: document.id,
310310
});
311311
const res = await server.post("/api/hooks.interactive", {
@@ -322,7 +322,7 @@ describe("#hooks.interactive", () => {
322322
const { user } = await seed();
323323
const payload = JSON.stringify({
324324
token: "wrong-verification-token",
325-
user: { id: user.serviceId, name: user.name },
325+
user: { id: user.authentications[0].providerId, name: user.name },
326326
callback_id: "doesnt-matter",
327327
});
328328
const res = await server.post("/api/hooks.interactive", {

server/auth/email.js

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// @flow
22
import subMinutes from "date-fns/sub_minutes";
33
import Router from "koa-router";
4+
import { find } from "lodash";
45
import { AuthorizationError } from "../errors";
56
import mailer from "../mailer";
67
import auth from "../middlewares/authentication";
@@ -19,23 +20,27 @@ router.post("email", async (ctx) => {
1920

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

22-
const user = await User.findOne({
23+
const user = await User.scope("withAuthentications").findOne({
2324
where: { email: email.toLowerCase() },
2425
});
2526

2627
if (user) {
27-
const team = await Team.findByPk(user.teamId);
28+
const team = await Team.scope("withAuthenticationProviders").findByPk(
29+
user.teamId
30+
);
2831
if (!team) {
2932
ctx.redirect(`/?notice=auth-error`);
3033
return;
3134
}
3235

3336
// If the user matches an email address associated with an SSO
34-
// signin then just forward them directly to that service's
35-
// login page
36-
if (user.service && user.service !== "email") {
37+
// provider then just forward them directly to that sign-in page
38+
if (user.authentications.length) {
39+
const authProvider = find(team.authenticationProviders, {
40+
id: user.authentications[0].authenticationProviderId,
41+
});
3742
ctx.body = {
38-
redirect: `${team.url}/auth/${user.service}`,
43+
redirect: `${team.url}/auth/${authProvider.name}`,
3944
};
4045
return;
4146
}
@@ -87,11 +92,7 @@ router.get("email.callback", auth({ required: false }), async (ctx) => {
8792
throw new AuthorizationError();
8893
}
8994

90-
if (!user.service) {
91-
user.service = "email";
92-
user.lastActiveAt = new Date();
93-
await user.save();
94-
}
95+
await user.update({ lastActiveAt: new Date() });
9596

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

0 commit comments

Comments
 (0)