diff --git a/src/middleware/basic-auth/index.ts b/src/middleware/basic-auth/index.ts index 3c1cad5e0..d23400d17 100644 --- a/src/middleware/basic-auth/index.ts +++ b/src/middleware/basic-auth/index.ts @@ -5,32 +5,9 @@ import type { Context } from '../../context' import { HTTPException } from '../../http-exception' -import type { HonoRequest } from '../../request' import type { MiddlewareHandler } from '../../types' +import { auth } from '../../utils/basic-auth' import { timingSafeEqual } from '../../utils/buffer' -import { decodeBase64 } from '../../utils/encode' - -const CREDENTIALS_REGEXP = /^ *(?:[Bb][Aa][Ss][Ii][Cc]) +([A-Za-z0-9._~+/-]+=*) *$/ -const USER_PASS_REGEXP = /^([^:]*):(.*)$/ -const utf8Decoder = new TextDecoder() -const auth = (req: HonoRequest) => { - const match = CREDENTIALS_REGEXP.exec(req.header('Authorization') || '') - if (!match) { - return undefined - } - - let userPass = undefined - // If an invalid string is passed to atob(), it throws a `DOMException`. - try { - userPass = USER_PASS_REGEXP.exec(utf8Decoder.decode(decodeBase64(match[1]))) - } catch {} // Do nothing - - if (!userPass) { - return undefined - } - - return { username: userPass[1], password: userPass[2] } -} type BasicAuthOptions = | { diff --git a/src/utils/basic-auth.test.ts b/src/utils/basic-auth.test.ts new file mode 100644 index 000000000..1031eacf7 --- /dev/null +++ b/src/utils/basic-auth.test.ts @@ -0,0 +1,63 @@ +import { HonoRequest } from '../request' +import { auth } from './basic-auth' + +describe('auth', () => { + it('auth() - not include Authorization Header', () => { + const req = new HonoRequest(new Request('http://localhost/auth')) + const res = auth(req) + expect(res).toBeUndefined() + }) + + it('auth() - invalid Authorization Header format', () => { + const req = new HonoRequest( + new Request('http://localhost/auth', { + headers: { Authorization: 'InvalidAuthHeader' }, + }) + ) + const res = auth(req) + expect(res).toBeUndefined() + }) + + it('auth() - invalid Base64 string in Authorization Header', () => { + const req = new HonoRequest( + new Request('http://localhost/auth', { + headers: { Authorization: 'Basic InvalidBase64' }, + }) + ) + const res = auth(req) + expect(res).toBeUndefined() + }) + + it('auth() - valid Authorization Header', () => { + const validBase64 = btoa('username:password') + const req = new HonoRequest( + new Request('http://localhost/auth', { + headers: { Authorization: `Basic ${validBase64}` }, + }) + ) + const res = auth(req) + expect(res).toEqual({ username: 'username', password: 'password' }) + }) + + it('auth() - empty username', () => { + const validBase64 = btoa(':password') + const req = new HonoRequest( + new Request('http://localhost/auth', { + headers: { Authorization: `Basic ${validBase64}` }, + }) + ) + const res = auth(req) + expect(res).toEqual({ username: '', password: 'password' }) + }) + + it('auth() - empty password', () => { + const validBase64 = btoa('username:') + const req = new HonoRequest( + new Request('http://localhost/auth', { + headers: { Authorization: `Basic ${validBase64}` }, + }) + ) + const res = auth(req) + expect(res).toEqual({ username: 'username', password: '' }) + }) +}) diff --git a/src/utils/basic-auth.ts b/src/utils/basic-auth.ts new file mode 100644 index 000000000..37d895913 --- /dev/null +++ b/src/utils/basic-auth.ts @@ -0,0 +1,27 @@ +import type { HonoRequest } from '../request' +import { decodeBase64 } from './encode' + +const CREDENTIALS_REGEXP = /^ *(?:[Bb][Aa][Ss][Ii][Cc]) +([A-Za-z0-9._~+/-]+=*) *$/ +const USER_PASS_REGEXP = /^([^:]*):(.*)$/ +const utf8Decoder = new TextDecoder() + +export type Auth = (req: HonoRequest) => { username: string; password: string } | undefined + +export const auth: Auth = (req: HonoRequest) => { + const match = CREDENTIALS_REGEXP.exec(req.header('Authorization') || '') + if (!match) { + return undefined + } + + let userPass = undefined + // If an invalid string is passed to atob(), it throws a `DOMException`. + try { + userPass = USER_PASS_REGEXP.exec(utf8Decoder.decode(decodeBase64(match[1]))) + } catch {} // Do nothing + + if (!userPass) { + return undefined + } + + return { username: userPass[1], password: userPass[2] } +}