1
0
mirror of https://github.com/sasjs/server.git synced 2025-12-10 19:34:34 +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'
interface userResponse {
interface UserResponse {
id: number
username: string
displayName: string
}
interface userDetailsResponse {
interface UserDetailsResponse {
id: number
displayName: string
username: string
@@ -38,7 +38,7 @@ export default class UserController {
* Get list of all users (username, displayname). All users can request this.
*
*/
@Example<userResponse[]>([
@Example<UserResponse[]>([
{
id: 123,
username: 'johnusername',
@@ -51,7 +51,7 @@ export default class UserController {
}
])
@Get('/')
public async getAllUsers(): Promise<userResponse[]> {
public async getAllUsers(): Promise<UserResponse[]> {
return getAllUsers()
}
@@ -59,7 +59,7 @@ export default class UserController {
* Create user with the following attributes: UserId, UserName, Password, isAdmin, isActive. Admin only task.
*
*/
@Example<userDetailsResponse>({
@Example<UserDetailsResponse>({
id: 1234,
displayName: 'John Snow',
username: 'johnSnow01',
@@ -69,7 +69,7 @@ export default class UserController {
@Post('/')
public async createUser(
@Body() body: UserPayload
): Promise<userDetailsResponse> {
): Promise<UserDetailsResponse> {
return createUser(body)
}
@@ -79,7 +79,7 @@ export default class UserController {
* @example userId 1234
*/
@Get('{userId}')
public async getUser(@Path() userId: number): Promise<userDetailsResponse> {
public async getUser(@Path() userId: number): Promise<UserDetailsResponse> {
return getUser(userId)
}
@@ -88,7 +88,7 @@ export default class UserController {
* @param userId The user's identifier
* @example userId "1234"
*/
@Example<userDetailsResponse>({
@Example<UserDetailsResponse>({
id: 1234,
displayName: 'John Snow',
username: 'johnSnow01',
@@ -99,7 +99,7 @@ export default class UserController {
public async updateUser(
@Path() userId: number,
@Body() body: UserPayload
): Promise<userDetailsResponse> {
): Promise<UserDetailsResponse> {
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({})
.select({ _id: 0, id: 1, username: 1, displayName: 1 })
.exec()
const createUser = async (data: any): Promise<userDetailsResponse> => {
const createUser = async (data: any): Promise<UserDetailsResponse> => {
const { displayName, username, password, isAdmin, isActive } = data
// Checking if user is already in the database
@@ -145,7 +145,13 @@ const createUser = async (data: any): Promise<userDetailsResponse> => {
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) => {

View File

@@ -1,10 +1,9 @@
import express from 'express'
import bcrypt from 'bcryptjs'
import mongoose from 'mongoose'
import jwt from 'jsonwebtoken'
import AuthController from '../../controllers/auth'
import Client from '../../model/Client'
import User from '../../model/User'
import {
authenticateAccessToken,
@@ -22,7 +21,6 @@ import { InfoJWT } from '../../types'
const authRouter = express.Router()
const clientIDs = new Set()
const authCodes: { [key: string]: { [key: string]: string } } = {}
export const populateClients = async () => {
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) => {
const { error, value } = authorizeValidation(req.body)
const { error, value: body } = authorizeValidation(req.body)
if (error) return res.status(400).send(error.details[0].message)
const { username, password, clientId } = value
const { clientId } = body
// Verify client ID
if (!clientIDs.has(clientId)) {
return res.status(403).send('Invalid clientId.')
}
// Authenticate User
const user = await User.findOne({ username })
if (!user) return res.status(403).send('Username is not found.')
const controller = new AuthController()
try {
const response = await controller.authorize(body)
const validPass = await bcrypt.compare(password, user.password)
if (!validPass) return res.status(403).send('Invalid password.')
// generate authorization code against clientId
const userInfo: InfoJWT = {
clientId,
userId: user.id
res.send(response)
} catch (err: any) {
res.status(403).send(err.toString())
}
const code = saveCode(user.id, clientId, generateAuthCode(userInfo))
res.json({ code })
})
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)
const { clientId, code } = value
const controller = new AuthController()
try {
const response = await controller.token(body)
const userInfo = await verifyAuthCode(clientId, code)
if (!userInfo) return res.sendStatus(403)
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 })
res.send(response)
} catch (err: any) {
res.status(403).send(err.toString())
}
})
authRouter.post('/refresh', authenticateRefreshToken, async (req: any, res) => {
const { userId, clientId } = req.user
const userInfo = {
userId,
clientId
const userInfo: InfoJWT = req.user
const controller = new AuthController()
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) => {
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)
})
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

View File

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

View File

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

View File

@@ -7,7 +7,7 @@ import UserController from '../../../controllers/user'
import { getTmpFilesFolderPath } from '../../../utils/file'
import { folderExists, fileExists, readFile, deleteFolder } from '@sasjs/utils'
import path from 'path'
import { generateAccessToken } from '../auth'
import { generateAccessToken } from '../../../controllers/auth'
import { saveTokensInDB } from '../../../utils'
const clientId = 'someclientID'

View File

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

View File

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

View File

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