Skip to content

Commit

Permalink
feat: improve type declarations using generic Request/Response types
Browse files Browse the repository at this point in the history
  • Loading branch information
Lordfirespeed committed Aug 24, 2024
1 parent 1352b2a commit dac6bd6
Show file tree
Hide file tree
Showing 2 changed files with 61 additions and 31 deletions.
34 changes: 20 additions & 14 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,25 @@ import type {
GenerateCsrfTokenConfig,
RequestMethod,
ResolvedCSRFCookieOptions,
doubleCsrfProtection,
DoubleCsrfProtection,
} from "./types"

function setSecretCookie(
req: CSRFRequest,
res: CSRFResponse,
function setSecretCookie<
Request extends CSRFRequest = CSRFRequest,
Response extends CSRFResponse<Request> = CSRFResponse<Request>
>(
req: Request,
res: Response,
secret: string,
{ name, ...options }: ResolvedCSRFCookieOptions,
): void {
res.cookie(name, secret, options)
}

export function doubleCsrf({
export function doubleCsrf<
Request extends CSRFRequest = CSRFRequest,
Response extends CSRFResponse<Request> = CSRFResponse<Request>
>({
getSecret,
getSessionIdentifier,
cookieOptions,
Expand All @@ -38,7 +44,7 @@ export function doubleCsrf({
return header
},
errorConfig: { statusCode = 403, message = "invalid csrf token", code = "ERR_BAD_CSRF_TOKEN" } = {},
}: DoubleCsrfConfig): DoubleCsrfUtilities {
}: DoubleCsrfConfig<Request, Response>): DoubleCsrfUtilities<Request, Response> {
const ignoredMethodsSet = new Set(ignoredMethods)
const defaultCookieOptions: ResolvedCSRFCookieOptions = Object.assign(
{
Expand All @@ -58,8 +64,8 @@ export function doubleCsrf({
})

const generateTokenAndHash = async (
req: CSRFRequest,
res: CSRFResponse,
req: Request,
res: Response,
{ overwrite, validateOnReuse }: Omit<GenerateCsrfTokenConfig, "cookieOptions">,
) => {
const getSecretResult = await getSecret(req, res)
Expand Down Expand Up @@ -106,8 +112,8 @@ export function doubleCsrf({
// The value returned from this should ONLY be sent to the client via a response payload.
// Do NOT send the csrfToken as a cookie, embed it in your HTML response, or as JSON.
const generateToken: CsrfTokenCreator = async (
req: CSRFRequest,
res: CSRFResponse,
req: Request,
res: Response,
{ cookieOptions = defaultCookieOptions, overwrite = false, validateOnReuse = true } = {},
) => {
const { csrfToken, csrfTokenHash } = await generateTokenAndHash(req, res, {
Expand All @@ -121,10 +127,10 @@ export function doubleCsrf({
return csrfToken
}

const getCsrfCookieFromRequest = (req: CSRFRequest) => req.cookies?.[defaultCookieOptions.name]
const getCsrfCookieFromRequest = (req: Request) => req.cookies?.[defaultCookieOptions.name]

// given a secret array, iterates over it and checks whether one of the secrets makes the token and hash pair valid
const validateTokenAndHashPair: CsrfTokenAndHashPairValidator = async (
const validateTokenAndHashPair: CsrfTokenAndHashPairValidator<Request, Response> = async (
req,
res,
{ incomingHash, incomingToken, possibleSecrets },
Expand All @@ -141,7 +147,7 @@ export function doubleCsrf({
return false
}

const validateRequest: CsrfRequestValidator = async (req, res) => {
const validateRequest: CsrfRequestValidator<Request, Response> = async (req, res) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
const csrfCookie = getCsrfCookieFromRequest(req)
if (typeof csrfCookie !== "object") return false
Expand All @@ -165,7 +171,7 @@ export function doubleCsrf({
)
}

const doubleCsrfProtection: doubleCsrfProtection = async (req, res, next) => {
const doubleCsrfProtection: DoubleCsrfProtection<Request, Response> = async (req, res, next) => {
if (ignoredMethodsSet.has(req.method as RequestMethod)) {
next()
return
Expand Down
58 changes: 41 additions & 17 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export type CSRFRequest = IncomingMessage & {
cookies: Record<string, Cookie>
}

export type CSRFResponse = ServerResponse & {
export type CSRFResponse<Request extends CSRFRequest = CSRFRequest> = ServerResponse<Request> & {
cookie: (name: string, value: string, options?: SerializeOptions) => unknown
}

Expand All @@ -25,15 +25,30 @@ type ExtraCookieOptions = {
export type CSRFCookieOptions = SerializeOptions & ExtraCookieOptions
export type ResolvedCSRFCookieOptions = SerializeOptions & Required<ExtraCookieOptions>

export type TokenRetriever = (req: CSRFRequest, res: CSRFResponse) => string | null | undefined | Promise<string | null | undefined>
export type CsrfSecretRetriever = (req: CSRFRequest, res: CSRFResponse) => string | Array<string> | Promise<string | Array<string>>
export type doubleCsrfProtection = (req: CSRFRequest, res: CSRFResponse, next: NextFunction) => Promise<void>
export type TokenRetriever<
Request extends CSRFRequest = CSRFRequest,
Response extends CSRFResponse<Request> = CSRFResponse<Request>
> = (req: Request, res: Response) => string | null | undefined | Promise<string | null | undefined>
export type CsrfSecretRetriever<
Request extends CSRFRequest = CSRFRequest,
Response extends CSRFResponse<Request> = CSRFResponse<Request>
> = (req: Request, res: Response) => string | Array<string> | Promise<string | Array<string>>
export type DoubleCsrfProtection<
Request extends CSRFRequest = CSRFRequest,
Response extends CSRFResponse<Request> = CSRFResponse<Request>
> = (req: Request, res: Response, next: NextFunction) => Promise<void>
export type RequestMethod = "GET" | "HEAD" | "PATCH" | "PUT" | "POST" | "DELETE" | "CONNECT" | "OPTIONS" | "TRACE"
export type CsrfIgnoredMethods = Array<RequestMethod>
export type CsrfRequestValidator = (req: CSRFRequest, res: CSRFResponse) => Promise<boolean>
export type CsrfTokenAndHashPairValidator = (
req: CSRFRequest,
res: CSRFResponse,
export type CsrfRequestValidator<
Request extends CSRFRequest = CSRFRequest,
Response extends CSRFResponse<Request> = CSRFResponse<Request>
> = (req: Request, res: Response) => Promise<boolean>
export type CsrfTokenAndHashPairValidator<
Request extends CSRFRequest = CSRFRequest,
Response extends CSRFResponse<Request> = CSRFResponse<Request>
> = (
req: Request,
res: Response,
{
incomingHash,
incomingToken,
Expand All @@ -44,7 +59,10 @@ export type CsrfTokenAndHashPairValidator = (
possibleSecrets: Array<string>
},
) => Promise<boolean>
export type CsrfTokenCreator = (req: CSRFRequest, res: CSRFResponse, options?: GenerateCsrfTokenOptions) => Promise<string>
export type CsrfTokenCreator<
Request extends CSRFRequest = CSRFRequest,
Response extends CSRFResponse<Request> = CSRFResponse<Request>
> = (req: Request, res: Response, options?: GenerateCsrfTokenOptions) => Promise<string>
export type CsrfErrorConfig = {
statusCode: keyof typeof statusMessages
message: string
Expand All @@ -57,7 +75,10 @@ export type GenerateCsrfTokenConfig = {
cookieOptions: CSRFCookieOptions
}
export type GenerateCsrfTokenOptions = Partial<GenerateCsrfTokenConfig>
export type DoubleCsrfConfig = {
export type DoubleCsrfConfig<
Request extends CSRFRequest = CSRFRequest,
Response extends CSRFResponse<Request> = CSRFResponse<Request>
> = {
/**
* A function that returns a secret or an array of secrets.
* The first secret should be the newest/preferred secret.
Expand All @@ -76,15 +97,15 @@ export type DoubleCsrfConfig = {
* }
* ```
*/
getSecret: CsrfSecretRetriever
getSecret: CsrfSecretRetriever<Request, Response>

/**
* A function that should return the session identifier for the request.
* @param req The request object
* @returns the session identifier for the request
* @default (req) => req.session.id
*/
getSessionIdentifier: (req: CSRFRequest, res: CSRFResponse) => string | Promise<string>
getSessionIdentifier: (req: Request, res: Response) => string | Promise<string>

/**
* The options for HTTPOnly cookie that will be set on the response.
Expand Down Expand Up @@ -126,7 +147,7 @@ export type DoubleCsrfConfig = {
* }
* ```
*/
getTokenFromRequest?: TokenRetriever
getTokenFromRequest?: TokenRetriever<Request, Response>

/**
* Configuration for the error that is thrown any time XSRF token validation fails.
Expand All @@ -135,7 +156,10 @@ export type DoubleCsrfConfig = {
errorConfig?: CsrfErrorConfigOptions
}

export interface DoubleCsrfUtilities {
export interface DoubleCsrfUtilities<
Request extends CSRFRequest = CSRFRequest,
Response extends CSRFResponse<Request> = CSRFResponse<Request>
> {
/**
* The error that will be thrown if a request is invalid.
*/
Expand All @@ -158,14 +182,14 @@ export interface DoubleCsrfUtilities {
* });
* ```
*/
generateToken: CsrfTokenCreator
generateToken: CsrfTokenCreator<Request, Response>

/**
* Validates the request, assuring that the csrf token and hash pair are valid.
* @param req
* @returns true if the request is valid, false otherwise
*/
validateRequest: CsrfRequestValidator
validateRequest: CsrfRequestValidator<Request, Response>

/**
* Middleware that provides CSRF protection.
Expand All @@ -181,5 +205,5 @@ export interface DoubleCsrfUtilities {
* });
* ```
*/
doubleCsrfProtection: doubleCsrfProtection
doubleCsrfProtection: DoubleCsrfProtection<Request, Response>
}

0 comments on commit dac6bd6

Please sign in to comment.