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:
222
src/controllers/auth.ts
Normal file
222
src/controllers/auth.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user