diff --git a/api/src/app.ts b/api/src/app.ts index a41895c..ce828dd 100644 --- a/api/src/app.ts +++ b/api/src/app.ts @@ -85,21 +85,25 @@ if (MODE?.trim() !== 'server' || CORS?.trim() === 'enable') { * With Mongo Store * ***********************************/ if (MODE?.trim() === 'server') { + let store: MongoStore | undefined + // NOTE: when exporting app.js as agent for supertest // we should exclude connecting to the real database if (process.env.NODE_ENV !== 'test') { const clientPromise = connectDB().then((conn) => conn!.getClient() as any) - app.use( - session({ - secret: process.env.SESSION_SECRET as string, - saveUninitialized: false, // don't create session until something stored - resave: false, //don't save session if unmodified - store: MongoStore.create({ clientPromise, collectionName: 'sessions' }), - cookie: cookieOptions - }) - ) + store = MongoStore.create({ clientPromise, collectionName: 'sessions' }) } + + app.use( + session({ + secret: process.env.SESSION_SECRET as string, + saveUninitialized: false, // don't create session until something stored + resave: false, //don't save session if unmodified + store, + cookie: cookieOptions + }) + ) } app.use(express.json({ limit: '100mb' })) app.use(express.static(path.join(__dirname, '../public'))) diff --git a/api/src/controllers/web.ts b/api/src/controllers/web.ts index e82f192..70ccf6a 100644 --- a/api/src/controllers/web.ts +++ b/api/src/controllers/web.ts @@ -108,6 +108,10 @@ const authorize = async ( ): Promise => { const userId = req.session.user?.userId if (!userId) throw new Error('Invalid userId.') + + const client = await Client.findOne({ clientId }) + if (!client) throw new Error('Invalid clientId.') + // generate authorization code against clientId const userInfo: InfoJWT = { clientId, diff --git a/api/src/routes/api/spec/auth.spec.ts b/api/src/routes/api/spec/auth.spec.ts index 4a0014d..0e1e5b1 100644 --- a/api/src/routes/api/spec/auth.spec.ts +++ b/api/src/routes/api/spec/auth.spec.ts @@ -49,114 +49,6 @@ describe('auth', () => { await mongoServer.stop() }) - describe('authorize', () => { - afterEach(async () => { - const collections = mongoose.connection.collections - const collection = collections['users'] - await collection.deleteMany({}) - }) - - it('should respond with authorization code', async () => { - await userController.createUser(user) - - const res = await request(app) - .post('/SASjsApi/auth/authorize') - .send({ - username: user.username, - password: user.password, - clientId - }) - .expect(200) - - expect(res.body).toHaveProperty('code') - }) - - it('should respond with Bad Request if username is missing', async () => { - const res = await request(app) - .post('/SASjsApi/auth/authorize') - .send({ - password: user.password, - clientId - }) - .expect(400) - - expect(res.text).toEqual(`"username" is required`) - expect(res.body).toEqual({}) - }) - - it('should respond with Bad Request if password is missing', async () => { - const res = await request(app) - .post('/SASjsApi/auth/authorize') - .send({ - username: user.username, - clientId - }) - .expect(400) - - expect(res.text).toEqual(`"password" is required`) - expect(res.body).toEqual({}) - }) - - it('should respond with Bad Request if clientId is missing', async () => { - const res = await request(app) - .post('/SASjsApi/auth/authorize') - .send({ - username: user.username, - password: user.password - }) - .expect(400) - - expect(res.text).toEqual(`"clientId" is required`) - expect(res.body).toEqual({}) - }) - - it('should respond with Forbidden if username is incorrect', async () => { - const res = await request(app) - .post('/SASjsApi/auth/authorize') - .send({ - username: user.username, - password: user.password, - clientId - }) - .expect(403) - - expect(res.text).toEqual('Error: Username is not found.') - expect(res.body).toEqual({}) - }) - - it('should respond with Forbidden if password is incorrect', async () => { - await userController.createUser(user) - - const res = await request(app) - .post('/SASjsApi/auth/authorize') - .send({ - username: user.username, - password: 'WrongPassword', - clientId - }) - .expect(403) - - expect(res.text).toEqual('Error: Invalid password.') - expect(res.body).toEqual({}) - }) - - it('should respond with Forbidden if clientId is incorrect', async () => { - await userController.createUser(user) - - const res = await request(app) - .post('/SASjsApi/auth/authorize') - .send({ - username: user.username, - password: user.password, - clientId: 'WrongClientID' - }) - .expect(403) - - expect(res.text).toEqual('Error: Invalid clientId.') - expect(res.body).toEqual({}) - }) - }) - describe('token', () => { const userInfo: InfoJWT = { clientId, diff --git a/api/src/routes/api/spec/web.spec.ts b/api/src/routes/api/spec/web.spec.ts new file mode 100644 index 0000000..f442432 --- /dev/null +++ b/api/src/routes/api/spec/web.spec.ts @@ -0,0 +1,172 @@ +import { Express } from 'express' +import mongoose, { Mongoose } from 'mongoose' +import { MongoMemoryServer } from 'mongodb-memory-server' +import request from 'supertest' +import appPromise from '../../../app' +import { UserController, ClientController } from '../../../controllers/' + +const clientId = 'someclientID' +const clientSecret = 'someclientSecret' +const user = { + id: 1234, + displayName: 'Test User', + username: 'testUsername', + password: '87654321', + isAdmin: false, + isActive: true +} + +describe('web', () => { + let app: Express + let con: Mongoose + let mongoServer: MongoMemoryServer + let csrfToken: string + let cookies: string + const userController = new UserController() + const clientController = new ClientController() + + beforeAll(async () => { + app = await appPromise + ;({ csrfToken, cookies } = await getCSRF(app)) + + mongoServer = await MongoMemoryServer.create() + con = await mongoose.connect(mongoServer.getUri()) + await clientController.createClient({ clientId, clientSecret }) + }) + + afterAll(async () => { + await con.connection.dropDatabase() + await con.connection.close() + await mongoServer.stop() + }) + + describe('home', () => { + it('should respond with CSRF Token', async () => { + await request(app) + .get('/') + .expect( + 'set-cookie', + /_csrf=.*; Max-Age=86400000; Path=\/; HttpOnly,XSRF-TOKEN=.*; Path=\// + ) + }) + }) + describe('SASLogon/login', () => { + afterEach(async () => { + const collections = mongoose.connection.collections + const collection = collections['users'] + await collection.deleteMany({}) + }) + + it('should respond with successful login', async () => { + await userController.createUser(user) + + const res = await request(app) + .post('/SASLogon/login') + .set('Cookie', cookies) + .set('x-xsrf-token', csrfToken) + .send({ + username: user.username, + password: user.password + }) + .expect(200) + + expect(res.body.loggedIn).toBeTruthy() + expect(res.body.user).toEqual({ + username: user.username, + displayName: user.displayName + }) + }) + }) + + describe('SASLogon/authorize', () => { + let authCookies: string + + beforeAll(async () => { + await userController.createUser(user) + + const credentials = { + username: user.username, + password: user.password + } + + ;({ cookies: authCookies } = await performLogin( + app, + credentials, + cookies, + csrfToken + )) + }) + + afterAll(async () => { + const collections = mongoose.connection.collections + const collection = collections['users'] + await collection.deleteMany({}) + }) + + it('should respond with authorization code', async () => { + const res = await request(app) + .post('/SASLogon/authorize') + .set('Cookie', [authCookies, cookies].join('; ')) + .set('x-xsrf-token', csrfToken) + .send({ clientId }) + + expect(res.body).toHaveProperty('code') + }) + + it('should respond with Bad Request if clientId is missing', async () => { + const res = await request(app) + .post('/SASLogon/authorize') + .set('Cookie', [authCookies, cookies].join('; ')) + .set('x-xsrf-token', csrfToken) + .send({}) + .expect(400) + + expect(res.text).toEqual(`"clientId" is required`) + expect(res.body).toEqual({}) + }) + + it('should respond with Forbidden if clientId is incorrect', async () => { + const res = await request(app) + .post('/SASLogon/authorize') + .set('Cookie', [authCookies, cookies].join('; ')) + .set('x-xsrf-token', csrfToken) + .send({ + clientId: 'WrongClientID' + }) + .expect(403) + + expect(res.text).toEqual('Error: Invalid clientId.') + expect(res.body).toEqual({}) + }) + }) +}) + +const getCSRF = async (app: Express) => { + // make request to get CSRF + const { header } = await request(app).get('/') + const cookies = header['set-cookie'].join() + + const csrfToken = extractCSRF(cookies) + return { csrfToken, cookies } +} + +const performLogin = async ( + app: Express, + credentials: { username: string; password: string }, + cookies: string, + csrfToken: string +) => { + const { header } = await request(app) + .post('/SASLogon/login') + .set('Cookie', cookies) + .set('x-xsrf-token', csrfToken) + .send(credentials) + + const newCookies: string = header['set-cookie'].join() + return { cookies: newCookies } +} + +const extractCSRF = (cookies: string) => + /_csrf=(.*); Max-Age=86400000; Path=\/; HttpOnly,XSRF-TOKEN=(.*); Path=\//.exec( + cookies + )![2] diff --git a/api/src/routes/web/web.ts b/api/src/routes/web/web.ts index 72d7ae7..376cc9c 100644 --- a/api/src/routes/web/web.ts +++ b/api/src/routes/web/web.ts @@ -26,7 +26,7 @@ webRouter.post('/SASLogon/login', async (req, res) => { const response = await controller.login(req, body) res.send(response) } catch (err: any) { - res.status(400).send(err.toString()) + res.status(403).send(err.toString()) } }) @@ -41,7 +41,7 @@ webRouter.post( const response = await controller.authorize(req, body) res.send(response) } catch (err: any) { - res.status(400).send(err.toString()) + res.status(403).send(err.toString()) } } ) @@ -51,7 +51,7 @@ webRouter.get('/logout', async (req, res) => { await controller.logout(req) res.status(200).send('OK!') } catch (err: any) { - res.status(400).send(err.toString()) + res.status(403).send(err.toString()) } })