perf: Improve speed of Azure login (parallelize two slow API requests)

chore: Improved types around passport
This commit is contained in:
Tom Moor
2022-04-30 16:57:58 -07:00
parent a736022c39
commit bb074edb0d
7 changed files with 115 additions and 42 deletions

View File

@@ -1,12 +1,16 @@
import passport from "@outlinewiki/koa-passport"; 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 { Strategy as AzureStrategy } from "@outlinewiki/passport-azure-ad-oauth2";
import jwt from "jsonwebtoken"; import jwt from "jsonwebtoken";
import { Request } from "koa";
import Router from "koa-router"; 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 env from "@server/env";
import { MicrosoftGraphError } from "@server/errors"; import { MicrosoftGraphError } from "@server/errors";
import passportMiddleware from "@server/middlewares/passport"; import passportMiddleware from "@server/middlewares/passport";
import { User } from "@server/models";
import { StateStore, request } from "@server/utils/passport"; import { StateStore, request } from "@server/utils/passport";
const router = new Router(); const router = new Router();
@@ -14,15 +18,14 @@ const providerName = "azure";
const AZURE_CLIENT_ID = process.env.AZURE_CLIENT_ID; const AZURE_CLIENT_ID = process.env.AZURE_CLIENT_ID;
const AZURE_CLIENT_SECRET = process.env.AZURE_CLIENT_SECRET; const AZURE_CLIENT_SECRET = process.env.AZURE_CLIENT_SECRET;
const AZURE_RESOURCE_APP_ID = process.env.AZURE_RESOURCE_APP_ID; 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: string[] = [];
const scopes = [];
export const config = { export const config = {
name: "Microsoft", name: "Microsoft",
enabled: !!AZURE_CLIENT_ID, enabled: !!AZURE_CLIENT_ID,
}; };
if (AZURE_CLIENT_ID) { if (AZURE_CLIENT_ID && AZURE_CLIENT_SECRET) {
const strategy = new AzureStrategy( const strategy = new AzureStrategy(
{ {
clientID: AZURE_CLIENT_ID, clientID: AZURE_CLIENT_ID,
@@ -31,23 +34,35 @@ if (AZURE_CLIENT_ID) {
useCommonEndpoint: true, useCommonEndpoint: true,
passReqToCallback: true, passReqToCallback: true,
resource: AZURE_RESOURCE_APP_ID, resource: AZURE_RESOURCE_APP_ID,
// @ts-expect-error StateStore
store: new StateStore(), store: new StateStore(),
// @ts-expect-error ts-migrate(7005) FIXME: Variable 'scopes' implicitly has an 'any[]' type.
scope: scopes, scope: scopes,
}, },
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'req' implicitly has an 'any' type. async function (
async function (req, accessToken, refreshToken, params, _, done) { req: Request,
accessToken: string,
refreshToken: string,
params: { id_token: string },
_profile: Profile,
done: (
err: Error | null,
user: User | null,
result?: AccountProvisionerResult
) => void
) {
try { try {
// see docs for what the fields in profile represent here: // see docs for what the fields in profile represent here:
// https://docs.microsoft.com/en-us/azure/active-directory/develop/access-tokens // https://docs.microsoft.com/en-us/azure/active-directory/develop/access-tokens
const profile = jwt.decode(params.id_token) as jwt.JwtPayload; const profile = jwt.decode(params.id_token) as jwt.JwtPayload;
// Load the users profile from the Microsoft Graph API const [profileResponse, organizationResponse] = await Promise.all([
// https://docs.microsoft.com/en-us/graph/api/resources/users?view=graph-rest-1.0 // Load the users profile from the Microsoft Graph API
const profileResponse = await request( // https://docs.microsoft.com/en-us/graph/api/resources/users?view=graph-rest-1.0
`https://graph.microsoft.com/v1.0/me`, request(`https://graph.microsoft.com/v1.0/me`, accessToken),
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) { if (!profileResponse) {
throw MicrosoftGraphError( 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) { if (!organizationResponse) {
throw MicrosoftGraphError( throw MicrosoftGraphError(
"Unable to load organization info from Microsoft Graph API" "Unable to load organization info from Microsoft Graph API"
@@ -100,7 +108,6 @@ if (AZURE_CLIENT_ID) {
providerId: profile.oid, providerId: profile.oid,
accessToken, accessToken,
refreshToken, refreshToken,
// @ts-expect-error ts-migrate(7005) FIXME: Variable 'scopes' implicitly has an 'any[]' type.
scopes, scopes,
}, },
}); });

View File

@@ -1,15 +1,19 @@
import passport from "@outlinewiki/koa-passport"; import passport from "@outlinewiki/koa-passport";
import { Request } from "koa";
import Router from "koa-router"; import Router from "koa-router";
import { capitalize } from "lodash"; 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 { 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 env from "@server/env";
import { import {
GoogleWorkspaceRequiredError, GoogleWorkspaceRequiredError,
GoogleWorkspaceInvalidError, GoogleWorkspaceInvalidError,
} from "@server/errors"; } from "@server/errors";
import passportMiddleware from "@server/middlewares/passport"; import passportMiddleware from "@server/middlewares/passport";
import { User } from "@server/models";
import { isDomainAllowed } from "@server/utils/authentication"; import { isDomainAllowed } from "@server/utils/authentication";
import { StateStore } from "@server/utils/passport"; import { StateStore } from "@server/utils/passport";
@@ -27,7 +31,15 @@ export const config = {
enabled: !!GOOGLE_CLIENT_ID, 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( passport.use(
new GoogleStrategy( new GoogleStrategy(
{ {
@@ -35,11 +47,21 @@ if (GOOGLE_CLIENT_ID) {
clientSecret: GOOGLE_CLIENT_SECRET, clientSecret: GOOGLE_CLIENT_SECRET,
callbackURL: `${env.URL}/auth/google.callback`, callbackURL: `${env.URL}/auth/google.callback`,
passReqToCallback: true, passReqToCallback: true,
// @ts-expect-error StateStore
store: new StateStore(), store: new StateStore(),
scope: scopes, scope: scopes,
}, },
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'req' implicitly has an 'any' type. async function (
async function (req, accessToken, refreshToken, profile, done) { req: Request,
accessToken: string,
refreshToken: string,
profile: GoogleProfile,
done: (
err: Error | null,
user: User | null,
result?: AccountProvisionerResult
) => void
) {
try { try {
const domain = profile._json.hd; const domain = profile._json.hd;

View File

@@ -1,8 +1,11 @@
import passport from "@outlinewiki/koa-passport"; import passport from "@outlinewiki/koa-passport";
import { Request } from "koa";
import Router from "koa-router"; 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 { 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 env from "@server/env";
import auth from "@server/middlewares/authentication"; import auth from "@server/middlewares/authentication";
import passportMiddleware from "@server/middlewares/passport"; import passportMiddleware from "@server/middlewares/passport";
@@ -11,11 +14,29 @@ import {
Collection, Collection,
Integration, Integration,
Team, Team,
User,
} from "@server/models"; } from "@server/models";
import { StateStore } from "@server/utils/passport"; import { StateStore } from "@server/utils/passport";
import * as Slack from "@server/utils/slack"; import * as Slack from "@server/utils/slack";
import { assertPresent, assertUuid } from "@server/validation"; 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 router = new Router();
const providerName = "slack"; const providerName = "slack";
const SLACK_CLIENT_ID = process.env.SLACK_KEY; const SLACK_CLIENT_ID = process.env.SLACK_KEY;
@@ -32,18 +53,28 @@ export const config = {
enabled: !!SLACK_CLIENT_ID, enabled: !!SLACK_CLIENT_ID,
}; };
if (SLACK_CLIENT_ID) { if (SLACK_CLIENT_ID && SLACK_CLIENT_SECRET) {
const strategy = new SlackStrategy( const strategy = new SlackStrategy(
{ {
clientID: SLACK_CLIENT_ID, clientID: SLACK_CLIENT_ID,
clientSecret: SLACK_CLIENT_SECRET, clientSecret: SLACK_CLIENT_SECRET,
callbackURL: `${env.URL}/auth/slack.callback`, callbackURL: `${env.URL}/auth/slack.callback`,
passReqToCallback: true, passReqToCallback: true,
// @ts-expect-error StateStore
store: new StateStore(), store: new StateStore(),
scope: scopes, scope: scopes,
}, },
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'req' implicitly has an 'any' type. async function (
async function (req, accessToken, refreshToken, profile, done) { req: Request,
accessToken: string,
refreshToken: string,
profile: SlackProfile,
done: (
err: Error | null,
user: User | null,
result?: AccountProvisionerResult
) => void
) {
try { try {
const result = await accountProvisioner({ const result = await accountProvisioner({
ip: req.ip, ip: req.ip,

View File

@@ -0,0 +1,3 @@
declare module "@outlinewiki/passport-azure-ad-oauth2" {
export { default as Strategy } from "passport-oauth2";
}

View File

@@ -0,0 +1,3 @@
declare module "passport-google-oauth2" {
export { default as Strategy } from "passport-oauth2";
}

View File

@@ -0,0 +1,3 @@
declare module "passport-slack-oauth2" {
export { default as Strategy } from "passport-oauth2";
}

View File

@@ -1,17 +1,18 @@
import crypto from "crypto"; import crypto from "crypto";
import { addMinutes, subMinutes } from "date-fns"; import { addMinutes, subMinutes } from "date-fns";
import type { Request } from "express";
import fetch from "fetch-with-proxy"; import fetch from "fetch-with-proxy";
import { Context } from "koa"; import {
StateStoreStoreCallback,
StateStoreVerifyCallback,
} from "passport-oauth2";
import { OAuthStateMismatchError } from "../errors"; import { OAuthStateMismatchError } from "../errors";
import { getCookieDomain } from "./domains"; import { getCookieDomain } from "./domains";
export class StateStore { export class StateStore {
key = "state"; key = "state";
store = ( store = (ctx: Request, callback: StateStoreStoreCallback) => {
ctx: Context,
callback: (err: Error | null, state: string) => void
) => {
// Produce a random string as state // Produce a random string as state
const state = crypto.randomBytes(8).toString("hex"); const state = crypto.randomBytes(8).toString("hex");
@@ -25,15 +26,17 @@ export class StateStore {
}; };
verify = ( verify = (
ctx: Context, ctx: Request,
providedState: string, providedState: string,
callback: (err: Error | null, success?: boolean) => void callback: StateStoreVerifyCallback
) => { ) => {
const state = ctx.cookies.get(this.key); const state = ctx.cookies.get(this.key);
if (!state) { if (!state) {
return callback( 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) { 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);
}; };
} }