diff --git a/packages/nestjs-oidc/package.json b/packages/nestjs-oidc/package.json index e0f5536..18b145f 100644 --- a/packages/nestjs-oidc/package.json +++ b/packages/nestjs-oidc/package.json @@ -1,7 +1,7 @@ { "name": "nestjs-oidc", "description": "OpenID-Connect client module for NestJS.", - "version": "0.0.8", + "version": "0.1.0", "author": "Hubert", "main": "lib/index.js", "module": "esm/index.js", @@ -39,7 +39,7 @@ ], "scripts": { "serve": "run -T concurrently --raw \"tsc --project tsconfig.json -watch\"", - "build": "run -T rimraf dist && yarn build:version && yarn build:cjs && yarn build:esm", + "build": "run -T rimraf -rf lib esm dist && yarn build:version && yarn build:cjs && yarn build:esm", "build:version": "node -p \"'export const version = ' + JSON.stringify(require('./package.json').version) + ';'\" > src/version.ts", "build:cjs": "run -T tsc --project tsconfig.build.json", "build:esm": "run -T tsc --project tsconfig.build.json --module es2015 --outDir esm", @@ -52,8 +52,10 @@ "dependencies": { "@nestjs/passport": "^10.0.0", "cookie": "^0.5.0", + "express-session": "^1.17.3", "flatted": "^3.2.6", "jose": "^4.15.9", + "memorystore": "^1.6.7", "openid-client": "^5.6.5", "passport": "^0.6.0", "querystring": "^0.2.1", @@ -67,14 +69,14 @@ "@types/passport": "^1.0.9", "@types/uuid": "^9.0.7", "connect-mongo": "^5.1.0", - "express-session": "^1.17.3" + "connect-redis": "^7.1.1" }, "peerDependencies": { "@nestjs/common": "^10.0.0", "@nestjs/core": "^10.0.0", "@nestjs/graphql": "^11.0.0", "connect-mongo": "4.6.0", - "express-session": "^1.17.3", + "connect-redis": "^7.1.1", "graphql": "^16.0.0", "graphql-parse-resolve-info": "^4.12.0" }, @@ -85,7 +87,7 @@ "connect-mongo": { "optional": true }, - "express-session": { + "connect-redis": { "optional": true }, "graphql": { diff --git a/packages/nestjs-oidc/src/filters/misdirected-exception.filter.ts b/packages/nestjs-oidc/src/filters/misdirected-exception.filter.ts index b6f8880..d95e28c 100644 --- a/packages/nestjs-oidc/src/filters/misdirected-exception.filter.ts +++ b/packages/nestjs-oidc/src/filters/misdirected-exception.filter.ts @@ -25,9 +25,9 @@ export class MisdirectedExceptionFilter implements ExceptionFilter { const requestedChannel = request.params.channelType; const originalTenant = request.user?.profile.tenantId; const originalChannel = request.user?.profile.channelType; - httpAdapter.redirect( + return httpAdapter.redirect( response, - HttpStatus.MOVED_PERMANENTLY, + HttpStatus.FOUND, `/tenant-switch-warn?requestedTenant=${requestedTenant}&requestedChannel=${requestedChannel}&originalTenant=${originalTenant}&originalChannel=${originalChannel}`, ); } diff --git a/packages/nestjs-oidc/src/filters/unauthorized.filter.ts b/packages/nestjs-oidc/src/filters/unauthorized.filter.ts index b896a78..7f53734 100644 --- a/packages/nestjs-oidc/src/filters/unauthorized.filter.ts +++ b/packages/nestjs-oidc/src/filters/unauthorized.filter.ts @@ -31,9 +31,9 @@ export class UnauthorizedFilter implements ExceptionFilter { const prefix = this.getPrefixFromRequest(request); // If you're using the multitenancy authentication, you'll need to get the prefix - httpAdapter.redirect( + return httpAdapter.redirect( response, - HttpStatus.MOVED_PERMANENTLY, + HttpStatus.FOUND, `${prefix}/login?redirect=${httpAdapter.getRequestUrl(request)}${searchParams ? `&${searchParams}` : ''}`, ); } diff --git a/packages/nestjs-oidc/src/oidc.setup-session.ts b/packages/nestjs-oidc/src/oidc.setup-session.ts index 4f5f920..f613c7e 100644 --- a/packages/nestjs-oidc/src/oidc.setup-session.ts +++ b/packages/nestjs-oidc/src/oidc.setup-session.ts @@ -1,6 +1,29 @@ import { INestApplication } from '@nestjs/common'; +import { SessionOptions } from 'express-session'; import { sessionInMemory } from './session/session-in-memory'; +import { sessionInMongo } from './session/session-in-mongo'; +import { sessionInRedis } from './session/session-in-redis'; -export const setupSession = (app: INestApplication, name: string) => { - return sessionInMemory(app, name); -}; +/** + * setup session + */ +export function setupSession( + app: INestApplication, + type: 'memory' | 'mongo' | 'redis' = 'memory', + options?: Partial & { + [key: string]: any; + }, +) { + switch (type) { + case 'memory': + return sessionInMemory(app, options); + case 'mongo': + if (!options?.connectMongoOptions) throw new Error('connectMongoOptions is required for session type mongo'); + return sessionInMongo(app, options as any); + case 'redis': + if (!options?.connectRedisOptions) throw new Error('connectRedisOptions is required for session type redis'); + return sessionInRedis(app, options as any); + default: + throw new Error(`session type ${type} is not supported`); + } +} diff --git a/packages/nestjs-oidc/src/session/base-session.ts b/packages/nestjs-oidc/src/session/base-session.ts new file mode 100644 index 0000000..aab167d --- /dev/null +++ b/packages/nestjs-oidc/src/session/base-session.ts @@ -0,0 +1,37 @@ +import session, { SessionOptions } from 'express-session'; +import { v4 as uuid } from 'uuid'; +import { ChannelType } from '../interfaces/index'; + +// https://github.com/expressjs/session/issues/725#issuecomment-605922223 +Object.defineProperty(session.Cookie.prototype, 'sameSite', { + // sameSite cannot be set to `None` if cookie is not marked secure + get() { + return this._sameSite === 'none' && !this.secure ? 'lax' : this._sameSite; + }, + set(value) { + this._sameSite = value; + }, +}); + +export const defaultOptions = function (): SessionOptions { + return { + secret: process.env.SESSION_SECRET || uuid(), // to sign session id + resave: false, // will default to false in near future: https://github.com/expressjs/session#resave + saveUninitialized: false, // will default to false in near future: https://github.com/expressjs/session#saveuninitialized + rolling: true, // keep session alive + proxy: true, // trust first proxy + cookie: { + maxAge: 60 * 60 * 1000, // session expires in 1hr, refreshed by `rolling: true` option. + httpOnly: true, // so that cookie can't be accessed via client-side script + secure: 'auto', // set to true if your communication is over HTTPS + sameSite: 'none', // set to 'none' if your communication is over HTTPS + }, + }; +}; + +declare module 'express-session' { + interface SessionData { + tenantId?: string; + channelType?: ChannelType; + } +} diff --git a/packages/nestjs-oidc/src/session/express-session.ts b/packages/nestjs-oidc/src/session/express-session.ts deleted file mode 100644 index 21849f9..0000000 --- a/packages/nestjs-oidc/src/session/express-session.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { loadPackage } from '@nestjs/common/utils/load-package.util'; -import { SessionOptions } from 'express-session'; -import { v4 as uuid } from 'uuid'; -import { ChannelType } from '../interfaces/index'; - -const defaultOptions: SessionOptions = { - secret: process.env.SESSION_SECRET || uuid(), // to sign session id - resave: false, // will default to false in near future: https://github.com/expressjs/session#resave - saveUninitialized: false, // will default to false in near future: https://github.com/expressjs/session#saveuninitialized - rolling: true, // keep session alive - cookie: { - maxAge: 30 * 60 * 1000, // session expires in 1hr, refreshed by `rolling: true` option. - httpOnly: true, // so that cookie can't be accessed via client-side script - secure: false, - //sameSite: 'lax', - }, -}; - -export function createExpressSession(options: Partial) { - const session = loadPackage('express-session', 'MemoryStore', () => - require('express-session'), - ) as typeof import('express-session'); - - // if (process.env.NODE_ENV === 'production') { - // defaultOptions.cookie.secure = true; // https only - // } - - return session({ - ...defaultOptions, - ...options, - }); -} - -declare module 'express-session' { - interface SessionData { - tenantId?: string; - channelType?: ChannelType; - } -} diff --git a/packages/nestjs-oidc/src/session/index.ts b/packages/nestjs-oidc/src/session/index.ts index 8010ac1..3237c1f 100644 --- a/packages/nestjs-oidc/src/session/index.ts +++ b/packages/nestjs-oidc/src/session/index.ts @@ -1,2 +1,3 @@ -export * from './session-in-memory'; -export * from './session-mongo'; +export * from './session-in-memory'; +export * from './session-in-mongo'; +export * from './session-in-redis'; diff --git a/packages/nestjs-oidc/src/session/session-in-memory.ts b/packages/nestjs-oidc/src/session/session-in-memory.ts index 546d5d9..1f39772 100644 --- a/packages/nestjs-oidc/src/session/session-in-memory.ts +++ b/packages/nestjs-oidc/src/session/session-in-memory.ts @@ -1,18 +1,31 @@ import { INestApplication } from '@nestjs/common'; +import session, { SessionOptions } from 'express-session'; +import createMemoryStore from 'memorystore'; import passport from 'passport'; -import { createExpressSession } from './express-session'; +import { defaultOptions } from './base-session'; +const MomeryStore = createMemoryStore(session); + +/** + * setup session with in-memory store + */ export function sessionInMemory( app: INestApplication, - name: string, - options?: { - sessionStrategy?: (options: { name: string; [key: string]: any }) => any; - // rest of sessionStrategy options - [key: string]: any; + options?: Partial & { + memoryOptions?: ConstructorParameters>[0]; }, ) { - const { sessionStrategy, ...rest } = options ?? {}; - app.use((sessionStrategy ?? createExpressSession)({ ...rest, name })); + const { memoryOptions, ...rest } = options ?? {}; + app.use( + session({ + ...defaultOptions(), + ...rest, + store: new MomeryStore({ + checkPeriod: 86400000, // prune expired entries every 24h + ...memoryOptions, + }), + }), + ); app.use(passport.initialize()); app.use(passport.session()); } diff --git a/packages/nestjs-oidc/src/session/session-mongo.ts b/packages/nestjs-oidc/src/session/session-in-mongo.ts similarity index 63% rename from packages/nestjs-oidc/src/session/session-mongo.ts rename to packages/nestjs-oidc/src/session/session-in-mongo.ts index ce929be..fa6ad15 100644 --- a/packages/nestjs-oidc/src/session/session-mongo.ts +++ b/packages/nestjs-oidc/src/session/session-in-mongo.ts @@ -1,28 +1,28 @@ import { INestApplication } from '@nestjs/common'; import { loadPackage } from '@nestjs/common/utils/load-package.util'; +import session, { SessionOptions } from 'express-session'; import passport from 'passport'; import ConnectMongo from 'connect-mongo'; -import { createExpressSession } from './express-session'; +import { defaultOptions } from './base-session'; -export const sessionMongo = ( +/** + * setup session with mongo store + */ +export const sessionInMongo = ( app: INestApplication, - name: string, - options: { + options: Partial & { connectMongoOptions: ConnectMongoOptions; - sessionStrategy?: (options: { name: string; store: ConnectMongo; [key: string]: any }) => any; - // rest of sessionStrategy options - [key: string]: any; }, ) => { const MongoStore = loadPackage('connect-mongo', 'SessionModule', () => require('connect-mongo'), ) as typeof ConnectMongo; - const { sessionStrategy, connectMongoOptions, ...rest } = options; + const { connectMongoOptions, ...rest } = options; app.use( - (sessionStrategy ?? createExpressSession)({ + session({ + ...defaultOptions(), ...rest, - name, store: MongoStore.create(connectMongoOptions), }), ); diff --git a/packages/nestjs-oidc/src/session/session-in-redis.ts b/packages/nestjs-oidc/src/session/session-in-redis.ts new file mode 100644 index 0000000..3f7b60c --- /dev/null +++ b/packages/nestjs-oidc/src/session/session-in-redis.ts @@ -0,0 +1,30 @@ +import { INestApplication } from '@nestjs/common'; +import { loadPackage } from '@nestjs/common/utils/load-package.util'; +import session, { SessionOptions } from 'express-session'; +import passport from 'passport'; +import ConnectRedis from 'connect-redis'; +import { defaultOptions } from './base-session'; + +/** + * setup session with redis store + */ +export const sessionInRedis = ( + app: INestApplication, + options: Partial & { + connectRedisOptions: ConstructorParameters[0]; + }, +) => { + const RedisStore = loadPackage('connect-redis', 'SessionModule', () => require('connect-redis')) + .default as typeof ConnectRedis; + + const { connectRedisOptions, ...rest } = options; + app.use( + session({ + ...defaultOptions(), + ...rest, + store: new RedisStore(connectRedisOptions), + }), + ); + app.use(passport.initialize()); + app.use(passport.session()); +}; diff --git a/packages/nestjs-oidc/src/version.ts b/packages/nestjs-oidc/src/version.ts index cfc9af3..b41a6df 100644 --- a/packages/nestjs-oidc/src/version.ts +++ b/packages/nestjs-oidc/src/version.ts @@ -1 +1 @@ -export const version = "0.0.8"; +export const version = "0.1.0"; diff --git a/yarn.lock b/yarn.lock index af42a68..e27e7f5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10225,6 +10225,15 @@ __metadata: languageName: node linkType: hard +"connect-redis@npm:^7.1.1": + version: 7.1.1 + resolution: "connect-redis@npm:7.1.1::__archiveUrl=https%3A%2F%2Fregistry.npmmirror.com%2Fconnect-redis%2F-%2Fconnect-redis-7.1.1.tgz" + peerDependencies: + express-session: ">=1" + checksum: ac91ee818d0f467866b6982f66b3423fee58de9da3562618f6d1df2ddeea426354c1efe70b3f799be1b52e3cc67f2043b9ae203678fd3ff9db5dff44b078f0ca + languageName: node + linkType: hard + "consola@npm:^2.15.0": version: 2.15.3 resolution: "consola@npm:2.15.3" @@ -10997,7 +11006,7 @@ __metadata: languageName: node linkType: hard -"debug@npm:^4.3.1": +"debug@npm:^4.3.0, debug@npm:^4.3.1": version: 4.3.7 resolution: "debug@npm:4.3.7::__archiveUrl=https%3A%2F%2Fregistry.npmmirror.com%2Fdebug%2F-%2Fdebug-4.3.7.tgz" dependencies: @@ -17553,6 +17562,16 @@ __metadata: languageName: node linkType: hard +"lru-cache@npm:^4.0.3": + version: 4.1.5 + resolution: "lru-cache@npm:4.1.5::__archiveUrl=https%3A%2F%2Fregistry.npmmirror.com%2Flru-cache%2F-%2Flru-cache-4.1.5.tgz" + dependencies: + pseudomap: ^1.0.2 + yallist: ^2.1.2 + checksum: 4bb4b58a36cd7dc4dcec74cbe6a8f766a38b7426f1ff59d4cf7d82a2aa9b9565cd1cb98f6ff60ce5cd174524868d7bc9b7b1c294371851356066ca9ac4cf135a + languageName: node + linkType: hard + "lru-cache@npm:^5.1.1": version: 5.1.1 resolution: "lru-cache@npm:5.1.1" @@ -17853,6 +17872,16 @@ __metadata: languageName: node linkType: hard +"memorystore@npm:^1.6.7": + version: 1.6.7 + resolution: "memorystore@npm:1.6.7::__archiveUrl=https%3A%2F%2Fregistry.npmmirror.com%2Fmemorystore%2F-%2Fmemorystore-1.6.7.tgz" + dependencies: + debug: ^4.3.0 + lru-cache: ^4.0.3 + checksum: caa5cc523ec39ad8b317ddb5560771df28a10266281066f554bd77b2179a34f21f06891fffbcfe875ed1afc79cf7e106714eff98189ded1e6bfc273159d07948 + languageName: node + linkType: hard + "meow@npm:^10.1.5": version: 10.1.5 resolution: "meow@npm:10.1.5" @@ -18544,10 +18573,12 @@ __metadata: "@types/passport": ^1.0.9 "@types/uuid": ^9.0.7 connect-mongo: ^5.1.0 + connect-redis: ^7.1.1 cookie: ^0.5.0 express-session: ^1.17.3 flatted: ^3.2.6 jose: ^4.15.9 + memorystore: ^1.6.7 openid-client: ^5.6.5 passport: ^0.6.0 querystring: ^0.2.1 @@ -18557,7 +18588,7 @@ __metadata: "@nestjs/core": ^10.0.0 "@nestjs/graphql": ^11.0.0 connect-mongo: 4.6.0 - express-session: ^1.17.3 + connect-redis: ^7.1.1 graphql: ^16.0.0 graphql-parse-resolve-info: ^4.12.0 peerDependenciesMeta: @@ -18565,7 +18596,7 @@ __metadata: optional: true connect-mongo: optional: true - express-session: + connect-redis: optional: true graphql: optional: true