diff --git a/package.json b/package.json index ba8603624..fd586582a 100644 --- a/package.json +++ b/package.json @@ -78,11 +78,13 @@ ], "devDependencies": { "@cloudflare/workers-types": "^3.3.0", + "@types/crypto-js": "^4.1.1", "@types/jest": "^27.4.0", "@types/mustache": "^4.1.2", "@types/node": "^17.0.8", "@typescript-eslint/eslint-plugin": "^5.9.0", "@typescript-eslint/parser": "^5.9.0", + "crypto-js": "^4.1.1", "eslint": "^7.26.0", "eslint-config-prettier": "^8.3.0", "eslint-define-config": "^1.2.1", @@ -103,4 +105,4 @@ "engines": { "node": ">=11.0.0" } -} \ No newline at end of file +} diff --git a/src/middleware/basic-auth/README.md b/src/middleware/basic-auth/README.md index 22a4e821a..624b4e9ce 100644 --- a/src/middleware/basic-auth/README.md +++ b/src/middleware/basic-auth/README.md @@ -2,8 +2,6 @@ ## Usage -index.js: - ```js import { Hono } from 'hono' import { basicAuth } from 'hono/basic-auth' @@ -24,3 +22,26 @@ app.get('/auth/page', (c) => { app.fire() ``` + +For Fastly Compute@Edge, polyfill `crypto` or use `crypto-js`. + +Install: + +``` +npm i crypto-js +``` + +Override `hashFunction`: + +```js +import { SHA256 } from 'crypto-js' + +app.use( + '/auth/*', + basicAuth({ + username: 'hono', + password: 'acoolproject', + hashFunction: (d: string) => SHA256(d).toString(), // <--- + }) +) +``` diff --git a/src/middleware/basic-auth/basic-auth.test.ts b/src/middleware/basic-auth/basic-auth.test.ts index 79b4955da..1c0c8e081 100644 --- a/src/middleware/basic-auth/basic-auth.test.ts +++ b/src/middleware/basic-auth/basic-auth.test.ts @@ -1,5 +1,6 @@ import { Hono } from '../../hono' import { basicAuth } from './basic-auth' +import { SHA256 } from 'crypto-js' describe('Basic Auth by Middleware', () => { const crypto = global.crypto @@ -52,9 +53,19 @@ describe('Basic Auth by Middleware', () => { ) ) + app.use( + '/auth-override-func/*', + basicAuth({ + username: username, + password: password, + hashFunction: (data: string) => SHA256(data).toString(), + }) + ) + app.get('/auth/*', () => new Response('auth')) app.get('/auth-unicode/*', () => new Response('auth')) app.get('/auth-multi/*', () => new Response('auth')) + app.get('/auth-override-func/*', () => new Response('auth')) it('Unauthorized', async () => { const req = new Request('http://localhost/auth/a') @@ -104,4 +115,15 @@ describe('Basic Auth by Middleware', () => { expect(res.status).toBe(200) expect(await res.text()).toBe('auth') }) + + it('should authorize with sha256 function override', async () => { + const credential = Buffer.from(username + ':' + password).toString('base64') + + const req = new Request('http://localhost/auth-override-func/a') + req.headers.set('Authorization', `Basic ${credential}`) + const res = await app.dispatch(req) + expect(res).not.toBeNull() + expect(res.status).toBe(200) + expect(await res.text()).toBe('auth') + }) }) diff --git a/src/middleware/basic-auth/basic-auth.ts b/src/middleware/basic-auth/basic-auth.ts index fcc4b8048..1f5b935d0 100644 --- a/src/middleware/basic-auth/basic-auth.ts +++ b/src/middleware/basic-auth/basic-auth.ts @@ -33,7 +33,7 @@ const auth = (req: Request) => { } export const basicAuth = ( - options: { username: string; password: string; realm?: string }, + options: { username: string; password: string; realm?: string; hashFunction?: Function }, ...users: { username: string; password: string }[] ) => { if (!options) { @@ -43,7 +43,6 @@ export const basicAuth = ( if (!options.realm) { options.realm = 'Secure Area' } - users.unshift({ username: options.username, password: options.password }) return async (ctx: Context, next: Function) => { @@ -51,8 +50,16 @@ export const basicAuth = ( if (requestUser) { for (const user of users) { - const usernameEqual = await timingSafeEqual(user.username, requestUser.username) - const passwordEqual = await timingSafeEqual(user.password, requestUser.password) + const usernameEqual = await timingSafeEqual( + user.username, + requestUser.username, + options.hashFunction + ) + const passwordEqual = await timingSafeEqual( + user.password, + requestUser.password, + options.hashFunction + ) if (usernameEqual && passwordEqual) { // Authorized OK return next() diff --git a/src/utils/buffer.test.ts b/src/utils/buffer.test.ts index f2b35fe0f..42d469113 100644 --- a/src/utils/buffer.test.ts +++ b/src/utils/buffer.test.ts @@ -1,4 +1,5 @@ import { timingSafeEqual } from './buffer' +import { SHA256 as sha256CryptoJS } from 'crypto-js' describe('buffer', () => { it('positive', async () => { @@ -13,6 +14,7 @@ describe('buffer', () => { expect(await timingSafeEqual(undefined, undefined)).toBe(true) expect(await timingSafeEqual(true, true)).toBe(true) expect(await timingSafeEqual(false, false)).toBe(true) + expect(await timingSafeEqual(true, true, (d: string) => sha256CryptoJS(d).toString())) }) it('negative', async () => { diff --git a/src/utils/buffer.ts b/src/utils/buffer.ts index 32fbce702..183316cb4 100644 --- a/src/utils/buffer.ts +++ b/src/utils/buffer.ts @@ -23,9 +23,13 @@ export const equal = (a: ArrayBuffer, b: ArrayBuffer) => { export const timingSafeEqual = async ( a: string | object | boolean, - b: string | object | boolean + b: string | object | boolean, + hashFunction?: Function ) => { - const sa = await sha256(a) - const sb = await sha256(b) + if (!hashFunction) { + hashFunction = sha256 + } + const sa = await hashFunction(a) + const sb = await hashFunction(b) return sa === sb && a === b } diff --git a/yarn.lock b/yarn.lock index 7b27dbc16..58d2290bf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -728,6 +728,11 @@ dependencies: "@babel/types" "^7.3.0" +"@types/crypto-js@^4.1.1": + version "4.1.1" + resolved "https://registry.yarnpkg.com/@types/crypto-js/-/crypto-js-4.1.1.tgz#602859584cecc91894eb23a4892f38cfa927890d" + integrity sha512-BG7fQKZ689HIoc5h+6D2Dgq1fABRa0RbBWKBd9SP/MVRVXROflpm5fhwyATX5duFmbStzyzyycPB8qUYKDH3NA== + "@types/graceful-fs@^4.1.2": version "4.1.5" resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.5.tgz#21ffba0d98da4350db64891f92a9e5db3cdb4e15" @@ -1299,6 +1304,11 @@ cross-spawn@^7.0.2, cross-spawn@^7.0.3: shebang-command "^2.0.0" which "^2.0.1" +crypto-js@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-4.1.1.tgz#9e485bcf03521041bd85844786b83fb7619736cf" + integrity sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw== + cssom@^0.4.4: version "0.4.4" resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.4.4.tgz#5a66cf93d2d0b661d80bf6a44fb65f5c2e4e0a10"