From a779ba5c0ea8ec0405c3f6a222eaa127ee087252 Mon Sep 17 00:00:00 2001 From: Balu Babu Date: Tue, 14 Mar 2023 19:19:22 +0530 Subject: [PATCH] hotfix: adding dynamic redirection in self-host auth system (HBE-173) (#40) * chore: completed base auth implementation with redirectUrl * chore: completed base auth fix with redirect_uri * chore: added whitelist based redirection * chore: added a env variable for session secret in main.ts * chore: removed migrations folder from prisma directory --- packages/hoppscotch-backend/.env.example | 1 + packages/hoppscotch-backend/package.json | 1 + packages/hoppscotch-backend/pnpm-lock.yaml | 40 ++++++++++++++++++ .../src/auth/auth.controller.ts | 41 +++++++++++++------ .../src/auth/guards/github-sso.guard.ts | 15 +++++++ .../src/auth/guards/google-sso.guard.ts | 15 +++++++ .../src/auth/guards/microsoft-sso-.guard.ts | 15 +++++++ .../src/auth/guards/multi.guard.ts | 5 --- .../hoppscotch-backend/src/auth/helper.ts | 16 ++++++-- .../src/auth/strategies/github.strategy.ts | 1 + .../src/auth/strategies/google.strategy.ts | 1 + .../src/auth/strategies/microsoft.strategy.ts | 1 + packages/hoppscotch-backend/src/main.ts | 7 ++++ 13 files changed, 139 insertions(+), 20 deletions(-) create mode 100644 packages/hoppscotch-backend/src/auth/guards/github-sso.guard.ts create mode 100644 packages/hoppscotch-backend/src/auth/guards/google-sso.guard.ts create mode 100644 packages/hoppscotch-backend/src/auth/guards/microsoft-sso-.guard.ts delete mode 100644 packages/hoppscotch-backend/src/auth/guards/multi.guard.ts diff --git a/packages/hoppscotch-backend/.env.example b/packages/hoppscotch-backend/.env.example index 726cb8bc7..b3e2cc532 100644 --- a/packages/hoppscotch-backend/.env.example +++ b/packages/hoppscotch-backend/.env.example @@ -11,6 +11,7 @@ TOKEN_SALT_COMPLEXITY=10 MAGIC_LINK_TOKEN_VALIDITY=3 REFRESH_TOKEN_VALIDITY="604800000" # Default validity is 7 days ACCESS_TOKEN_VALIDITY="120000" # Default validity is 1 day +SESSION_SECRET='add some secret here' # Hoppscotch App Domain Config APP_DOMAIN="************************************************"" diff --git a/packages/hoppscotch-backend/package.json b/packages/hoppscotch-backend/package.json index ef2e23a40..1aa5be7f3 100644 --- a/packages/hoppscotch-backend/package.json +++ b/packages/hoppscotch-backend/package.json @@ -38,6 +38,7 @@ "cookie": "^0.5.0", "cookie-parser": "^1.4.6", "express": "^4.17.1", + "express-session": "^1.17.3", "fp-ts": "^2.13.1", "graphql": "^15.5.0", "graphql-query-complexity": "^0.12.0", diff --git a/packages/hoppscotch-backend/pnpm-lock.yaml b/packages/hoppscotch-backend/pnpm-lock.yaml index b79b0c6bc..7fd762484 100644 --- a/packages/hoppscotch-backend/pnpm-lock.yaml +++ b/packages/hoppscotch-backend/pnpm-lock.yaml @@ -38,6 +38,7 @@ specifiers: eslint-config-prettier: ^8.5.0 eslint-plugin-prettier: ^4.2.1 express: ^4.17.1 + express-session: ^1.17.3 fp-ts: ^2.13.1 graphql: ^15.5.0 graphql-query-complexity: ^0.12.0 @@ -85,6 +86,7 @@ dependencies: cookie: 0.5.0 cookie-parser: 1.4.6 express: 4.18.2 + express-session: 1.17.3 fp-ts: 2.13.1 graphql: 15.8.0 graphql-query-complexity: 0.12.0_graphql@15.8.0 @@ -2927,6 +2929,11 @@ packages: engines: {node: '>= 0.6'} dev: false + /cookie/0.4.2: + resolution: {integrity: sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==} + engines: {node: '>= 0.6'} + dev: false + /cookie/0.5.0: resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} engines: {node: '>= 0.6'} @@ -3376,6 +3383,22 @@ packages: jest-util: 29.4.3 dev: true + /express-session/1.17.3: + resolution: {integrity: sha512-4+otWXlShYlG1Ma+2Jnn+xgKUZTMJ5QD3YvfilX3AcocOAbIkVylSWEklzALe/+Pu4qV6TYBj5GwOBFfdKqLBw==} + engines: {node: '>= 0.8.0'} + dependencies: + cookie: 0.4.2 + cookie-signature: 1.0.6 + debug: 2.6.9 + depd: 2.0.0 + on-headers: 1.0.2 + parseurl: 1.3.3 + safe-buffer: 5.2.1 + uid-safe: 2.1.5 + transitivePeerDependencies: + - supports-color + dev: false + /express/4.18.2: resolution: {integrity: sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==} engines: {node: '>= 0.10.0'} @@ -5017,6 +5040,11 @@ packages: dependencies: ee-first: 1.1.1 + /on-headers/1.0.2: + resolution: {integrity: sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==} + engines: {node: '>= 0.8'} + dev: false + /once/1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} dependencies: @@ -5340,6 +5368,11 @@ packages: /queue-microtask/1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + /random-bytes/1.0.0: + resolution: {integrity: sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==} + engines: {node: '>= 0.8'} + dev: false + /randombytes/2.1.0: resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} dependencies: @@ -6092,6 +6125,13 @@ packages: hasBin: true dev: true + /uid-safe/2.1.5: + resolution: {integrity: sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==} + engines: {node: '>= 0.8'} + dependencies: + random-bytes: 1.0.0 + dev: false + /uid/2.0.1: resolution: {integrity: sha512-PF+1AnZgycpAIEmNtjxGBVmKbZAQguaa4pBUq6KNaGEcpzZ2klCNZLM34tsjp76maN00TttiiUf6zkIBpJQm2A==} engines: {node: '>=8'} diff --git a/packages/hoppscotch-backend/src/auth/auth.controller.ts b/packages/hoppscotch-backend/src/auth/auth.controller.ts index bde2fc906..c456a27f2 100644 --- a/packages/hoppscotch-backend/src/auth/auth.controller.ts +++ b/packages/hoppscotch-backend/src/auth/auth.controller.ts @@ -20,7 +20,9 @@ import { AuthUser } from 'src/types/AuthUser'; import { RTCookie } from 'src/decorators/rt-cookie.decorator'; import { AuthGuard } from '@nestjs/passport'; import { authCookieHandler, throwHTTPErr } from './helper'; - +import { GoogleSSOGuard } from './guards/google-sso.guard'; +import { GithubSSOGuard } from './guards/github-sso.guard'; +import { MicrosoftSSOGuard } from './guards/microsoft-sso-.guard'; @Controller({ path: 'auth', version: '1' }) export class AuthController { constructor(private authService: AuthService) {} @@ -44,7 +46,7 @@ export class AuthController { async verify(@Body() data: VerifyMagicDto, @Res() res: Response) { const authTokens = await this.authService.verifyMagicLinkTokens(data); if (E.isLeft(authTokens)) throwHTTPErr(authTokens.left); - authCookieHandler(res, authTokens.right, false); + authCookieHandler(res, authTokens.right, false, null); } /** @@ -63,14 +65,14 @@ export class AuthController { user, ); if (E.isLeft(newTokenPair)) throwHTTPErr(newTokenPair.left); - authCookieHandler(res, newTokenPair.right, false); + authCookieHandler(res, newTokenPair.right, false, null); } /** ** Route to initiate SSO auth via Google */ @Get('google') - @UseGuards(AuthGuard('google')) + @UseGuards(GoogleSSOGuard) async googleAuth(@Request() req) {} /** @@ -78,18 +80,23 @@ export class AuthController { * @see https://auth0.com/docs/get-started/authentication-and-authorization-flow/authorization-code-flow#how-it-works */ @Get('google/callback') - @UseGuards(AuthGuard('google')) + @UseGuards(GoogleSSOGuard) async googleAuthRedirect(@Request() req, @Res() res) { const authTokens = await this.authService.generateAuthTokens(req.user.uid); if (E.isLeft(authTokens)) throwHTTPErr(authTokens.left); - authCookieHandler(res, authTokens.right, true); + authCookieHandler( + res, + authTokens.right, + true, + req.authInfo.state.redirect_uri, + ); } /** ** Route to initiate SSO auth via Github */ @Get('github') - @UseGuards(AuthGuard('github')) + @UseGuards(GithubSSOGuard) async githubAuth(@Request() req) {} /** @@ -97,18 +104,23 @@ export class AuthController { * @see https://auth0.com/docs/get-started/authentication-and-authorization-flow/authorization-code-flow#how-it-works */ @Get('github/callback') - @UseGuards(AuthGuard('github')) + @UseGuards(GithubSSOGuard) async githubAuthRedirect(@Request() req, @Res() res) { const authTokens = await this.authService.generateAuthTokens(req.user.uid); if (E.isLeft(authTokens)) throwHTTPErr(authTokens.left); - authCookieHandler(res, authTokens.right, true); + authCookieHandler( + res, + authTokens.right, + true, + req.authInfo.state.redirect_uri, + ); } /** ** Route to initiate SSO auth via Microsoft */ @Get('microsoft') - @UseGuards(AuthGuard('microsoft')) + @UseGuards(MicrosoftSSOGuard) async microsoftAuth(@Request() req) {} /** @@ -116,11 +128,16 @@ export class AuthController { * @see https://auth0.com/docs/get-started/authentication-and-authorization-flow/authorization-code-flow#how-it-works */ @Get('microsoft/callback') - @UseGuards(AuthGuard('microsoft')) + @UseGuards(MicrosoftSSOGuard) async microsoftAuthRedirect(@Request() req, @Res() res) { const authTokens = await this.authService.generateAuthTokens(req.user.uid); if (E.isLeft(authTokens)) throwHTTPErr(authTokens.left); - authCookieHandler(res, authTokens.right, true); + authCookieHandler( + res, + authTokens.right, + true, + req.authInfo.state.redirect_uri, + ); } /** diff --git a/packages/hoppscotch-backend/src/auth/guards/github-sso.guard.ts b/packages/hoppscotch-backend/src/auth/guards/github-sso.guard.ts new file mode 100644 index 000000000..a76b13c8f --- /dev/null +++ b/packages/hoppscotch-backend/src/auth/guards/github-sso.guard.ts @@ -0,0 +1,15 @@ +import { ExecutionContext, Injectable } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; + +@Injectable() +export class GithubSSOGuard extends AuthGuard('github') { + getAuthenticateOptions(context: ExecutionContext) { + const req = context.switchToHttp().getRequest(); + + return { + state: { + redirect_uri: req.query.redirect_uri, + }, + }; + } +} diff --git a/packages/hoppscotch-backend/src/auth/guards/google-sso.guard.ts b/packages/hoppscotch-backend/src/auth/guards/google-sso.guard.ts new file mode 100644 index 000000000..ea5094a1a --- /dev/null +++ b/packages/hoppscotch-backend/src/auth/guards/google-sso.guard.ts @@ -0,0 +1,15 @@ +import { ExecutionContext, Injectable } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; + +@Injectable() +export class GoogleSSOGuard extends AuthGuard('google') { + getAuthenticateOptions(context: ExecutionContext) { + const req = context.switchToHttp().getRequest(); + + return { + state: { + redirect_uri: req.query.redirect_uri, + }, + }; + } +} diff --git a/packages/hoppscotch-backend/src/auth/guards/microsoft-sso-.guard.ts b/packages/hoppscotch-backend/src/auth/guards/microsoft-sso-.guard.ts new file mode 100644 index 000000000..8479c57cb --- /dev/null +++ b/packages/hoppscotch-backend/src/auth/guards/microsoft-sso-.guard.ts @@ -0,0 +1,15 @@ +import { ExecutionContext, Injectable } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; + +@Injectable() +export class MicrosoftSSOGuard extends AuthGuard('microsoft') { + getAuthenticateOptions(context: ExecutionContext) { + const req = context.switchToHttp().getRequest(); + + return { + state: { + redirect_uri: req.query.redirect_uri, + }, + }; + } +} diff --git a/packages/hoppscotch-backend/src/auth/guards/multi.guard.ts b/packages/hoppscotch-backend/src/auth/guards/multi.guard.ts deleted file mode 100644 index 42fe8c1d7..000000000 --- a/packages/hoppscotch-backend/src/auth/guards/multi.guard.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { AuthGuard } from '@nestjs/passport'; - -@Injectable() -export class MultiAuthGuard extends AuthGuard(['google1', 'google2']) {} diff --git a/packages/hoppscotch-backend/src/auth/helper.ts b/packages/hoppscotch-backend/src/auth/helper.ts index add663c11..9a6ca0113 100644 --- a/packages/hoppscotch-backend/src/auth/helper.ts +++ b/packages/hoppscotch-backend/src/auth/helper.ts @@ -30,6 +30,7 @@ export const authCookieHandler = ( res: Response, authTokens: AuthTokens, redirect: boolean, + redirectUrl: string | null, ) => { const currentTime = DateTime.now(); const accessTokenValidity = currentTime @@ -55,9 +56,18 @@ export const authCookieHandler = ( sameSite: 'lax', maxAge: refreshTokenValidity, }); - if (redirect) { - res.status(HttpStatus.OK).redirect(process.env.REDIRECT_URL); - } else res.status(HttpStatus.OK).send(); + + if (!redirect) { + res.status(HttpStatus.OK).send(); + } + + // check to see if redirectUrl is a whitelisted url + const whitelistedOrigins = process.env.WHITELISTED_ORIGINS.split(','); + if (!whitelistedOrigins.includes(redirectUrl)) + // if it is not redirect by default to REDIRECT_URL + redirectUrl = process.env.REDIRECT_URL; + + res.status(HttpStatus.OK).redirect(redirectUrl); }; /** diff --git a/packages/hoppscotch-backend/src/auth/strategies/github.strategy.ts b/packages/hoppscotch-backend/src/auth/strategies/github.strategy.ts index e01bc59a0..d95f27219 100644 --- a/packages/hoppscotch-backend/src/auth/strategies/github.strategy.ts +++ b/packages/hoppscotch-backend/src/auth/strategies/github.strategy.ts @@ -17,6 +17,7 @@ export class GithubStrategy extends PassportStrategy(Strategy) { clientSecret: process.env.GITHUB_CLIENT_SECRET, callbackURL: process.env.GITHUB_CALLBACK_URL, scope: [process.env.GITHUB_SCOPE], + store: true, }); } diff --git a/packages/hoppscotch-backend/src/auth/strategies/google.strategy.ts b/packages/hoppscotch-backend/src/auth/strategies/google.strategy.ts index 7dd5cc2d7..4df6cb472 100644 --- a/packages/hoppscotch-backend/src/auth/strategies/google.strategy.ts +++ b/packages/hoppscotch-backend/src/auth/strategies/google.strategy.ts @@ -18,6 +18,7 @@ export class GoogleStrategy extends PassportStrategy(Strategy) { callbackURL: process.env.GOOGLE_CALLBACK_URL, scope: process.env.GOOGLE_SCOPE.split(','), passReqToCallback: true, + store: true, }); } diff --git a/packages/hoppscotch-backend/src/auth/strategies/microsoft.strategy.ts b/packages/hoppscotch-backend/src/auth/strategies/microsoft.strategy.ts index cdffce8e6..0904d069b 100644 --- a/packages/hoppscotch-backend/src/auth/strategies/microsoft.strategy.ts +++ b/packages/hoppscotch-backend/src/auth/strategies/microsoft.strategy.ts @@ -17,6 +17,7 @@ export class MicrosoftStrategy extends PassportStrategy(Strategy) { clientSecret: process.env.MICROSOFT_CLIENT_SECRET, callbackURL: process.env.MICROSOFT_CALLBACK_URL, scope: [process.env.MICROSOFT_SCOPE], + store: true, }); } diff --git a/packages/hoppscotch-backend/src/main.ts b/packages/hoppscotch-backend/src/main.ts index 4e1d06c56..aedac38d0 100644 --- a/packages/hoppscotch-backend/src/main.ts +++ b/packages/hoppscotch-backend/src/main.ts @@ -3,6 +3,7 @@ import { json } from 'express'; import { AppModule } from './app.module'; import * as cookieParser from 'cookie-parser'; import { VersioningType } from '@nestjs/common'; +import * as session from 'express-session'; import { emitGQLSchemaFile } from './gql-schema'; async function bootstrap() { @@ -12,6 +13,12 @@ async function bootstrap() { const app = await NestFactory.create(AppModule); + app.use( + session({ + secret: process.env.SESSION_SECRET, + }), + ); + // Increase fil upload limit to 50MB app.use( json({