Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 14 additions & 6 deletions express-basic-auth.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/// <reference types="express" />

import { Request, RequestHandler } from 'express'
import { Request, RequestHandler, Response } from 'express'

/**
* This is the middleware builder.
Expand All @@ -16,14 +16,14 @@ declare function expressBasicAuth(options: expressBasicAuth.BasicAuthMiddlewareO
declare namespace expressBasicAuth {
/**
* Time safe string comparison function to protect against timing attacks.
*
*
* It is important to provide the arguments in the correct order, as the runtime
* depends only on the `userInput` argument. Switching the order would expose the `secret`
* to timing attacks.
*
*
* @param userInput The user input to be compared
* @param secret The secret value the user input should be compared with
*
*
* @returns true if `userInput` matches `secret`, false if not
*/
export function safeCompare(userInput: string, secret: string): boolean
Expand All @@ -46,7 +46,15 @@ declare namespace expressBasicAuth {
* })
*/
export interface IBasicAuthedRequest extends Request {
auth: { user: string, password: string }
auth?: { user: string, password: string }
}

export interface IUnauthorizeOptions {
challenge: boolean;
getResponseBody: any;
req: IBasicAuthedRequest;
realm: any;
res: Response;
}

type Authorizer = (username: string, password: string) => boolean
Expand Down Expand Up @@ -140,7 +148,7 @@ declare namespace expressBasicAuth {
* function authorizer(username, password, authorize) {
* if(username.startsWith('A') && password.startsWith('secret'))
* return authorize(null, true)
*
*
* return authorize(null, false)
* }
*/
Expand Down
2 changes: 2 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export {};
//# sourceMappingURL=index.d.ts.map
1 change: 1 addition & 0 deletions index.d.ts.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

186 changes: 98 additions & 88 deletions index.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions index.js.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

135 changes: 135 additions & 0 deletions index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import assert from 'assert';
import auth from 'basic-auth';
import { timingSafeEqual } from 'crypto';
import { NextFunction, RequestHandler, Response } from 'express';
import { BasicAuthMiddlewareOptions, IBasicAuthedRequest, IUnauthorizeOptions } from './express-basic-auth';


buildMiddleware.safeCompare = safeCompare;
module.exports = buildMiddleware;

function buildMiddleware(options: BasicAuthMiddlewareOptions): RequestHandler {
const { challenge,
authorizer,
isAsync,
getResponseBody,
realm
} = setupOptions(options);
return function authMiddleware(req: IBasicAuthedRequest, res: Response, next: NextFunction): any {
const unauthOpts: IUnauthorizeOptions = {
challenge,
getResponseBody,
req,
realm,
res,
};

const authentication = auth(req);
if (!authentication) {
return unauthorized(unauthOpts);
}

req.auth = {
user: authentication.name,
password: authentication.pass
}

if (isAsync) {
return authorizer(authentication.name, authentication.pass, (err: Error, approved: boolean) => {
return authorizerCallback(err, approved, next, unauthOpts);
});
} else if (!authorizer(authentication.name, authentication.pass)) {
return unauthorized(unauthOpts);
}
return next();
}
}

function setupOptions(options: any) {
const challenge = options.challenge === true;
const users = options['users'] || {}; // tslint:disable-line
const authorizer = options['authorizer'] || ((user: string, pass: string) => { return staticUsersAuthorizer(user, pass, users); });
const isAsync = options['authorizeAsync'] === true;
const getResponseBody = ensureFunction(options.unauthorizedResponse, '');
const realm = ensureFunction(options['realm']);

assert(typeof users === 'object', `Expected an object for the basic auth users, found ${typeof users} instead`);
assert(typeof authorizer === 'function', `Expected a function for the basic auth authorizer, found ${typeof authorizer} instead`);

return {
challenge,
users,
authorizer,
isAsync,
getResponseBody,
realm,
};
}

function authorizerCallback(err: any, approved: boolean, next: NextFunction, options: IUnauthorizeOptions) {
assert.ifError(err);
if (approved) {
return next();
}
return unauthorized(options);
}

function unauthorized({
challenge,
getResponseBody,
req,
realm,
res
}: IUnauthorizeOptions) {
if (challenge) {
let challengeString = 'Basic'
const realmName = realm(req);
if (realmName) {
challengeString = `${challengeString} realm="${realmName}"`;
}
res.set('WWW-Authenticate', challengeString);
}

//TODO: Allow response body to be JSON (maybe autodetect?)
const response = getResponseBody(req);

if (typeof response == 'string') {
return res.status(401).send(response);
}
return res.status(401).json(response);
}

function ensureFunction(option: BasicAuthMiddlewareOptions, defaultValue: any = null) {
if (option == undefined) {
return () => { return defaultValue };
}
if (typeof option !== 'function') {
return () => { return option };
}
return option;
}

function staticUsersAuthorizer(username: string, password: string, users: []): boolean {
for (var currentUser in users) {
const checkUser = safeCompare(username, currentUser);
const checkPassword = safeCompare(password, users[currentUser]);
if (checkUser && checkPassword) {
return true
}
}
return false
}

// Credits for the actual algorithm go to github/@Bruce17
// Thanks to github/@hraban for making me implement this
function safeCompare(userInput:string , secret: string) {
const userInputLength = Buffer.byteLength(userInput)
const secretLength = Buffer.byteLength(secret)

const userInputBuffer = Buffer.alloc(userInputLength, 0, 'utf8')
userInputBuffer.write(userInput)
const secretBuffer = Buffer.alloc(userInputLength, 0, 'utf8')
secretBuffer.write(secret)

return !!(timingSafeEqual(userInputBuffer, secretBuffer) && (userInputLength === secretLength)) // tslint:disable-line
}
Loading