Repository: hapijs/hapi (JavaScript)

Issue:
Cannot skip required payload authentication with injected credentials
<!--
  ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️
  You must complete this entire issue template to receive support. You MUST NOT remove, change, or replace the template with your own format. A missing or incomplete report will cause your issue to be closed without comment. Please respect the time and experience that went into this template. It is here for a reason. Thank you!
  ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️
-->

#### Support plan

<!--
We are here to help!

Community based support is, by its nature, limited to available community members able to help. Most community support issues are resolved within 2 weeks. Before submitting an issue, please review the various support options available at https://hapi.dev/support/. That page includes useful information about different ways to ask questions.
-->

* *is this issue currently blocking your project?* (yes/no): No
* *is this issue affecting a production system?* (yes/no): No

#### Context

* *node version*: v12.19.1
* *module version with issue*:  20.0.3
* *last module version without issue*: I believe this has always behaved this way
* *environment* (e.g. node, browser, native): node
* *used with* (e.g. hapi application, another framework, standalone, ...): framework
* *any other relevant information*:

#### What are you trying to achieve or the steps to reproduce?
When trying to inject credentials to an authentication scheme requiring payload authentication, then the payload authentication cannot be skipped.

<!--
Describe your issue in detail, including full steps to reproduce the issue, any configuration, schemas, code samples, or inputs needed. Make sure to wrap all code examples in backticks so that they display correctly. Before submitting an issue, make sure to click on the Preview tab above to verify everything is formatted correctly.
-->

```js
'use strict';

const assert = require('assert');
const Hapi = require('@hapi/hapi');

async function run() {
  const server = Hapi.server();

  server.auth.scheme('scheme', () => ({
    authenticate() {
      throw new Error('authenticate error');
    },
    payload() {
      throw new Error('payload error');
    },
    options: {
      payload: true,
    },
  }));

  server.auth.strategy('default', 'scheme');

  server.route({
    method: 'POST',
    path: '/test',
    options: {
      auth: 'default',
      handler(request) {
        return request.auth.credentials;
      },
    },
  });

  const credentials = {
    hello: 'world',
  };

  const { result } = await server.inject({
    method: 'POST',
    url: '/test',
    auth: {
      strategy: 'default',
      credentials,
    },
  });

  assert.deepStrictEqual(result, credentials);
}

run()
  .catch((error) => {
    console.log(error);
    process.exit(1);
  });

```

#### What was the result you got?
The test fails because payload authentication could not be skipped, while the scheme authentication was.

#### What result did you expect?
Both the payload authentication and scheme authentication are skipped, when credentials are injected. If we update the authentication scheme to not require payload authentication, the test would pass.

I am not actually sure this is the expected behavior or not, for now I manually skip the payload authentication on my scheme when credentials are injected.


Discussion:
> I am not actually sure this is the expected behavior or not

It's not entirely clear to me either, but from some quick testing, the test suite fails if payload validation is skipped on injected requests 😄 
@cjihrig Is there any chance to change that and skip payload authentication when credentials are injected? Does it make sense?
I think the best way to support this without breaking any existing code would be to add a new option to `server.inject()` under the `options.auth` object, and have it default to the current behavior.
Nice, would the following API make sense?

#### options.auth
+ `payload` Disables payload authentication when set to false. Only required when an authentication strategy *requires* payload authentication. Defaults to **true**

#### Usage

```js
const { result } = await server.inject({
    method: 'POST',
    url: '/test',
    auth: {
      strategy: 'default',
      credentials: {},
      payload: false,
    },
  });
```

That proposal makes sense to me.
LGTM too.
Hi @Nargonath @cjihrig I recently created a pull request with these changes at #4274. Have some time to take a look?



Patch:
diff --git a/API.md b/API.md
index 34e32c08c..1e98c6590 100755
--- a/API.md
+++ b/API.md
@@ -1893,6 +1893,10 @@ injections, with some additional options and response properties:
           and are validated directly as if they were received via an authentication scheme.
           Defaults to no artifacts.
 
+        - `payload` - (optional) disables payload authentication when set to false.
+          Only required when an authentication strategy requires payload authentication.
+          Defaults to `true`.
+
     - `app` - (optional) sets the initial value of `request.app`, defaults to `{}`.
 
     - `plugins` - (optional) sets the initial value of `request.plugins`, defaults to `{}`.
diff --git a/lib/auth.js b/lib/auth.js
index 6b8a8027c..4bea46795 100755
--- a/lib/auth.js
+++ b/lib/auth.js
@@ -5,6 +5,7 @@ const Bounce = require('@hapi/bounce');
 const Hoek = require('@hapi/hoek');
 
 const Config = require('./config');
+const Request = require('./request');
 
 
 const internals = {
@@ -361,7 +362,7 @@ exports = module.exports = internals.Auth = class {
 
     static async payload(request) {
 
-        if (!request.auth.isAuthenticated) {
+        if (!request.auth.isAuthenticated || !request.auth[Request.symbols.authPayload]) {
             return;
         }
 
diff --git a/lib/request.js b/lib/request.js
index 1cfc0b89d..3f92b850d 100755
--- a/lib/request.js
+++ b/lib/request.js
@@ -66,6 +66,7 @@ exports = module.exports = internals.Request = class {
             isAuthenticated: false,
             isAuthorized: false,
             isInjected: options.auth ? true : false,
+            [internals.Request.symbols.authPayload]: options.auth && options.auth.payload !== undefined ? options.auth.payload : true,
             credentials: options.auth ? options.auth.credentials : null,                                    // Special keys: 'app', 'user', 'scope'
             artifacts: options.auth && options.auth.artifacts || null,                                      // Scheme-specific artifacts
             strategy: options.auth ? options.auth.strategy : null,
@@ -619,6 +620,9 @@ exports = module.exports = internals.Request = class {
 
 internals.Request.reserved = internals.reserved;
 
+internals.Request.symbols = {
+    authPayload: Symbol('auth.payload')
+};
 
 internals.Info = class {
 


Test patch:
diff --git a/test/auth.js b/test/auth.js
index 972d37ba8..fb83afea9 100755
--- a/test/auth.js
+++ b/test/auth.js
@@ -42,7 +42,7 @@ describe('authentication', () => {
             strategy: 'default',
             mode: 'required',
             error: null
-        });
+        }, { symbols: false });
     });
 
     it('disables authentication on a route', async () => {
@@ -1100,7 +1100,7 @@ describe('authentication', () => {
                 strategy: 'default',
                 mode: 'required',
                 error: null
-            });
+            }, { symbols: false });
         });
 
         it('matches scope (access array)', async () => {
@@ -1759,6 +1759,28 @@ describe('authentication', () => {
             expect(res.statusCode).to.equal(204);
         });
 
+        it('skips required payload authentication when disabled on injection', async () => {
+
+            const server = Hapi.server();
+            server.auth.scheme('custom', internals.implementation);
+            server.auth.strategy('default', 'custom');
+            server.auth.default('default');
+            server.route({
+                method: 'POST',
+                path: '/',
+                options: {
+                    handler: (request) => null,
+                    auth: {
+                        mode: 'try',
+                        payload: true
+                    }
+                }
+            });
+
+            const res = await server.inject({ method: 'POST', url: '/', auth: { credentials: { payload: Boom.internal('payload error') }, payload: false, strategy: 'default' } });
+            expect(res.statusCode).to.equal(204);
+        });
+
         it('errors on missing payload when required', async () => {
 
             const server = Hapi.server();