mirror of
https://github.com/outline/outline.git
synced 2025-12-30 07:19:52 -06:00
perf: Improve speed of Azure login (parallelize two slow API requests)
chore: Improved types around passport
This commit is contained in:
@@ -1,12 +1,16 @@
|
||||
import passport from "@outlinewiki/koa-passport";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module '@out... Remove this comment to see the full error message
|
||||
import { Strategy as AzureStrategy } from "@outlinewiki/passport-azure-ad-oauth2";
|
||||
import jwt from "jsonwebtoken";
|
||||
import { Request } from "koa";
|
||||
import Router from "koa-router";
|
||||
import accountProvisioner from "@server/commands/accountProvisioner";
|
||||
import { Profile } from "passport";
|
||||
import accountProvisioner, {
|
||||
AccountProvisionerResult,
|
||||
} from "@server/commands/accountProvisioner";
|
||||
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";
|
||||
|
||||
const router = new Router();
|
||||
@@ -14,15 +18,14 @@ const providerName = "azure";
|
||||
const AZURE_CLIENT_ID = process.env.AZURE_CLIENT_ID;
|
||||
const AZURE_CLIENT_SECRET = process.env.AZURE_CLIENT_SECRET;
|
||||
const AZURE_RESOURCE_APP_ID = process.env.AZURE_RESOURCE_APP_ID;
|
||||
// @ts-expect-error ts-migrate(7034) FIXME: Variable 'scopes' implicitly has type 'any[]' in s... Remove this comment to see the full error message
|
||||
const scopes = [];
|
||||
const scopes: string[] = [];
|
||||
|
||||
export const config = {
|
||||
name: "Microsoft",
|
||||
enabled: !!AZURE_CLIENT_ID,
|
||||
};
|
||||
|
||||
if (AZURE_CLIENT_ID) {
|
||||
if (AZURE_CLIENT_ID && AZURE_CLIENT_SECRET) {
|
||||
const strategy = new AzureStrategy(
|
||||
{
|
||||
clientID: AZURE_CLIENT_ID,
|
||||
@@ -31,23 +34,35 @@ if (AZURE_CLIENT_ID) {
|
||||
useCommonEndpoint: true,
|
||||
passReqToCallback: true,
|
||||
resource: AZURE_RESOURCE_APP_ID,
|
||||
// @ts-expect-error StateStore
|
||||
store: new StateStore(),
|
||||
// @ts-expect-error ts-migrate(7005) FIXME: Variable 'scopes' implicitly has an 'any[]' type.
|
||||
scope: scopes,
|
||||
},
|
||||
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'req' implicitly has an 'any' type.
|
||||
async function (req, accessToken, refreshToken, params, _, done) {
|
||||
async function (
|
||||
req: Request,
|
||||
accessToken: string,
|
||||
refreshToken: string,
|
||||
params: { id_token: string },
|
||||
_profile: Profile,
|
||||
done: (
|
||||
err: Error | null,
|
||||
user: User | null,
|
||||
result?: AccountProvisionerResult
|
||||
) => void
|
||||
) {
|
||||
try {
|
||||
// see docs for what the fields in profile represent here:
|
||||
// https://docs.microsoft.com/en-us/azure/active-directory/develop/access-tokens
|
||||
const profile = jwt.decode(params.id_token) as jwt.JwtPayload;
|
||||
|
||||
// Load the users profile from the Microsoft Graph API
|
||||
// https://docs.microsoft.com/en-us/graph/api/resources/users?view=graph-rest-1.0
|
||||
const profileResponse = await request(
|
||||
`https://graph.microsoft.com/v1.0/me`,
|
||||
accessToken
|
||||
);
|
||||
const [profileResponse, organizationResponse] = await Promise.all([
|
||||
// Load the users profile from the Microsoft Graph API
|
||||
// https://docs.microsoft.com/en-us/graph/api/resources/users?view=graph-rest-1.0
|
||||
request(`https://graph.microsoft.com/v1.0/me`, accessToken),
|
||||
// Load the organization profile from the Microsoft Graph API
|
||||
// https://docs.microsoft.com/en-us/graph/api/organization-get?view=graph-rest-1.0
|
||||
request(`https://graph.microsoft.com/v1.0/organization`, accessToken),
|
||||
]);
|
||||
|
||||
if (!profileResponse) {
|
||||
throw MicrosoftGraphError(
|
||||
@@ -55,13 +70,6 @@ if (AZURE_CLIENT_ID) {
|
||||
);
|
||||
}
|
||||
|
||||
// Load the organization profile from the Microsoft Graph API
|
||||
// https://docs.microsoft.com/en-us/graph/api/organization-get?view=graph-rest-1.0
|
||||
const organizationResponse = await request(
|
||||
`https://graph.microsoft.com/v1.0/organization`,
|
||||
accessToken
|
||||
);
|
||||
|
||||
if (!organizationResponse) {
|
||||
throw MicrosoftGraphError(
|
||||
"Unable to load organization info from Microsoft Graph API"
|
||||
@@ -100,7 +108,6 @@ if (AZURE_CLIENT_ID) {
|
||||
providerId: profile.oid,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
// @ts-expect-error ts-migrate(7005) FIXME: Variable 'scopes' implicitly has an 'any[]' type.
|
||||
scopes,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
import passport from "@outlinewiki/koa-passport";
|
||||
import { Request } from "koa";
|
||||
import Router from "koa-router";
|
||||
import { capitalize } from "lodash";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module 'pass... Remove this comment to see the full error message
|
||||
import { Profile } from "passport";
|
||||
import { Strategy as GoogleStrategy } from "passport-google-oauth2";
|
||||
import accountProvisioner from "@server/commands/accountProvisioner";
|
||||
import accountProvisioner, {
|
||||
AccountProvisionerResult,
|
||||
} from "@server/commands/accountProvisioner";
|
||||
import env from "@server/env";
|
||||
import {
|
||||
GoogleWorkspaceRequiredError,
|
||||
GoogleWorkspaceInvalidError,
|
||||
} from "@server/errors";
|
||||
import passportMiddleware from "@server/middlewares/passport";
|
||||
import { User } from "@server/models";
|
||||
import { isDomainAllowed } from "@server/utils/authentication";
|
||||
import { StateStore } from "@server/utils/passport";
|
||||
|
||||
@@ -27,7 +31,15 @@ export const config = {
|
||||
enabled: !!GOOGLE_CLIENT_ID,
|
||||
};
|
||||
|
||||
if (GOOGLE_CLIENT_ID) {
|
||||
type GoogleProfile = Profile & {
|
||||
email: string;
|
||||
picture: string;
|
||||
_json: {
|
||||
hd: string;
|
||||
};
|
||||
};
|
||||
|
||||
if (GOOGLE_CLIENT_ID && GOOGLE_CLIENT_SECRET) {
|
||||
passport.use(
|
||||
new GoogleStrategy(
|
||||
{
|
||||
@@ -35,11 +47,21 @@ if (GOOGLE_CLIENT_ID) {
|
||||
clientSecret: GOOGLE_CLIENT_SECRET,
|
||||
callbackURL: `${env.URL}/auth/google.callback`,
|
||||
passReqToCallback: true,
|
||||
// @ts-expect-error StateStore
|
||||
store: new StateStore(),
|
||||
scope: scopes,
|
||||
},
|
||||
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'req' implicitly has an 'any' type.
|
||||
async function (req, accessToken, refreshToken, profile, done) {
|
||||
async function (
|
||||
req: Request,
|
||||
accessToken: string,
|
||||
refreshToken: string,
|
||||
profile: GoogleProfile,
|
||||
done: (
|
||||
err: Error | null,
|
||||
user: User | null,
|
||||
result?: AccountProvisionerResult
|
||||
) => void
|
||||
) {
|
||||
try {
|
||||
const domain = profile._json.hd;
|
||||
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import passport from "@outlinewiki/koa-passport";
|
||||
import { Request } from "koa";
|
||||
import Router from "koa-router";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module 'pass... Remove this comment to see the full error message
|
||||
import { Profile } from "passport";
|
||||
import { Strategy as SlackStrategy } from "passport-slack-oauth2";
|
||||
import accountProvisioner from "@server/commands/accountProvisioner";
|
||||
import accountProvisioner, {
|
||||
AccountProvisionerResult,
|
||||
} from "@server/commands/accountProvisioner";
|
||||
import env from "@server/env";
|
||||
import auth from "@server/middlewares/authentication";
|
||||
import passportMiddleware from "@server/middlewares/passport";
|
||||
@@ -11,11 +14,29 @@ import {
|
||||
Collection,
|
||||
Integration,
|
||||
Team,
|
||||
User,
|
||||
} from "@server/models";
|
||||
import { StateStore } from "@server/utils/passport";
|
||||
import * as Slack from "@server/utils/slack";
|
||||
import { assertPresent, assertUuid } from "@server/validation";
|
||||
|
||||
type SlackProfile = Profile & {
|
||||
team: {
|
||||
id: string;
|
||||
name: string;
|
||||
domain: string;
|
||||
image_192: string;
|
||||
image_230: string;
|
||||
};
|
||||
user: {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
image_192: string;
|
||||
image_230: string;
|
||||
};
|
||||
};
|
||||
|
||||
const router = new Router();
|
||||
const providerName = "slack";
|
||||
const SLACK_CLIENT_ID = process.env.SLACK_KEY;
|
||||
@@ -32,18 +53,28 @@ export const config = {
|
||||
enabled: !!SLACK_CLIENT_ID,
|
||||
};
|
||||
|
||||
if (SLACK_CLIENT_ID) {
|
||||
if (SLACK_CLIENT_ID && SLACK_CLIENT_SECRET) {
|
||||
const strategy = new SlackStrategy(
|
||||
{
|
||||
clientID: SLACK_CLIENT_ID,
|
||||
clientSecret: SLACK_CLIENT_SECRET,
|
||||
callbackURL: `${env.URL}/auth/slack.callback`,
|
||||
passReqToCallback: true,
|
||||
// @ts-expect-error StateStore
|
||||
store: new StateStore(),
|
||||
scope: scopes,
|
||||
},
|
||||
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'req' implicitly has an 'any' type.
|
||||
async function (req, accessToken, refreshToken, profile, done) {
|
||||
async function (
|
||||
req: Request,
|
||||
accessToken: string,
|
||||
refreshToken: string,
|
||||
profile: SlackProfile,
|
||||
done: (
|
||||
err: Error | null,
|
||||
user: User | null,
|
||||
result?: AccountProvisionerResult
|
||||
) => void
|
||||
) {
|
||||
try {
|
||||
const result = await accountProvisioner({
|
||||
ip: req.ip,
|
||||
|
||||
3
server/typings/outlinewiki__passport-azure-ad-oauth2.d.ts
vendored
Normal file
3
server/typings/outlinewiki__passport-azure-ad-oauth2.d.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
declare module "@outlinewiki/passport-azure-ad-oauth2" {
|
||||
export { default as Strategy } from "passport-oauth2";
|
||||
}
|
||||
3
server/typings/passport-google-oauth2.d.ts
vendored
Normal file
3
server/typings/passport-google-oauth2.d.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
declare module "passport-google-oauth2" {
|
||||
export { default as Strategy } from "passport-oauth2";
|
||||
}
|
||||
3
server/typings/passport-slack-oauth2.d.ts
vendored
Normal file
3
server/typings/passport-slack-oauth2.d.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
declare module "passport-slack-oauth2" {
|
||||
export { default as Strategy } from "passport-oauth2";
|
||||
}
|
||||
@@ -1,17 +1,18 @@
|
||||
import crypto from "crypto";
|
||||
import { addMinutes, subMinutes } from "date-fns";
|
||||
import type { Request } from "express";
|
||||
import fetch from "fetch-with-proxy";
|
||||
import { Context } from "koa";
|
||||
import {
|
||||
StateStoreStoreCallback,
|
||||
StateStoreVerifyCallback,
|
||||
} from "passport-oauth2";
|
||||
import { OAuthStateMismatchError } from "../errors";
|
||||
import { getCookieDomain } from "./domains";
|
||||
|
||||
export class StateStore {
|
||||
key = "state";
|
||||
|
||||
store = (
|
||||
ctx: Context,
|
||||
callback: (err: Error | null, state: string) => void
|
||||
) => {
|
||||
store = (ctx: Request, callback: StateStoreStoreCallback) => {
|
||||
// Produce a random string as state
|
||||
const state = crypto.randomBytes(8).toString("hex");
|
||||
|
||||
@@ -25,15 +26,17 @@ export class StateStore {
|
||||
};
|
||||
|
||||
verify = (
|
||||
ctx: Context,
|
||||
ctx: Request,
|
||||
providedState: string,
|
||||
callback: (err: Error | null, success?: boolean) => void
|
||||
callback: StateStoreVerifyCallback
|
||||
) => {
|
||||
const state = ctx.cookies.get(this.key);
|
||||
|
||||
if (!state) {
|
||||
return callback(
|
||||
OAuthStateMismatchError("State not return in OAuth flow")
|
||||
OAuthStateMismatchError("State not return in OAuth flow"),
|
||||
false,
|
||||
state
|
||||
);
|
||||
}
|
||||
|
||||
@@ -44,10 +47,11 @@ export class StateStore {
|
||||
});
|
||||
|
||||
if (state !== providedState) {
|
||||
return callback(OAuthStateMismatchError());
|
||||
return callback(OAuthStateMismatchError(), false, state);
|
||||
}
|
||||
|
||||
callback(null, true);
|
||||
// @ts-expect-error Type in library is wrong
|
||||
callback(null, true, state);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user