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";
|
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,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
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 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);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user