mirror of
https://github.com/sasjs/server.git
synced 2025-12-10 19:34:34 +00:00
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0d913baff1 | ||
|
|
3671736c3d | ||
|
|
f7fcc7741a | ||
|
|
18052fdbf6 | ||
|
|
5966016853 | ||
|
|
87c03c5f8d | ||
|
|
77f8d30baf | ||
|
|
78bea7c154 | ||
|
|
9c3b155c12 | ||
|
|
98e501334f | ||
|
|
bbfd53e79e | ||
| 254bc07da7 | |||
| f978814ca7 | |||
| 68515f95a6 | |||
| d3a516c36e | |||
| c3e3befc17 |
24
CHANGELOG.md
24
CHANGELOG.md
@@ -1,3 +1,27 @@
|
||||
## [0.14.1](https://github.com/sasjs/server/compare/v0.14.0...v0.14.1) (2022-08-04)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **apps:** App Stream logo fix ([87c03c5](https://github.com/sasjs/server/commit/87c03c5f8dbdfc151d4ff3722ecbcd3f7e409aea))
|
||||
* **cookie:** XSRF cookie is removed and passed token in head section ([77f8d30](https://github.com/sasjs/server/commit/77f8d30baf9b1077279c29f1c3e5ca02a5436bc0))
|
||||
* **env:** check added for not providing WHITELIST ([5966016](https://github.com/sasjs/server/commit/5966016853369146b27ac5781808cb51d65c887f))
|
||||
* **web:** show login on logged-out state ([f7fcc77](https://github.com/sasjs/server/commit/f7fcc7741aa2af93a4a2b1e651003704c9bbff0c))
|
||||
|
||||
# [0.14.0](https://github.com/sasjs/server/compare/v0.13.3...v0.14.0) (2022-08-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add restriction on add/remove user to public group ([d3a516c](https://github.com/sasjs/server/commit/d3a516c36e45aa1cc76c30c744e6a0e5bd553165))
|
||||
* call jwt.verify in synchronous way ([254bc07](https://github.com/sasjs/server/commit/254bc07da744a9708109bfb792be70aa3f6284f4))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add public group to DB on seed ([c3e3bef](https://github.com/sasjs/server/commit/c3e3befc17102ee1754e1403193040b4f79fb2a7))
|
||||
* bypass authentication when route is enabled for public group ([68515f9](https://github.com/sasjs/server/commit/68515f95a65d422e29c0ed6028f3ea0ae8d9b1bf))
|
||||
|
||||
## [0.13.3](https://github.com/sasjs/server/compare/v0.13.2...v0.13.3) (2022-08-02)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import path from 'path'
|
||||
import express, { ErrorRequestHandler } from 'express'
|
||||
import csrf from 'csurf'
|
||||
import csrf, { CookieOptions } from 'csurf'
|
||||
import cookieParser from 'cookie-parser'
|
||||
import dotenv from 'dotenv'
|
||||
|
||||
@@ -32,9 +32,10 @@ const app = express()
|
||||
|
||||
const { PROTOCOL } = process.env
|
||||
|
||||
export const cookieOptions = {
|
||||
export const cookieOptions: CookieOptions = {
|
||||
secure: PROTOCOL === ProtocolType.HTTPS,
|
||||
httpOnly: true,
|
||||
sameSite: PROTOCOL === ProtocolType.HTTPS ? 'none' : undefined,
|
||||
maxAge: 24 * 60 * 60 * 1000 // 24 hours
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
Body
|
||||
} from 'tsoa'
|
||||
|
||||
import Group, { GroupPayload } from '../model/Group'
|
||||
import Group, { GroupPayload, PUBLIC_GROUP_NAME } from '../model/Group'
|
||||
import User from '../model/User'
|
||||
import { UserResponse } from './user'
|
||||
|
||||
@@ -241,6 +241,13 @@ const updateUsersListInGroup = async (
|
||||
message: 'Group not found.'
|
||||
}
|
||||
|
||||
if (group.name === PUBLIC_GROUP_NAME)
|
||||
throw {
|
||||
code: 400,
|
||||
status: 'Bad Request',
|
||||
message: `Can't add/remove user to '${PUBLIC_GROUP_NAME}' group.`
|
||||
}
|
||||
|
||||
const user = await User.findOne({ id: userId })
|
||||
if (!user)
|
||||
throw {
|
||||
|
||||
@@ -5,7 +5,9 @@ import {
|
||||
fetchLatestAutoExec,
|
||||
ModeType,
|
||||
verifyTokenInDB,
|
||||
isAuthorizingRoute
|
||||
isAuthorizingRoute,
|
||||
isPublicRoute,
|
||||
publicUser
|
||||
} from '../utils'
|
||||
import { desktopUser } from './desktop'
|
||||
import { authorize } from './authorize'
|
||||
@@ -41,7 +43,7 @@ export const authenticateAccessToken: RequestHandler = async (
|
||||
return res.sendStatus(401)
|
||||
}
|
||||
|
||||
authenticateToken(
|
||||
await authenticateToken(
|
||||
req,
|
||||
res,
|
||||
nextFunction,
|
||||
@@ -50,8 +52,12 @@ export const authenticateAccessToken: RequestHandler = async (
|
||||
)
|
||||
}
|
||||
|
||||
export const authenticateRefreshToken: RequestHandler = (req, res, next) => {
|
||||
authenticateToken(
|
||||
export const authenticateRefreshToken: RequestHandler = async (
|
||||
req,
|
||||
res,
|
||||
next
|
||||
) => {
|
||||
await authenticateToken(
|
||||
req,
|
||||
res,
|
||||
next,
|
||||
@@ -60,7 +66,7 @@ export const authenticateRefreshToken: RequestHandler = (req, res, next) => {
|
||||
)
|
||||
}
|
||||
|
||||
const authenticateToken = (
|
||||
const authenticateToken = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction,
|
||||
@@ -83,12 +89,12 @@ const authenticateToken = (
|
||||
|
||||
const authHeader = req.headers['authorization']
|
||||
const token = authHeader?.split(' ')[1]
|
||||
if (!token) return res.sendStatus(401)
|
||||
|
||||
jwt.verify(token, key, async (err: any, data: any) => {
|
||||
if (err) return res.sendStatus(401)
|
||||
try {
|
||||
if (!token) throw 'Unauthorized'
|
||||
|
||||
const data: any = jwt.verify(token, key)
|
||||
|
||||
// verify this valid token's entry in DB
|
||||
const user = await verifyTokenInDB(
|
||||
data?.userId,
|
||||
data?.clientId,
|
||||
@@ -101,8 +107,16 @@ const authenticateToken = (
|
||||
req.user = user
|
||||
if (tokenType === 'accessToken') req.accessToken = token
|
||||
return next()
|
||||
} else return res.sendStatus(401)
|
||||
} else throw 'Unauthorized'
|
||||
}
|
||||
return res.sendStatus(401)
|
||||
})
|
||||
|
||||
throw 'Unauthorized'
|
||||
} catch (error) {
|
||||
if (await isPublicRoute(req)) {
|
||||
req.user = publicUser
|
||||
return next()
|
||||
}
|
||||
|
||||
res.sendStatus(401)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
PermissionSettingForRoute,
|
||||
PermissionType
|
||||
} from '../controllers/permission'
|
||||
import { getPath } from '../utils'
|
||||
import { getPath, isPublicRoute } from '../utils'
|
||||
|
||||
export const authorize: RequestHandler = async (req, res, next) => {
|
||||
const { user } = req
|
||||
@@ -17,6 +17,9 @@ export const authorize: RequestHandler = async (req, res, next) => {
|
||||
// no need to check for permissions when user is admin
|
||||
if (user.isAdmin) return next()
|
||||
|
||||
// no need to check for permissions when route is Public
|
||||
if (await isPublicRoute(req)) return next()
|
||||
|
||||
const dbUser = await User.findOne({ id: user.userId })
|
||||
if (!dbUser) return res.sendStatus(401)
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@ import { GroupDetailsResponse } from '../controllers'
|
||||
import User, { IUser } from './User'
|
||||
const AutoIncrement = require('mongoose-sequence')(mongoose)
|
||||
|
||||
export const PUBLIC_GROUP_NAME = 'Public'
|
||||
|
||||
export interface GroupPayload {
|
||||
/**
|
||||
* Name of the group
|
||||
|
||||
@@ -5,6 +5,7 @@ import request from 'supertest'
|
||||
import appPromise from '../../../app'
|
||||
import { UserController, GroupController } from '../../../controllers/'
|
||||
import { generateAccessToken, saveTokensInDB } from '../../../utils'
|
||||
import { PUBLIC_GROUP_NAME } from '../../../model/Group'
|
||||
|
||||
const clientId = 'someclientID'
|
||||
const adminUser = {
|
||||
@@ -27,6 +28,12 @@ const group = {
|
||||
description: 'DC group for testing purposes.'
|
||||
}
|
||||
|
||||
const PUBLIC_GROUP = {
|
||||
name: PUBLIC_GROUP_NAME,
|
||||
description:
|
||||
'A special group that can be used to bypass authentication for particular routes.'
|
||||
}
|
||||
|
||||
const userController = new UserController()
|
||||
const groupController = new GroupController()
|
||||
|
||||
@@ -535,6 +542,24 @@ describe('group', () => {
|
||||
expect(res.text).toEqual('User not found.')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Bad Request when adding user to Public group', async () => {
|
||||
const dbGroup = await groupController.createGroup(PUBLIC_GROUP)
|
||||
const dbUser = await userController.createUser({
|
||||
...user,
|
||||
username: 'publicUser'
|
||||
})
|
||||
|
||||
const res = await request(app)
|
||||
.post(`/SASjsApi/group/${dbGroup.groupId}/${dbUser.id}`)
|
||||
.auth(adminAccessToken, { type: 'bearer' })
|
||||
.send()
|
||||
.expect(400)
|
||||
|
||||
expect(res.text).toEqual(
|
||||
`Can't add/remove user to '${PUBLIC_GROUP_NAME}' group.`
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('RemoveUser', () => {
|
||||
|
||||
@@ -39,12 +39,11 @@ describe('web', () => {
|
||||
|
||||
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=\//
|
||||
)
|
||||
const res = await request(app).get('/').expect(200)
|
||||
|
||||
expect(res.text).toMatch(
|
||||
/<script>document.cookie = '(XSRF-TOKEN=.*; Max-Age=86400; SameSite=Strict; Path=\/;)'<\/script>/
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -154,10 +153,10 @@ describe('web', () => {
|
||||
|
||||
const getCSRF = async (app: Express) => {
|
||||
// make request to get CSRF
|
||||
const { header } = await request(app).get('/')
|
||||
const { header, text } = await request(app).get('/')
|
||||
const cookies = header['set-cookie'].join()
|
||||
|
||||
const csrfToken = extractCSRF(cookies)
|
||||
const csrfToken = extractCSRF(text)
|
||||
return { csrfToken, cookies }
|
||||
}
|
||||
|
||||
@@ -177,7 +176,7 @@ const performLogin = async (
|
||||
return { cookies: newCookies }
|
||||
}
|
||||
|
||||
const extractCSRF = (cookies: string) =>
|
||||
/_csrf=(.*); Max-Age=86400000; Path=\/; HttpOnly,XSRF-TOKEN=(.*); Path=\//.exec(
|
||||
cookies
|
||||
)![2]
|
||||
const extractCSRF = (text: string) =>
|
||||
/<script>document.cookie = 'XSRF-TOKEN=(.*); Max-Age=86400; SameSite=Strict; Path=\/;'<\/script>/.exec(
|
||||
text
|
||||
)![1]
|
||||
|
||||
@@ -26,6 +26,7 @@ export const style = `<style>
|
||||
}
|
||||
.app-container .app img{
|
||||
width: 100%;
|
||||
height: calc(100% - 30px);
|
||||
margin-bottom: 10px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
@@ -11,11 +11,15 @@ webRouter.get('/', async (req, res) => {
|
||||
try {
|
||||
response = await controller.home()
|
||||
} catch (_) {
|
||||
response = 'Web Build is not present'
|
||||
response = '<html><head></head><body>Web Build is not present</body></html>'
|
||||
} finally {
|
||||
res.cookie('XSRF-TOKEN', req.csrfToken())
|
||||
const codeToInject = `<script>document.cookie = 'XSRF-TOKEN=${req.csrfToken()}; Max-Age=86400; SameSite=Strict; Path=/;'</script>`
|
||||
const injectedContent = response?.replace(
|
||||
'</head>',
|
||||
`${codeToInject}</head>`
|
||||
)
|
||||
|
||||
return res.send(response)
|
||||
return res.send(injectedContent)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ export * from './getRunTimeAndFilePath'
|
||||
export * from './getServerUrl'
|
||||
export * from './instantiateLogger'
|
||||
export * from './isDebugOn'
|
||||
export * from './isPublicRoute'
|
||||
export * from './zipped'
|
||||
export * from './parseLogToArray'
|
||||
export * from './removeTokensInDB'
|
||||
|
||||
31
api/src/utils/isPublicRoute.ts
Normal file
31
api/src/utils/isPublicRoute.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { Request } from 'express'
|
||||
import { getPath } from './getAuthorizedRoutes'
|
||||
import Group, { PUBLIC_GROUP_NAME } from '../model/Group'
|
||||
import Permission from '../model/Permission'
|
||||
import { PermissionSettingForRoute } from '../controllers'
|
||||
import { RequestUser } from '../types'
|
||||
|
||||
export const isPublicRoute = async (req: Request): Promise<boolean> => {
|
||||
const group = await Group.findOne({ name: PUBLIC_GROUP_NAME })
|
||||
if (group) {
|
||||
const path = getPath(req)
|
||||
|
||||
const groupPermission = await Permission.findOne({
|
||||
path,
|
||||
group: group?._id
|
||||
})
|
||||
if (groupPermission?.setting === PermissionSettingForRoute.grant)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
export const publicUser: RequestUser = {
|
||||
userId: 0,
|
||||
clientId: 'public_app',
|
||||
username: 'publicUser',
|
||||
displayName: 'Public User',
|
||||
isAdmin: false,
|
||||
isActive: true
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import Client from '../model/Client'
|
||||
import Group from '../model/Group'
|
||||
import Group, { PUBLIC_GROUP_NAME } from '../model/Group'
|
||||
import User from '../model/User'
|
||||
import Configuration, { ConfigurationType } from '../model/Configuration'
|
||||
|
||||
@@ -31,6 +31,15 @@ export const seedDB = async (): Promise<ConfigurationType> => {
|
||||
console.log(`DB Seed - Group created: ${GROUP.name}`)
|
||||
}
|
||||
|
||||
// Checking if 'Public' Group is already in the database
|
||||
const publicGroupExist = await Group.findOne({ name: PUBLIC_GROUP.name })
|
||||
if (!publicGroupExist) {
|
||||
const group = new Group(PUBLIC_GROUP)
|
||||
await group.save()
|
||||
|
||||
console.log(`DB Seed - Group created: ${PUBLIC_GROUP.name}`)
|
||||
}
|
||||
|
||||
// Checking if user is already in the database
|
||||
let usernameExist = await User.findOne({ username: ADMIN_USER.username })
|
||||
if (!usernameExist) {
|
||||
@@ -68,6 +77,13 @@ const GROUP = {
|
||||
name: 'AllUsers',
|
||||
description: 'Group contains all users'
|
||||
}
|
||||
|
||||
const PUBLIC_GROUP = {
|
||||
name: PUBLIC_GROUP_NAME,
|
||||
description:
|
||||
'A special group that can be used to bypass authentication for particular routes.'
|
||||
}
|
||||
|
||||
const CLIENT = {
|
||||
clientId: 'clientID1',
|
||||
clientSecret: 'clientSecret'
|
||||
|
||||
@@ -125,8 +125,27 @@ const verifyCORS = (): string[] => {
|
||||
|
||||
if (CORS) {
|
||||
const corsTypes = Object.values(CorsType)
|
||||
|
||||
if (!corsTypes.includes(CORS as CorsType))
|
||||
errors.push(`- CORS '${CORS}'\n - valid options ${corsTypes}`)
|
||||
|
||||
if (CORS === CorsType.ENABLED) {
|
||||
const { WHITELIST } = process.env
|
||||
|
||||
const urls = WHITELIST?.trim()
|
||||
.split(' ')
|
||||
.filter((url) => !!url)
|
||||
if (urls?.length) {
|
||||
urls.forEach((url) => {
|
||||
if (!url.startsWith('http://') && !url.startsWith('https://'))
|
||||
errors.push(
|
||||
`- CORS '${CORS}'\n - provided WHITELIST ${url} is not valid`
|
||||
)
|
||||
})
|
||||
} else {
|
||||
errors.push(`- CORS '${CORS}'\n - provide at least one WHITELIST URL`)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const { MODE } = process.env
|
||||
process.env.CORS =
|
||||
|
||||
@@ -22,7 +22,7 @@ function App() {
|
||||
<HashRouter>
|
||||
<Header />
|
||||
<Routes>
|
||||
<Route path="/" element={<Login />} />
|
||||
<Route path="*" element={<Login />} />
|
||||
</Routes>
|
||||
</HashRouter>
|
||||
</ThemeProvider>
|
||||
|
||||
@@ -80,7 +80,18 @@ const AppContextProvider = (props: { children: ReactNode }) => {
|
||||
})
|
||||
.catch(() => {
|
||||
setLoggedIn(false)
|
||||
axios.get('/') // get CSRF TOKEN
|
||||
// get CSRF TOKEN and set cookie
|
||||
axios
|
||||
.get('/')
|
||||
.then((res) => res.data)
|
||||
.then((data: string) => {
|
||||
const result =
|
||||
/<script>document.cookie = '(XSRF-TOKEN=.*; Max-Age=86400; SameSite=Strict; Path=\/;)'<\/script>/.exec(
|
||||
data
|
||||
)?.[1]
|
||||
|
||||
if (result) document.cookie = result
|
||||
})
|
||||
})
|
||||
|
||||
axios
|
||||
|
||||
Reference in New Issue
Block a user