diff --git a/server/routes/auth/providers/azure.ts b/server/routes/auth/providers/azure.ts index 9adefe81b..446ebe667 100644 --- a/server/routes/auth/providers/azure.ts +++ b/server/routes/auth/providers/azure.ts @@ -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, }, }); diff --git a/server/routes/auth/providers/google.ts b/server/routes/auth/providers/google.ts index 91c157fbc..95e827935 100644 --- a/server/routes/auth/providers/google.ts +++ b/server/routes/auth/providers/google.ts @@ -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; diff --git a/server/routes/auth/providers/slack.ts b/server/routes/auth/providers/slack.ts index a3c492852..caafdb2bb 100644 --- a/server/routes/auth/providers/slack.ts +++ b/server/routes/auth/providers/slack.ts @@ -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, diff --git a/server/typings/outlinewiki__passport-azure-ad-oauth2.d.ts b/server/typings/outlinewiki__passport-azure-ad-oauth2.d.ts new file mode 100644 index 000000000..c86eb2b48 --- /dev/null +++ b/server/typings/outlinewiki__passport-azure-ad-oauth2.d.ts @@ -0,0 +1,3 @@ +declare module "@outlinewiki/passport-azure-ad-oauth2" { + export { default as Strategy } from "passport-oauth2"; +} diff --git a/server/typings/passport-google-oauth2.d.ts b/server/typings/passport-google-oauth2.d.ts new file mode 100644 index 000000000..40883c914 --- /dev/null +++ b/server/typings/passport-google-oauth2.d.ts @@ -0,0 +1,3 @@ +declare module "passport-google-oauth2" { + export { default as Strategy } from "passport-oauth2"; +} diff --git a/server/typings/passport-slack-oauth2.d.ts b/server/typings/passport-slack-oauth2.d.ts new file mode 100644 index 000000000..6029bfa71 --- /dev/null +++ b/server/typings/passport-slack-oauth2.d.ts @@ -0,0 +1,3 @@ +declare module "passport-slack-oauth2" { + export { default as Strategy } from "passport-oauth2"; +} diff --git a/server/utils/passport.ts b/server/utils/passport.ts index b1ae3d919..2df44dc42 100644 --- a/server/utils/passport.ts +++ b/server/utils/passport.ts @@ -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); }; }