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";
// @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,
},
});

View File

@@ -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;

View File

@@ -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,

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 { 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);
};
}