├── .github
└── workflows
│ └── ci-plugin.yml
├── .gitignore
├── API.md
├── LICENSE.md
├── README.md
├── lib
├── index.d.ts
└── index.js
├── package.json
└── test
├── esm.js
├── index.js
└── index.ts
/.github/workflows/ci-plugin.yml:
--------------------------------------------------------------------------------
1 | name: ci
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | pull_request:
8 | workflow_dispatch:
9 |
10 | jobs:
11 | test:
12 | uses: hapijs/.github/.github/workflows/ci-plugin.yml@master
13 | with:
14 | min-node-version: 14
15 | min-hapi-version: 20
16 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | **/node_modules
2 | **/package-lock.json
3 |
4 | coverage.*
5 |
6 | **/.DS_Store
7 | **/._*
8 |
9 | **/*.pem
10 |
11 | **/.vs
12 | **/.vscode
13 | **/.idea
14 |
--------------------------------------------------------------------------------
/API.md:
--------------------------------------------------------------------------------
1 |
2 | ## Usage
3 |
4 | Basic authentication requires validating a username and password combination. The `'basic'` scheme takes the following options:
5 |
6 | - `validate` - (required) a user lookup and password validation function with the signature `[async] function(request, username, password, h)` where:
7 | - `request` - is the hapi request object of the request which is being authenticated.
8 | - `username` - the username received from the client.
9 | - `password` - the password received from the client.
10 | - `h` - the response toolkit.
11 | - Returns an object `{ isValid, credentials, response }` where:
12 | - `isValid` - `true` if both the username was found and the password matched, otherwise `false`.
13 | - `credentials` - a credentials object passed back to the application in `request.auth.credentials`.
14 | - `response` - Optional. If provided will be used immediately as a takeover response. Can be used to redirect the client, for example. Don't need to provide `isValid` or `credentials` if `response` is provided
15 | - Throwing an error from this function will replace default `Boom.unauthorized` error
16 | - Typically, `credentials` are only included when `isValid` is `true`, but there are cases when the application needs to know who tried to authenticate even when it fails (e.g. with authentication mode `'try'`).
17 | - `allowEmptyUsername` - (optional) if `true`, allows making requests with an empty username. Defaults to `false`.
18 | - `unauthorizedAttributes` - (optional) if set, passed directly to [Boom.unauthorized](https://github.com/hapijs/boom#boomunauthorizedmessage-scheme-attributes) if no custom `err` is thrown. Useful for setting realm attribute in WWW-Authenticate header. Defaults to `undefined`.
19 |
20 | ```javascript
21 | const Bcrypt = require('bcrypt');
22 | const Hapi = require('@hapi/hapi');
23 |
24 | const users = {
25 | john: {
26 | username: 'john',
27 | password: '$2a$10$iqJSHD.BGr0E2IxQwYgJmeP3NvhPrXAeLSaGCj6IR/XU5QtjVu5Tm', // 'secret'
28 | name: 'John Doe',
29 | id: '2133d32a'
30 | }
31 | };
32 |
33 | const validate = async (request, username, password, h) => {
34 |
35 | if (username === 'help') {
36 | return { response: h.redirect('https://hapijs.com/help') }; // custom response
37 | }
38 |
39 | const user = users[username];
40 | if (!user) {
41 | return { credentials: null, isValid: false };
42 | }
43 |
44 | const isValid = await Bcrypt.compare(password, user.password);
45 | const credentials = { id: user.id, name: user.name };
46 |
47 | return { isValid, credentials };
48 | };
49 |
50 | const main = async () => {
51 |
52 | const server = Hapi.server({ port: 4000 });
53 |
54 | await server.register(require('@hapi/basic'));
55 |
56 | server.auth.strategy('simple', 'basic', { validate });
57 | server.auth.default('simple');
58 |
59 | server.route({
60 | method: 'GET',
61 | path: '/',
62 | handler: function (request, h) {
63 |
64 | return 'welcome';
65 | }
66 | });
67 |
68 | await server.start();
69 |
70 | return server;
71 | };
72 |
73 | main()
74 | .then((server) => console.log(`Server listening on ${server.info.uri}`))
75 | .catch((err) => {
76 |
77 | console.error(err);
78 | process.exit(1);
79 | });
80 | ```
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright (c) 2012-2022, Project contributors
2 | Copyright (c) 2012-2020, Sideway Inc
3 | Copyright (c) 2012-2014, Walmart.
4 | All rights reserved.
5 |
6 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
7 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
8 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
9 | * The names of any contributors may not be used to endorse or promote products derived from this software without specific prior written permission.
10 |
11 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS OFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
12 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # @hapi/basic
4 |
5 | #### Basic authentication plugin.
6 |
7 | **basic** is part of the **hapi** ecosystem and was designed to work seamlessly with the [hapi web framework](https://hapi.dev) and its other components (but works great on its own or with other frameworks). If you are using a different web framework and find this module useful, check out [hapi](https://hapi.dev) – they work even better together.
8 |
9 | ### Visit the [hapi.dev](https://hapi.dev) Developer Portal for tutorials, documentation, and support
10 |
11 | ## Useful resources
12 |
13 | - [Documentation and API](https://hapi.dev/family/basic/)
14 | - [Versions status](https://hapi.dev/resources/status/#basic) (builds, dependencies, node versions, licenses, eol)
15 | - [Changelog](https://hapi.dev/family/basic/changelog/)
16 | - [Project policies](https://hapi.dev/policies/)
17 | - [Free and commercial support options](https://hapi.dev/support/)
18 |
--------------------------------------------------------------------------------
/lib/index.d.ts:
--------------------------------------------------------------------------------
1 | // Type definitions for @hapi/basic 5.1
2 | // Project: https://github.com/hapijs/basic
3 | // Definitions by: AJP
4 | // Rodrigo Saboya
5 | // Silas Rech
6 | // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
7 | // TypeScript Version: 2.8
8 |
9 | import { Plugin, Request, ResponseToolkit } from '@hapi/hapi';
10 |
11 | declare namespace Basic {
12 | interface ValidateCustomResponse {
13 | response: any,
14 | }
15 |
16 | interface ValidateResponse {
17 | isValid: boolean,
18 | credentials?: any,
19 | }
20 |
21 | interface Validate {
22 | (request: Request, username: string, password: string, h: ResponseToolkit): Promise;
23 | }
24 | }
25 |
26 | declare var Basic: Plugin<{}>;
27 |
28 | export = Basic;
29 |
--------------------------------------------------------------------------------
/lib/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const Boom = require('@hapi/boom');
4 | const Hoek = require('@hapi/hoek');
5 |
6 |
7 | const internals = {};
8 |
9 |
10 | exports.plugin = {
11 | pkg: require('../package.json'),
12 | requirements: {
13 | hapi: '>=20.0.0'
14 | },
15 |
16 | register(server, options) {
17 |
18 | server.auth.scheme('basic', internals.implementation);
19 | }
20 | };
21 |
22 |
23 | internals.implementation = function (server, options) {
24 |
25 | Hoek.assert(options, 'Missing basic auth strategy options');
26 | Hoek.assert(typeof options.validate === 'function', 'options.validate must be a valid function in basic scheme');
27 |
28 | const settings = Hoek.clone(options);
29 |
30 | const scheme = {
31 | authenticate: async function (request, h) {
32 |
33 | const authorization = request.headers.authorization;
34 |
35 | if (!authorization) {
36 | throw Boom.unauthorized(null, 'Basic', settings.unauthorizedAttributes);
37 | }
38 |
39 | const parts = authorization.split(/\s+/);
40 |
41 | if (parts[0].toLowerCase() !== 'basic') {
42 | throw Boom.unauthorized(null, 'Basic', settings.unauthorizedAttributes);
43 | }
44 |
45 | if (parts.length !== 2) {
46 | throw Boom.badRequest('Bad HTTP authentication header format', 'Basic');
47 | }
48 |
49 | const credentialsPart = Buffer.from(parts[1], 'base64').toString();
50 | const sep = credentialsPart.indexOf(':');
51 | if (sep === -1) {
52 | throw Boom.badRequest('Bad header internal syntax', 'Basic');
53 | }
54 |
55 | const username = credentialsPart.slice(0, sep);
56 | const password = credentialsPart.slice(sep + 1);
57 |
58 | if (!username &&
59 | !settings.allowEmptyUsername) {
60 |
61 | throw Boom.unauthorized('HTTP authentication header missing username', 'Basic', settings.unauthorizedAttributes);
62 | }
63 |
64 | const { isValid, credentials, response } = await settings.validate(request, username, password, h);
65 |
66 | if (response !== undefined) {
67 | return h.response(response).takeover();
68 | }
69 |
70 | if (!isValid) {
71 | return h.unauthenticated(Boom.unauthorized('Bad username or password', 'Basic', settings.unauthorizedAttributes), credentials ? { credentials } : null);
72 | }
73 |
74 | if (!credentials ||
75 | typeof credentials !== 'object') {
76 |
77 | throw Boom.badImplementation('Bad credentials object received for Basic auth validation');
78 | }
79 |
80 | return h.authenticated({ credentials });
81 | }
82 | };
83 |
84 | return scheme;
85 | };
86 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@hapi/basic",
3 | "description": "Basic authentication plugin",
4 | "version": "7.0.2",
5 | "repository": "git://github.com/hapijs/basic",
6 | "main": "lib/index.js",
7 | "types": "lib/index.d.ts",
8 | "files": [
9 | "lib"
10 | ],
11 | "keywords": [
12 | "hapi",
13 | "plugin",
14 | "auth",
15 | "basic"
16 | ],
17 | "eslintConfig": {
18 | "extends": [
19 | "plugin:@hapi/module"
20 | ]
21 | },
22 | "dependencies": {
23 | "@hapi/boom": "^10.0.1",
24 | "@hapi/hoek": "^11.0.2"
25 | },
26 | "devDependencies": {
27 | "@hapi/code": "^9.0.3",
28 | "@hapi/eslint-plugin": "^6.0.0",
29 | "@hapi/hapi": "^21.3.2",
30 | "@hapi/lab": "^25.1.2",
31 | "@types/node": "^14.18.48",
32 | "joi": "^17.9.2",
33 | "typescript": "^5.1.3"
34 | },
35 | "scripts": {
36 | "test": "lab -a @hapi/code -t 100 -L -Y",
37 | "test-cov-html": "lab -a @hapi/code -r html -o coverage.html"
38 | },
39 | "license": "BSD-3-Clause"
40 | }
41 |
--------------------------------------------------------------------------------
/test/esm.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const Code = require('@hapi/code');
4 | const Lab = require('@hapi/lab');
5 |
6 |
7 | const { before, describe, it } = exports.lab = Lab.script();
8 | const expect = Code.expect;
9 |
10 |
11 | describe('import()', () => {
12 |
13 | let Basic;
14 |
15 | before(async () => {
16 |
17 | Basic = await import('../lib/index.js');
18 | });
19 |
20 | it('exposes all methods and classes as named imports', () => {
21 |
22 | expect(Object.keys(Basic)).to.equal([
23 | 'default',
24 | 'plugin'
25 | ]);
26 | });
27 | });
28 |
--------------------------------------------------------------------------------
/test/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const Basic = require('..');
4 | const Boom = require('@hapi/boom');
5 | const Code = require('@hapi/code');
6 | const Hapi = require('@hapi/hapi');
7 | const Lab = require('@hapi/lab');
8 |
9 |
10 | const internals = {};
11 |
12 |
13 | const { it, describe } = exports.lab = Lab.script();
14 | const expect = Code.expect;
15 |
16 |
17 | describe('Basic authentication', () => {
18 |
19 | it('returns a reply on successful auth', async () => {
20 |
21 | const server = Hapi.server();
22 | await server.register(Basic);
23 | server.auth.strategy('default', 'basic', { validate: internals.user });
24 |
25 | server.route({
26 | method: 'POST',
27 | path: '/',
28 | handler: function (request, h) {
29 |
30 | return 'ok';
31 | },
32 | options: {
33 | auth: 'default'
34 | }
35 | });
36 |
37 | const request = { method: 'POST', url: '/', headers: { authorization: internals.header('john', '123:45') } };
38 | const res = await server.inject(request);
39 |
40 | expect(res.result).to.equal('ok');
41 | });
42 |
43 | it('returns an error on wrong scheme', async () => {
44 |
45 | const server = Hapi.server();
46 | await server.register(Basic);
47 | server.auth.strategy('default', 'basic', { validate: internals.user });
48 |
49 | server.route({
50 | method: 'POST',
51 | path: '/',
52 | handler: function (request, h) {
53 |
54 | return 'ok';
55 | },
56 | options: {
57 | auth: 'default'
58 | }
59 | });
60 |
61 | const request = { method: 'POST', url: '/', headers: { authorization: 'Steve something' } };
62 | const res = await server.inject(request);
63 |
64 | expect(res.statusCode).to.equal(401);
65 | });
66 |
67 | it('returns a reply on failed optional auth', async () => {
68 |
69 | const server = Hapi.server();
70 |
71 | await server.register(Basic);
72 |
73 | server.auth.strategy('default', 'basic', { validate: internals.user });
74 | server.route({
75 | method: 'POST',
76 | path: '/',
77 | handler: function (request, h) {
78 |
79 | return 'ok';
80 | },
81 | options: {
82 | auth: {
83 | strategy: 'default',
84 | mode: 'optional'
85 | }
86 | }
87 | });
88 |
89 | const request = { method: 'POST', url: '/' };
90 |
91 | const res = await server.inject(request);
92 | expect(res.result).to.equal('ok');
93 | });
94 |
95 | it('returns an error on bad password', async () => {
96 |
97 | const server = Hapi.server();
98 | await server.register(Basic);
99 |
100 | server.auth.strategy('default', 'basic', { validate: internals.user });
101 | server.route({
102 | method: 'POST',
103 | path: '/',
104 | handler: function (request, h) {
105 |
106 | return 'ok';
107 | },
108 | options: {
109 | auth: 'default'
110 | }
111 | });
112 |
113 | const request = { method: 'POST', url: '/', headers: { authorization: internals.header('john', 'abcd') } };
114 |
115 | const res = await server.inject(request);
116 | expect(res.statusCode).to.equal(401);
117 | });
118 |
119 | it('returns an error on bad header format', async () => {
120 |
121 | const server = Hapi.server();
122 | await server.register(Basic);
123 |
124 | server.auth.strategy('default', 'basic', { validate: internals.user });
125 | server.route({
126 | method: 'POST',
127 | path: '/',
128 | handler: function (request, h) {
129 |
130 | return 'ok';
131 | },
132 | options: {
133 | auth: 'default'
134 | }
135 | });
136 |
137 | const request = { method: 'POST', url: '/', headers: { authorization: 'basic' } };
138 |
139 | const res = await server.inject(request);
140 |
141 | expect(res.result).to.exist();
142 | expect(res.statusCode).to.equal(400);
143 | expect(res.result.isMissing).to.equal(undefined);
144 | });
145 |
146 | it('returns an error on bad header internal syntax', async () => {
147 |
148 | const server = Hapi.server();
149 | await server.register(Basic);
150 |
151 | server.auth.strategy('default', 'basic', { validate: internals.user });
152 | server.route({
153 | method: 'POST',
154 | path: '/',
155 | handler: function (request, h) {
156 |
157 | return 'ok';
158 | },
159 | options: {
160 | auth: 'default'
161 | }
162 | });
163 |
164 | const request = { method: 'POST', url: '/', headers: { authorization: 'basic 123' } };
165 |
166 | const res = await server.inject(request);
167 |
168 | expect(res.result).to.exist();
169 | expect(res.statusCode).to.equal(400);
170 | expect(res.result.isMissing).to.equal(undefined);
171 | });
172 |
173 | it('returns an error on missing username', async () => {
174 |
175 | const server = Hapi.server();
176 | await server.register(Basic);
177 |
178 | server.auth.strategy('default', 'basic', { validate: internals.user });
179 | server.route({
180 | method: 'POST',
181 | path: '/',
182 | handler: function (request, h) {
183 |
184 | return 'ok';
185 | },
186 | options: {
187 | auth: 'default'
188 | }
189 | });
190 |
191 | const request = { method: 'POST', url: '/', headers: { authorization: internals.header('', '') } };
192 |
193 | const res = await server.inject(request);
194 |
195 | expect(res.result).to.exist();
196 | expect(res.statusCode).to.equal(401);
197 | });
198 |
199 | it('allows missing username', async () => {
200 |
201 | const server = Hapi.server();
202 | await server.register(Basic);
203 |
204 | server.auth.strategy('default', 'basic', {
205 | validate: () => ({ isValid: true, credentials: {} }),
206 | allowEmptyUsername: true
207 | });
208 |
209 | server.route({
210 | method: 'GET',
211 | path: '/',
212 | handler: function (request, h) {
213 |
214 | return 'ok';
215 | },
216 | options: {
217 | auth: 'default'
218 | }
219 | });
220 |
221 | const res = await server.inject({ method: 'GET', url: '/', headers: { authorization: internals.header('', 'abcd') } });
222 |
223 | expect(res.statusCode).to.equal(200);
224 | });
225 |
226 | it('returns an error on unknown user', async () => {
227 |
228 | const server = Hapi.server();
229 | await server.register(Basic);
230 |
231 | server.auth.strategy('default', 'basic', { validate: internals.user });
232 | server.route({
233 | method: 'POST',
234 | path: '/',
235 | handler: function (request, h) {
236 |
237 | return 'ok';
238 | },
239 | options: {
240 | auth: 'default'
241 | }
242 | });
243 |
244 | const request = { method: 'POST', url: '/', headers: { authorization: internals.header('doe', '123:45') } };
245 | const res = await server.inject(request);
246 |
247 | expect(res.result).to.exist();
248 | expect(res.statusCode).to.equal(401);
249 | });
250 |
251 | it('replies with thrown custom error', async () => {
252 |
253 | const server = Hapi.server({ debug: false });
254 | await server.register(Basic);
255 |
256 | server.auth.strategy('default', 'basic', { validate: internals.user });
257 | server.route({
258 | method: 'POST',
259 | path: '/',
260 | handler: function (request, h) {
261 |
262 | return 'ok';
263 | },
264 | options: {
265 | auth: 'default'
266 | }
267 | });
268 |
269 | const request = { method: 'POST', url: '/', headers: { authorization: internals.header('jane', '123:45') } };
270 |
271 | const res = await server.inject(request);
272 |
273 | expect(res.result).to.exist();
274 | expect(res.result.message).to.equal('Some other problem');
275 | expect(res.statusCode).to.equal(400);
276 | });
277 |
278 | it('replies with response response', async () => {
279 |
280 | const server = Hapi.server({ debug: false });
281 | await server.register(Basic);
282 |
283 | server.auth.strategy('default', 'basic', { validate: internals.user });
284 | server.route({
285 | method: 'POST',
286 | path: '/',
287 | handler: function (request, h) {
288 |
289 | return 'ok';
290 | },
291 | options: {
292 | auth: 'default'
293 | }
294 | });
295 |
296 | const request = { method: 'POST', url: '/', headers: { authorization: internals.header('bob', '123:45') } };
297 |
298 | const res = await server.inject(request);
299 |
300 | expect(res.statusCode).to.equal(302);
301 | expect(res.headers.location).to.equal('https://hapijs.com');
302 | });
303 |
304 | it('returns an error on non-object credentials error', async () => {
305 |
306 | const server = Hapi.server({ debug: false });
307 | await server.register(Basic);
308 |
309 | server.auth.strategy('default', 'basic', { validate: internals.user });
310 | server.route({
311 | method: 'POST',
312 | path: '/',
313 | handler: function (request, h) {
314 |
315 | return 'ok';
316 | },
317 | options: {
318 | auth: 'default'
319 | }
320 | });
321 |
322 | const request = { method: 'POST', url: '/', headers: { authorization: internals.header('invalid1', '123:45') } };
323 |
324 | const res = await server.inject(request);
325 |
326 | expect(res.result).to.exist();
327 | expect(res.statusCode).to.equal(500);
328 | });
329 |
330 | it('returns an error on missing credentials error', async () => {
331 |
332 | const server = Hapi.server({ debug: false });
333 | await server.register(Basic);
334 |
335 | server.auth.strategy('default', 'basic', { validate: internals.user });
336 | server.route({
337 | method: 'POST',
338 | path: '/',
339 | handler: function (request, h) {
340 |
341 | return 'ok';
342 | },
343 | options: {
344 | auth: 'default'
345 | }
346 | });
347 |
348 | const request = { method: 'POST', url: '/', headers: { authorization: internals.header('invalid2', '123:45') } };
349 |
350 | const res = await server.inject(request);
351 |
352 | expect(res.result).to.exist();
353 | expect(res.statusCode).to.equal(500);
354 | });
355 |
356 | it('returns an error on insufficient scope', async () => {
357 |
358 | const server = Hapi.server();
359 | await server.register(Basic);
360 |
361 | server.auth.strategy('default', 'basic', { validate: internals.user });
362 | server.route({
363 | method: 'POST',
364 | path: '/',
365 | handler: function (request, h) {
366 |
367 | return 'ok';
368 | },
369 | options: {
370 | auth: {
371 | strategy: 'default',
372 | scope: 'x'
373 | }
374 | }
375 | });
376 |
377 | const request = { method: 'POST', url: '/', headers: { authorization: internals.header('john', '123:45') } };
378 |
379 | const res = await server.inject(request);
380 |
381 | expect(res.result).to.exist();
382 | expect(res.statusCode).to.equal(403);
383 | });
384 |
385 | it('returns an error on insufficient scope specified as an array', async () => {
386 |
387 | const server = Hapi.server();
388 | await server.register(Basic);
389 |
390 | server.auth.strategy('default', 'basic', { validate: internals.user });
391 |
392 | server.route({
393 | method: 'POST',
394 | path: '/',
395 | handler: function (request, h) {
396 |
397 | return 'ok';
398 | },
399 | options: {
400 | auth: {
401 | strategy: 'default',
402 | scope: ['x', 'y']
403 | }
404 | }
405 | });
406 |
407 | const request = { method: 'POST', url: '/', headers: { authorization: internals.header('john', '123:45') } };
408 |
409 | const res = await server.inject(request);
410 |
411 | expect(res.result).to.exist();
412 | expect(res.statusCode).to.equal(403);
413 | });
414 |
415 | it('authenticates scope specified as an array', async () => {
416 |
417 | const server = Hapi.server();
418 | await server.register(Basic);
419 |
420 | server.auth.strategy('default', 'basic', { validate: internals.user });
421 | server.route({
422 | method: 'POST',
423 | path: '/',
424 | handler: function (request, h) {
425 |
426 | return 'ok';
427 | },
428 | options: {
429 | auth: {
430 | strategy: 'default',
431 | scope: ['x', 'y', 'a']
432 | }
433 | }
434 | });
435 |
436 | const request = { method: 'POST', url: '/', headers: { authorization: internals.header('john', '123:45') } };
437 |
438 | const res = await server.inject(request);
439 |
440 | expect(res.result).to.exist();
441 | expect(res.statusCode).to.equal(200);
442 | });
443 |
444 | it('asks for credentials if server has one default strategy', async () => {
445 |
446 | const server = Hapi.server();
447 | await server.register(Basic);
448 |
449 |
450 | server.auth.strategy('default', 'basic', { validate: internals.user });
451 | server.route({
452 | path: '/',
453 | method: 'GET',
454 | options: {
455 | auth: 'default',
456 | handler: function (request, h) {
457 |
458 | return 'ok';
459 | }
460 | }
461 | });
462 |
463 | const validOptions = { method: 'GET', url: '/', headers: { authorization: internals.header('john', '123:45') } };
464 | const res1 = await server.inject(validOptions);
465 |
466 | expect(res1.result).to.exist();
467 | expect(res1.statusCode).to.equal(200);
468 |
469 | const res2 = await server.inject('/');
470 |
471 | expect(res2.result).to.exist();
472 | expect(res2.statusCode).to.equal(401);
473 | });
474 |
475 | it('errors on a route that has payload validation required', async () => {
476 |
477 | const server = Hapi.server();
478 | await server.register(Basic);
479 |
480 | server.auth.strategy('default', 'basic', { validate: internals.user });
481 |
482 | const fn = function () {
483 |
484 | server.route({
485 | method: 'POST',
486 | path: '/',
487 | handler: function (request, h) {
488 |
489 | return 'ok';
490 | },
491 | options: {
492 | auth: {
493 | strategy: 'default',
494 | mode: 'required',
495 | payload: 'required'
496 | }
497 | }
498 | });
499 | };
500 |
501 | expect(fn).to.throw('Payload validation can only be required when all strategies support it in /');
502 | });
503 |
504 | it('errors on a route that has payload validation as optional', async () => {
505 |
506 | const server = Hapi.server();
507 | await server.register(Basic);
508 |
509 | server.auth.strategy('default', 'basic', { validate: internals.user });
510 |
511 | const fn = function () {
512 |
513 | server.route({
514 | method: 'POST',
515 | path: '/',
516 | handler: function (request, h) {
517 |
518 | return 'ok';
519 | },
520 | options: {
521 | auth: {
522 | strategy: 'default',
523 | mode: 'required',
524 | payload: 'optional'
525 | }
526 | }
527 | });
528 | };
529 |
530 | expect(fn).to.throw('Payload authentication requires at least one strategy with payload support in /');
531 | });
532 |
533 | it('adds a route that has payload validation as none', async () => {
534 |
535 | const server = Hapi.server();
536 | await server.register(Basic);
537 |
538 | server.auth.strategy('default', 'basic', { validate: internals.user });
539 |
540 | const fn = function () {
541 |
542 | server.route({
543 | method: 'POST',
544 | path: '/',
545 | handler: function (request, h) {
546 |
547 | return 'ok';
548 | },
549 | options: {
550 | auth: {
551 | strategy: 'default',
552 | mode: 'required',
553 | payload: false
554 | }
555 | }
556 | });
557 | };
558 |
559 | expect(fn).to.not.throw();
560 | });
561 |
562 | it('includes additional attributes in WWW-Authenticate header', async () => {
563 |
564 | const server = Hapi.server();
565 | await server.register(Basic);
566 |
567 | server.auth.strategy('default', 'basic', {
568 | validate: internals.user,
569 | unauthorizedAttributes: { realm: 'hapi' }
570 | });
571 |
572 | server.route({
573 | method: 'POST',
574 | path: '/',
575 | handler: function (request, h) {
576 |
577 | return 'ok';
578 | },
579 | options: {
580 | auth: 'default'
581 | }
582 | });
583 |
584 | const request = { method: 'POST', url: '/' };
585 |
586 | const res = await server.inject(request);
587 |
588 | const wwwAuth = 'www-authenticate';
589 | expect(res.headers).to.include(wwwAuth);
590 | expect(res.headers[wwwAuth]).to.equal('Basic realm=\"hapi\"');
591 | });
592 | });
593 |
594 |
595 | internals.header = function (username, password) {
596 |
597 | return 'Basic ' + (Buffer.from(username + ':' + password, 'utf8')).toString('base64');
598 | };
599 |
600 |
601 | internals.user = async function (request, username, password, h) {
602 |
603 | if (username === 'john') {
604 | return await Promise.resolve({
605 | isValid: password === '123:45',
606 | credentials: {
607 | user: 'john',
608 | scope: ['a'],
609 | tos: '1.0.0'
610 | }
611 | });
612 | }
613 |
614 | if (username === 'jane') {
615 | throw Boom.badRequest('Some other problem');
616 | }
617 |
618 | if (username === 'bob') {
619 | return await Promise.resolve({ response: h.redirect('https://hapijs.com') });
620 | }
621 |
622 | if (username === 'invalid1') {
623 | return await Promise.resolve({
624 | isValid: true,
625 | credentials: 'bad'
626 | });
627 | }
628 |
629 | if (username === 'invalid2') {
630 | return await Promise.resolve({
631 | isValid: true,
632 | credentials: null
633 | });
634 | }
635 |
636 | return { isValid: false };
637 | };
638 |
--------------------------------------------------------------------------------
/test/index.ts:
--------------------------------------------------------------------------------
1 | // from https://github.com/hapijs/hapi-auth-basic#hapi-auth-basic
2 |
3 | import * as Basic from '..';
4 | import { Server } from '@hapi/hapi';
5 | import { types } from '@hapi/lab';
6 |
7 | import type { Plugin } from '@hapi/hapi';
8 |
9 | const server = new Server();
10 |
11 | types.expect.type>(Basic);
12 |
13 | interface User {
14 | username: string;
15 | password: string;
16 | name: string;
17 | id: string;
18 | }
19 |
20 | const users: {[index: string]: User} = {
21 | john: {
22 | username: 'john',
23 | password: '$2a$10$iqJSHD.BGr0E2IxQwYgJmeP3NvhPrXAeLSaGCj6IR/XU5QtjVu5Tm', // 'secret'
24 | name: 'John Doe',
25 | id: '2133d32a'
26 | }
27 | };
28 |
29 | const validate: Basic.Validate = async (request, username, password, h) => {
30 |
31 | const user = users[username];
32 | if (!user) {
33 | return { isValid: false, credentials: null };
34 | }
35 |
36 | const isValid = true; // No need to check for type tests
37 |
38 | return { isValid, credentials: { id: user.id, name: user.name } };
39 | };
40 |
41 | server.register(Basic).then(() => {
42 |
43 | server.auth.strategy('simple', 'basic', { validate });
44 | server.auth.default('simple');
45 |
46 | server.route({
47 | method: 'GET',
48 | path: '/',
49 | handler: () => null,
50 | options: { auth: 'simple' }
51 | });
52 | });
53 |
--------------------------------------------------------------------------------