mirror of
https://github.com/sasjs/server.git
synced 2026-01-15 18:00:05 +00:00
feat: JWT saved in DB + logout api added
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import mongoose from 'mongoose'
|
import { string } from 'joi'
|
||||||
|
import mongoose, { Schema } from 'mongoose'
|
||||||
|
|
||||||
const userSchema = new mongoose.Schema({
|
const userSchema = new mongoose.Schema({
|
||||||
displayname: {
|
displayname: {
|
||||||
@@ -20,7 +21,23 @@ const userSchema = new mongoose.Schema({
|
|||||||
isactive: {
|
isactive: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true
|
default: true
|
||||||
}
|
},
|
||||||
|
tokens: [
|
||||||
|
{
|
||||||
|
clientid: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
accesstoken: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
refreshtoken: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
export default mongoose.model('User', userSchema)
|
export default mongoose.model('User', userSchema)
|
||||||
|
|||||||
@@ -1,18 +1,24 @@
|
|||||||
import express from 'express'
|
import express from 'express'
|
||||||
import bcrypt from 'bcryptjs'
|
import bcrypt from 'bcryptjs'
|
||||||
import mongoose, { Mongoose } from 'mongoose'
|
import mongoose from 'mongoose'
|
||||||
import jwt from 'jsonwebtoken'
|
import jwt from 'jsonwebtoken'
|
||||||
|
|
||||||
import Client from '../../model/Client'
|
import Client from '../../model/Client'
|
||||||
import User from '../../model/User'
|
import User from '../../model/User'
|
||||||
import { authorizeValidation, tokenValidation } from '../../utils'
|
import {
|
||||||
|
authenticateToken,
|
||||||
|
authorizeValidation,
|
||||||
|
removeTokensInDB,
|
||||||
|
saveTokensInDB,
|
||||||
|
tokenValidation
|
||||||
|
} from '../../utils'
|
||||||
import { InfoJWT } from '../../types'
|
import { InfoJWT } from '../../types'
|
||||||
|
|
||||||
const authRouter = express.Router()
|
const authRouter = express.Router()
|
||||||
|
|
||||||
const clients: { [key: string]: string } = {}
|
const clients: { [key: string]: string } = {}
|
||||||
const clientIDs = new Set()
|
const clientIDs = new Set()
|
||||||
const authCodes: { [key: string]: string } = {}
|
const authCodes: { [key: string]: { [key: string]: string } } = {}
|
||||||
|
|
||||||
export const populateClients = async () => {
|
export const populateClients = async () => {
|
||||||
const result = await Client.find()
|
const result = await Client.find()
|
||||||
@@ -37,11 +43,14 @@ export const connectDB = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const saveCode = (client_id: string, code: string) =>
|
export const saveCode = (username: string, client_id: string, code: string) => {
|
||||||
(authCodes[client_id] = code)
|
if (authCodes[username]) return (authCodes[username][client_id] = code)
|
||||||
export const deleteCode = (client_id: string) => delete authCodes[client_id]
|
|
||||||
|
|
||||||
const refreshTokens: string[] = []
|
authCodes[username] = { [client_id]: code }
|
||||||
|
return authCodes[username][client_id]
|
||||||
|
}
|
||||||
|
export const deleteCode = (username: string, client_id: string) =>
|
||||||
|
delete authCodes[username][client_id]
|
||||||
|
|
||||||
authRouter.post('/authorize', async (req, res) => {
|
authRouter.post('/authorize', async (req, res) => {
|
||||||
const { error, value } = authorizeValidation(req.body)
|
const { error, value } = authorizeValidation(req.body)
|
||||||
@@ -49,6 +58,11 @@ authRouter.post('/authorize', async (req, res) => {
|
|||||||
|
|
||||||
const { username, password, client_id } = value
|
const { username, password, client_id } = value
|
||||||
|
|
||||||
|
// Verify client ID
|
||||||
|
if (!clientIDs.has(client_id)) {
|
||||||
|
return res.status(403).send('Invalid client_id.')
|
||||||
|
}
|
||||||
|
|
||||||
// Authenticate User
|
// Authenticate User
|
||||||
const user = await User.findOne({ username })
|
const user = await User.findOne({ username })
|
||||||
if (!user) return res.status(403).send('Username is not found.')
|
if (!user) return res.status(403).send('Username is not found.')
|
||||||
@@ -56,20 +70,13 @@ authRouter.post('/authorize', async (req, res) => {
|
|||||||
const validPass = await bcrypt.compare(password, user.password)
|
const validPass = await bcrypt.compare(password, user.password)
|
||||||
if (!validPass) return res.status(403).send('Invalid password.')
|
if (!validPass) return res.status(403).send('Invalid password.')
|
||||||
|
|
||||||
// Verify client ID
|
|
||||||
if (!clientIDs.has(client_id)) {
|
|
||||||
return res.status(403).send('Invalid client_id.')
|
|
||||||
}
|
|
||||||
|
|
||||||
// generate authorization code against client_id
|
// generate authorization code against client_id
|
||||||
const userInfo: InfoJWT = {
|
const userInfo: InfoJWT = {
|
||||||
client_id,
|
client_id,
|
||||||
username,
|
username
|
||||||
isadmin: user.isadmin,
|
|
||||||
isactive: user.isactive
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const code = saveCode(client_id, generateAuthCode(userInfo))
|
const code = saveCode(username, client_id, generateAuthCode(userInfo))
|
||||||
|
|
||||||
res.json({ code })
|
res.json({ code })
|
||||||
})
|
})
|
||||||
@@ -78,22 +85,23 @@ authRouter.post('/token', async (req, res) => {
|
|||||||
const { error, value } = tokenValidation(req.body)
|
const { error, value } = tokenValidation(req.body)
|
||||||
if (error) return res.status(400).send(error.details[0].message)
|
if (error) return res.status(400).send(error.details[0].message)
|
||||||
|
|
||||||
const { client_id, client_secret, code } = value
|
const { client_id, code } = value
|
||||||
|
|
||||||
const userInfo = await verifyAuthCode(client_id, client_secret, code)
|
const userInfo = await verifyAuthCode(client_id, code)
|
||||||
|
if (!userInfo) return res.sendStatus(403)
|
||||||
|
|
||||||
if (!userInfo) {
|
if (authCodes[userInfo.username][client_id] !== code)
|
||||||
return res.sendStatus(403)
|
return res.sendStatus(403)
|
||||||
}
|
|
||||||
|
deleteCode(userInfo.username, client_id)
|
||||||
|
|
||||||
const accessToken = generateAccessToken(userInfo)
|
const accessToken = generateAccessToken(userInfo)
|
||||||
const refreshToken = jwt.sign(
|
const refreshToken = jwt.sign(
|
||||||
userInfo,
|
userInfo,
|
||||||
process.env.REFRESH_TOKEN_SECRET as string
|
process.env.REFRESH_TOKEN_SECRET as string
|
||||||
)
|
)
|
||||||
refreshTokens.push(refreshToken)
|
|
||||||
|
|
||||||
deleteCode(client_id)
|
await saveTokensInDB(userInfo.username, client_id, accessToken, refreshToken)
|
||||||
|
|
||||||
res.json({ accessToken: accessToken, refreshToken: refreshToken })
|
res.json({ accessToken: accessToken, refreshToken: refreshToken })
|
||||||
})
|
})
|
||||||
@@ -113,9 +121,10 @@ authRouter.post('/token', async (req, res) => {
|
|||||||
// )
|
// )
|
||||||
// })
|
// })
|
||||||
|
|
||||||
authRouter.delete('/logout', (req, res) => {
|
authRouter.delete('/logout', authenticateToken, async (req: any, res) => {
|
||||||
const index = refreshTokens.findIndex(req.body.token)
|
const { user } = req
|
||||||
if (index > -1) refreshTokens.splice(index, 1)
|
|
||||||
|
await removeTokensInDB(user.username, user.client_id)
|
||||||
|
|
||||||
res.sendStatus(204)
|
res.sendStatus(204)
|
||||||
})
|
})
|
||||||
@@ -132,7 +141,6 @@ export const generateAuthCode = (data: InfoJWT) =>
|
|||||||
|
|
||||||
const verifyAuthCode = async (
|
const verifyAuthCode = async (
|
||||||
client_id: string,
|
client_id: string,
|
||||||
client_secret: string,
|
|
||||||
code: string
|
code: string
|
||||||
): Promise<InfoJWT | undefined> => {
|
): Promise<InfoJWT | undefined> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
@@ -141,15 +149,9 @@ const verifyAuthCode = async (
|
|||||||
|
|
||||||
const clientInfo: InfoJWT = {
|
const clientInfo: InfoJWT = {
|
||||||
client_id: data?.client_id,
|
client_id: data?.client_id,
|
||||||
username: data?.username,
|
username: data?.username
|
||||||
isadmin: data?.isadmin,
|
|
||||||
isactive: data?.isactive
|
|
||||||
}
|
}
|
||||||
if (
|
if (clientInfo.client_id === client_id) {
|
||||||
clientInfo.client_id === client_id &&
|
|
||||||
clients[client_id] === client_secret &&
|
|
||||||
authCodes[client_id] === code
|
|
||||||
) {
|
|
||||||
return resolve(clientInfo)
|
return resolve(clientInfo)
|
||||||
}
|
}
|
||||||
return resolve(undefined)
|
return resolve(undefined)
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
import express from 'express'
|
import express from 'express'
|
||||||
import jwt from 'jsonwebtoken'
|
|
||||||
import dotenv from 'dotenv'
|
import dotenv from 'dotenv'
|
||||||
import { InfoJWT } from '../../types'
|
|
||||||
|
|
||||||
import driveRouter from './drive'
|
import driveRouter from './drive'
|
||||||
import stpRouter from './stp'
|
import stpRouter from './stp'
|
||||||
import userRouter from './user'
|
import userRouter from './user'
|
||||||
import clientRouter from './client'
|
import clientRouter from './client'
|
||||||
import authRouter, { connectDB } from './auth'
|
import authRouter, { connectDB } from './auth'
|
||||||
|
import { authenticateToken } from '../../utils'
|
||||||
|
|
||||||
dotenv.config()
|
dotenv.config()
|
||||||
connectDB()
|
connectDB()
|
||||||
@@ -20,32 +19,9 @@ router.use('/user', authenticateToken, verifyAdmin, userRouter)
|
|||||||
router.use('/client', authenticateToken, verifyAdmin, clientRouter)
|
router.use('/client', authenticateToken, verifyAdmin, clientRouter)
|
||||||
router.use('/auth', authRouter)
|
router.use('/auth', authRouter)
|
||||||
|
|
||||||
function authenticateToken(req: any, res: any, next: any) {
|
|
||||||
const authHeader = req.headers['authorization']
|
|
||||||
const token = authHeader && authHeader.split(' ')[1]
|
|
||||||
if (token == null) return res.sendStatus(401)
|
|
||||||
|
|
||||||
jwt.verify(
|
|
||||||
token,
|
|
||||||
process.env.ACCESS_TOKEN_SECRET as string,
|
|
||||||
(err: any, data: any) => {
|
|
||||||
if (err) return res.sendStatus(403)
|
|
||||||
|
|
||||||
const user: InfoJWT = {
|
|
||||||
client_id: data?.client_id,
|
|
||||||
username: data?.username,
|
|
||||||
isadmin: data?.isadmin,
|
|
||||||
isactive: data?.isactive
|
|
||||||
}
|
|
||||||
req.user = user
|
|
||||||
next()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function verifyAdmin(req: any, res: any, next: any) {
|
function verifyAdmin(req: any, res: any, next: any) {
|
||||||
const { user } = req
|
const { user } = req
|
||||||
if (!user.isadmin) return res.status(403).send('Admin account required')
|
if (!user?.isadmin) return res.status(403).send('Admin account required')
|
||||||
next()
|
next()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,13 +4,17 @@ import request from 'supertest'
|
|||||||
import app from '../../../app'
|
import app from '../../../app'
|
||||||
import { createUser } from '../../../controllers/createUser'
|
import { createUser } from '../../../controllers/createUser'
|
||||||
import { createClient } from '../../../controllers/createClient'
|
import { createClient } from '../../../controllers/createClient'
|
||||||
import { generateAuthCode, populateClients, saveCode } from '../auth'
|
import {
|
||||||
|
generateAccessToken,
|
||||||
|
generateAuthCode,
|
||||||
|
populateClients,
|
||||||
|
saveCode
|
||||||
|
} from '../auth'
|
||||||
import { InfoJWT } from '../../../types'
|
import { InfoJWT } from '../../../types'
|
||||||
|
import { saveTokensInDB, verifyTokenInDB } from '../../../utils'
|
||||||
|
|
||||||
const client = {
|
const client_id = 'someclientID'
|
||||||
client_id: 'someclientID',
|
const client_secret = 'someclientSecret'
|
||||||
client_secret: 'someclientSecret'
|
|
||||||
}
|
|
||||||
const user = {
|
const user = {
|
||||||
displayname: 'Test User',
|
displayname: 'Test User',
|
||||||
username: 'testUsername',
|
username: 'testUsername',
|
||||||
@@ -26,7 +30,7 @@ describe('auth', () => {
|
|||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
mongoServer = await MongoMemoryServer.create()
|
mongoServer = await MongoMemoryServer.create()
|
||||||
con = await mongoose.connect(mongoServer.getUri())
|
con = await mongoose.connect(mongoServer.getUri())
|
||||||
await createClient(client)
|
await createClient({ client_id, client_secret })
|
||||||
await populateClients()
|
await populateClients()
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -51,7 +55,7 @@ describe('auth', () => {
|
|||||||
.send({
|
.send({
|
||||||
username: user.username,
|
username: user.username,
|
||||||
password: user.password,
|
password: user.password,
|
||||||
client_id: client.client_id
|
client_id
|
||||||
})
|
})
|
||||||
.expect(200)
|
.expect(200)
|
||||||
|
|
||||||
@@ -63,7 +67,7 @@ describe('auth', () => {
|
|||||||
.post('/SASjsApi/auth/authorize')
|
.post('/SASjsApi/auth/authorize')
|
||||||
.send({
|
.send({
|
||||||
password: user.password,
|
password: user.password,
|
||||||
client_id: client.client_id
|
client_id
|
||||||
})
|
})
|
||||||
.expect(400)
|
.expect(400)
|
||||||
|
|
||||||
@@ -76,7 +80,7 @@ describe('auth', () => {
|
|||||||
.post('/SASjsApi/auth/authorize')
|
.post('/SASjsApi/auth/authorize')
|
||||||
.send({
|
.send({
|
||||||
username: user.username,
|
username: user.username,
|
||||||
client_id: client.client_id
|
client_id
|
||||||
})
|
})
|
||||||
.expect(400)
|
.expect(400)
|
||||||
|
|
||||||
@@ -103,7 +107,7 @@ describe('auth', () => {
|
|||||||
.send({
|
.send({
|
||||||
username: user.username,
|
username: user.username,
|
||||||
password: user.password,
|
password: user.password,
|
||||||
client_id: client.client_id
|
client_id
|
||||||
})
|
})
|
||||||
.expect(403)
|
.expect(403)
|
||||||
|
|
||||||
@@ -119,7 +123,7 @@ describe('auth', () => {
|
|||||||
.send({
|
.send({
|
||||||
username: user.username,
|
username: user.username,
|
||||||
password: 'WrongPassword',
|
password: 'WrongPassword',
|
||||||
client_id: client.client_id
|
client_id
|
||||||
})
|
})
|
||||||
.expect(403)
|
.expect(403)
|
||||||
|
|
||||||
@@ -146,10 +150,8 @@ describe('auth', () => {
|
|||||||
|
|
||||||
describe('token', () => {
|
describe('token', () => {
|
||||||
const userInfo: InfoJWT = {
|
const userInfo: InfoJWT = {
|
||||||
client_id: client.client_id,
|
client_id,
|
||||||
username: user.username,
|
username: user.username
|
||||||
isadmin: user.isadmin,
|
|
||||||
isactive: user.isactive
|
|
||||||
}
|
}
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await createUser(user)
|
await createUser(user)
|
||||||
@@ -161,13 +163,16 @@ describe('auth', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should respond with access and refresh tokens', async () => {
|
it('should respond with access and refresh tokens', async () => {
|
||||||
const code = saveCode(userInfo.client_id, generateAuthCode(userInfo))
|
const code = saveCode(
|
||||||
|
userInfo.username,
|
||||||
|
userInfo.client_id,
|
||||||
|
generateAuthCode(userInfo)
|
||||||
|
)
|
||||||
|
|
||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.post('/SASjsApi/auth/token')
|
.post('/SASjsApi/auth/token')
|
||||||
.send({
|
.send({
|
||||||
client_id: client.client_id,
|
client_id,
|
||||||
client_secret: client.client_secret,
|
|
||||||
code
|
code
|
||||||
})
|
})
|
||||||
.expect(200)
|
.expect(200)
|
||||||
@@ -180,8 +185,7 @@ describe('auth', () => {
|
|||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.post('/SASjsApi/auth/token')
|
.post('/SASjsApi/auth/token')
|
||||||
.send({
|
.send({
|
||||||
client_id: client.client_id,
|
client_id
|
||||||
client_secret: client.client_secret
|
|
||||||
})
|
})
|
||||||
.expect(400)
|
.expect(400)
|
||||||
|
|
||||||
@@ -190,12 +194,15 @@ describe('auth', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should respond with Bad Request if client_id is missing', async () => {
|
it('should respond with Bad Request if client_id is missing', async () => {
|
||||||
const code = saveCode(userInfo.client_id, generateAuthCode(userInfo))
|
const code = saveCode(
|
||||||
|
userInfo.username,
|
||||||
|
userInfo.client_id,
|
||||||
|
generateAuthCode(userInfo)
|
||||||
|
)
|
||||||
|
|
||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.post('/SASjsApi/auth/token')
|
.post('/SASjsApi/auth/token')
|
||||||
.send({
|
.send({
|
||||||
client_secret: client.client_secret,
|
|
||||||
code
|
code
|
||||||
})
|
})
|
||||||
.expect(400)
|
.expect(400)
|
||||||
@@ -204,27 +211,11 @@ describe('auth', () => {
|
|||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should respond with Bad Request if client_secret is missing', async () => {
|
|
||||||
const code = saveCode(userInfo.client_id, generateAuthCode(userInfo))
|
|
||||||
|
|
||||||
const res = await request(app)
|
|
||||||
.post('/SASjsApi/auth/token')
|
|
||||||
.send({
|
|
||||||
client_id: client.client_id,
|
|
||||||
code
|
|
||||||
})
|
|
||||||
.expect(400)
|
|
||||||
|
|
||||||
expect(res.text).toEqual(`"client_secret" is required`)
|
|
||||||
expect(res.body).toEqual({})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with Forbidden if code is invalid', async () => {
|
it('should respond with Forbidden if code is invalid', async () => {
|
||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.post('/SASjsApi/auth/token')
|
.post('/SASjsApi/auth/token')
|
||||||
.send({
|
.send({
|
||||||
client_id: client.client_id,
|
client_id,
|
||||||
client_secret: client.client_secret,
|
|
||||||
code: 'InvalidCode'
|
code: 'InvalidCode'
|
||||||
})
|
})
|
||||||
.expect(403)
|
.expect(403)
|
||||||
@@ -233,28 +224,16 @@ describe('auth', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should respond with Forbidden if client_id is invalid', async () => {
|
it('should respond with Forbidden if client_id is invalid', async () => {
|
||||||
const code = saveCode(userInfo.client_id, generateAuthCode(userInfo))
|
const code = saveCode(
|
||||||
|
userInfo.username,
|
||||||
|
userInfo.client_id,
|
||||||
|
generateAuthCode(userInfo)
|
||||||
|
)
|
||||||
|
|
||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.post('/SASjsApi/auth/token')
|
.post('/SASjsApi/auth/token')
|
||||||
.send({
|
.send({
|
||||||
client_id: 'WrongClientID',
|
client_id: 'WrongClientID',
|
||||||
client_secret: client.client_secret,
|
|
||||||
code
|
|
||||||
})
|
|
||||||
.expect(403)
|
|
||||||
|
|
||||||
expect(res.body).toEqual({})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respond with Forbidden if client_secret is invalid', async () => {
|
|
||||||
const code = saveCode(userInfo.client_id, generateAuthCode(userInfo))
|
|
||||||
|
|
||||||
const res = await request(app)
|
|
||||||
.post('/SASjsApi/auth/token')
|
|
||||||
.send({
|
|
||||||
client_id: client.client_id,
|
|
||||||
client_secret: 'WrongClientSecret',
|
|
||||||
code
|
code
|
||||||
})
|
})
|
||||||
.expect(403)
|
.expect(403)
|
||||||
@@ -262,4 +241,47 @@ describe('auth', () => {
|
|||||||
expect(res.body).toEqual({})
|
expect(res.body).toEqual({})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('logout', () => {
|
||||||
|
const accessToken = generateAccessToken({
|
||||||
|
client_id,
|
||||||
|
username: user.username
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await createUser(user)
|
||||||
|
await saveTokensInDB(
|
||||||
|
user.username,
|
||||||
|
client_id,
|
||||||
|
accessToken,
|
||||||
|
'refreshToken'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
const collections = mongoose.connection.collections
|
||||||
|
const collection = collections['users']
|
||||||
|
await collection.deleteMany({})
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
const collections = mongoose.connection.collections
|
||||||
|
const collection = collections['users']
|
||||||
|
await collection.deleteMany({})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respond no content and remove access/refresh tokens from DB', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.delete('/SASjsApi/auth/logout')
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.send()
|
||||||
|
.expect(204)
|
||||||
|
|
||||||
|
expect(res.body).toEqual({})
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await verifyTokenInDB(user.username, client_id, accessToken)
|
||||||
|
).toBeUndefined()
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -4,8 +4,21 @@ import request from 'supertest'
|
|||||||
import app from '../../../app'
|
import app from '../../../app'
|
||||||
import { createClient } from '../../../controllers/createClient'
|
import { createClient } from '../../../controllers/createClient'
|
||||||
import { generateAccessToken } from '../auth'
|
import { generateAccessToken } from '../auth'
|
||||||
|
import { createUser } from '../../../controllers/createUser'
|
||||||
|
import { saveTokensInDB } from '../../../utils'
|
||||||
|
|
||||||
const client = {
|
const client = {
|
||||||
|
client_id: 'someclientID',
|
||||||
|
client_secret: 'someclientSecret'
|
||||||
|
}
|
||||||
|
const adminUser = {
|
||||||
|
displayname: 'Test Admin',
|
||||||
|
username: 'testAdminUsername',
|
||||||
|
password: '12345678',
|
||||||
|
isadmin: true,
|
||||||
|
isactive: true
|
||||||
|
}
|
||||||
|
const newClient = {
|
||||||
client_id: 'newClientID',
|
client_id: 'newClientID',
|
||||||
client_secret: 'newClientSecret'
|
client_secret: 'newClientSecret'
|
||||||
}
|
}
|
||||||
@@ -27,10 +40,18 @@ describe('user', () => {
|
|||||||
|
|
||||||
describe('create', () => {
|
describe('create', () => {
|
||||||
const adminAccessToken = generateAccessToken({
|
const adminAccessToken = generateAccessToken({
|
||||||
client_id: 'someClientID',
|
client_id: client.client_id,
|
||||||
username: 'someAdminUsername',
|
username: adminUser.username
|
||||||
isadmin: true,
|
})
|
||||||
isactive: true
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await createUser(adminUser)
|
||||||
|
await saveTokensInDB(
|
||||||
|
adminUser.username,
|
||||||
|
client.client_id,
|
||||||
|
adminAccessToken,
|
||||||
|
'refreshToken'
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
@@ -43,17 +64,17 @@ describe('user', () => {
|
|||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.post('/SASjsApi/client')
|
.post('/SASjsApi/client')
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
.auth(adminAccessToken, { type: 'bearer' })
|
||||||
.send(client)
|
.send(newClient)
|
||||||
.expect(200)
|
.expect(200)
|
||||||
|
|
||||||
expect(res.body.client_id).toEqual(client.client_id)
|
expect(res.body.client_id).toEqual(newClient.client_id)
|
||||||
expect(res.body.client_secret).toEqual(client.client_secret)
|
expect(res.body.client_secret).toEqual(newClient.client_secret)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should respond with Unauthorized if access token is not present', async () => {
|
it('should respond with Unauthorized if access token is not present', async () => {
|
||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.post('/SASjsApi/client')
|
.post('/SASjsApi/client')
|
||||||
.send(client)
|
.send(newClient)
|
||||||
.expect(401)
|
.expect(401)
|
||||||
|
|
||||||
expect(res.text).toEqual('Unauthorized')
|
expect(res.text).toEqual('Unauthorized')
|
||||||
@@ -61,17 +82,29 @@ describe('user', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should respond with Forbideen if access token is not of an admin account', async () => {
|
it('should respond with Forbideen if access token is not of an admin account', async () => {
|
||||||
const accessToken = generateAccessToken({
|
const user = {
|
||||||
client_id: 'someClientID',
|
displayname: 'User 1',
|
||||||
username: 'someUsername',
|
username: 'username1',
|
||||||
|
password: '12345678',
|
||||||
isadmin: false,
|
isadmin: false,
|
||||||
isactive: true
|
isactive: true
|
||||||
|
}
|
||||||
|
const accessToken = generateAccessToken({
|
||||||
|
client_id: client.client_id,
|
||||||
|
username: user.username
|
||||||
})
|
})
|
||||||
|
await createUser(user)
|
||||||
|
await saveTokensInDB(
|
||||||
|
user.username,
|
||||||
|
client.client_id,
|
||||||
|
accessToken,
|
||||||
|
'refreshToken'
|
||||||
|
)
|
||||||
|
|
||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.post('/SASjsApi/client')
|
.post('/SASjsApi/client')
|
||||||
.auth(accessToken, { type: 'bearer' })
|
.auth(accessToken, { type: 'bearer' })
|
||||||
.send(client)
|
.send(newClient)
|
||||||
.expect(403)
|
.expect(403)
|
||||||
|
|
||||||
expect(res.text).toEqual('Admin account required')
|
expect(res.text).toEqual('Admin account required')
|
||||||
@@ -79,12 +112,12 @@ describe('user', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should respond with Forbidden if client_id is already present', async () => {
|
it('should respond with Forbidden if client_id is already present', async () => {
|
||||||
await createClient(client)
|
await createClient(newClient)
|
||||||
|
|
||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.post('/SASjsApi/client')
|
.post('/SASjsApi/client')
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
.auth(adminAccessToken, { type: 'bearer' })
|
||||||
.send(client)
|
.send(newClient)
|
||||||
.expect(403)
|
.expect(403)
|
||||||
|
|
||||||
expect(res.text).toEqual('Error: Client ID already exists.')
|
expect(res.text).toEqual('Error: Client ID already exists.')
|
||||||
@@ -96,7 +129,7 @@ describe('user', () => {
|
|||||||
.post('/SASjsApi/client')
|
.post('/SASjsApi/client')
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
.auth(adminAccessToken, { type: 'bearer' })
|
||||||
.send({
|
.send({
|
||||||
...client,
|
...newClient,
|
||||||
client_id: undefined
|
client_id: undefined
|
||||||
})
|
})
|
||||||
.expect(400)
|
.expect(400)
|
||||||
@@ -110,7 +143,7 @@ describe('user', () => {
|
|||||||
.post('/SASjsApi/client')
|
.post('/SASjsApi/client')
|
||||||
.auth(adminAccessToken, { type: 'bearer' })
|
.auth(adminAccessToken, { type: 'bearer' })
|
||||||
.send({
|
.send({
|
||||||
...client,
|
...newClient,
|
||||||
client_secret: undefined
|
client_secret: undefined
|
||||||
})
|
})
|
||||||
.expect(400)
|
.expect(400)
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import mongoose, { Mongoose } from 'mongoose'
|
||||||
|
import { MongoMemoryServer } from 'mongodb-memory-server'
|
||||||
import request from 'supertest'
|
import request from 'supertest'
|
||||||
import app from '../../../app'
|
import app from '../../../app'
|
||||||
import { getTreeExample } from '../../../controllers/deploy'
|
import { getTreeExample } from '../../../controllers/deploy'
|
||||||
@@ -5,15 +7,50 @@ import { getTmpFilesFolderPath } from '../../../utils/file'
|
|||||||
import { folderExists, fileExists, readFile, deleteFolder } from '@sasjs/utils'
|
import { folderExists, fileExists, readFile, deleteFolder } from '@sasjs/utils'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { generateAccessToken } from '../auth'
|
import { generateAccessToken } from '../auth'
|
||||||
|
import { createUser } from '../../../controllers/createUser'
|
||||||
|
import { saveTokensInDB } from '../../../utils'
|
||||||
|
|
||||||
|
const client = {
|
||||||
|
clientid: 'someclientID',
|
||||||
|
clientsecret: 'someclientSecret'
|
||||||
|
}
|
||||||
|
const user = {
|
||||||
|
displayname: 'Test User',
|
||||||
|
username: 'testUsername',
|
||||||
|
password: '87654321',
|
||||||
|
isadmin: false,
|
||||||
|
isactive: true
|
||||||
|
}
|
||||||
|
|
||||||
describe('files', () => {
|
describe('files', () => {
|
||||||
const accessToken = generateAccessToken({
|
let con: Mongoose
|
||||||
client_id: 'someClientID',
|
let mongoServer: MongoMemoryServer
|
||||||
username: 'username',
|
|
||||||
isadmin: false,
|
beforeAll(async () => {
|
||||||
isactive: true
|
mongoServer = await MongoMemoryServer.create()
|
||||||
|
con = await mongoose.connect(mongoServer.getUri())
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await con.connection.dropDatabase()
|
||||||
|
await con.connection.close()
|
||||||
|
await mongoServer.stop()
|
||||||
})
|
})
|
||||||
describe('deploy', () => {
|
describe('deploy', () => {
|
||||||
|
const accessToken = generateAccessToken({
|
||||||
|
client_id: client.clientid,
|
||||||
|
username: user.username
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await createUser(user)
|
||||||
|
await saveTokensInDB(
|
||||||
|
user.username,
|
||||||
|
client.clientid,
|
||||||
|
accessToken,
|
||||||
|
'refreshToken'
|
||||||
|
)
|
||||||
|
})
|
||||||
const shouldFailAssertion = async (payload: any) => {
|
const shouldFailAssertion = async (payload: any) => {
|
||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.post('/SASjsApi/drive/deploy')
|
.post('/SASjsApi/drive/deploy')
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import request from 'supertest'
|
|||||||
import app from '../../../app'
|
import app from '../../../app'
|
||||||
import { createUser } from '../../../controllers/createUser'
|
import { createUser } from '../../../controllers/createUser'
|
||||||
import { generateAccessToken } from '../auth'
|
import { generateAccessToken } from '../auth'
|
||||||
|
import { saveTokensInDB } from '../../../utils'
|
||||||
|
|
||||||
const client = {
|
const client = {
|
||||||
clientid: 'someclientID',
|
clientid: 'someclientID',
|
||||||
@@ -42,9 +43,17 @@ describe('user', () => {
|
|||||||
describe('create', () => {
|
describe('create', () => {
|
||||||
const adminAccessToken = generateAccessToken({
|
const adminAccessToken = generateAccessToken({
|
||||||
client_id: client.clientid,
|
client_id: client.clientid,
|
||||||
username: adminUser.username,
|
username: adminUser.username
|
||||||
isadmin: adminUser.isadmin,
|
})
|
||||||
isactive: adminUser.isactive
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await createUser(adminUser)
|
||||||
|
await saveTokensInDB(
|
||||||
|
adminUser.username,
|
||||||
|
client.clientid,
|
||||||
|
adminAccessToken,
|
||||||
|
'refreshToken'
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
@@ -79,10 +88,15 @@ describe('user', () => {
|
|||||||
it('should respond with Forbideen if access token is not of an admin account', async () => {
|
it('should respond with Forbideen if access token is not of an admin account', async () => {
|
||||||
const accessToken = generateAccessToken({
|
const accessToken = generateAccessToken({
|
||||||
client_id: client.clientid,
|
client_id: client.clientid,
|
||||||
username: user.username,
|
username: user.username
|
||||||
isadmin: user.isadmin,
|
|
||||||
isactive: user.isactive
|
|
||||||
})
|
})
|
||||||
|
await createUser(user)
|
||||||
|
await saveTokensInDB(
|
||||||
|
user.username,
|
||||||
|
client.clientid,
|
||||||
|
accessToken,
|
||||||
|
'refreshToken'
|
||||||
|
)
|
||||||
|
|
||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.post('/SASjsApi/user')
|
.post('/SASjsApi/user')
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ userRouter.post('/', async (req, res) => {
|
|||||||
displayname: savedUser.displayname,
|
displayname: savedUser.displayname,
|
||||||
username: savedUser.username,
|
username: savedUser.username,
|
||||||
isadmin: savedUser.isadmin,
|
isadmin: savedUser.isadmin,
|
||||||
isactive: savedUser.isactive
|
isactive: savedUser.isactive,
|
||||||
|
tokens: []
|
||||||
})
|
})
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
res.status(403).send(err.toString())
|
res.status(403).send(err.toString())
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
export interface InfoJWT {
|
export interface InfoJWT {
|
||||||
client_id: string
|
client_id: string
|
||||||
username: string
|
username: string
|
||||||
isadmin: boolean
|
|
||||||
isactive: boolean
|
|
||||||
}
|
}
|
||||||
|
|||||||
25
src/utils/authenticateToken.ts
Normal file
25
src/utils/authenticateToken.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import jwt from 'jsonwebtoken'
|
||||||
|
import { verifyTokenInDB } from './verifyTokenInDB'
|
||||||
|
|
||||||
|
export const authenticateToken = (req: any, res: any, next: any) => {
|
||||||
|
const authHeader = req.headers['authorization']
|
||||||
|
const token = authHeader?.split(' ')[1]
|
||||||
|
if (!token) return res.sendStatus(401)
|
||||||
|
|
||||||
|
jwt.verify(
|
||||||
|
token,
|
||||||
|
process.env.ACCESS_TOKEN_SECRET as string,
|
||||||
|
async (err: any, data: any) => {
|
||||||
|
if (err) return res.sendStatus(403)
|
||||||
|
|
||||||
|
// verify this valid token's entry in DB
|
||||||
|
const user = await verifyTokenInDB(data?.username, data?.client_id, token)
|
||||||
|
|
||||||
|
if (user) {
|
||||||
|
req.user = user
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
return res.sendStatus(403)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,4 +1,8 @@
|
|||||||
|
export * from './authenticateToken'
|
||||||
export * from './file'
|
export * from './file'
|
||||||
|
export * from './removeTokensInDB'
|
||||||
|
export * from './saveTokensInDB'
|
||||||
export * from './sleep'
|
export * from './sleep'
|
||||||
export * from './upload'
|
export * from './upload'
|
||||||
export * from './validation'
|
export * from './validation'
|
||||||
|
export * from './verifyTokenInDB'
|
||||||
|
|||||||
14
src/utils/removeTokensInDB.ts
Normal file
14
src/utils/removeTokensInDB.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import User from '../model/User'
|
||||||
|
|
||||||
|
export const removeTokensInDB = async (username: string, client_id: string) => {
|
||||||
|
const user = await User.findOne({ username })
|
||||||
|
|
||||||
|
const tokenObjIndex = user.tokens.findIndex(
|
||||||
|
(tokenObj: any) => tokenObj.clientid === client_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if (tokenObjIndex > -1) {
|
||||||
|
user.tokens.splice(tokenObjIndex, 1)
|
||||||
|
await user.save()
|
||||||
|
}
|
||||||
|
}
|
||||||
25
src/utils/saveTokensInDB.ts
Normal file
25
src/utils/saveTokensInDB.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import User from '../model/User'
|
||||||
|
|
||||||
|
export const saveTokensInDB = async (
|
||||||
|
username: string,
|
||||||
|
client_id: string,
|
||||||
|
accessToken: string,
|
||||||
|
refreshToken: string
|
||||||
|
) => {
|
||||||
|
const user = await User.findOne({ username })
|
||||||
|
|
||||||
|
const currentTokenObj = user.tokens.find(
|
||||||
|
(tokenObj: any) => tokenObj.clientid === client_id
|
||||||
|
)
|
||||||
|
if (currentTokenObj) {
|
||||||
|
currentTokenObj.accesstoken = accessToken
|
||||||
|
currentTokenObj.refreshtoken = refreshToken
|
||||||
|
} else {
|
||||||
|
user.tokens.push({
|
||||||
|
clientid: client_id,
|
||||||
|
accesstoken: accessToken,
|
||||||
|
refreshtoken: refreshToken
|
||||||
|
})
|
||||||
|
}
|
||||||
|
await user.save()
|
||||||
|
}
|
||||||
@@ -13,7 +13,6 @@ export const authorizeValidation = (data: any): Joi.ValidationResult =>
|
|||||||
export const tokenValidation = (data: any): Joi.ValidationResult =>
|
export const tokenValidation = (data: any): Joi.ValidationResult =>
|
||||||
Joi.object({
|
Joi.object({
|
||||||
client_id: Joi.string().required(),
|
client_id: Joi.string().required(),
|
||||||
client_secret: Joi.string().required(),
|
|
||||||
code: Joi.string().required()
|
code: Joi.string().required()
|
||||||
}).validate(data)
|
}).validate(data)
|
||||||
|
|
||||||
|
|||||||
22
src/utils/verifyTokenInDB.ts
Normal file
22
src/utils/verifyTokenInDB.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import User from '../model/User'
|
||||||
|
|
||||||
|
export const verifyTokenInDB = async (
|
||||||
|
username: string,
|
||||||
|
client_id: string,
|
||||||
|
token: string
|
||||||
|
) => {
|
||||||
|
const dbUser = await User.findOne({ username })
|
||||||
|
|
||||||
|
const currentTokenObj = dbUser.tokens.find(
|
||||||
|
(tokenObj: any) => tokenObj.clientid === client_id
|
||||||
|
)
|
||||||
|
|
||||||
|
return currentTokenObj?.accesstoken === token
|
||||||
|
? {
|
||||||
|
client_id,
|
||||||
|
username,
|
||||||
|
isadmin: dbUser.isadmin,
|
||||||
|
isactive: dbUser.isactive
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user