1
0
mirror of https://github.com/sasjs/server.git synced 2026-01-05 05:40:06 +00:00

chore: auth docs also added

This commit is contained in:
Saad Jutt
2021-11-05 15:57:36 +05:00
parent 52c3823f20
commit dbbdaa83f0
9 changed files with 289 additions and 124 deletions

222
src/controllers/auth.ts Normal file
View File

@@ -0,0 +1,222 @@
import { Security, Route, Tags, Example, Post, Body, Query, Hidden } from 'tsoa'
import jwt from 'jsonwebtoken'
import bcrypt from 'bcryptjs'
import User from '../model/User'
import { InfoJWT } from '../types'
import { removeTokensInDB, saveTokensInDB } from '../utils'
@Route('SASjsApi/auth')
@Tags('Auth')
export default class AuthController {
static authCodes: { [key: string]: { [key: string]: string } } = {}
static saveCode = (userId: number, clientId: string, code: string) => {
if (AuthController.authCodes[userId])
return (AuthController.authCodes[userId][clientId] = code)
AuthController.authCodes[userId] = { [clientId]: code }
return AuthController.authCodes[userId][clientId]
}
static deleteCode = (userId: number, clientId: string) =>
delete AuthController.authCodes[userId][clientId]
/**
* Accept a valid username/password, plus a CLIENT_ID, and return an AUTH_CODE
*
*/
@Example<AuthorizeResponse>({
code: 'someRandomCryptoString'
})
@Post('/authorize')
public async authorize(
@Body() body: AuthorizePayload
): Promise<AuthorizeResponse> {
return authorize(body)
}
/**
* Accepts client/auth code and returns access/refresh tokens
*
*/
@Example<TokenResponse>({
accessToken: 'someRandomCryptoString',
refreshToken: 'someRandomCryptoString'
})
@Post('/token')
public async token(@Body() body: TokenPayload): Promise<TokenResponse> {
return token(body)
}
/**
* Returns new access/refresh tokens
*
*/
@Example<TokenResponse>({
accessToken: 'someRandomCryptoString',
refreshToken: 'someRandomCryptoString'
})
@Security('bearerAuth')
@Post('/refresh')
public async refresh(
@Query() @Hidden() data?: InfoJWT
): Promise<TokenResponse> {
return refresh(data!)
}
/**
* Logout terminate access/refresh tokens and returns nothing
*
*/
@Security('bearerAuth')
@Post('/logout')
public async logout(@Query() @Hidden() data?: InfoJWT) {
return logout(data!)
}
}
const authorize = async (data: any): Promise<AuthorizeResponse> => {
const { username, password, clientId } = data
// Authenticate User
const user = await User.findOne({ username })
if (!user) throw new Error('Username is not found.')
const validPass = await bcrypt.compare(password, user.password)
if (!validPass) throw new Error('Invalid password.')
// generate authorization code against clientId
const userInfo: InfoJWT = {
clientId,
userId: user.id
}
const code = AuthController.saveCode(
user.id,
clientId,
generateAuthCode(userInfo)
)
return { code }
}
const token = async (data: any): Promise<TokenResponse> => {
const { clientId, code } = data
const userInfo = await verifyAuthCode(clientId, code)
if (!userInfo) throw new Error('Invalid Auth Code')
if (AuthController.authCodes[userInfo.userId][clientId] !== code)
throw new Error('Invalid Auth Code')
AuthController.deleteCode(userInfo.userId, clientId)
const accessToken = generateAccessToken(userInfo)
const refreshToken = generateRefreshToken(userInfo)
await saveTokensInDB(userInfo.userId, clientId, accessToken, refreshToken)
return { accessToken, refreshToken }
}
const refresh = async (userInfo: InfoJWT): Promise<TokenResponse> => {
const accessToken = generateAccessToken(userInfo)
const refreshToken = generateRefreshToken(userInfo)
await saveTokensInDB(
userInfo.userId,
userInfo.clientId,
accessToken,
refreshToken
)
return { accessToken, refreshToken }
}
const logout = async (userInfo: InfoJWT) => {
await removeTokensInDB(userInfo.userId, userInfo.clientId)
}
interface AuthorizePayload {
/**
* Username for user
* @example "johnSnow01"
*/
username: string
/**
* Password for user
* @example "secretpassword"
*/
password: string
/**
* Client ID
* @example "someFormattedClientID1234"
*/
clientId: string
}
interface AuthorizeResponse {
/**
* Authorization code
* @example "someRandomCryptoString"
*/
code: string
}
interface TokenPayload {
/**
* Client ID
* @example "someFormattedClientID1234"
*/
clientId: string
/**
* Authorization code
* @example "someRandomCryptoString"
*/
code: string
}
interface TokenResponse {
/**
* Access Token
* @example "someRandomCryptoString"
*/
accessToken: string
/**
* Refresh Token
* @example "someRandomCryptoString"
*/
refreshToken: string
}
export const generateAuthCode = (data: InfoJWT) =>
jwt.sign(data, process.env.AUTH_CODE_SECRET as string, {
expiresIn: '30s'
})
export const generateAccessToken = (data: InfoJWT) =>
jwt.sign(data, process.env.ACCESS_TOKEN_SECRET as string, {
expiresIn: '1h'
})
export const generateRefreshToken = (data: InfoJWT) =>
jwt.sign(data, process.env.REFRESH_TOKEN_SECRET as string, {
expiresIn: '1day'
})
const verifyAuthCode = async (
clientId: string,
code: string
): Promise<InfoJWT | undefined> => {
return new Promise((resolve, reject) => {
jwt.verify(code, process.env.AUTH_CODE_SECRET as string, (err, data) => {
if (err) return resolve(undefined)
const clientInfo: InfoJWT = {
clientId: data?.clientId,
userId: data?.userId
}
if (clientInfo.clientId === clientId) {
return resolve(clientInfo)
}
return resolve(undefined)
})
})
}

View File

@@ -16,13 +16,13 @@ import bcrypt from 'bcryptjs'
import User, { UserPayload } from '../model/User' import User, { UserPayload } from '../model/User'
interface userResponse { interface UserResponse {
id: number id: number
username: string username: string
displayName: string displayName: string
} }
interface userDetailsResponse { interface UserDetailsResponse {
id: number id: number
displayName: string displayName: string
username: string username: string
@@ -38,7 +38,7 @@ export default class UserController {
* Get list of all users (username, displayname). All users can request this. * Get list of all users (username, displayname). All users can request this.
* *
*/ */
@Example<userResponse[]>([ @Example<UserResponse[]>([
{ {
id: 123, id: 123,
username: 'johnusername', username: 'johnusername',
@@ -51,7 +51,7 @@ export default class UserController {
} }
]) ])
@Get('/') @Get('/')
public async getAllUsers(): Promise<userResponse[]> { public async getAllUsers(): Promise<UserResponse[]> {
return getAllUsers() return getAllUsers()
} }
@@ -59,7 +59,7 @@ export default class UserController {
* Create user with the following attributes: UserId, UserName, Password, isAdmin, isActive. Admin only task. * Create user with the following attributes: UserId, UserName, Password, isAdmin, isActive. Admin only task.
* *
*/ */
@Example<userDetailsResponse>({ @Example<UserDetailsResponse>({
id: 1234, id: 1234,
displayName: 'John Snow', displayName: 'John Snow',
username: 'johnSnow01', username: 'johnSnow01',
@@ -69,7 +69,7 @@ export default class UserController {
@Post('/') @Post('/')
public async createUser( public async createUser(
@Body() body: UserPayload @Body() body: UserPayload
): Promise<userDetailsResponse> { ): Promise<UserDetailsResponse> {
return createUser(body) return createUser(body)
} }
@@ -79,7 +79,7 @@ export default class UserController {
* @example userId 1234 * @example userId 1234
*/ */
@Get('{userId}') @Get('{userId}')
public async getUser(@Path() userId: number): Promise<userDetailsResponse> { public async getUser(@Path() userId: number): Promise<UserDetailsResponse> {
return getUser(userId) return getUser(userId)
} }
@@ -88,7 +88,7 @@ export default class UserController {
* @param userId The user's identifier * @param userId The user's identifier
* @example userId "1234" * @example userId "1234"
*/ */
@Example<userDetailsResponse>({ @Example<UserDetailsResponse>({
id: 1234, id: 1234,
displayName: 'John Snow', displayName: 'John Snow',
username: 'johnSnow01', username: 'johnSnow01',
@@ -99,7 +99,7 @@ export default class UserController {
public async updateUser( public async updateUser(
@Path() userId: number, @Path() userId: number,
@Body() body: UserPayload @Body() body: UserPayload
): Promise<userDetailsResponse> { ): Promise<UserDetailsResponse> {
return updateUser(userId, body) return updateUser(userId, body)
} }
@@ -118,12 +118,12 @@ export default class UserController {
} }
} }
const getAllUsers = async (): Promise<userResponse[]> => const getAllUsers = async (): Promise<UserResponse[]> =>
await User.find({}) await User.find({})
.select({ _id: 0, id: 1, username: 1, displayName: 1 }) .select({ _id: 0, id: 1, username: 1, displayName: 1 })
.exec() .exec()
const createUser = async (data: any): Promise<userDetailsResponse> => { const createUser = async (data: any): Promise<UserDetailsResponse> => {
const { displayName, username, password, isAdmin, isActive } = data const { displayName, username, password, isAdmin, isActive } = data
// Checking if user is already in the database // Checking if user is already in the database
@@ -145,7 +145,13 @@ const createUser = async (data: any): Promise<userDetailsResponse> => {
const savedUser = await user.save() const savedUser = await user.save()
return savedUser return {
id: savedUser.id,
displayName: savedUser.displayName,
username: savedUser.username,
isActive: savedUser.isActive,
isAdmin: savedUser.isAdmin
}
} }
const getUser = async (id: number) => { const getUser = async (id: number) => {

View File

@@ -1,10 +1,9 @@
import express from 'express' import express from 'express'
import bcrypt from 'bcryptjs'
import mongoose from 'mongoose' import mongoose from 'mongoose'
import jwt from 'jsonwebtoken' import jwt from 'jsonwebtoken'
import AuthController from '../../controllers/auth'
import Client from '../../model/Client' import Client from '../../model/Client'
import User from '../../model/User'
import { import {
authenticateAccessToken, authenticateAccessToken,
@@ -22,7 +21,6 @@ import { InfoJWT } from '../../types'
const authRouter = express.Router() const authRouter = express.Router()
const clientIDs = new Set() const clientIDs = new Set()
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()
@@ -46,127 +44,63 @@ export const connectDB = () => {
} }
} }
export const saveCode = (userId: number, clientId: string, code: string) => {
if (authCodes[userId]) return (authCodes[userId][clientId] = code)
authCodes[userId] = { [clientId]: code }
return authCodes[userId][clientId]
}
export const deleteCode = (userId: number, clientId: string) =>
delete authCodes[userId][clientId]
authRouter.post('/authorize', async (req, res) => { authRouter.post('/authorize', async (req, res) => {
const { error, value } = authorizeValidation(req.body) const { error, value: body } = authorizeValidation(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 { username, password, clientId } = value const { clientId } = body
// Verify client ID // Verify client ID
if (!clientIDs.has(clientId)) { if (!clientIDs.has(clientId)) {
return res.status(403).send('Invalid clientId.') return res.status(403).send('Invalid clientId.')
} }
// Authenticate User const controller = new AuthController()
const user = await User.findOne({ username }) try {
if (!user) return res.status(403).send('Username is not found.') const response = await controller.authorize(body)
const validPass = await bcrypt.compare(password, user.password) res.send(response)
if (!validPass) return res.status(403).send('Invalid password.') } catch (err: any) {
res.status(403).send(err.toString())
// generate authorization code against clientId
const userInfo: InfoJWT = {
clientId,
userId: user.id
} }
const code = saveCode(user.id, clientId, generateAuthCode(userInfo))
res.json({ code })
}) })
authRouter.post('/token', async (req, res) => { authRouter.post('/token', async (req, res) => {
const { error, value } = tokenValidation(req.body) const { error, value: body } = 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 { clientId, code } = value const controller = new AuthController()
try {
const response = await controller.token(body)
const userInfo = await verifyAuthCode(clientId, code) res.send(response)
if (!userInfo) return res.sendStatus(403) } catch (err: any) {
res.status(403).send(err.toString())
if (authCodes[userInfo.userId][clientId] !== code) return res.sendStatus(403) }
deleteCode(userInfo.userId, clientId)
const accessToken = generateAccessToken(userInfo)
const refreshToken = jwt.sign(
userInfo,
process.env.REFRESH_TOKEN_SECRET as string
)
await saveTokensInDB(userInfo.userId, clientId, accessToken, refreshToken)
res.json({ accessToken: accessToken, refreshToken: refreshToken })
}) })
authRouter.post('/refresh', authenticateRefreshToken, async (req: any, res) => { authRouter.post('/refresh', authenticateRefreshToken, async (req: any, res) => {
const { userId, clientId } = req.user const userInfo: InfoJWT = req.user
const userInfo = {
userId, const controller = new AuthController()
clientId try {
const response = await controller.refresh(userInfo)
res.send(response)
} catch (err: any) {
res.status(403).send(err.toString())
} }
const accessToken = generateAccessToken(userInfo)
const refreshToken = jwt.sign(
userInfo,
process.env.REFRESH_TOKEN_SECRET as string
)
await saveTokensInDB(userInfo.userId, clientId, accessToken, refreshToken)
res.json({ accessToken: accessToken, refreshToken: refreshToken })
}) })
authRouter.delete('/logout', authenticateAccessToken, async (req: any, res) => { authRouter.delete('/logout', authenticateAccessToken, async (req: any, res) => {
const { user } = req const userInfo: InfoJWT = req.user
await removeTokensInDB(user.username, user.clientId) const controller = new AuthController()
try {
await controller.logout(userInfo)
} catch (e) {}
res.sendStatus(204) res.sendStatus(204)
}) })
export const generateAccessToken = (data: InfoJWT) =>
jwt.sign(data, process.env.ACCESS_TOKEN_SECRET as string, {
expiresIn: '1h'
})
export const generateRefreshToken = (data: InfoJWT) =>
jwt.sign(data, process.env.REFRESH_TOKEN_SECRET as string, {
expiresIn: '1day'
})
export const generateAuthCode = (data: InfoJWT) =>
jwt.sign(data, process.env.AUTH_CODE_SECRET as string, {
expiresIn: '30s'
})
const verifyAuthCode = async (
clientId: string,
code: string
): Promise<InfoJWT | undefined> => {
return new Promise((resolve, reject) => {
jwt.verify(code, process.env.AUTH_CODE_SECRET as string, (err, data) => {
if (err) return resolve(undefined)
const clientInfo: InfoJWT = {
clientId: data?.clientId,
userId: data?.userId
}
if (clientInfo.clientId === clientId) {
return resolve(clientInfo)
}
return resolve(undefined)
})
})
}
export default authRouter export default authRouter

View File

@@ -4,13 +4,12 @@ import request from 'supertest'
import app from '../../../app' import app from '../../../app'
import UserController from '../../../controllers/user' import UserController from '../../../controllers/user'
import ClientController from '../../../controllers/client' import ClientController from '../../../controllers/client'
import { import AuthController, {
generateAccessToken, generateAccessToken,
generateAuthCode, generateAuthCode,
generateRefreshToken, generateRefreshToken
populateClients, } from '../../../controllers/auth'
saveCode import { populateClients } from '../auth'
} from '../auth'
import { InfoJWT } from '../../../types' import { InfoJWT } from '../../../types'
import { saveTokensInDB, verifyTokenInDB } from '../../../utils' import { saveTokensInDB, verifyTokenInDB } from '../../../utils'
@@ -115,7 +114,7 @@ describe('auth', () => {
}) })
.expect(403) .expect(403)
expect(res.text).toEqual('Username is not found.') expect(res.text).toEqual('Error: Username is not found.')
expect(res.body).toEqual({}) expect(res.body).toEqual({})
}) })
@@ -131,7 +130,7 @@ describe('auth', () => {
}) })
.expect(403) .expect(403)
expect(res.text).toEqual('Invalid password.') expect(res.text).toEqual('Error: Invalid password.')
expect(res.body).toEqual({}) expect(res.body).toEqual({})
}) })
@@ -167,7 +166,7 @@ describe('auth', () => {
}) })
it('should respond with access and refresh tokens', async () => { it('should respond with access and refresh tokens', async () => {
const code = saveCode( const code = AuthController.saveCode(
userInfo.userId, userInfo.userId,
userInfo.clientId, userInfo.clientId,
generateAuthCode(userInfo) generateAuthCode(userInfo)
@@ -198,7 +197,7 @@ describe('auth', () => {
}) })
it('should respond with Bad Request if clientId is missing', async () => { it('should respond with Bad Request if clientId is missing', async () => {
const code = saveCode( const code = AuthController.saveCode(
userInfo.userId, userInfo.userId,
userInfo.clientId, userInfo.clientId,
generateAuthCode(userInfo) generateAuthCode(userInfo)
@@ -228,7 +227,7 @@ describe('auth', () => {
}) })
it('should respond with Forbidden if clientId is invalid', async () => { it('should respond with Forbidden if clientId is invalid', async () => {
const code = saveCode( const code = AuthController.saveCode(
userInfo.userId, userInfo.userId,
userInfo.clientId, userInfo.clientId,
generateAuthCode(userInfo) generateAuthCode(userInfo)

View File

@@ -4,7 +4,7 @@ import request from 'supertest'
import app from '../../../app' import app from '../../../app'
import UserController from '../../../controllers/user' import UserController from '../../../controllers/user'
import ClientController from '../../../controllers/client' import ClientController from '../../../controllers/client'
import { generateAccessToken } from '../auth' import { generateAccessToken } from '../../../controllers/auth'
import { saveTokensInDB } from '../../../utils' import { saveTokensInDB } from '../../../utils'
const client = { const client = {

View File

@@ -7,7 +7,7 @@ import UserController from '../../../controllers/user'
import { getTmpFilesFolderPath } from '../../../utils/file' 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 '../../../controllers/auth'
import { saveTokensInDB } from '../../../utils' import { saveTokensInDB } from '../../../utils'
const clientId = 'someclientID' const clientId = 'someclientID'

View File

@@ -3,7 +3,7 @@ import { MongoMemoryServer } from 'mongodb-memory-server'
import request from 'supertest' import request from 'supertest'
import app from '../../../app' import app from '../../../app'
import UserController from '../../../controllers/user' import UserController from '../../../controllers/user'
import { generateAccessToken } from '../auth' import { generateAccessToken } from '../../../controllers/auth'
import { saveTokensInDB } from '../../../utils' import { saveTokensInDB } from '../../../utils'
const clientId = 'someclientID' const clientId = 'someclientID'

View File

@@ -1,7 +1,7 @@
import User from '../model/User' import User from '../model/User'
export const removeTokensInDB = async (username: string, clientId: string) => { export const removeTokensInDB = async (userId: number, clientId: string) => {
const user = await User.findOne({ username }) const user = await User.findOne({ id: userId })
if (!user) return if (!user) return
const tokenObjIndex = user.tokens.findIndex( const tokenObjIndex = user.tokens.findIndex(

View File

@@ -18,6 +18,10 @@
{ {
"name": "Client", "name": "Client",
"description": "Operations about clients" "description": "Operations about clients"
},
{
"name": "Auth",
"description": "Operations about auth"
} }
], ],
"specVersion": 3 "specVersion": 3