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
This commit is contained in:
@@ -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="************************************************""
|
||||
|
||||
@@ -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",
|
||||
|
||||
40
packages/hoppscotch-backend/pnpm-lock.yaml
generated
40
packages/hoppscotch-backend/pnpm-lock.yaml
generated
@@ -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'}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
|
||||
@Injectable()
|
||||
export class MultiAuthGuard extends AuthGuard(['google1', 'google2']) {}
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user