feat: Add API key support

This commit is contained in:
2023-06-20 01:47:00 +02:00
parent 5c1ed32cb6
commit eac88b2ec3
14 changed files with 406 additions and 75 deletions

56
src/libs/CookieManager.ts Normal file
View File

@ -0,0 +1,56 @@
import type { ServerResponse } from 'node:http'
export default class CookieManager {
private cookies: Record<string, string> = {}
public constructor(data?: string | Record<string, string>) {
if (typeof data === 'string') {
data.split(';').forEach((keyValuePair) => {
const [key, value] = keyValuePair.split('=')
this.cookies[key.trim()] = value.trim().replace(/%3B/g, ';')
})
} else if (typeof data === 'object') {
this.cookies = data
}
}
public static addCookie(res: ServerResponse, cookie: {
key: string
value: string
expire?: string
maxAge?: number
domain?: string
path?: string
secure?: boolean
httpOnly?: boolean
sameSite?: 'Lax' | 'None' | 'Strict'}
) {
const items: Array<string> = [`${cookie.key}=${cookie.value.replace(/;/g, '%3B')}`]
if (cookie.expire) {
items.push(`Expires=${cookie.expire}`)
}
if (cookie.maxAge) {
items.push(`Max-Age=${cookie.maxAge}`)
}
if (cookie.domain) {
items.push(`Domain=${cookie.domain}`)
}
if (cookie.path) {
items.push(`Path=${cookie.path}`)
}
if (cookie.secure) {
items.push('Secure')
}
if (cookie.httpOnly) {
items.push('HttpOnly')
}
if (cookie.sameSite) {
items.push(`SameSite=${cookie.sameSite}`)
}
res.setHeader('Set-Cookie', items.join('; '))
}
public get(key: string): string | undefined {
return this.cookies[key]
}
}

View File

@ -52,6 +52,11 @@ export default interface RFC7807 {
instance?: string
}
/**
*
* @param error the error (base items are type, status, title details and instance)
* @returns
*/
export function buildRFC7807(error: RFC7807 & Record<string, any>): Response {
return new Response(JSON.stringify(error), {
headers: {

123
src/libs/validateAuth.ts Normal file
View File

@ -0,0 +1,123 @@
import DaoFactory from '../models/DaoFactory'
import CookieManager from './CookieManager'
import RFC7807, { buildRFC7807 } from './RFCs/RFC7807'
interface Permission {
name: string
/**
* if set it will be usable by users
*
* else only users with the `admin.` prefix in the key can run it
*/
api: boolean
/**
* if set to true it will pass if a cookie authenticate a valid user
*/
cookie: boolean
}
/**
* validate the authentification
* @param request the request
* @param permission the permission to validate
* @returns a Response if the request is invalid, null otherwise
*
* TODO: implement rate limiting
* http/2.0 429 TOO MANY REQUESTS
* Content-Type: application/json
* X-RateLimit-Limit: 1000
* X-RateLimit-Remaining: 0
* X-RateLimit-Reset: 123456789
* X-RateLimit-Reset-After: 9 // Number of seconds before the remaining Rate go back to 0
*/
export async function validateAuth(request: Request, permission: Permission): Promise<Response | null> {
const apiKeyHeader = request.headers.get('Authorization')
const cookieHeader = request.headers.get('Cookie')
if (apiKeyHeader) {
const apiKey = apiKeyHeader.slice(apiKeyHeader.lastIndexOf(' ') + 1)
const dao = await DaoFactory.get('apiKey').findOne({
key: apiKey
})
const perm = permission.name.split('.')
const match = dao?.permissions.find((it) => {
const itSplit = it.split('.')
if (itSplit[0] === 'admin') {
itSplit.shift()
}
for (let idx = 0; idx < itSplit.length; idx++) {
const permissionLayer = itSplit[idx]
const requestPermissionLayer = perm[idx]
if (permissionLayer === '*') {
return true
} else if (permissionLayer !== requestPermissionLayer) {
return false
}
}
return itSplit.length === perm.length
// return it.endsWith(permission.name)
})
if (match && (permission.api || match.startsWith('admin.'))) {
return null
} else if (permission.api) {
return buildRFC7807({
type: '/unauthorized-access',
status: 401,
title: 'Unauthorized access',
details: `you are missing the permission "${permission.name}" or is not an admin`
})
}
} else if (permission.api && !permission.cookie) {
return buildRFC7807({
type: '/unauthorized-access',
status: 401,
title: 'Unauthorized access',
details: `You MUST define an API key fo use this endpoint`
})
}
if (cookieHeader && permission.cookie) {
// TODO: make a better cookie implementation
const cookies = new CookieManager(cookieHeader)
const userCookie = cookies.get('userId')
if (!userCookie) {
return buildRFC7807({
type: '/unauthorized-access',
status: 401,
title: 'Unauthorized access',
details: `you must be connected to use this endpoint (missing the userId cookie)`
})
}
const dao = await DaoFactory.get('user').get(userCookie)
if (!dao) {
return buildRFC7807({
type: '/unauthorized-access',
status: 401,
title: 'Unauthorized access',
details: `the user does not exists`
})
}
return null
} else if (!permission.api && permission.cookie) {
return buildRFC7807({
type: '/unauthorized-access',
status: 401,
title: 'Unauthorized access',
details: `You MUST be connected to your account to use this endpoint`
})
} else if (permission.api && permission.cookie) {
return buildRFC7807({
type: '/unauthorized-access',
status: 401,
title: 'Unauthorized access',
details: `You must be connected or use an API key to access this endpoint`
})
}
return buildRFC7807({
type: '/unauthorized-access',
status: 401,
title: 'Unauthorized access',
details: `the following endpoint is not currently accessible by any means`
})
}